Repository: oclif/core (ts)

Issue:
CLI fails to start when plugin with init hook installed from github
**Describe the bug**
Installing plugins with `init` hook from github url causes all subsequent command executions to throw `ModuleLoadError` and exit 

**To Reproduce**
1. `sf plugins install https://github.com/oclif/plugin-test-esm-1`
2. `sf plugins --core`

```
» sf plugins install https://github.com/oclif/plugin-test-esm-1
? This plugin isn't signed by Salesforce. Only install the plugin if you trust its creator. Do you want to continue the installation?,
Yes
Because you approved this plugin, you can avoid future installation confirmations by adding the plugin to the unsignedPluginAllowList.json file. For details, see https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_allowlist.htm.
Installing plugin https://github.com/oclif/plugin-test-esm-1... ⣽ [2/4] Fetching packages...
warning "https://github.com/oclif/plugin-test-esm-1 > @oclif/plugin-plugins > @oclif/core > ts-node@10.9.1" has unmet peer dependency "@types/node@*".
warning "https://github.com/oclif/plugin-test-esm-1 > @oclif/plugin-plugins > @oclif/core > ts-node@10.9.1" has unmet peer dependency "typescript@>=2.7".
Installing plugin https://github.com/oclif/plugin-test-esm-1... ⢿ [4/4] Building fresh packages...
Installing plugin https://github.com/oclif/plugin-test-esm-1... ⡿ [3/5] Fetching packages...
warning "oclif > yeoman-environment@3.15.1" has unmet peer dependency "mem-fs@^1.2.0 || ^2.0.0".
Installing plugin https://github.com/oclif/plugin-test-esm-1... ⣟ [4/5] Linking dependencies...
warning "oclif > yeoman-environment@3.15.1" has unmet peer dependency "mem-fs@^1.2.0 || ^2.0.0".
warning "oclif > yeoman-environment@3.15.1" has unmet peer dependency "mem-fs-editor@^8.1.2 || ^9.0.0".
warning "oclif > yeoman-environment@3.15.1" has unmet peer dependency "mem-fs@^1.2.0 || ^2.0.0".
warning "oclif > yeoman-environment@3.15.1" has unmet peer dependency "mem-fs-editor@^8.1.2 || ^9.0.0".
warning "oclif > yeoman-environment@3.15.1" has unmet peer dependency "mem-fs@^1.2.0 || ^2.0.0".
Installing plugin https://github.com/oclif/plugin-test-esm-1... ⣯ [4/5] Linking dependencies...
warning "oclif > yeoman-environment@3.15.1" has unmet peer dependency "mem-fs@^1.2.0 || ^2.0.0".
Installing plugin https://github.com/oclif/plugin-test-esm-1... ⢿ [5/5] Building fresh packages...
Installing plugin https://github.com/oclif/plugin-test-esm-1... installed v0.6.0
```

```
» sf plugins --core
 ›   ModuleLoadError: [MODULE_NOT_FOUND] require failed to load
 ›   /Users/mdonnalley/.local/share/sf/node_modules/@oclif/plugin-test-esm-1/dist/hooks/init/init: Cannot find module
 ›   '/Users/mdonnalley/.local/share/sf/node_modules/@oclif/plugin-test-esm-1/dist/hooks/init/init'
 ›   Require stack:
 ›   - /Users/mdonnalley/.nvm/versions/node/v18.15.0/lib/node_modules/@salesforce/cli/node_modules/@oclif/core/lib/module-loader.js
 ›   - /Users/mdonnalley/.nvm/versions/node/v18.15.0/lib/node_modules/@salesforce/cli/node_modules/@oclif/core/lib/help/index.js
 ›   - /Users/mdonnalley/.nvm/versions/node/v18.15.0/lib/node_modules/@salesforce/cli/node_modules/@oclif/core/lib/flags.js
 ›   - /Users/mdonnalley/.nvm/versions/node/v18.15.0/lib/node_modules/@salesforce/cli/node_modules/@oclif/core/lib/cli-ux/styled/table
 ›   .js
 ›   - /Users/mdonnalley/.nvm/versions/node/v18.15.0/lib/node_modules/@salesforce/cli/node_modules/@oclif/core/lib/cli-ux/styled/index
 ›   .js
 ›   - /Users/mdonnalley/.nvm/versions/node/v18.15.0/lib/node_modules/@salesforce/cli/node_modules/@oclif/core/lib/cli-ux/index.js
 ›   - /Users/mdonnalley/.nvm/versions/node/v18.15.0/lib/node_modules/@salesforce/cli/node_modules/@oclif/core/lib/index.js
 ›   Code: MODULE_NOT_FOUND
```

## Workarounds
- Once you're in this state, you cannot uninstall the plugin using `plugins uninstall` - instead, you need to manually remove the plugin from `~/.local/share/<CLI>/package.json`

Pull request:
fix: handle ModuleLoadError from hooks
- Never allow `MODULE_NOT_FOUND` error from hook to exit the process
- Allow user plugins from github urls to be auto-transpiled in production (CJS only)

QA
(from `salesforcecli/cli` repo)
- yarn link `@oclif/core`
- `bin/run.js plugins install https://github.com/oclif/plugin-test-esm-1.git`
- `bin/run.js plugins`
- `bin/run.js plugins install https://github.com/oclif/plugin-test-cjs-1.git`
- `bin/run.js plugins`

You should see no errors.
You should also see the hook text being output by the init hook from `@oclif/plugin-test-cjs-1`
You should **not** see the hook text being output by the init hook from `@oclif/plugin-test-esm-1` since we can't auto-transpile ESM plugins in production. This will be fixed by this [commit](https://github.com/oclif/plugin-plugins/pull/683/commits/88e9d021ad34128bc97bf8b6881739543aca99c7) in plugin-plugins

