Skip to content

Commit e073680

Browse files
authored
refactor(setup): simplify config creation and fix test hanging (Fission-AI#537)
* refactor(setup): simplify config creation and fix test hanging - Replace interactive config prompts with automatic config creation using default schema. The generated config includes helpful comments explaining context and rules options. - Remove unused promptForConfig, promptForArtifactRules, and isExitPromptError functions from config-prompts.ts - Add forceExit: true to vitest config to prevent worker processes from hanging after tests complete * docs: add schema-alias-support change proposal Proposal to add schema alias support so `openspec-default` and `spec-driven` can be used interchangeably, enabling a rename without breaking existing configs. * fix(test): remove invalid forceExit config and add proper teardown - Remove `forceExit: true` from vitest.config.ts (Jest option, not Vitest) - Add actual teardown logic in vitest.setup.ts that forces exit after 1s grace period if processes are still hanging
1 parent fdb05a7 commit e073680

5 files changed

Lines changed: 96 additions & 275 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-01-20
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Why
2+
3+
We want to rename `spec-driven` to `openspec-default` to better reflect that it's the standard/default workflow. However, renaming directly would break existing projects that have `schema: spec-driven` in their `openspec/config.yaml`. Adding alias support allows both names to work interchangeably, enabling a smooth transition with no breaking changes.
4+
5+
## What Changes
6+
7+
- Add schema alias resolution in the schema resolver
8+
- `openspec-default` and `spec-driven` will both resolve to the same schema
9+
- The physical directory remains `schemas/spec-driven/` (or could be renamed to `schemas/openspec-default/` with `spec-driven` as the alias)
10+
- All CLI commands and config files accept either name
11+
- No changes required to existing user configs
12+
13+
## Capabilities
14+
15+
### New Capabilities
16+
17+
- `schema-aliases`: Support for schema name aliases so multiple names can resolve to the same schema directory
18+
19+
### Modified Capabilities
20+
21+
<!-- No existing spec-level behavior is changing - this is purely additive -->
22+
23+
## Impact
24+
25+
- `src/core/artifact-graph/resolver.ts` - Add alias resolution logic
26+
- `schemas/` directory - Potentially rename `spec-driven` to `openspec-default`
27+
- Documentation - Update to prefer `openspec-default` while noting `spec-driven` still works
28+
- Default schema constants - Update `DEFAULT_SCHEMA` to `openspec-default`

src/commands/artifact-workflow.ts

Lines changed: 31 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
import { createChange, validateChangeName } from '../utils/change-utils.js';
3131
import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate } from '../core/templates/skill-templates.js';
3232
import { FileSystemUtils } from '../utils/file-system.js';
33-
import { promptForConfig, serializeConfig, isExitPromptError } from '../core/config-prompts.js';
33+
import { serializeConfig } from '../core/config-prompts.js';
3434
import { readProjectConfig } from '../core/project-config.js';
3535

