diff --git a/domains/integrations/openspec.ts b/domains/integrations/openspec.ts index 79b9c4ea..3fb1f82e 100644 --- a/domains/integrations/openspec.ts +++ b/domains/integrations/openspec.ts @@ -157,7 +157,6 @@ function isCommandAvailable(command: string): boolean { } async function ensureOpenSpecCli( - scope: InstallScope, projectPath: string, shouldInstall = true, ): Promise<'ready' | 'missing' | 'failed'> { @@ -168,10 +167,9 @@ async function ensureOpenSpecCli( const label = alreadyInstalled ? 'Upgrading' : 'Installing'; console.warn(` ${label} OpenSpec CLI...`); try { - const npmArgs = - scope === 'global' - ? ['install', '-g', '@fission-ai/openspec@latest'] - : ['install', '@fission-ai/openspec@latest']; + // OpenSpec is invoked as a PATH command below; keep the CLI install global + // regardless of Comet's project/global skill installation scope. + const npmArgs = ['install', '-g', '@fission-ai/openspec@latest']; execFileSync(getNpmExecutable(), npmArgs, { cwd: projectPath, stdio: 'inherit', @@ -315,7 +313,7 @@ async function installOpenSpec( shouldInstallCli = true, mirrorOpenCodePlatformIds: string[] = [], ): Promise<'installed' | 'failed' | 'skipped'> { - const cliStatus = await ensureOpenSpecCli(scope, projectPath, shouldInstallCli); + const cliStatus = await ensureOpenSpecCli(projectPath, shouldInstallCli); if (cliStatus === 'failed') { console.error( ` OpenSpec CLI not available. Install manually: npm install -g @fission-ai/openspec@latest`, diff --git a/test/domains/integrations/openspec.test.ts b/test/domains/integrations/openspec.test.ts index 47ce5829..0b8e97b4 100644 --- a/test/domains/integrations/openspec.test.ts +++ b/test/domains/integrations/openspec.test.ts @@ -102,6 +102,26 @@ describe('openspec', () => { expect(mockedExecFileSync).toHaveBeenCalledTimes(4); }); + it('installs the OpenSpec CLI globally for project scope to avoid project node_modules', async () => { + mockedExecFileSync.mockReturnValueOnce(Buffer.from('/usr/bin/openspec')); + mockedExecFileSync.mockReturnValueOnce(Buffer.from('upgraded')); + mockedExecFileSync.mockReturnValueOnce(Buffer.from('/usr/bin/openspec')); + mockedExecFileSync.mockReturnValueOnce(Buffer.from('ok')); + + const { getNpmExecutable, installOpenSpec } = + await import('../../../domains/integrations/openspec.js'); + const result = await installOpenSpec('/tmp/test', ['claude'], 'project'); + + expect(result).toBe('installed'); + const npmCall = mockedExecFileSync.mock.calls.find( + ([command, args]) => + command === getNpmExecutable() && + Array.isArray(args) && + args.includes('@fission-ai/openspec@latest'), + ); + expect(npmCall?.[1]).toEqual(['install', '-g', '@fission-ai/openspec@latest']); + }); + it('returns failed when openspec CLI is not available', async () => { mockedExecFileSync.mockImplementationOnce(() => { throw new Error('not found');