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
16 changes: 8 additions & 8 deletions src/commands/workflow/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function instructionsCommand(
artifactId: string | undefined,
options: InstructionsOptions
): Promise<void> {
const spinner = ora('Generating instructions...').start();
const spinner = options.json ? undefined : ora('Generating instructions...').start();

try {
const projectRoot = process.cwd();
Expand All @@ -60,7 +60,7 @@ export async function instructionsCommand(
const context = loadChangeContext(projectRoot, changeName, options.schema);

if (!artifactId) {
spinner.stop();
spinner?.stop();
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
throw new Error(
`Missing required argument <artifact>. Valid artifacts:\n ${validIds.join('\n ')}`
Expand All @@ -70,7 +70,7 @@ export async function instructionsCommand(
const artifact = context.graph.getArtifact(artifactId);

if (!artifact) {
spinner.stop();
spinner?.stop();
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
throw new Error(
`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`
Expand All @@ -80,7 +80,7 @@ export async function instructionsCommand(
const instructions = generateInstructions(context, artifactId, projectRoot);
const isBlocked = instructions.dependencies.some((d) => !d.done);

spinner.stop();
spinner?.stop();

if (options.json) {
console.log(JSON.stringify(instructions, null, 2));
Expand All @@ -89,7 +89,7 @@ export async function instructionsCommand(

printInstructionsText(instructions, isBlocked);
} catch (error) {
spinner.stop();
spinner?.stop();
throw error;
}
}
Expand Down Expand Up @@ -400,7 +400,7 @@ export async function generateApplyInstructions(
}

export async function applyInstructionsCommand(options: ApplyInstructionsOptions): Promise<void> {
const spinner = ora('Generating apply instructions...').start();
const spinner = options.json ? undefined : ora('Generating apply instructions...').start();

try {
const projectRoot = process.cwd();
Expand All @@ -414,7 +414,7 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions
// generateApplyInstructions uses loadChangeContext which auto-detects schema
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema);

spinner.stop();
spinner?.stop();

if (options.json) {
console.log(JSON.stringify(instructions, null, 2));
Expand All @@ -423,7 +423,7 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions

printApplyInstructionsText(instructions);
} catch (error) {
spinner.stop();
spinner?.stop();
throw error;
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/commands/workflow/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface StatusOptions {
// -----------------------------------------------------------------------------

export async function statusCommand(options: StatusOptions): Promise<void> {
const spinner = ora('Loading change status...').start();
const spinner = options.json ? undefined : ora('Loading change status...').start();

try {
const projectRoot = process.cwd();
Expand All @@ -44,7 +44,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
if (!options.change) {
const available = await getAvailableChanges(projectRoot);
if (available.length === 0) {
spinner.stop();
spinner?.stop();
if (options.json) {
console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
return;
Expand All @@ -53,7 +53,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
return;
}
// Changes exist but --change not provided
spinner.stop();
spinner?.stop();
throw new Error(
`Missing required option --change. Available changes:\n ${available.join('\n ')}`
);
Expand All @@ -70,7 +70,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
const context = loadChangeContext(projectRoot, changeName, options.schema);
const status = formatChangeStatus(context);

spinner.stop();
spinner?.stop();

if (options.json) {
console.log(JSON.stringify(status, null, 2));
Expand All @@ -79,7 +79,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {

printStatusText(status);
} catch (error) {
spinner.stop();
spinner?.stop();
throw error;
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/commands/workflow/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface TemplateInfo {
// -----------------------------------------------------------------------------

export async function templatesCommand(options: TemplatesOptions): Promise<void> {
const spinner = ora('Loading templates...').start();
const spinner = options.json ? undefined : ora('Loading templates...').start();

try {
const projectRoot = process.cwd();
Expand Down Expand Up @@ -72,7 +72,7 @@ export async function templatesCommand(options: TemplatesOptions): Promise<void>
source,
}));

spinner.stop();
spinner?.stop();

if (options.json) {
const output: Record<string, { path: string; source: string }> = {};
Expand All @@ -92,7 +92,7 @@ export async function templatesCommand(options: TemplatesOptions): Promise<void>
console.log(` ${t.templatePath}`);
}
} catch (error) {
spinner.stop();
spinner?.stop();
throw error;
}
}
46 changes: 46 additions & 0 deletions test/cli-e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ async function prepareFixture(fixtureName: string): Promise<string> {
return projectDir;
}

function expectJsonOnlyOutput(result: Awaited<ReturnType<typeof runCLI>>) {
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe('');
expect(() => JSON.parse(result.stdout)).not.toThrow();
}

afterAll(async () => {
await Promise.all(tempRoots.map((dir) => fs.rm(dir, { recursive: true, force: true })));
});
Expand Down Expand Up @@ -71,6 +77,46 @@ describe('openspec CLI e2e basics', () => {
expect(json.items.some((item: any) => item.id === 'c1' && item.type === 'change')).toBe(true);
});

it('keeps list --json free of spinner output', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['list', '--json'], { cwd: projectDir });
expectJsonOnlyOutput(result);
});

it('keeps schemas --json free of spinner output', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['schemas', '--json'], { cwd: projectDir });
expectJsonOnlyOutput(result);
});

it('keeps status --json free of spinner output', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['status', '--change', 'c1', '--json'], { cwd: projectDir });
expectJsonOnlyOutput(result);
});

it('keeps instructions --json free of spinner output', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['instructions', 'proposal', '--change', 'c1', '--json'], {
cwd: projectDir,
});
expectJsonOnlyOutput(result);
});

it('keeps instructions apply --json free of spinner output', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['instructions', 'apply', '--change', 'c1', '--json'], {
cwd: projectDir,
});
expectJsonOnlyOutput(result);
});

it('keeps templates --json free of spinner output', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['templates', '--json'], { cwd: projectDir });
expectJsonOnlyOutput(result);
});

it('returns an error for unknown items in the fixture', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['validate', 'does-not-exist'], { cwd: projectDir });
Expand Down
4 changes: 4 additions & 0 deletions test/commands/artifact-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ describe('artifact-workflow CLI commands', () => {
cwd: tempDir,
});
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe('');

const json = JSON.parse(result.stdout);
expect(json.changeName).toBe('json-change');
Expand Down Expand Up @@ -256,6 +257,7 @@ describe('artifact-workflow CLI commands', () => {
cwd: tempDir,
});
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe('');

const json = JSON.parse(result.stdout);
expect(json.artifactId).toBe('design');
Expand Down Expand Up @@ -309,6 +311,7 @@ describe('artifact-workflow CLI commands', () => {
it('outputs JSON mapping of templates', async () => {
const result = await runCLI(['templates', '--json'], { cwd: tempDir });
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe('');

const json = JSON.parse(result.stdout);
expect(json.proposal).toBeDefined();
Expand Down Expand Up @@ -405,6 +408,7 @@ describe('artifact-workflow CLI commands', () => {
{ cwd: tempDir }
);
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe('');

const json = JSON.parse(result.stdout);
expect(json.changeName).toBe('json-apply');
Expand Down
Loading