3636
// -----------------------------------------------------------------------------
@@ -945,89 +945,37 @@ ${template.content}
945945
console.log(chalk.dim(' schema: spec-driven'));
946946
console.log();
947947
} else {
948-
// Prompt for config creation
948+
// Create config with default schema
949+
const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
950+
949951
try {
950-
const configResult = await promptForConfig(projectRoot);
951-
952-
if (configResult.createConfig && configResult.schema) {
953-
// Build config object
954-
const config = {
955-
schema: configResult.schema,
956-
context: configResult.context,
957-
rules: configResult.rules,
958-
};
959-
960-
// Serialize to YAML
961-
const yamlContent = serializeConfig(config);
962-
963-
// Write config file
964-
try {
965-
await FileSystemUtils.writeFile(configPath, yamlContent);
966-
967-
console.log();
968-
console.log(chalk.green('✓ Created openspec/config.yaml'));
969-
console.log();
970-
console.log('━'.repeat(70));
971-
console.log();
972-
console.log(chalk.bold('📖 Config created at: openspec/config.yaml'));
973-
974-
// Display summary
975-
const contextLines = config.context ? config.context.split('\n').length : 0;
976-
const rulesCount = config.rules ? Object.keys(config.rules).length : 0;
977-
978-
console.log(` • Default schema: ${chalk.cyan(config.schema)}`);
979-
if (contextLines > 0) {
980-
console.log(` • Project context: ${chalk.cyan(`Added (${contextLines} lines)`)}`);
981-
}
982-
if (rulesCount > 0) {
983-
console.log(` • Rules: ${chalk.cyan(`${rulesCount} artifact${rulesCount > 1 ? 's' : ''} configured`)}`);
984-
}
985-
console.log();
986-
987-
// Usage examples
988-
console.log(chalk.bold('Usage:'));
989-
console.log(' • New changes automatically use this schema');
990-
console.log(' • Context injected into all artifact instructions');
991-
console.log(' • Rules applied to matching artifacts');
992-
console.log();
993-
994-
// Git commit suggestion
995-
console.log(chalk.bold('To share with team:'));
996-
console.log(chalk.dim(' git add openspec/config.yaml .claude/'));
997-
console.log(chalk.dim(' git commit -m "Setup OpenSpec experimental workflow with project config"'));
998-
console.log();
999-
} catch (writeError) {
1000-
// Handle file write errors
1001-
console.error();
1002-
console.error(chalk.red('✗ Failed to write openspec/config.yaml'));
1003-
console.error(chalk.dim(` ${(writeError as Error).message}`));
1004-
console.error();
1005-
console.error('Fallback: Create config manually:');
1006-
console.error(chalk.dim(' 1. Create openspec/config.yaml'));
1007-
console.error(chalk.dim(' 2. Copy the following content:'));
1008-
console.error();
1009-
console.error(chalk.dim(yamlContent));
1010-
console.error();
1011-
}
1012-
} else {
1013-
// User chose not to create config
1014-
console.log();
1015-
console.log(chalk.blue('ℹ️ Skipped config creation.'));
1016-
console.log(' You can create openspec/config.yaml manually later.');
1017-
console.log();
1018-
}
1019-
} catch (promptError) {
1020-
if (isExitPromptError(promptError)) {
1021-
// User cancelled (Ctrl+C)
1022-
console.log();
1023-
console.log(chalk.blue('ℹ️ Config creation cancelled'));
1024-
console.log(' Skills and commands already created');
1025-
console.log(' Run setup again to create config later');
1026-
console.log();
1027-
} else {
1028-
// Unexpected error
1029-
throw promptError;
1030-
}
952+
await FileSystemUtils.writeFile(configPath, yamlContent);
953+
954+
console.log();
955+
console.log(chalk.green('✓ Created openspec/config.yaml'));
956+
console.log();
957+
console.log(` Default schema: ${chalk.cyan(DEFAULT_SCHEMA)}`);
958+
console.log();
959+
console.log(chalk.dim(' Edit the file to add project context and per-artifact rules.'));
960+
console.log();
961+
962+
// Git commit suggestion
963+
console.log(chalk.bold('To share with team:'));
964+
console.log(chalk.dim(' git add openspec/config.yaml .claude/'));
965+
console.log(chalk.dim(' git commit -m "Setup OpenSpec experimental workflow"'));
966+
console.log();
967+
} catch (writeError) {
968+
// Handle file write errors
969+
console.error();
970+
console.error(chalk.red('✗ Failed to write openspec/config.yaml'));
971+
console.error(chalk.dim(` ${(writeError as Error).message}`));
972+
console.error();
973+
console.error('Fallback: Create config manually:');
974+
console.error(chalk.dim(' 1. Create openspec/config.yaml'));
975+
console.error(chalk.dim(' 2. Copy the following content:'));
976+
console.error();
977+
console.error(chalk.dim(yamlContent));
978+
console.error();
1031979
}
1032980
}
1033981

