From 130c2836b34a8a3296805be507cf8e6a4a6e92cc Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:04:29 +0800 Subject: [PATCH 1/8] fix: normalize oclif v2 plugin hooks for v4 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the global aio-cli (oclif v2) runs commands from this plugin (oclif v4), v4's Config.load re-uses the v2 Plugin objects as-is. Those objects store hooks as plain string arrays, but v4's runHook expects {identifier, target} objects — causing hook.target to be undefined and crashing path.extname() with ERR_INVALID_ARG_TYPE. Normalize all plugin hooks to v4 format once in BaseCommand.init() so every command that calls this.config.runHook works correctly. Co-Authored-By: Claude Sonnet 4.6 --- src/BaseCommand.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/BaseCommand.js b/src/BaseCommand.js index 54a8326c..7af6ec00 100644 --- a/src/BaseCommand.js +++ b/src/BaseCommand.js @@ -40,6 +40,17 @@ class BaseCommand extends Command { async init () { await super.init() + // Normalize hooks from plugins loaded by oclif v2 into this v4 Config. + // oclif v2 stores hooks as string arrays; v4 expects {identifier, target} objects. + for (const plugin of this.config.getPluginsList()) { + if (!plugin.hooks) continue + for (const [event, hooks] of Object.entries(plugin.hooks)) { + plugin.hooks[event] = (Array.isArray(hooks) ? hooks : [hooks]).map(h => + typeof h === 'string' ? { identifier: 'default', target: h } : h + ) + } + } + // setup a prompt that outputs to stderr this.prompt = inquirer.createPromptModule({ output: process.stderr }) From a9be07afafac07154cf1a914eb1ab34f4f702707 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:11:46 +0800 Subject: [PATCH 2/8] test: add coverage for oclif v2 hook normalization in BaseCommand.init Co-Authored-By: Claude Sonnet 4.6 --- test/BaseCommand.test.js | 27 +++++++++++++++++++++++++++ test/jest.setup.js | 1 + 2 files changed, 28 insertions(+) diff --git a/test/BaseCommand.test.js b/test/BaseCommand.test.js index a1607731..734b22c0 100644 --- a/test/BaseCommand.test.js +++ b/test/BaseCommand.test.js @@ -288,6 +288,33 @@ test('init', async () => { expect(inquirer.createPromptModule).toHaveBeenCalledWith({ output: process.stderr }) }) +test('init normalizes oclif v2 string hooks to v4 object format', async () => { + const cmd = new TheCommand([]) + const plugin = { + hooks: { + 'pre-deploy-event-reg': ['./src/hooks/pre-deploy-event-reg.js'], + 'post-deploy-event-reg': './src/hooks/post-deploy-event-reg.js', + 'already-v4': [{ identifier: 'default', target: './src/hooks/foo.js' }] + } + } + cmd.config = global.createOclifMockConfig({ + getPluginsList: jest.fn().mockReturnValue([plugin]) + }) + await cmd.init() + expect(plugin.hooks['pre-deploy-event-reg']).toEqual([{ identifier: 'default', target: './src/hooks/pre-deploy-event-reg.js' }]) + expect(plugin.hooks['post-deploy-event-reg']).toEqual([{ identifier: 'default', target: './src/hooks/post-deploy-event-reg.js' }]) + expect(plugin.hooks['already-v4']).toEqual([{ identifier: 'default', target: './src/hooks/foo.js' }]) +}) + +test('init skips plugins with no hooks', async () => { + const cmd = new TheCommand([]) + const plugin = { name: 'no-hooks-plugin' } + cmd.config = global.createOclifMockConfig({ + getPluginsList: jest.fn().mockReturnValue([plugin]) + }) + await expect(cmd.init()).resolves.not.toThrow() +}) + test('catch', async () => { const cmd = new TheCommand([]) cmd.config = global.createOclifMockConfig() diff --git a/test/jest.setup.js b/test/jest.setup.js index e0ea6df3..a2a7bf0e 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -53,6 +53,7 @@ global.createOclifMockConfig = (overrides = {}) => ({ runHook: jest.fn().mockResolvedValue({ successes: [] }), runCommand: jest.fn(), findCommand: jest.fn(), + getPluginsList: jest.fn().mockReturnValue([]), ...overrides }) From cab610ea2cb0b6263dc0edcd51bb1f2633e3d9c8 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:19:07 +0800 Subject: [PATCH 3/8] fix: normalize oclif v2 plugin hooks for v4 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the global aio-cli (oclif v2) runs commands from this plugin (oclif v4), v4's Config.load re-uses the v2 Plugin objects as-is. Those objects store hooks as plain string arrays, but v4's runHook expects {identifier, target} objects — causing hook.target to be undefined and crashing path.extname() with ERR_INVALID_ARG_TYPE. Normalize all plugin hooks to v4 format in BaseCommand.init(), but only when string hooks are actually present to avoid unnecessary mutation of already-normalized plugin objects. Co-Authored-By: Claude Sonnet 4.6 --- src/BaseCommand.js | 11 +++++++++-- test/BaseCommand.test.js | 19 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/BaseCommand.js b/src/BaseCommand.js index 7af6ec00..0c6100be 100644 --- a/src/BaseCommand.js +++ b/src/BaseCommand.js @@ -42,10 +42,17 @@ class BaseCommand extends Command { await super.init() // Normalize hooks from plugins loaded by oclif v2 into this v4 Config. // oclif v2 stores hooks as string arrays; v4 expects {identifier, target} objects. + // Only mutate when string hooks are present; guard against frozen plugin references. for (const plugin of this.config.getPluginsList()) { - if (!plugin.hooks) continue + if (!plugin.hooks) { + continue + } for (const [event, hooks] of Object.entries(plugin.hooks)) { - plugin.hooks[event] = (Array.isArray(hooks) ? hooks : [hooks]).map(h => + const hooksArr = Array.isArray(hooks) ? hooks : [hooks] + if (!hooksArr.some(h => typeof h === 'string')) { + continue + } + plugin.hooks[event] = hooksArr.map(h => typeof h === 'string' ? { identifier: 'default', target: h } : h ) } diff --git a/test/BaseCommand.test.js b/test/BaseCommand.test.js index 734b22c0..aa336a16 100644 --- a/test/BaseCommand.test.js +++ b/test/BaseCommand.test.js @@ -294,7 +294,8 @@ test('init normalizes oclif v2 string hooks to v4 object format', async () => { hooks: { 'pre-deploy-event-reg': ['./src/hooks/pre-deploy-event-reg.js'], 'post-deploy-event-reg': './src/hooks/post-deploy-event-reg.js', - 'already-v4': [{ identifier: 'default', target: './src/hooks/foo.js' }] + 'already-v4': [{ identifier: 'default', target: './src/hooks/foo.js' }], + 'mixed': ['./src/hooks/string.js', { identifier: 'named', target: './src/hooks/obj.js' }] } } cmd.config = global.createOclifMockConfig({ @@ -304,6 +305,10 @@ test('init normalizes oclif v2 string hooks to v4 object format', async () => { expect(plugin.hooks['pre-deploy-event-reg']).toEqual([{ identifier: 'default', target: './src/hooks/pre-deploy-event-reg.js' }]) expect(plugin.hooks['post-deploy-event-reg']).toEqual([{ identifier: 'default', target: './src/hooks/post-deploy-event-reg.js' }]) expect(plugin.hooks['already-v4']).toEqual([{ identifier: 'default', target: './src/hooks/foo.js' }]) + expect(plugin.hooks['mixed']).toEqual([ + { identifier: 'default', target: './src/hooks/string.js' }, + { identifier: 'named', target: './src/hooks/obj.js' } + ]) }) test('init skips plugins with no hooks', async () => { @@ -315,6 +320,18 @@ test('init skips plugins with no hooks', async () => { await expect(cmd.init()).resolves.not.toThrow() }) +test('init does not mutate hooks already in v4 format', async () => { + const cmd = new TheCommand([]) + const original = [{ identifier: 'default', target: './src/hooks/foo.js' }] + const plugin = { hooks: { 'some-event': original } } + cmd.config = global.createOclifMockConfig({ + getPluginsList: jest.fn().mockReturnValue([plugin]) + }) + await cmd.init() + expect(plugin.hooks['some-event']).toBe(original) // same reference, not replaced +}) + + test('catch', async () => { const cmd = new TheCommand([]) cmd.config = global.createOclifMockConfig() From 3fae47ddeb90167a789c28711fe5435db81375cb Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:23:42 +0800 Subject: [PATCH 4/8] fix: normalize oclif v2 plugin hooks for v4 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the global aio-cli (oclif v2) runs commands from this plugin (oclif v4), v4's Config.load re-uses the v2 Plugin objects as-is. Those objects store hooks as plain string arrays, but v4's runHook expects {identifier, target} objects — causing hook.target to be undefined and crashing path.extname() with ERR_INVALID_ARG_TYPE. Normalize all plugin hooks to v4 format in BaseCommand.init(), but only when string hooks are actually present to avoid unnecessary mutation of already-normalized plugin objects. Uses getPluginsList() if available, falling back to this.config.plugins Map. Co-Authored-By: Claude Sonnet 4.6 --- src/BaseCommand.js | 5 ++++- test/BaseCommand.test.js | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/BaseCommand.js b/src/BaseCommand.js index 0c6100be..46b22b8a 100644 --- a/src/BaseCommand.js +++ b/src/BaseCommand.js @@ -43,7 +43,10 @@ class BaseCommand extends Command { // Normalize hooks from plugins loaded by oclif v2 into this v4 Config. // oclif v2 stores hooks as string arrays; v4 expects {identifier, target} objects. // Only mutate when string hooks are present; guard against frozen plugin references. - for (const plugin of this.config.getPluginsList()) { + const pluginList = typeof this.config.getPluginsList === 'function' + ? this.config.getPluginsList() + : [...(this.config.plugins?.values() ?? [])] + for (const plugin of pluginList) { if (!plugin.hooks) { continue } diff --git a/test/BaseCommand.test.js b/test/BaseCommand.test.js index aa336a16..f07aaca0 100644 --- a/test/BaseCommand.test.js +++ b/test/BaseCommand.test.js @@ -331,6 +331,27 @@ test('init does not mutate hooks already in v4 format', async () => { expect(plugin.hooks['some-event']).toBe(original) // same reference, not replaced }) +test('init falls back to this.config.plugins Map when getPluginsList is unavailable', async () => { + const cmd = new TheCommand([]) + const plugin = { hooks: { 'pre-deploy-event-reg': ['./src/hooks/hook.js'] } } + const mockConfig = global.createOclifMockConfig({ + plugins: new Map([['test-plugin', plugin]]) + }) + delete mockConfig.getPluginsList + cmd.config = mockConfig + await cmd.init() + expect(plugin.hooks['pre-deploy-event-reg']).toEqual([{ identifier: 'default', target: './src/hooks/hook.js' }]) +}) + +test('init handles config with neither getPluginsList nor plugins without throwing', async () => { + const cmd = new TheCommand([]) + const mockConfig = global.createOclifMockConfig() + delete mockConfig.getPluginsList + delete mockConfig.plugins + cmd.config = mockConfig + await expect(cmd.init()).resolves.not.toThrow() +}) + test('catch', async () => { const cmd = new TheCommand([]) From a9c060db6b4d3623bd0818bdce1ab204331778e6 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:29:01 +0800 Subject: [PATCH 5/8] fix lint issues --- test/BaseCommand.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/BaseCommand.test.js b/test/BaseCommand.test.js index f07aaca0..afe6b744 100644 --- a/test/BaseCommand.test.js +++ b/test/BaseCommand.test.js @@ -295,7 +295,7 @@ test('init normalizes oclif v2 string hooks to v4 object format', async () => { 'pre-deploy-event-reg': ['./src/hooks/pre-deploy-event-reg.js'], 'post-deploy-event-reg': './src/hooks/post-deploy-event-reg.js', 'already-v4': [{ identifier: 'default', target: './src/hooks/foo.js' }], - 'mixed': ['./src/hooks/string.js', { identifier: 'named', target: './src/hooks/obj.js' }] + mixed: ['./src/hooks/string.js', { identifier: 'named', target: './src/hooks/obj.js' }] } } cmd.config = global.createOclifMockConfig({ @@ -352,7 +352,6 @@ test('init handles config with neither getPluginsList nor plugins without throwi await expect(cmd.init()).resolves.not.toThrow() }) - test('catch', async () => { const cmd = new TheCommand([]) cmd.config = global.createOclifMockConfig() From cab6c5594274a3971f30163702cba7d92a90a3fa Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:35:42 +0800 Subject: [PATCH 6/8] Update src/BaseCommand.js Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/BaseCommand.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/BaseCommand.js b/src/BaseCommand.js index 46b22b8a..051fc988 100644 --- a/src/BaseCommand.js +++ b/src/BaseCommand.js @@ -55,7 +55,13 @@ class BaseCommand extends Command { if (!hooksArr.some(h => typeof h === 'string')) { continue } - plugin.hooks[event] = hooksArr.map(h => + try { + plugin.hooks[event] = hooksArr.map(h => + typeof h === 'string' ? { identifier: 'default', target: h } : h + ) + } catch (e) { + // plugin hooks object is frozen or sealed; skip normalization for this event + } typeof h === 'string' ? { identifier: 'default', target: h } : h ) } From de859f6d830dc2fd74748e8cc485b1e9ec9c9593 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:47:07 +0800 Subject: [PATCH 7/8] Revert "Update src/BaseCommand.js" This reverts commit cab6c5594274a3971f30163702cba7d92a90a3fa. --- src/BaseCommand.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/BaseCommand.js b/src/BaseCommand.js index 051fc988..46b22b8a 100644 --- a/src/BaseCommand.js +++ b/src/BaseCommand.js @@ -55,13 +55,7 @@ class BaseCommand extends Command { if (!hooksArr.some(h => typeof h === 'string')) { continue } - try { - plugin.hooks[event] = hooksArr.map(h => - typeof h === 'string' ? { identifier: 'default', target: h } : h - ) - } catch (e) { - // plugin hooks object is frozen or sealed; skip normalization for this event - } + plugin.hooks[event] = hooksArr.map(h => typeof h === 'string' ? { identifier: 'default', target: h } : h ) } From 55857780c9cf2d4137499a05b98bcef901290054 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:50:39 +0800 Subject: [PATCH 8/8] fix: normalize oclif v2 plugin hooks for v4 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the global aio-cli (oclif v2) runs commands from this plugin (oclif v4), v4's Config.load re-uses the v2 Plugin objects as-is. Those objects store hooks as plain string arrays, but v4's runHook expects {identifier, target} objects — causing hook.target to be undefined and crashing path.extname() with ERR_INVALID_ARG_TYPE. Normalize all plugin hooks to v4 format in BaseCommand.init(), but only when string hooks are actually present to avoid unnecessary mutation of already-normalized plugin objects. Uses getPluginsList() if available, falling back to this.config.plugins Map. Wraps assignment in try/catch to handle frozen plugin hook objects. Co-Authored-By: Claude Sonnet 4.6 --- src/BaseCommand.js | 10 +++++++--- test/BaseCommand.test.js | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/BaseCommand.js b/src/BaseCommand.js index 46b22b8a..e089e067 100644 --- a/src/BaseCommand.js +++ b/src/BaseCommand.js @@ -55,9 +55,13 @@ class BaseCommand extends Command { if (!hooksArr.some(h => typeof h === 'string')) { continue } - plugin.hooks[event] = hooksArr.map(h => - typeof h === 'string' ? { identifier: 'default', target: h } : h - ) + try { + plugin.hooks[event] = hooksArr.map(h => + typeof h === 'string' ? { identifier: 'default', target: h } : h + ) + } catch { + // plugin.hooks is frozen or sealed; skip normalization for this event + } } } diff --git a/test/BaseCommand.test.js b/test/BaseCommand.test.js index afe6b744..40edd1b1 100644 --- a/test/BaseCommand.test.js +++ b/test/BaseCommand.test.js @@ -352,6 +352,17 @@ test('init handles config with neither getPluginsList nor plugins without throwi await expect(cmd.init()).resolves.not.toThrow() }) +test('init skips normalization gracefully when plugin hooks object is frozen', async () => { + const cmd = new TheCommand([]) + const plugin = { hooks: Object.freeze({ 'pre-deploy-event-reg': ['./src/hooks/hook.js'] }) } + cmd.config = global.createOclifMockConfig({ + getPluginsList: jest.fn().mockReturnValue([plugin]) + }) + await expect(cmd.init()).resolves.not.toThrow() + // hooks remain as-is since the frozen object blocked the assignment + expect(plugin.hooks['pre-deploy-event-reg']).toEqual(['./src/hooks/hook.js']) +}) + test('catch', async () => { const cmd = new TheCommand([]) cmd.config = global.createOclifMockConfig()