Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command } from 'commander';
import { spawn, execSync } from 'node:child_process';
import { spawn } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
Expand All @@ -22,7 +22,8 @@ import {
import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
import { OPENSPEC_DIR_NAME } from '../core/config.js';
import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
import { isPromptCancellationError } from './shared-output.js';
import { UpdateCommand } from '../core/update.js';
import { asErrorMessage, isPromptCancellationError } from './shared-output.js';

type ProfileAction = 'both' | 'delivery' | 'workflows' | 'keep';

Expand Down Expand Up @@ -621,10 +622,11 @@ export function registerConfigCommand(program: Command): void {

if (applyNow) {
try {
execSync('npx openspec update', { stdio: 'inherit', cwd: projectDir });
await new UpdateCommand().execute(projectDir);
console.log('Run `openspec update` in your other projects to apply.');
} catch {
console.error('`openspec update` failed. Please run it manually to apply the profile changes.');
} catch (error) {
console.error(`\`openspec update\` failed: ${asErrorMessage(error)}`);
console.error('Please run it manually to apply the profile changes.');
process.exitCode = 1;
}
return;
Expand Down
49 changes: 34 additions & 15 deletions test/commands/config-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,6 @@ import { Command } from 'commander';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { execSync } from 'node:child_process';

vi.mock('node:child_process', async () => {
const actual = await vi.importActual<typeof import('node:child_process')>('node:child_process');
return {
...actual,
execSync: vi.fn(),
};
});

vi.mock('@inquirer/prompts', () => ({
select: vi.fn(),
Expand Down Expand Up @@ -150,10 +141,10 @@ describe('config profile interactive flow', () => {

consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.mocked(execSync).mockReset();
});

afterEach(() => {
vi.unstubAllEnvs();
process.env = originalEnv;
process.chdir(originalCwd);
(process.stdout as NodeJS.WriteStream & { isTTY?: boolean }).isTTY = originalTTY;
Expand Down Expand Up @@ -377,12 +368,15 @@ describe('config profile interactive flow', () => {
});
});

it('confirmed project apply should run openspec update in the project', async () => {
it('confirmed project apply should update in process without resolving openspec from PATH', async () => {
const { saveGlobalConfig, getGlobalConfig } = await import('../../src/core/global-config.js');
const { select, confirm } = await getPromptMocks();

saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'both', workflows: ['propose', 'explore', 'apply', 'update', 'sync', 'archive'] });
fs.mkdirSync(path.join(tempDir, 'openspec'), { recursive: true });
const emptyBinDir = path.join(tempDir, 'empty-bin');
fs.mkdirSync(emptyBinDir);
vi.stubEnv('PATH', emptyBinDir);

select.mockResolvedValueOnce('delivery');
select.mockResolvedValueOnce('skills');
Expand All @@ -391,10 +385,35 @@ describe('config profile interactive flow', () => {
await runConfigCommand(['profile']);

expect(getGlobalConfig().delivery).toBe('skills');
expect(execSync).toHaveBeenCalledWith('npx openspec update', {
stdio: 'inherit',
cwd: fs.realpathSync(tempDir),
});
expect(process.exitCode).toBeUndefined();
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(consoleLogSpy).toHaveBeenCalledWith('No configured tools found.');
expect(consoleLogSpy).toHaveBeenCalledWith('Run `openspec update` in your other projects to apply.');
});

it('confirmed project apply should report the update failure reason', async () => {
const { saveGlobalConfig } = await import('../../src/core/global-config.js');
const { UpdateCommand } = await import('../../src/core/update.js');
const { select, confirm } = await getPromptMocks();

saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'both', workflows: ['propose', 'explore', 'apply', 'update', 'sync', 'archive'] });
fs.mkdirSync(path.join(tempDir, 'openspec'), { recursive: true });
const executeSpy = vi.spyOn(UpdateCommand.prototype, 'execute')
.mockRejectedValueOnce(new Error('permission denied'));

select.mockResolvedValueOnce('delivery');
select.mockResolvedValueOnce('skills');
confirm.mockResolvedValueOnce(true);

try {
await runConfigCommand(['profile']);
} finally {
executeSpy.mockRestore();
}

expect(consoleErrorSpy).toHaveBeenCalledWith('`openspec update` failed: permission denied');
expect(consoleErrorSpy).toHaveBeenCalledWith('Please run it manually to apply the profile changes.');
expect(process.exitCode).toBe(1);
});

it('core preset should preserve delivery setting', async () => {
Expand Down
Loading