@fission-ai/openspec 1.14.0 → 1.14.2
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 +15 -31
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +88 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +20 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +18 -0
- package/dist/cli/commands/schema.d.ts +6 -0
- package/dist/cli/commands/schema.js +57 -0
- package/dist/cli/commands/spec.d.ts +3 -0
- package/dist/cli/commands/spec.js +42 -0
- package/dist/cli/commands/store.d.ts +3 -0
- package/dist/cli/commands/store.js +130 -0
- package/dist/cli/commands/workset.d.ts +3 -0
- package/dist/cli/commands/workset.js +96 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +86 -43
- package/dist/commands/completion.js +4 -1
- package/dist/commands/config.d.ts +16 -7
- package/dist/commands/config.js +342 -395
- package/dist/commands/context.d.ts +8 -2
- package/dist/commands/context.js +22 -37
- package/dist/commands/doctor.d.ts +6 -7
- package/dist/commands/doctor.js +14 -27
- package/dist/commands/schema.d.ts +19 -5
- package/dist/commands/schema.js +662 -702
- package/dist/commands/spec.d.ts +13 -4
- package/dist/commands/spec.js +91 -120
- package/dist/commands/store.d.ts +27 -2
- package/dist/commands/store.js +1 -120
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/instructions.js +1 -1
- package/dist/commands/workflow/shared.d.ts +1 -1
- package/dist/commands/workflow/shared.js +1 -1
- package/dist/commands/workset.d.ts +22 -2
- package/dist/commands/workset.js +1 -88
- package/dist/core/archive.js +32 -12
- package/dist/core/artifact-graph/outputs.js +39 -1
- package/dist/core/completion-tip.js +3 -1
- package/dist/core/completions/completion-provider.js +4 -2
- package/dist/core/config-schema.d.ts +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +11 -0
- package/dist/core/parsers/requirement-blocks.js +57 -0
- package/dist/core/specs-apply.js +5 -1
- package/dist/core/templates/workflows/apply-change.js +4 -3
- package/dist/core/templates/workflows/continue-change.js +17 -0
- package/dist/core/templates/workflows/explore.js +8 -4
- package/dist/core/templates/workflows/update-change.js +4 -4
- package/dist/core/templates/workflows/verify-change.js +26 -20
- package/dist/core/validation/validator.js +23 -2
- package/dist/core/view.js +2 -26
- package/dist/utils/change-metadata.d.ts +17 -0
- package/dist/utils/change-metadata.js +27 -2
- package/dist/utils/file-system.d.ts +7 -0
- package/dist/utils/file-system.js +25 -0
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +28 -4
package/dist/commands/config.js
CHANGED
|
@@ -5,8 +5,6 @@ import { getGlobalConfigPath, getGlobalConfig, isConfigRootObject, isGlobalConfi
|
|
|
5
5
|
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
|
|
6
6
|
import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
|
|
7
7
|
import { OPENSPEC_DIR_NAME } from '../core/config.js';
|
|
8
|
-
import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
|
|
9
|
-
import { UpdateCommand } from '../core/update.js';
|
|
10
8
|
import { asErrorMessage, isPromptCancellationError } from './shared-output.js';
|
|
11
9
|
// cross-spawn finds `.cmd` shims such as `code.cmd` on Windows and escapes each
|
|
12
10
|
// argument for cmd.exe; elsewhere it is plain spawn. Loaded lazily so other
|
|
@@ -269,11 +267,14 @@ export function diffProfileState(before, after) {
|
|
|
269
267
|
lines,
|
|
270
268
|
};
|
|
271
269
|
}
|
|
272
|
-
function maybeWarnProjectConfigDrift(projectDir, state, colorize) {
|
|
270
|
+
async function maybeWarnProjectConfigDrift(projectDir, state, colorize) {
|
|
273
271
|
const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
|
|
274
272
|
if (!fs.existsSync(openspecDir)) {
|
|
275
273
|
return;
|
|
276
274
|
}
|
|
275
|
+
// Loaded here, not at the top: it pulls in every tool's command adapter,
|
|
276
|
+
// which `config path` and `config list` never need.
|
|
277
|
+
const { hasProjectConfigDrift } = await import('../core/profile-sync-drift.js');
|
|
277
278
|
if (!hasProjectConfigDrift(projectDir, state.workflows, state.delivery)) {
|
|
278
279
|
return;
|
|
279
280
|
}
|
|
@@ -282,431 +283,377 @@ function maybeWarnProjectConfigDrift(projectDir, state, colorize) {
|
|
|
282
283
|
function printConfigProfileApplyGuidance() {
|
|
283
284
|
console.log('Config updated. Run `openspec update` in your projects to apply.');
|
|
284
285
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
.command('path')
|
|
305
|
-
.description('Show config file location')
|
|
306
|
-
.action(() => {
|
|
307
|
-
console.log(getGlobalConfigPath());
|
|
308
|
-
});
|
|
309
|
-
// config list
|
|
310
|
-
configCmd
|
|
311
|
-
.command('list')
|
|
312
|
-
.description('Show all current settings')
|
|
313
|
-
.option('--json', 'Output as JSON')
|
|
314
|
-
.action((options) => {
|
|
315
|
-
const config = getGlobalConfig();
|
|
316
|
-
if (options.json) {
|
|
317
|
-
console.log(JSON.stringify(config, null, 2));
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
// Read raw config to determine which values are explicit vs defaults
|
|
321
|
-
const configPath = getGlobalConfigPath();
|
|
322
|
-
let rawConfig = {};
|
|
323
|
-
try {
|
|
324
|
-
if (fs.existsSync(configPath)) {
|
|
325
|
-
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
326
|
-
// A non-object root holds no explicit settings, and reading a key
|
|
327
|
-
// off `null` would crash this read-only command.
|
|
328
|
-
if (isConfigRootObject(parsed)) {
|
|
329
|
-
rawConfig = parsed;
|
|
330
|
-
}
|
|
286
|
+
export function configPathCommand() {
|
|
287
|
+
console.log(getGlobalConfigPath());
|
|
288
|
+
}
|
|
289
|
+
export function configListCommand(options) {
|
|
290
|
+
const config = getGlobalConfig();
|
|
291
|
+
if (options.json) {
|
|
292
|
+
console.log(JSON.stringify(config, null, 2));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Read raw config to determine which values are explicit vs defaults
|
|
296
|
+
const configPath = getGlobalConfigPath();
|
|
297
|
+
let rawConfig = {};
|
|
298
|
+
try {
|
|
299
|
+
if (fs.existsSync(configPath)) {
|
|
300
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
301
|
+
// A non-object root holds no explicit settings, and reading a key
|
|
302
|
+
// off `null` would crash this read-only command.
|
|
303
|
+
if (isConfigRootObject(parsed)) {
|
|
304
|
+
rawConfig = parsed;
|
|
331
305
|
}
|
|
332
306
|
}
|
|
333
|
-
catch {
|
|
334
|
-
// If reading fails, treat all as defaults
|
|
335
|
-
}
|
|
336
|
-
console.log(formatValueYaml(config));
|
|
337
|
-
// Annotate profile settings
|
|
338
|
-
const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
|
|
339
|
-
const deliverySource = rawConfig.delivery !== undefined ? '(explicit)' : '(default)';
|
|
340
|
-
console.log(`\nProfile settings:`);
|
|
341
|
-
console.log(` profile: ${config.profile} ${profileSource}`);
|
|
342
|
-
console.log(` delivery: ${config.delivery} ${deliverySource}`);
|
|
343
|
-
if (config.profile === 'core') {
|
|
344
|
-
console.log(` workflows: ${CORE_WORKFLOWS.join(', ')} (from core profile)`);
|
|
345
|
-
}
|
|
346
|
-
else if (config.workflows && config.workflows.length > 0) {
|
|
347
|
-
console.log(` workflows: ${config.workflows.join(', ')} (explicit)`);
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
console.log(` workflows: (none)`);
|
|
351
|
-
}
|
|
352
307
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
configCmd
|
|
356
|
-
.command('get <key>')
|
|
357
|
-
.description('Get a specific value (raw, scriptable)')
|
|
358
|
-
.action((key) => {
|
|
359
|
-
const config = getGlobalConfig();
|
|
360
|
-
const value = getNestedValue(config, key);
|
|
361
|
-
if (value === undefined) {
|
|
362
|
-
process.exitCode = 1;
|
|
363
|
-
return;
|
|
308
|
+
catch {
|
|
309
|
+
// If reading fails, treat all as defaults
|
|
364
310
|
}
|
|
365
|
-
|
|
366
|
-
|
|
311
|
+
console.log(formatValueYaml(config));
|
|
312
|
+
// Annotate profile settings
|
|
313
|
+
const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
|
|
314
|
+
const deliverySource = rawConfig.delivery !== undefined ? '(explicit)' : '(default)';
|
|
315
|
+
console.log(`\nProfile settings:`);
|
|
316
|
+
console.log(` profile: ${config.profile} ${profileSource}`);
|
|
317
|
+
console.log(` delivery: ${config.delivery} ${deliverySource}`);
|
|
318
|
+
if (config.profile === 'core') {
|
|
319
|
+
console.log(` workflows: ${CORE_WORKFLOWS.join(', ')} (from core profile)`);
|
|
320
|
+
}
|
|
321
|
+
else if (config.workflows && config.workflows.length > 0) {
|
|
322
|
+
console.log(` workflows: ${config.workflows.join(', ')} (explicit)`);
|
|
367
323
|
}
|
|
368
324
|
else {
|
|
369
|
-
console.log(
|
|
325
|
+
console.log(` workflows: (none)`);
|
|
370
326
|
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export function configGetCommand(key) {
|
|
330
|
+
const config = getGlobalConfig();
|
|
331
|
+
const value = getNestedValue(config, key);
|
|
332
|
+
if (value === undefined) {
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (typeof value === 'object' && value !== null) {
|
|
337
|
+
console.log(JSON.stringify(value));
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
console.log(String(value));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export function configSetCommand(key, value, options) {
|
|
344
|
+
const allowUnknown = Boolean(options.allowUnknown);
|
|
345
|
+
const keyValidation = validateConfigKeyPath(key);
|
|
346
|
+
// --allow-unknown relaxes the known-key check, but never the prototype-safety check.
|
|
347
|
+
const unsafeKey = hasUnsafeKeySegment(key);
|
|
348
|
+
if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
|
|
349
|
+
const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
|
|
350
|
+
console.error(`Error: Invalid configuration key "${key}".${reason}`);
|
|
351
|
+
console.error('Use "openspec config list" to see available keys.');
|
|
352
|
+
if (!allowUnknown && !unsafeKey) {
|
|
353
|
+
console.error('Pass --allow-unknown to bypass this check.');
|
|
354
|
+
}
|
|
355
|
+
process.exitCode = 1;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (refuseUnreadableConfig()) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const config = getGlobalConfig();
|
|
362
|
+
const coercedValue = coerceValue(value, options.string || false);
|
|
363
|
+
// Create a copy to validate before saving
|
|
364
|
+
const newConfig = JSON.parse(JSON.stringify(config));
|
|
365
|
+
setNestedValue(newConfig, key, coercedValue);
|
|
366
|
+
// Validate the new config
|
|
367
|
+
const validation = validateConfig(newConfig);
|
|
368
|
+
if (!validation.success) {
|
|
369
|
+
console.error(`Error: Invalid configuration - ${validation.error}`);
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Apply changes and save
|
|
374
|
+
setNestedValue(config, key, coercedValue);
|
|
375
|
+
saveGlobalConfig(config);
|
|
376
|
+
const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
|
|
377
|
+
console.log(`Set ${key} = ${displayValue}`);
|
|
378
|
+
}
|
|
379
|
+
export function configUnsetCommand(key) {
|
|
380
|
+
if (refuseUnreadableConfig()) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const config = getGlobalConfig();
|
|
384
|
+
const existed = deleteNestedValue(config, key);
|
|
385
|
+
if (existed) {
|
|
386
|
+
saveGlobalConfig(config);
|
|
387
|
+
console.log(`Unset ${key} (reverted to default)`);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
console.log(`Key "${key}" was not set`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
export async function configResetCommand(options) {
|
|
394
|
+
if (!options.all) {
|
|
395
|
+
console.error('Error: --all flag is required for reset');
|
|
396
|
+
console.error('Usage: openspec config reset --all [-y]');
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!options.yes) {
|
|
401
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
402
|
+
let confirmed;
|
|
403
|
+
try {
|
|
404
|
+
confirmed = await confirm({
|
|
405
|
+
message: 'Reset all configuration to defaults?',
|
|
406
|
+
default: false,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (isPromptCancellationError(error)) {
|
|
411
|
+
console.log('Reset cancelled.');
|
|
412
|
+
process.exitCode = 130;
|
|
413
|
+
return;
|
|
389
414
|
}
|
|
390
|
-
|
|
391
|
-
return;
|
|
415
|
+
throw error;
|
|
392
416
|
}
|
|
393
|
-
if (
|
|
417
|
+
if (!confirmed) {
|
|
418
|
+
console.log('Reset cancelled.');
|
|
394
419
|
return;
|
|
395
420
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
421
|
+
}
|
|
422
|
+
// A reset is the one write meant to replace a file that cannot be parsed.
|
|
423
|
+
saveGlobalConfig({ ...DEFAULT_CONFIG }, { replaceUnreadable: true });
|
|
424
|
+
console.log('Configuration reset to defaults');
|
|
425
|
+
}
|
|
426
|
+
export async function configEditCommand() {
|
|
427
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
428
|
+
if (!editor) {
|
|
429
|
+
console.error('Error: No editor configured');
|
|
430
|
+
console.error('Set the EDITOR or VISUAL environment variable to your preferred editor');
|
|
431
|
+
console.error('Example: export EDITOR=vim');
|
|
432
|
+
process.exitCode = 1;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const configPath = getGlobalConfigPath();
|
|
436
|
+
// Ensure config file exists with defaults
|
|
437
|
+
if (!fs.existsSync(configPath)) {
|
|
438
|
+
saveGlobalConfig({ ...DEFAULT_CONFIG });
|
|
439
|
+
}
|
|
440
|
+
// Wait for the editor to close; a failure is reported, never thrown.
|
|
441
|
+
const outcome = await runEditor(editor, configPath);
|
|
442
|
+
if ('error' in outcome || outcome.code !== 0) {
|
|
443
|
+
reportEditorFailure(editor, outcome);
|
|
444
|
+
process.exitCode = 1;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const rawConfig = fs.readFileSync(configPath, 'utf-8');
|
|
449
|
+
const parsedConfig = JSON.parse(rawConfig);
|
|
450
|
+
const validation = validateConfig(parsedConfig);
|
|
403
451
|
if (!validation.success) {
|
|
404
452
|
console.error(`Error: Invalid configuration - ${validation.error}`);
|
|
405
453
|
process.exitCode = 1;
|
|
406
|
-
return;
|
|
407
454
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
console.log(`Set ${key} = ${displayValue}`);
|
|
413
|
-
});
|
|
414
|
-
// config unset
|
|
415
|
-
configCmd
|
|
416
|
-
.command('unset <key>')
|
|
417
|
-
.description('Remove a key (revert to default)')
|
|
418
|
-
.action((key) => {
|
|
419
|
-
if (refuseUnreadableConfig()) {
|
|
420
|
-
return;
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
if (error.code === 'ENOENT') {
|
|
458
|
+
console.error(`Error: Config file not found at ${configPath}`);
|
|
421
459
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
saveGlobalConfig(config);
|
|
426
|
-
console.log(`Unset ${key} (reverted to default)`);
|
|
460
|
+
else if (error instanceof SyntaxError) {
|
|
461
|
+
console.error(`Error: Invalid JSON in ${configPath}`);
|
|
462
|
+
console.error(error.message);
|
|
427
463
|
}
|
|
428
464
|
else {
|
|
429
|
-
console.
|
|
465
|
+
console.error(`Error: Unable to validate configuration - ${error instanceof Error ? error.message : String(error)}`);
|
|
430
466
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
467
|
+
process.exitCode = 1;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
export async function configProfileCommand(preset) {
|
|
471
|
+
if (refuseUnreadableConfig()) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// Preset shortcut: `openspec config profile core`
|
|
475
|
+
if (preset === 'core') {
|
|
476
|
+
const config = getGlobalConfig();
|
|
477
|
+
config.profile = 'core';
|
|
478
|
+
config.workflows = [...CORE_WORKFLOWS];
|
|
479
|
+
// Preserve delivery setting
|
|
480
|
+
saveGlobalConfig(config);
|
|
481
|
+
printConfigProfileApplyGuidance();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (preset) {
|
|
485
|
+
console.error(`Error: Unknown profile preset "${preset}". Available presets: core`);
|
|
486
|
+
process.exitCode = 1;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// Non-interactive check
|
|
490
|
+
if (!process.stdout.isTTY) {
|
|
491
|
+
console.error('Interactive mode required. Use `openspec config profile core` or set config via environment/flags.');
|
|
492
|
+
process.exitCode = 1;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// Interactive picker
|
|
496
|
+
const { select, checkbox, confirm } = await import('@inquirer/prompts');
|
|
497
|
+
const chalk = (await import('chalk')).default;
|
|
498
|
+
try {
|
|
499
|
+
const config = getGlobalConfig();
|
|
500
|
+
const currentState = resolveCurrentProfileState(config);
|
|
501
|
+
console.log(chalk.bold('\nCurrent profile settings'));
|
|
502
|
+
console.log(` Delivery: ${currentState.delivery}`);
|
|
503
|
+
console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`);
|
|
504
|
+
console.log(chalk.dim(' Delivery = where workflows are installed (skills, commands, or both)'));
|
|
505
|
+
console.log(chalk.dim(' Workflows = which actions are available (propose, explore, apply, etc.)'));
|
|
506
|
+
console.log();
|
|
507
|
+
const action = await select({
|
|
508
|
+
message: 'What do you want to configure?',
|
|
509
|
+
choices: [
|
|
510
|
+
{
|
|
511
|
+
value: 'both',
|
|
512
|
+
name: 'Delivery and workflows',
|
|
513
|
+
description: 'Update install mode and available actions together',
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
value: 'delivery',
|
|
517
|
+
name: 'Delivery only',
|
|
518
|
+
description: 'Change where workflows are installed',
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
value: 'workflows',
|
|
522
|
+
name: 'Workflows only',
|
|
523
|
+
description: 'Change which workflow actions are available',
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
value: 'keep',
|
|
527
|
+
name: 'Keep current settings (exit)',
|
|
528
|
+
description: 'Leave configuration unchanged and exit',
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
});
|
|
532
|
+
if (action === 'keep') {
|
|
533
|
+
console.log('No config changes.');
|
|
534
|
+
await maybeWarnProjectConfigDrift(process.cwd(), currentState, chalk.yellow);
|
|
443
535
|
return;
|
|
444
536
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
537
|
+
const nextState = {
|
|
538
|
+
profile: currentState.profile,
|
|
539
|
+
delivery: currentState.delivery,
|
|
540
|
+
workflows: [...currentState.workflows],
|
|
541
|
+
};
|
|
542
|
+
let workflowSelectionChanged = false;
|
|
543
|
+
if (action === 'both' || action === 'delivery') {
|
|
544
|
+
const deliveryChoices = [
|
|
545
|
+
{
|
|
546
|
+
value: 'both',
|
|
547
|
+
name: 'Both (skills + commands)',
|
|
548
|
+
description: 'Install workflows as both skills and slash commands',
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
value: 'skills',
|
|
552
|
+
name: 'Skills only',
|
|
553
|
+
description: 'Install workflows only as skills',
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
value: 'commands',
|
|
557
|
+
name: 'Commands only',
|
|
558
|
+
description: 'Install workflows only as slash commands',
|
|
559
|
+
},
|
|
560
|
+
];
|
|
561
|
+
for (const choice of deliveryChoices) {
|
|
562
|
+
if (choice.value === currentState.delivery) {
|
|
563
|
+
choice.name += ' [current]';
|
|
459
564
|
}
|
|
460
|
-
throw error;
|
|
461
|
-
}
|
|
462
|
-
if (!confirmed) {
|
|
463
|
-
console.log('Reset cancelled.');
|
|
464
|
-
return;
|
|
465
565
|
}
|
|
566
|
+
nextState.delivery = await select({
|
|
567
|
+
message: 'Delivery mode (how workflows are installed):',
|
|
568
|
+
choices: deliveryChoices,
|
|
569
|
+
default: currentState.delivery,
|
|
570
|
+
});
|
|
466
571
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
else if (error instanceof SyntaxError) {
|
|
510
|
-
console.error(`Error: Invalid JSON in ${configPath}`);
|
|
511
|
-
console.error(error.message);
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
console.error(`Error: Unable to validate configuration - ${error instanceof Error ? error.message : String(error)}`);
|
|
515
|
-
}
|
|
516
|
-
process.exitCode = 1;
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
// config profile [preset]
|
|
520
|
-
configCmd
|
|
521
|
-
.command('profile [preset]')
|
|
522
|
-
.description('Configure workflow profile (interactive picker or preset shortcut)')
|
|
523
|
-
.action(async (preset) => {
|
|
524
|
-
if (refuseUnreadableConfig()) {
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
// Preset shortcut: `openspec config profile core`
|
|
528
|
-
if (preset === 'core') {
|
|
529
|
-
const config = getGlobalConfig();
|
|
530
|
-
config.profile = 'core';
|
|
531
|
-
config.workflows = [...CORE_WORKFLOWS];
|
|
532
|
-
// Preserve delivery setting
|
|
533
|
-
saveGlobalConfig(config);
|
|
534
|
-
printConfigProfileApplyGuidance();
|
|
572
|
+
if (action === 'both' || action === 'workflows') {
|
|
573
|
+
const formatWorkflowChoice = (workflow) => {
|
|
574
|
+
const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
|
|
575
|
+
name: workflow,
|
|
576
|
+
description: `Workflow: ${workflow}`,
|
|
577
|
+
};
|
|
578
|
+
return {
|
|
579
|
+
value: workflow,
|
|
580
|
+
name: metadata.name,
|
|
581
|
+
description: metadata.description,
|
|
582
|
+
short: metadata.name,
|
|
583
|
+
checked: currentState.workflows.includes(workflow),
|
|
584
|
+
};
|
|
585
|
+
};
|
|
586
|
+
const selectedWorkflows = await checkbox({
|
|
587
|
+
// The `instructions` option was removed in @inquirer/checkbox v5.
|
|
588
|
+
// Its replacement, the built-in keys help tip, renders
|
|
589
|
+
// "↑↓ navigate • space select • ⏎ submit" by default — a superset of
|
|
590
|
+
// the hint this used to pass — so no theme override is needed here.
|
|
591
|
+
message: 'Select workflows to make available:',
|
|
592
|
+
pageSize: ALL_WORKFLOWS.length,
|
|
593
|
+
theme: {
|
|
594
|
+
icon: {
|
|
595
|
+
checked: '[x]',
|
|
596
|
+
unchecked: '[ ]',
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
choices: ALL_WORKFLOWS.map(formatWorkflowChoice),
|
|
600
|
+
});
|
|
601
|
+
nextState.workflows = selectedWorkflows;
|
|
602
|
+
workflowSelectionChanged =
|
|
603
|
+
selectedWorkflows.length !== currentState.workflows.length ||
|
|
604
|
+
selectedWorkflows.some((workflow) => !currentState.workflows.includes(workflow));
|
|
605
|
+
nextState.profile = workflowSelectionChanged
|
|
606
|
+
? deriveProfileFromWorkflowSelection(selectedWorkflows)
|
|
607
|
+
: currentState.profile;
|
|
608
|
+
}
|
|
609
|
+
const diff = diffProfileState(currentState, nextState);
|
|
610
|
+
if (!diff.hasChanges) {
|
|
611
|
+
console.log('No config changes.');
|
|
612
|
+
await maybeWarnProjectConfigDrift(process.cwd(), nextState, chalk.yellow);
|
|
535
613
|
return;
|
|
536
614
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return;
|
|
615
|
+
console.log(chalk.bold('\nConfig changes:'));
|
|
616
|
+
for (const line of diff.lines) {
|
|
617
|
+
console.log(` ${line}`);
|
|
541
618
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
619
|
+
console.log();
|
|
620
|
+
config.profile = nextState.profile;
|
|
621
|
+
config.delivery = nextState.delivery;
|
|
622
|
+
if (currentState.profile !== 'custom' || workflowSelectionChanged) {
|
|
623
|
+
config.workflows = nextState.workflows;
|
|
547
624
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`);
|
|
557
|
-
console.log(chalk.dim(' Delivery = where workflows are installed (skills, commands, or both)'));
|
|
558
|
-
console.log(chalk.dim(' Workflows = which actions are available (propose, explore, apply, etc.)'));
|
|
559
|
-
console.log();
|
|
560
|
-
const action = await select({
|
|
561
|
-
message: 'What do you want to configure?',
|
|
562
|
-
choices: [
|
|
563
|
-
{
|
|
564
|
-
value: 'both',
|
|
565
|
-
name: 'Delivery and workflows',
|
|
566
|
-
description: 'Update install mode and available actions together',
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
value: 'delivery',
|
|
570
|
-
name: 'Delivery only',
|
|
571
|
-
description: 'Change where workflows are installed',
|
|
572
|
-
},
|
|
573
|
-
{
|
|
574
|
-
value: 'workflows',
|
|
575
|
-
name: 'Workflows only',
|
|
576
|
-
description: 'Change which workflow actions are available',
|
|
577
|
-
},
|
|
578
|
-
{
|
|
579
|
-
value: 'keep',
|
|
580
|
-
name: 'Keep current settings (exit)',
|
|
581
|
-
description: 'Leave configuration unchanged and exit',
|
|
582
|
-
},
|
|
583
|
-
],
|
|
625
|
+
saveGlobalConfig(config);
|
|
626
|
+
// Check if inside an OpenSpec project
|
|
627
|
+
const projectDir = process.cwd();
|
|
628
|
+
const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
|
|
629
|
+
if (fs.existsSync(openspecDir)) {
|
|
630
|
+
const applyNow = await confirm({
|
|
631
|
+
message: 'Apply changes to this project now?',
|
|
632
|
+
default: true,
|
|
584
633
|
});
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const nextState = {
|
|
591
|
-
profile: currentState.profile,
|
|
592
|
-
delivery: currentState.delivery,
|
|
593
|
-
workflows: [...currentState.workflows],
|
|
594
|
-
};
|
|
595
|
-
let workflowSelectionChanged = false;
|
|
596
|
-
if (action === 'both' || action === 'delivery') {
|
|
597
|
-
const deliveryChoices = [
|
|
598
|
-
{
|
|
599
|
-
value: 'both',
|
|
600
|
-
name: 'Both (skills + commands)',
|
|
601
|
-
description: 'Install workflows as both skills and slash commands',
|
|
602
|
-
},
|
|
603
|
-
{
|
|
604
|
-
value: 'skills',
|
|
605
|
-
name: 'Skills only',
|
|
606
|
-
description: 'Install workflows only as skills',
|
|
607
|
-
},
|
|
608
|
-
{
|
|
609
|
-
value: 'commands',
|
|
610
|
-
name: 'Commands only',
|
|
611
|
-
description: 'Install workflows only as slash commands',
|
|
612
|
-
},
|
|
613
|
-
];
|
|
614
|
-
for (const choice of deliveryChoices) {
|
|
615
|
-
if (choice.value === currentState.delivery) {
|
|
616
|
-
choice.name += ' [current]';
|
|
617
|
-
}
|
|
634
|
+
if (applyNow) {
|
|
635
|
+
try {
|
|
636
|
+
const { UpdateCommand } = await import('../core/update.js');
|
|
637
|
+
await new UpdateCommand().execute(projectDir);
|
|
638
|
+
console.log('Run `openspec update` in your other projects to apply.');
|
|
618
639
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
if (action === 'both' || action === 'workflows') {
|
|
626
|
-
const formatWorkflowChoice = (workflow) => {
|
|
627
|
-
const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
|
|
628
|
-
name: workflow,
|
|
629
|
-
description: `Workflow: ${workflow}`,
|
|
630
|
-
};
|
|
631
|
-
return {
|
|
632
|
-
value: workflow,
|
|
633
|
-
name: metadata.name,
|
|
634
|
-
description: metadata.description,
|
|
635
|
-
short: metadata.name,
|
|
636
|
-
checked: currentState.workflows.includes(workflow),
|
|
637
|
-
};
|
|
638
|
-
};
|
|
639
|
-
const selectedWorkflows = await checkbox({
|
|
640
|
-
// The `instructions` option was removed in @inquirer/checkbox v5.
|
|
641
|
-
// Its replacement, the built-in keys help tip, renders
|
|
642
|
-
// "↑↓ navigate • space select • ⏎ submit" by default — a superset of
|
|
643
|
-
// the hint this used to pass — so no theme override is needed here.
|
|
644
|
-
message: 'Select workflows to make available:',
|
|
645
|
-
pageSize: ALL_WORKFLOWS.length,
|
|
646
|
-
theme: {
|
|
647
|
-
icon: {
|
|
648
|
-
checked: '[x]',
|
|
649
|
-
unchecked: '[ ]',
|
|
650
|
-
},
|
|
651
|
-
},
|
|
652
|
-
choices: ALL_WORKFLOWS.map(formatWorkflowChoice),
|
|
653
|
-
});
|
|
654
|
-
nextState.workflows = selectedWorkflows;
|
|
655
|
-
workflowSelectionChanged =
|
|
656
|
-
selectedWorkflows.length !== currentState.workflows.length ||
|
|
657
|
-
selectedWorkflows.some((workflow) => !currentState.workflows.includes(workflow));
|
|
658
|
-
nextState.profile = workflowSelectionChanged
|
|
659
|
-
? deriveProfileFromWorkflowSelection(selectedWorkflows)
|
|
660
|
-
: currentState.profile;
|
|
661
|
-
}
|
|
662
|
-
const diff = diffProfileState(currentState, nextState);
|
|
663
|
-
if (!diff.hasChanges) {
|
|
664
|
-
console.log('No config changes.');
|
|
665
|
-
maybeWarnProjectConfigDrift(process.cwd(), nextState, chalk.yellow);
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
console.log(chalk.bold('\nConfig changes:'));
|
|
669
|
-
for (const line of diff.lines) {
|
|
670
|
-
console.log(` ${line}`);
|
|
671
|
-
}
|
|
672
|
-
console.log();
|
|
673
|
-
config.profile = nextState.profile;
|
|
674
|
-
config.delivery = nextState.delivery;
|
|
675
|
-
if (currentState.profile !== 'custom' || workflowSelectionChanged) {
|
|
676
|
-
config.workflows = nextState.workflows;
|
|
677
|
-
}
|
|
678
|
-
saveGlobalConfig(config);
|
|
679
|
-
// Check if inside an OpenSpec project
|
|
680
|
-
const projectDir = process.cwd();
|
|
681
|
-
const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
|
|
682
|
-
if (fs.existsSync(openspecDir)) {
|
|
683
|
-
const applyNow = await confirm({
|
|
684
|
-
message: 'Apply changes to this project now?',
|
|
685
|
-
default: true,
|
|
686
|
-
});
|
|
687
|
-
if (applyNow) {
|
|
688
|
-
try {
|
|
689
|
-
await new UpdateCommand().execute(projectDir);
|
|
690
|
-
console.log('Run `openspec update` in your other projects to apply.');
|
|
691
|
-
}
|
|
692
|
-
catch (error) {
|
|
693
|
-
console.error(`\`openspec update\` failed: ${asErrorMessage(error)}`);
|
|
694
|
-
console.error('Please run it manually to apply the profile changes.');
|
|
695
|
-
process.exitCode = 1;
|
|
696
|
-
}
|
|
697
|
-
return;
|
|
640
|
+
catch (error) {
|
|
641
|
+
console.error(`\`openspec update\` failed: ${asErrorMessage(error)}`);
|
|
642
|
+
console.error('Please run it manually to apply the profile changes.');
|
|
643
|
+
process.exitCode = 1;
|
|
698
644
|
}
|
|
699
|
-
}
|
|
700
|
-
printConfigProfileApplyGuidance();
|
|
701
|
-
}
|
|
702
|
-
catch (error) {
|
|
703
|
-
if (isPromptCancellationError(error)) {
|
|
704
|
-
console.log('Config profile cancelled.');
|
|
705
|
-
process.exitCode = 130;
|
|
706
645
|
return;
|
|
707
646
|
}
|
|
708
|
-
throw error;
|
|
709
647
|
}
|
|
710
|
-
|
|
648
|
+
printConfigProfileApplyGuidance();
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
if (isPromptCancellationError(error)) {
|
|
652
|
+
console.log('Config profile cancelled.');
|
|
653
|
+
process.exitCode = 130;
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
711
658
|
}
|
|
712
659
|
//# sourceMappingURL=config.js.map
|