@fission-ai/openspec 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -14
- package/dist/cli/index.js +67 -2
- package/dist/commands/change.js +4 -3
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +221 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +198 -0
- package/dist/commands/show.js +3 -2
- package/dist/commands/spec.js +4 -3
- package/dist/commands/validate.js +21 -2
- package/dist/core/archive.js +4 -1
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +362 -0
- package/dist/core/completions/completion-provider.d.ts +60 -0
- package/dist/core/completions/completion-provider.js +102 -0
- package/dist/core/completions/factory.d.ts +51 -0
- package/dist/core/completions/factory.js +57 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +58 -0
- package/dist/core/completions/generators/zsh-generator.js +319 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +136 -0
- package/dist/core/completions/installers/zsh-installer.js +449 -0
- package/dist/core/completions/types.d.ts +78 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-schema.d.ts +76 -0
- package/dist/core/config-schema.js +200 -0
- package/dist/core/configurators/slash/opencode.js +0 -3
- package/dist/core/global-config.d.ts +29 -0
- package/dist/core/global-config.js +87 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +2 -1
- package/dist/utils/file-system.js +19 -3
- package/dist/utils/interactive.d.ts +12 -1
- package/dist/utils/interactive.js +7 -2
- package/dist/utils/item-discovery.d.ts +1 -0
- package/dist/utils/item-discovery.js +23 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +147 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js';
|
|
5
|
+
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
|
|
6
|
+
/**
|
|
7
|
+
* Register the config command and all its subcommands.
|
|
8
|
+
*
|
|
9
|
+
* @param program - The Commander program instance
|
|
10
|
+
*/
|
|
11
|
+
export function registerConfigCommand(program) {
|
|
12
|
+
const configCmd = program
|
|
13
|
+
.command('config')
|
|
14
|
+
.description('View and modify global OpenSpec configuration')
|
|
15
|
+
.option('--scope <scope>', 'Config scope (only "global" supported currently)')
|
|
16
|
+
.hook('preAction', (thisCommand) => {
|
|
17
|
+
const opts = thisCommand.opts();
|
|
18
|
+
if (opts.scope && opts.scope !== 'global') {
|
|
19
|
+
console.error('Error: Project-local config is not yet implemented');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
// config path
|
|
24
|
+
configCmd
|
|
25
|
+
.command('path')
|
|
26
|
+
.description('Show config file location')
|
|
27
|
+
.action(() => {
|
|
28
|
+
console.log(getGlobalConfigPath());
|
|
29
|
+
});
|
|
30
|
+
// config list
|
|
31
|
+
configCmd
|
|
32
|
+
.command('list')
|
|
33
|
+
.description('Show all current settings')
|
|
34
|
+
.option('--json', 'Output as JSON')
|
|
35
|
+
.action((options) => {
|
|
36
|
+
const config = getGlobalConfig();
|
|
37
|
+
if (options.json) {
|
|
38
|
+
console.log(JSON.stringify(config, null, 2));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(formatValueYaml(config));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// config get
|
|
45
|
+
configCmd
|
|
46
|
+
.command('get <key>')
|
|
47
|
+
.description('Get a specific value (raw, scriptable)')
|
|
48
|
+
.action((key) => {
|
|
49
|
+
const config = getGlobalConfig();
|
|
50
|
+
const value = getNestedValue(config, key);
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'object' && value !== null) {
|
|
56
|
+
console.log(JSON.stringify(value));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(String(value));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// config set
|
|
63
|
+
configCmd
|
|
64
|
+
.command('set <key> <value>')
|
|
65
|
+
.description('Set a value (auto-coerce types)')
|
|
66
|
+
.option('--string', 'Force value to be stored as string')
|
|
67
|
+
.option('--allow-unknown', 'Allow setting unknown keys')
|
|
68
|
+
.action((key, value, options) => {
|
|
69
|
+
const allowUnknown = Boolean(options.allowUnknown);
|
|
70
|
+
const keyValidation = validateConfigKeyPath(key);
|
|
71
|
+
if (!keyValidation.valid && !allowUnknown) {
|
|
72
|
+
const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
|
|
73
|
+
console.error(`Error: Invalid configuration key "${key}".${reason}`);
|
|
74
|
+
console.error('Use "openspec config list" to see available keys.');
|
|
75
|
+
console.error('Pass --allow-unknown to bypass this check.');
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const config = getGlobalConfig();
|
|
80
|
+
const coercedValue = coerceValue(value, options.string || false);
|
|
81
|
+
// Create a copy to validate before saving
|
|
82
|
+
const newConfig = JSON.parse(JSON.stringify(config));
|
|
83
|
+
setNestedValue(newConfig, key, coercedValue);
|
|
84
|
+
// Validate the new config
|
|
85
|
+
const validation = validateConfig(newConfig);
|
|
86
|
+
if (!validation.success) {
|
|
87
|
+
console.error(`Error: Invalid configuration - ${validation.error}`);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Apply changes and save
|
|
92
|
+
setNestedValue(config, key, coercedValue);
|
|
93
|
+
saveGlobalConfig(config);
|
|
94
|
+
const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
|
|
95
|
+
console.log(`Set ${key} = ${displayValue}`);
|
|
96
|
+
});
|
|
97
|
+
// config unset
|
|
98
|
+
configCmd
|
|
99
|
+
.command('unset <key>')
|
|
100
|
+
.description('Remove a key (revert to default)')
|
|
101
|
+
.action((key) => {
|
|
102
|
+
const config = getGlobalConfig();
|
|
103
|
+
const existed = deleteNestedValue(config, key);
|
|
104
|
+
if (existed) {
|
|
105
|
+
saveGlobalConfig(config);
|
|
106
|
+
console.log(`Unset ${key} (reverted to default)`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.log(`Key "${key}" was not set`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// config reset
|
|
113
|
+
configCmd
|
|
114
|
+
.command('reset')
|
|
115
|
+
.description('Reset configuration to defaults')
|
|
116
|
+
.option('--all', 'Reset all configuration (required)')
|
|
117
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
118
|
+
.action(async (options) => {
|
|
119
|
+
if (!options.all) {
|
|
120
|
+
console.error('Error: --all flag is required for reset');
|
|
121
|
+
console.error('Usage: openspec config reset --all [-y]');
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!options.yes) {
|
|
126
|
+
const confirmed = await confirm({
|
|
127
|
+
message: 'Reset all configuration to defaults?',
|
|
128
|
+
default: false,
|
|
129
|
+
});
|
|
130
|
+
if (!confirmed) {
|
|
131
|
+
console.log('Reset cancelled.');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
saveGlobalConfig({ ...DEFAULT_CONFIG });
|
|
136
|
+
console.log('Configuration reset to defaults');
|
|
137
|
+
});
|
|
138
|
+
// config edit
|
|
139
|
+
configCmd
|
|
140
|
+
.command('edit')
|
|
141
|
+
.description('Open config in $EDITOR')
|
|
142
|
+
.action(async () => {
|
|
143
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
144
|
+
if (!editor) {
|
|
145
|
+
console.error('Error: No editor configured');
|
|
146
|
+
console.error('Set the EDITOR or VISUAL environment variable to your preferred editor');
|
|
147
|
+
console.error('Example: export EDITOR=vim');
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const configPath = getGlobalConfigPath();
|
|
152
|
+
// Ensure config file exists with defaults
|
|
153
|
+
if (!fs.existsSync(configPath)) {
|
|
154
|
+
saveGlobalConfig({ ...DEFAULT_CONFIG });
|
|
155
|
+
}
|
|
156
|
+
// Spawn editor and wait for it to close
|
|
157
|
+
// Avoid shell parsing to correctly handle paths with spaces in both
|
|
158
|
+
// the editor path and config path
|
|
159
|
+
const child = spawn(editor, [configPath], {
|
|
160
|
+
stdio: 'inherit',
|
|
161
|
+
shell: false,
|
|
162
|
+
});
|
|
163
|
+
await new Promise((resolve, reject) => {
|
|
164
|
+
child.on('close', (code) => {
|
|
165
|
+
if (code === 0) {
|
|
166
|
+
resolve();
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
reject(new Error(`Editor exited with code ${code}`));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
child.on('error', reject);
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
const rawConfig = fs.readFileSync(configPath, 'utf-8');
|
|
176
|
+
const parsedConfig = JSON.parse(rawConfig);
|
|
177
|
+
const validation = validateConfig(parsedConfig);
|
|
178
|
+
if (!validation.success) {
|
|
179
|
+
console.error(`Error: Invalid configuration - ${validation.error}`);
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error.code === 'ENOENT') {
|
|
185
|
+
console.error(`Error: Config file not found at ${configPath}`);
|
|
186
|
+
}
|
|
187
|
+
else if (error instanceof SyntaxError) {
|
|
188
|
+
console.error(`Error: Invalid JSON in ${configPath}`);
|
|
189
|
+
console.error(error.message);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.error(`Error: Unable to validate configuration - ${error instanceof Error ? error.message : String(error)}`);
|
|
193
|
+
}
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=config.js.map
|
package/dist/commands/show.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { select } from '@inquirer/prompts';
|
|
2
1
|
import { isInteractive } from '../utils/interactive.js';
|
|
3
2
|
import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
|
|
4
3
|
import { ChangeCommand } from './change.js';
|
|
@@ -8,10 +7,11 @@ const CHANGE_FLAG_KEYS = new Set(['deltasOnly', 'requirementsOnly']);
|
|
|
8
7
|
const SPEC_FLAG_KEYS = new Set(['requirements', 'scenarios', 'requirement']);
|
|
9
8
|
export class ShowCommand {
|
|
10
9
|
async execute(itemName, options = {}) {
|
|
11
|
-
const interactive = isInteractive(options
|
|
10
|
+
const interactive = isInteractive(options);
|
|
12
11
|
const typeOverride = this.normalizeType(options.type);
|
|
13
12
|
if (!itemName) {
|
|
14
13
|
if (interactive) {
|
|
14
|
+
const { select } = await import('@inquirer/prompts');
|
|
15
15
|
const type = await select({
|
|
16
16
|
message: 'What would you like to show?',
|
|
17
17
|
choices: [
|
|
@@ -37,6 +37,7 @@ export class ShowCommand {
|
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
39
|
async runInteractiveByType(type, options) {
|
|
40
|
+
const { select } = await import('@inquirer/prompts');
|
|
40
41
|
if (type === 'change') {
|
|
41
42
|
const changes = await getActiveChangeIds();
|
|
42
43
|
if (changes.length === 0) {
|
package/dist/commands/spec.js
CHANGED
|
@@ -2,7 +2,6 @@ import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { MarkdownParser } from '../core/parsers/markdown-parser.js';
|
|
4
4
|
import { Validator } from '../core/validation/validator.js';
|
|
5
|
-
import { select } from '@inquirer/prompts';
|
|
6
5
|
import { isInteractive } from '../utils/interactive.js';
|
|
7
6
|
import { getSpecIds } from '../utils/item-discovery.js';
|
|
8
7
|
const SPECS_DIR = 'openspec/specs';
|
|
@@ -49,9 +48,10 @@ export class SpecCommand {
|
|
|
49
48
|
SPECS_DIR = 'openspec/specs';
|
|
50
49
|
async show(specId, options = {}) {
|
|
51
50
|
if (!specId) {
|
|
52
|
-
const canPrompt = isInteractive(options
|
|
51
|
+
const canPrompt = isInteractive(options);
|
|
53
52
|
const specIds = await getSpecIds();
|
|
54
53
|
if (canPrompt && specIds.length > 0) {
|
|
54
|
+
const { select } = await import('@inquirer/prompts');
|
|
55
55
|
specId = await select({
|
|
56
56
|
message: 'Select a spec to show',
|
|
57
57
|
choices: specIds.map(id => ({ name: id, value: id })),
|
|
@@ -178,9 +178,10 @@ export function registerSpecCommand(rootProgram) {
|
|
|
178
178
|
.action(async (specId, options) => {
|
|
179
179
|
try {
|
|
180
180
|
if (!specId) {
|
|
181
|
-
const canPrompt = isInteractive(options
|
|
181
|
+
const canPrompt = isInteractive(options);
|
|
182
182
|
const specIds = await getSpecIds();
|
|
183
183
|
if (canPrompt && specIds.length > 0) {
|
|
184
|
+
const { select } = await import('@inquirer/prompts');
|
|
184
185
|
specId = await select({
|
|
185
186
|
message: 'Select a spec to validate',
|
|
186
187
|
choices: specIds.map(id => ({ name: id, value: id })),
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { select } from '@inquirer/prompts';
|
|
2
1
|
import ora from 'ora';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import { Validator } from '../core/validation/validator.js';
|
|
@@ -7,7 +6,7 @@ import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
|
|
|
7
6
|
import { nearestMatches } from '../utils/match.js';
|
|
8
7
|
export class ValidateCommand {
|
|
9
8
|
async execute(itemName, options = {}) {
|
|
10
|
-
const interactive = isInteractive(options
|
|
9
|
+
const interactive = isInteractive(options);
|
|
11
10
|
// Handle bulk flags first
|
|
12
11
|
if (options.all || options.changes || options.specs) {
|
|
13
12
|
await this.runBulkValidation({
|
|
@@ -39,6 +38,7 @@ export class ValidateCommand {
|
|
|
39
38
|
return undefined;
|
|
40
39
|
}
|
|
41
40
|
async runInteractiveSelector(opts) {
|
|
41
|
+
const { select } = await import('@inquirer/prompts');
|
|
42
42
|
const choice = await select({
|
|
43
43
|
message: 'What would you like to validate?',
|
|
44
44
|
choices: [
|
|
@@ -178,6 +178,25 @@ export class ValidateCommand {
|
|
|
178
178
|
return { id, type: 'spec', valid: report.valid, issues: report.issues, durationMs };
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
|
+
if (queue.length === 0) {
|
|
182
|
+
spinner?.stop();
|
|
183
|
+
const summary = {
|
|
184
|
+
totals: { items: 0, passed: 0, failed: 0 },
|
|
185
|
+
byType: {
|
|
186
|
+
...(scope.changes ? { change: { items: 0, passed: 0, failed: 0 } } : {}),
|
|
187
|
+
...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0 } } : {}),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
if (opts.json) {
|
|
191
|
+
const out = { items: [], summary, version: '1.0' };
|
|
192
|
+
console.log(JSON.stringify(out, null, 2));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.log('No items found to validate.');
|
|
196
|
+
}
|
|
197
|
+
process.exitCode = 0;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
181
200
|
const results = [];
|
|
182
201
|
let index = 0;
|
|
183
202
|
let running = 0;
|
package/dist/core/archive.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { select, confirm } from '@inquirer/prompts';
|
|
4
3
|
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
5
4
|
import { Validator } from './validation/validator.js';
|
|
6
5
|
import chalk from 'chalk';
|
|
@@ -106,6 +105,7 @@ export class ArchiveCommand {
|
|
|
106
105
|
// Log warning when validation is skipped
|
|
107
106
|
const timestamp = new Date().toISOString();
|
|
108
107
|
if (!options.yes) {
|
|
108
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
109
109
|
const proceed = await confirm({
|
|
110
110
|
message: chalk.yellow('⚠️ WARNING: Skipping validation may archive invalid specs. Continue? (y/N)'),
|
|
111
111
|
default: false
|
|
@@ -128,6 +128,7 @@ export class ArchiveCommand {
|
|
|
128
128
|
const incompleteTasks = Math.max(progress.total - progress.completed, 0);
|
|
129
129
|
if (incompleteTasks > 0) {
|
|
130
130
|
if (!options.yes) {
|
|
131
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
131
132
|
const proceed = await confirm({
|
|
132
133
|
message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
|
|
133
134
|
default: false
|
|
@@ -157,6 +158,7 @@ export class ArchiveCommand {
|
|
|
157
158
|
}
|
|
158
159
|
let shouldUpdateSpecs = true;
|
|
159
160
|
if (!options.yes) {
|
|
161
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
160
162
|
shouldUpdateSpecs = await confirm({
|
|
161
163
|
message: 'Proceed with spec updates?',
|
|
162
164
|
default: true
|
|
@@ -228,6 +230,7 @@ export class ArchiveCommand {
|
|
|
228
230
|
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
|
|
229
231
|
}
|
|
230
232
|
async selectChange(changesDir) {
|
|
233
|
+
const { select } = await import('@inquirer/prompts');
|
|
231
234
|
// Get all directories in changes (excluding archive)
|
|
232
235
|
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
233
236
|
const changeDirs = entries
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CommandDefinition } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Registry of all OpenSpec CLI commands with their flags and metadata.
|
|
4
|
+
* This registry is used to generate shell completion scripts.
|
|
5
|
+
*/
|
|
6
|
+
export declare const COMMAND_REGISTRY: CommandDefinition[];
|
|
7
|
+
//# sourceMappingURL=command-registry.d.ts.map
|