src/core/config-prompts.ts

Lines changed: 30 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,39 @@
1-
import { stringify as stringifyYaml } from 'yaml';
2-
import { listSchemasWithInfo, resolveSchema } from './artifact-graph/resolver.js';
31
import type { ProjectConfig } from './project-config.js';
42

53
/**
6-
* Check if an error is an ExitPromptError (user cancelled with Ctrl+C).
7-
* Used instead of instanceof check since @inquirer modules use dynamic imports.
8-
*/
9-
export function isExitPromptError(error: unknown): boolean {
10-
return (
11-
error !== null &&
12-
typeof error === 'object' &&
13-
'name' in error &&
14-
(error as { name: string }).name === 'ExitPromptError'
15-
);
16-
}
17-
18-
/**
19-
* Result of interactive config creation prompts.
20-
*/
21-
export interface ConfigPromptResult {
22-
/** Whether to create config file */
23-
createConfig: boolean;
24-
/** Selected schema name */
25-
schema?: string;
26-
/** Project context (optional) */
27-
context?: string;
28-
/** Per-artifact rules (optional) */
29-
rules?: Record<string, string[]>;
30-
}
31-
32-
/**
33-
* Prompt user to create project config interactively.
34-
* Used by experimental setup command.
35-
*
36-
* @param projectRoot - Optional project root for project-local schema resolution
37-
* @returns Config prompt result
38-
* @throws ExitPromptError if user cancels (Ctrl+C)
39-
*/
40-
export async function promptForConfig(
41-
projectRoot?: string
42-
): Promise<ConfigPromptResult> {
43-
// Dynamic imports to prevent pre-commit hook hangs (see #367)
44-
const { confirm, select, editor, checkbox } = await import('@inquirer/prompts');
45-
46-
// Ask if user wants to create config
47-
const shouldCreate = await confirm({
48-
message: 'Create openspec/config.yaml?',
49-
default: true,
50-
});
51-
52-
if (!shouldCreate) {
53-
return { createConfig: false };
54-
}
55-
56-
// Get available schemas
57-
const schemas = listSchemasWithInfo(projectRoot);
58-
59-
if (schemas.length === 0) {
60-
throw new Error('No schemas found. Cannot create config.');
61-
}
62-
63-
// Prompt for schema selection
64-
const selectedSchema = await select({
65-
message: 'Default schema for new changes?',
66-
choices: schemas.map((s) => ({
67-
name: `${s.name} (${s.artifacts.join(' → ')})`,
68-
value: s.name,
69-
description: s.description || undefined,
70-
})),
71-
});
72-
73-
// Prompt for project context
74-
console.log('\nAdd project context? (optional)');
75-
console.log('Context is shown to AI when creating artifacts.');
76-
console.log('Examples: tech stack, conventions, style guides, domain knowledge\n');
77-
78-
const contextInput = await editor({
79-
message: 'Press Enter to skip, or edit context:',
80-
default: '',
81-
waitForUseInput: false,
82-
});
83-
84-
const context = contextInput.trim() || undefined;
85-
86-
// Prompt for per-artifact rules
87-
const addRules = await confirm({
88-
message: 'Add per-artifact rules? (optional)',
89-
default: false,
90-
});
91-
92-
let rules: Record<string, string[]> | undefined;
93-
94-
if (addRules) {
95-
// Load the selected schema to get artifact list
96-
const schema = resolveSchema(selectedSchema, projectRoot);
97-
const artifactIds = schema.artifacts.map((a) => a.id);
98-
99-
// Let user select which artifacts to add rules for
100-
const selectedArtifacts = await checkbox({
101-
message: 'Which artifacts should have custom rules?',
102-
choices: artifactIds.map((id) => ({
103-
name: id,
104-
value: id,
105-
})),
106-
});
107-
108-
if (selectedArtifacts.length > 0) {
109-
rules = {};
110-
111-
// For each selected artifact, collect rules line by line
112-
for (const artifactId of selectedArtifacts) {
113-
const artifactRules = await promptForArtifactRules(artifactId);
114-
if (artifactRules.length > 0) {
115-
rules[artifactId] = artifactRules;
116-
}
117-
}
118-
119-
// If no rules were actually added, set to undefined
120-
if (Object.keys(rules).length === 0) {
121-
rules = undefined;
122-
}
123-
}
124-
}
125-
126-
return {
127-
createConfig: true,
128-
schema: selectedSchema,
129-
context,
130-
rules,
131-
};
132-
}
133-
134-
/**
135-
* Prompt for rules for a specific artifact.
136-
* Collects rules one per line until user enters empty line.
137-
*
138-
* @param artifactId - The artifact ID to collect rules for
139-
* @returns Array of rules
140-
*/
141-
async function promptForArtifactRules(artifactId: string): Promise<string[]> {
142-
// Dynamic import to prevent pre-commit hook hangs (see #367)
143-
const { input } = await import('@inquirer/prompts');
144-
145-
const rules: string[] = [];
146-
147-
console.log(`\nRules for ${artifactId} artifact:`);
148-
console.log('Enter rules one per line, press Enter on empty line to finish:\n');
149-
150-
while (true) {
151-
const rule = await input({
152-
message: '│',
153-
validate: () => {
154-
// Empty string is valid (signals end of input)
155-
return true;
156-
},
157-
});
158-
159-
const trimmed = rule.trim();
160-
161-
// Empty line signals end of input
162-
if (!trimmed) {
163-
break;
164-
}
165-
166-
rules.push(trimmed);
167-
}
168-
169-
return rules;
170-
}
171-
172-
/**
173-
* Serialize config to YAML string with proper multi-line formatting.
4+
* Serialize config to YAML string with helpful comments.
1745
*
1756
* @param config - Partial config object (schema required, context/rules optional)
1767
* @returns YAML string ready to write to file
1778
*/
1789
export function serializeConfig(config: Partial<ProjectConfig>): string {
179-
// Build clean config object (only include defined fields)
180-
const cleanConfig: Record<string, unknown> = {
181-
schema: config.schema,
182-
};
183-
184-
if (config.context) {
185-
cleanConfig.context = config.context;
186-
}
187-
188-
if (config.rules && Object.keys(config.rules).length > 0) {
189-
cleanConfig.rules = config.rules;
190-
}
191-
192-
// Serialize to YAML with proper formatting
193-
return stringifyYaml(cleanConfig, {
194-
indent: 2,
195-
lineWidth: 0, // Don't wrap long lines
196-
defaultStringType: 'PLAIN',
197-
defaultKeyType: 'PLAIN',
198-
});
10+
const lines: string[] = [];
11+
12+
// Schema (required)
13+
lines.push(`schema: ${config.schema}`);
14+
lines.push('');
15+
16+
// Context section with comments
17+
lines.push('# Project context (optional)');
18+
lines.push('# This is shown to AI when creating artifacts.');
19+
lines.push('# Add your tech stack, conventions, style guides, domain knowledge, etc.');
20+
lines.push('# Example:');
21+
lines.push('# context: |');
22+
lines.push('# Tech stack: TypeScript, React, Node.js');
23+
lines.push('# We use conventional commits');
24+
lines.push('# Domain: e-commerce platform');
25+
lines.push('');
26+
27+
// Rules section with comments
28+
lines.push('# Per-artifact rules (optional)');
29+
lines.push('# Add custom rules for specific artifacts.');
30+
lines.push('# Example:');
31+
lines.push('# rules:');
32+
lines.push('# proposal:');
33+
lines.push('# - Keep proposals under 500 words');
34+
lines.push('# - Always include a "Non-goals" section');
35+
lines.push('# tasks:');
36+
lines.push('# - Break tasks into chunks of max 2 hours');
37+
38+
return lines.join('\n') + '\n';
19939
}

0 commit comments

Comments
 (0)