Fixes #835 

@W-14317382@

Patch:
diff --git a/src/config/config.ts b/src/config/config.ts
index a3fcef2..78df047 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -528,7 +528,17 @@ export class Config implements IConfig {
         } catch (error: any) {
           final.failures.push({error: error as Error, plugin: p})
           debug(error)
-          if (!captureErrors && error.oclif?.exit !== undefined) throw error
+          // Do not throw the error if
+          // captureErrors is set to true
+          // error.oclif.exit is undefined or 0
+          // error.code is MODULE_NOT_FOUND
+          if (
+            !captureErrors &&
+            error.oclif?.exit !== undefined &&
+            error.oclif?.exit !== 0 &&
+            error.code !== 'MODULE_NOT_FOUND'
+          )
+            throw error
         }
 
         marker?.addDetails({
diff --git a/src/config/plugin-loader.ts b/src/config/plugin-loader.ts
index 0fcc8a1..ea46638 100644
--- a/src/config/plugin-loader.ts
+++ b/src/config/plugin-loader.ts
@@ -94,7 +94,7 @@ export default class PluginLoader {
   private async loadPlugins(
     root: string,
     type: string,
-    plugins: ({name?: string; root?: string; tag?: string} | string)[],
+    plugins: ({name?: string; root?: string; tag?: string; url?: string} | string)[],
     parent?: Plugin.Plugin,
   ): Promise<void> {
     if (!plugins || plugins.length === 0) return
@@ -112,6 +112,7 @@ export default class PluginLoader {
           if (typeof plugin !== 'string') {
             opts.tag = plugin.tag || opts.tag
             opts.root = plugin.root || opts.root
+            opts.url = plugin.url
           }
 
           if (parent) {
diff --git a/src/config/ts-node.ts b/src/config/ts-node.ts
index 7a03283..b24ce1d 100644
--- a/src/config/ts-node.ts
+++ b/src/config/ts-node.ts
@@ -201,7 +201,7 @@ function determinePath(root: string, orig: string): string {
 export function tsPath(root: string, orig: string, plugin: Plugin): string
 export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): string | undefined
 export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): string | undefined {
-  const rootPlugin = Cache.getInstance().get('rootPlugin')
+  const rootPlugin = plugin?.options.isRoot ? plugin : Cache.getInstance().get('rootPlugin')
 
   if (!orig) return orig
   orig = orig.startsWith(root) ? orig : join(root, orig)
@@ -223,10 +223,16 @@ export function tsPath(root: string, orig: string | undefined, plugin?: Plugin):
       memoizedWarn(
         `${plugin?.name} is a linked ESM module and cannot be auto-transpiled. Existing compiled source will be used instead.`,
       )
+
+    if (plugin?.options.url)
+      memoizedWarn(
+        `${plugin?.name} is an ESM module installed from github and cannot be auto-transpiled. Existing compiled source will be used instead.`,
+      )
     return orig
   }
 
-  if (settings.tsnodeEnabled === undefined && isProduction && plugin?.type !== 'link') {
+  // Do not skip ts-node registration if the plugin is linked or installed from github
+  if (settings.tsnodeEnabled === undefined && isProduction && plugin?.type !== 'link' && !plugin?.options.url) {
     debug(`Skipping ts-node registration for ${root} because NODE_ENV is NOT "test" or "development"`)
     return orig
   }
diff --git a/src/interfaces/plugin.ts b/src/interfaces/plugin.ts
index d374e0f..7eac9cf 100644
--- a/src/interfaces/plugin.ts
+++ b/src/interfaces/plugin.ts
@@ -14,6 +14,7 @@ export interface PluginOptions {
   root: string
   tag?: string
   type?: string
+  url?: string
 }
 
 export interface Options extends PluginOptions {
@@ -56,6 +57,7 @@ export interface Plugin {
    * name from package.json
    */
   name: string
+  readonly options: Options
   /**
    * full package.json
    *


Test patch:
diff --git a/test/config/config.flexible.test.ts b/test/config/config.flexible.test.ts
index 2d95ea6..b68162c 100644
--- a/test/config/config.flexible.test.ts
+++ b/test/config/config.flexible.test.ts
@@ -104,6 +104,7 @@ describe('Config with flexible taxonomy', () => {
       moduleType: 'commonjs',
       hasManifest: false,
       isRoot: false,
+      options: {root: ''},
     }
 
     const pluginB: IPlugin = {
@@ -125,6 +126,7 @@ describe('Config with flexible taxonomy', () => {
       moduleType: 'commonjs',
       hasManifest: false,
       isRoot: false,
+      options: {root: ''},
     }
     const plugins = new Map().set(pluginA.name, pluginA).set(pluginB.name, pluginB)
 
diff --git a/test/config/config.test.ts b/test/config/config.test.ts
index 82f014a..1fd8752 100644
--- a/test/config/config.test.ts
+++ b/test/config/config.test.ts
@@ -293,6 +293,7 @@ describe('Config', () => {
         moduleType: 'commonjs',
         hasManifest: false,
         isRoot: false,
+        options: {root: ''},
       }
 
       const pluginB: IPlugin = {
@@ -314,6 +315,7 @@ describe('Config', () => {
         moduleType: 'commonjs',
         hasManifest: false,
         isRoot: false,
+        options: {root: ''},
       }
       const plugins = new Map().set(pluginA.name, pluginA).set(pluginB.name, pluginB)
       let test = fancy