@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/schema.js
CHANGED
|
@@ -384,829 +384,789 @@ const DEFAULT_ARTIFACTS = [
|
|
|
384
384
|
template: 'tasks.md',
|
|
385
385
|
},
|
|
386
386
|
];
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
.command('which [name]')
|
|
401
|
-
.description('Show where a schema resolves from')
|
|
402
|
-
.option('--json', 'Output as JSON')
|
|
403
|
-
.option('--all', 'List all schemas with their resolution sources')
|
|
404
|
-
.action(async (name, options) => {
|
|
405
|
-
try {
|
|
406
|
-
const projectRoot = process.cwd();
|
|
407
|
-
if (options?.all) {
|
|
408
|
-
// List all schemas
|
|
409
|
-
const schemas = getAllSchemasWithResolution(projectRoot);
|
|
410
|
-
if (options?.json) {
|
|
411
|
-
console.log(JSON.stringify(schemas, null, 2));
|
|
387
|
+
export async function schemaWhichCommand(name, options) {
|
|
388
|
+
try {
|
|
389
|
+
const projectRoot = process.cwd();
|
|
390
|
+
if (options?.all) {
|
|
391
|
+
// List all schemas
|
|
392
|
+
const schemas = getAllSchemasWithResolution(projectRoot);
|
|
393
|
+
if (options?.json) {
|
|
394
|
+
console.log(JSON.stringify(schemas, null, 2));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
if (schemas.length === 0) {
|
|
398
|
+
console.log('No schemas found.');
|
|
399
|
+
return;
|
|
412
400
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
console.log(
|
|
426
|
-
for (const schema of bySource.project) {
|
|
427
|
-
const shadowInfo = schema.shadows.length > 0
|
|
428
|
-
? ` (shadows: ${schema.shadows.map((s) => s.source).join(', ')})`
|
|
429
|
-
: '';
|
|
430
|
-
console.log(` ${schema.name}${shadowInfo}`);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (bySource.user.length > 0) {
|
|
434
|
-
console.log('\nUser schemas:');
|
|
435
|
-
for (const schema of bySource.user) {
|
|
436
|
-
const shadowInfo = schema.shadows.length > 0
|
|
437
|
-
? ` (shadows: ${schema.shadows.map((s) => s.source).join(', ')})`
|
|
438
|
-
: '';
|
|
439
|
-
console.log(` ${schema.name}${shadowInfo}`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (bySource.package.length > 0) {
|
|
443
|
-
console.log('\nPackage schemas:');
|
|
444
|
-
for (const schema of bySource.package) {
|
|
445
|
-
console.log(` ${schema.name}`);
|
|
446
|
-
}
|
|
401
|
+
// Group by source
|
|
402
|
+
const bySource = {
|
|
403
|
+
project: schemas.filter((s) => s.source === 'project'),
|
|
404
|
+
user: schemas.filter((s) => s.source === 'user'),
|
|
405
|
+
package: schemas.filter((s) => s.source === 'package'),
|
|
406
|
+
};
|
|
407
|
+
if (bySource.project.length > 0) {
|
|
408
|
+
console.log('\nProject schemas:');
|
|
409
|
+
for (const schema of bySource.project) {
|
|
410
|
+
const shadowInfo = schema.shadows.length > 0
|
|
411
|
+
? ` (shadows: ${schema.shadows.map((s) => s.source).join(', ')})`
|
|
412
|
+
: '';
|
|
413
|
+
console.log(` ${schema.name}${shadowInfo}`);
|
|
447
414
|
}
|
|
448
415
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (!resolution) {
|
|
458
|
-
const available = listSchemas(projectRoot);
|
|
459
|
-
if (options?.json) {
|
|
460
|
-
console.log(JSON.stringify({
|
|
461
|
-
error: `Schema '${name}' not found`,
|
|
462
|
-
available,
|
|
463
|
-
}, null, 2));
|
|
416
|
+
if (bySource.user.length > 0) {
|
|
417
|
+
console.log('\nUser schemas:');
|
|
418
|
+
for (const schema of bySource.user) {
|
|
419
|
+
const shadowInfo = schema.shadows.length > 0
|
|
420
|
+
? ` (shadows: ${schema.shadows.map((s) => s.source).join(', ')})`
|
|
421
|
+
: '';
|
|
422
|
+
console.log(` ${schema.name}${shadowInfo}`);
|
|
423
|
+
}
|
|
464
424
|
}
|
|
465
|
-
|
|
466
|
-
console.
|
|
467
|
-
|
|
425
|
+
if (bySource.package.length > 0) {
|
|
426
|
+
console.log('\nPackage schemas:');
|
|
427
|
+
for (const schema of bySource.package) {
|
|
428
|
+
console.log(` ${schema.name}`);
|
|
429
|
+
}
|
|
468
430
|
}
|
|
469
|
-
process.exitCode = 1;
|
|
470
|
-
return;
|
|
471
431
|
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (!name) {
|
|
435
|
+
console.error('Error: Schema name is required (or use --all to list all schemas)');
|
|
436
|
+
process.exitCode = 1;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const resolution = getSchemaResolution(name, projectRoot);
|
|
440
|
+
if (!resolution) {
|
|
441
|
+
const available = listSchemas(projectRoot);
|
|
472
442
|
if (options?.json) {
|
|
473
|
-
console.log(JSON.stringify(
|
|
443
|
+
console.log(JSON.stringify({
|
|
444
|
+
error: `Schema '${name}' not found`,
|
|
445
|
+
available,
|
|
446
|
+
}, null, 2));
|
|
474
447
|
}
|
|
475
448
|
else {
|
|
476
|
-
console.
|
|
477
|
-
console.
|
|
478
|
-
console.log(`Path: ${resolution.path}`);
|
|
479
|
-
if (resolution.shadows.length > 0) {
|
|
480
|
-
console.log('\nShadows:');
|
|
481
|
-
for (const shadow of resolution.shadows) {
|
|
482
|
-
console.log(` ${shadow.source}: ${shadow.path}`);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
449
|
+
console.error(`Error: Schema '${name}' not found`);
|
|
450
|
+
console.error(`Available schemas: ${available.join(', ')}`);
|
|
485
451
|
}
|
|
486
|
-
}
|
|
487
|
-
catch (error) {
|
|
488
|
-
console.error(`Error: ${error.message}`);
|
|
489
452
|
process.exitCode = 1;
|
|
453
|
+
return;
|
|
490
454
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
// Validate all project schemas
|
|
503
|
-
const projectSchemasDir = getProjectSchemasDir(projectRoot);
|
|
504
|
-
if (!fs.existsSync(projectSchemasDir)) {
|
|
505
|
-
if (options?.json) {
|
|
506
|
-
console.log(JSON.stringify({
|
|
507
|
-
valid: true,
|
|
508
|
-
message: 'No project schemas directory found',
|
|
509
|
-
schemas: [],
|
|
510
|
-
}, null, 2));
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
console.log('No project schemas directory found.');
|
|
514
|
-
}
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const entries = fs.readdirSync(projectSchemasDir, { withFileTypes: true });
|
|
518
|
-
const schemaResults = [];
|
|
519
|
-
let anyInvalid = false;
|
|
520
|
-
for (const entry of entries) {
|
|
521
|
-
if (!isSchemaDir(projectSchemasDir, entry))
|
|
522
|
-
continue;
|
|
523
|
-
const schemaDir = path.join(projectSchemasDir, entry.name);
|
|
524
|
-
const schemaPath = path.join(schemaDir, 'schema.yaml');
|
|
525
|
-
if (!fs.existsSync(schemaPath))
|
|
526
|
-
continue;
|
|
527
|
-
if (options?.verbose && !options?.json) {
|
|
528
|
-
console.log(`\nValidating ${entry.name}...`);
|
|
529
|
-
}
|
|
530
|
-
const result = validateSchema(schemaDir, options?.verbose && !options?.json);
|
|
531
|
-
schemaResults.push({
|
|
532
|
-
name: entry.name,
|
|
533
|
-
path: schemaDir,
|
|
534
|
-
valid: result.valid,
|
|
535
|
-
issues: result.issues,
|
|
536
|
-
});
|
|
537
|
-
if (!result.valid) {
|
|
538
|
-
anyInvalid = true;
|
|
539
|
-
}
|
|
455
|
+
if (options?.json) {
|
|
456
|
+
console.log(JSON.stringify(resolution, null, 2));
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
console.log(`Schema: ${resolution.name}`);
|
|
460
|
+
console.log(`Source: ${resolution.source}`);
|
|
461
|
+
console.log(`Path: ${resolution.path}`);
|
|
462
|
+
if (resolution.shadows.length > 0) {
|
|
463
|
+
console.log('\nShadows:');
|
|
464
|
+
for (const shadow of resolution.shadows) {
|
|
465
|
+
console.log(` ${shadow.source}: ${shadow.path}`);
|
|
540
466
|
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
console.error(`Error: ${error.message}`);
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
export async function schemaValidateCommand(name, options) {
|
|
476
|
+
try {
|
|
477
|
+
const projectRoot = process.cwd();
|
|
478
|
+
if (!name) {
|
|
479
|
+
// Validate all project schemas
|
|
480
|
+
const projectSchemasDir = getProjectSchemasDir(projectRoot);
|
|
481
|
+
if (!fs.existsSync(projectSchemasDir)) {
|
|
541
482
|
if (options?.json) {
|
|
542
483
|
console.log(JSON.stringify({
|
|
543
|
-
valid:
|
|
544
|
-
|
|
484
|
+
valid: true,
|
|
485
|
+
message: 'No project schemas directory found',
|
|
486
|
+
schemas: [],
|
|
545
487
|
}, null, 2));
|
|
546
488
|
}
|
|
547
489
|
else {
|
|
548
|
-
|
|
549
|
-
console.log('No schemas found in project.');
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
console.log('\nValidation Results:');
|
|
553
|
-
for (const result of schemaResults) {
|
|
554
|
-
const status = result.valid ? '✓' : '✗';
|
|
555
|
-
console.log(` ${status} ${result.name}`);
|
|
556
|
-
for (const issue of result.issues) {
|
|
557
|
-
console.log(` ${issue.level}: ${issue.message}`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
if (anyInvalid) {
|
|
562
|
-
process.exitCode = 1;
|
|
490
|
+
console.log('No project schemas directory found.');
|
|
563
491
|
}
|
|
564
492
|
return;
|
|
565
493
|
}
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
494
|
+
const entries = fs.readdirSync(projectSchemasDir, { withFileTypes: true });
|
|
495
|
+
const schemaResults = [];
|
|
496
|
+
let anyInvalid = false;
|
|
497
|
+
for (const entry of entries) {
|
|
498
|
+
if (!isSchemaDir(projectSchemasDir, entry))
|
|
499
|
+
continue;
|
|
500
|
+
const schemaDir = path.join(projectSchemasDir, entry.name);
|
|
501
|
+
const schemaPath = path.join(schemaDir, 'schema.yaml');
|
|
502
|
+
if (!fs.existsSync(schemaPath))
|
|
503
|
+
continue;
|
|
504
|
+
if (options?.verbose && !options?.json) {
|
|
505
|
+
console.log(`\nValidating ${entry.name}...`);
|
|
576
506
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
507
|
+
const result = validateSchema(schemaDir, options?.verbose && !options?.json);
|
|
508
|
+
schemaResults.push({
|
|
509
|
+
name: entry.name,
|
|
510
|
+
path: schemaDir,
|
|
511
|
+
valid: result.valid,
|
|
512
|
+
issues: result.issues,
|
|
513
|
+
});
|
|
514
|
+
if (!result.valid) {
|
|
515
|
+
anyInvalid = true;
|
|
580
516
|
}
|
|
581
|
-
process.exitCode = 1;
|
|
582
|
-
return;
|
|
583
517
|
}
|
|
584
|
-
if (options?.verbose && !options?.json) {
|
|
585
|
-
console.log(`Validating ${name}...`);
|
|
586
|
-
}
|
|
587
|
-
const result = validateSchema(schemaDir, options?.verbose && !options?.json);
|
|
588
518
|
if (options?.json) {
|
|
589
519
|
console.log(JSON.stringify({
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
valid: result.valid,
|
|
593
|
-
issues: result.issues,
|
|
520
|
+
valid: !anyInvalid,
|
|
521
|
+
schemas: schemaResults,
|
|
594
522
|
}, null, 2));
|
|
595
523
|
}
|
|
596
524
|
else {
|
|
597
|
-
if (
|
|
598
|
-
console.log(
|
|
599
|
-
|
|
600
|
-
console.log(` ${issue.level}: ${issue.message}`);
|
|
601
|
-
}
|
|
525
|
+
if (schemaResults.length === 0) {
|
|
526
|
+
console.log('No schemas found in project.');
|
|
527
|
+
return;
|
|
602
528
|
}
|
|
603
|
-
|
|
604
|
-
|
|
529
|
+
console.log('\nValidation Results:');
|
|
530
|
+
for (const result of schemaResults) {
|
|
531
|
+
const status = result.valid ? '✓' : '✗';
|
|
532
|
+
console.log(` ${status} ${result.name}`);
|
|
605
533
|
for (const issue of result.issues) {
|
|
606
|
-
console.log(`
|
|
534
|
+
console.log(` ${issue.level}: ${issue.message}`);
|
|
607
535
|
}
|
|
608
536
|
}
|
|
609
537
|
}
|
|
610
|
-
if (
|
|
538
|
+
if (anyInvalid) {
|
|
611
539
|
process.exitCode = 1;
|
|
612
540
|
}
|
|
541
|
+
return;
|
|
613
542
|
}
|
|
614
|
-
|
|
543
|
+
// Validate specific schema
|
|
544
|
+
const schemaDir = getSchemaDir(name, projectRoot);
|
|
545
|
+
if (!schemaDir) {
|
|
546
|
+
const available = listSchemas(projectRoot);
|
|
615
547
|
if (options?.json) {
|
|
616
548
|
console.log(JSON.stringify({
|
|
617
549
|
valid: false,
|
|
618
|
-
error:
|
|
550
|
+
error: `Schema '${name}' not found`,
|
|
551
|
+
available,
|
|
619
552
|
}, null, 2));
|
|
620
553
|
}
|
|
621
554
|
else {
|
|
622
|
-
console.error(`Error: ${
|
|
555
|
+
console.error(`Error: Schema '${name}' not found`);
|
|
556
|
+
console.error(`Available schemas: ${available.join(', ')}`);
|
|
623
557
|
}
|
|
624
558
|
process.exitCode = 1;
|
|
559
|
+
return;
|
|
625
560
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
error: `Invalid schema name '${destinationName}'. Use kebab-case (e.g., my-workflow)`,
|
|
644
|
-
}, null, 2));
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
console.error(`Error: Invalid schema name '${destinationName}'`);
|
|
648
|
-
console.error('Schema names must be kebab-case (e.g., my-workflow)');
|
|
561
|
+
if (options?.verbose && !options?.json) {
|
|
562
|
+
console.log(`Validating ${name}...`);
|
|
563
|
+
}
|
|
564
|
+
const result = validateSchema(schemaDir, options?.verbose && !options?.json);
|
|
565
|
+
if (options?.json) {
|
|
566
|
+
console.log(JSON.stringify({
|
|
567
|
+
name,
|
|
568
|
+
path: schemaDir,
|
|
569
|
+
valid: result.valid,
|
|
570
|
+
issues: result.issues,
|
|
571
|
+
}, null, 2));
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
if (result.valid) {
|
|
575
|
+
console.log(`✓ Schema '${name}' is valid`);
|
|
576
|
+
for (const issue of result.issues) {
|
|
577
|
+
console.log(` ${issue.level}: ${issue.message}`);
|
|
649
578
|
}
|
|
650
|
-
process.exitCode = 1;
|
|
651
|
-
return;
|
|
652
579
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (options?.json) {
|
|
658
|
-
console.log(JSON.stringify({
|
|
659
|
-
forked: false,
|
|
660
|
-
error: `Schema '${source}' not found`,
|
|
661
|
-
available,
|
|
662
|
-
}, null, 2));
|
|
663
|
-
}
|
|
664
|
-
else {
|
|
665
|
-
console.error(`Error: Schema '${source}' not found`);
|
|
666
|
-
console.error(`Available schemas: ${available.join(', ')}`);
|
|
580
|
+
else {
|
|
581
|
+
console.log(`✗ Schema '${name}' has errors:`);
|
|
582
|
+
for (const issue of result.issues) {
|
|
583
|
+
console.log(` ${issue.level}: ${issue.message}`);
|
|
667
584
|
}
|
|
668
|
-
process.exitCode = 1;
|
|
669
|
-
return;
|
|
670
585
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
586
|
+
}
|
|
587
|
+
if (!result.valid) {
|
|
588
|
+
process.exitCode = 1;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
if (options?.json) {
|
|
593
|
+
console.log(JSON.stringify({
|
|
594
|
+
valid: false,
|
|
595
|
+
error: error.message,
|
|
596
|
+
}, null, 2));
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
console.error(`Error: ${error.message}`);
|
|
600
|
+
}
|
|
601
|
+
process.exitCode = 1;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
export async function schemaForkCommand(source, name, options) {
|
|
605
|
+
const spinner = options?.json ? null : ora();
|
|
606
|
+
try {
|
|
607
|
+
const projectRoot = process.cwd();
|
|
608
|
+
const destinationName = name || `${source}-custom`;
|
|
609
|
+
// Validate destination name
|
|
610
|
+
if (!isValidSchemaName(destinationName)) {
|
|
611
|
+
if (options?.json) {
|
|
612
|
+
console.log(JSON.stringify({
|
|
613
|
+
forked: false,
|
|
614
|
+
error: `Invalid schema name '${destinationName}'. Use kebab-case (e.g., my-workflow)`,
|
|
615
|
+
}, null, 2));
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
console.error(`Error: Invalid schema name '${destinationName}'`);
|
|
619
|
+
console.error('Schema names must be kebab-case (e.g., my-workflow)');
|
|
697
620
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
}
|
|
711
|
-
process.exitCode = 1;
|
|
712
|
-
return;
|
|
621
|
+
process.exitCode = 1;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// Find source schema
|
|
625
|
+
const sourceDir = getSchemaDir(source, projectRoot);
|
|
626
|
+
if (!sourceDir) {
|
|
627
|
+
const available = listSchemas(projectRoot);
|
|
628
|
+
if (options?.json) {
|
|
629
|
+
console.log(JSON.stringify({
|
|
630
|
+
forked: false,
|
|
631
|
+
error: `Schema '${source}' not found`,
|
|
632
|
+
available,
|
|
633
|
+
}, null, 2));
|
|
713
634
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
635
|
+
else {
|
|
636
|
+
console.error(`Error: Schema '${source}' not found`);
|
|
637
|
+
console.error(`Available schemas: ${available.join(', ')}`);
|
|
638
|
+
}
|
|
639
|
+
process.exitCode = 1;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// Determine source location
|
|
643
|
+
const sourceResolution = getSchemaResolution(source, projectRoot);
|
|
644
|
+
const sourceLocation = sourceResolution?.source || 'package';
|
|
645
|
+
// Validate the complete source before a forced fork removes anything.
|
|
646
|
+
const trustedSourceDir = fs.realpathSync(sourceDir);
|
|
647
|
+
assertSchemaTreeCanBeCopied(trustedSourceDir);
|
|
648
|
+
// Validate the source's schema content up front too, so a structurally
|
|
649
|
+
// invalid source is rejected before the --force path can remove an
|
|
650
|
+
// existing destination. This keeps `fork --force` atomic — an unusable
|
|
651
|
+
// source never destroys a valid destination — matching `schema init`,
|
|
652
|
+
// which likewise validates before it overwrites.
|
|
653
|
+
parseSchema(fs.readFileSync(path.join(trustedSourceDir, 'schema.yaml'), 'utf-8'));
|
|
654
|
+
// Check destination
|
|
655
|
+
const schemasDir = getProjectSchemasDir(projectRoot);
|
|
656
|
+
const destinationDir = path.join(schemasDir, destinationName);
|
|
657
|
+
// Reject a self-fork. Forking a schema onto itself with --force would
|
|
658
|
+
// otherwise remove the source at the replacement step below and then
|
|
659
|
+
// fail the copy, destroying the only copy of the schema. Resolve both
|
|
660
|
+
// sides to their real paths (realpathSync follows symlinks; path.resolve
|
|
661
|
+
// is a fallback only for a destination that does not exist yet) so a
|
|
662
|
+
// symlink or a `.`/`..` spelling of the same directory is still caught.
|
|
663
|
+
const resolvedDestination = fs.existsSync(destinationDir)
|
|
664
|
+
? fs.realpathSync(destinationDir)
|
|
665
|
+
: path.resolve(destinationDir);
|
|
666
|
+
if (resolvedDestination === trustedSourceDir) {
|
|
667
|
+
throw new Error(`Cannot fork schema '${source}' onto itself; choose a different destination name`);
|
|
668
|
+
}
|
|
669
|
+
const destinationExists = fs.existsSync(destinationDir);
|
|
670
|
+
if (destinationExists && !options?.force) {
|
|
671
|
+
if (options?.json) {
|
|
672
|
+
console.log(JSON.stringify({
|
|
673
|
+
forked: false,
|
|
674
|
+
error: `Schema '${destinationName}' already exists`,
|
|
675
|
+
suggestion: 'Use --force to overwrite',
|
|
676
|
+
}, null, 2));
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
console.error(`Error: Schema '${destinationName}' already exists at ${destinationDir}`);
|
|
680
|
+
console.error('Use --force to overwrite');
|
|
681
|
+
}
|
|
682
|
+
process.exitCode = 1;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
// Fingerprint the destination the user authorized us to overwrite, BEFORE
|
|
686
|
+
// we spend time staging. Staging can take a while, and a concurrent
|
|
687
|
+
// process may edit the destination in that window; the fingerprint lets
|
|
688
|
+
// us detect such a change and abort rather than clobber it.
|
|
689
|
+
const authorizedDestinationFingerprint = destinationExists
|
|
690
|
+
? fingerprintDir(destinationDir)
|
|
691
|
+
: null;
|
|
692
|
+
// Stage the complete fork in a temporary sibling directory first, then
|
|
693
|
+
// swap it into place. This keeps `fork --force` atomic: an existing
|
|
694
|
+
// destination is only removed once the new fork has been fully copied,
|
|
695
|
+
// name-updated, and (via the up-front parseSchema above) validated. Any
|
|
696
|
+
// failure while staging leaves both the source and the existing
|
|
697
|
+
// destination exactly as they were.
|
|
698
|
+
if (spinner)
|
|
699
|
+
spinner.start(`Forking '${source}' to '${destinationName}'...`);
|
|
700
|
+
fs.mkdirSync(schemasDir, { recursive: true });
|
|
701
|
+
const stagingDir = fs.mkdtempSync(path.join(schemasDir, '.fork-staging-'));
|
|
702
|
+
try {
|
|
703
|
+
copyDirRecursive(trustedSourceDir, stagingDir);
|
|
704
|
+
// Update name in the staged schema.yaml via yaml's Document API
|
|
705
|
+
// instead of re-serializing the parsed object, so block scalars,
|
|
706
|
+
// comments, and key order in the source schema.yaml survive the fork.
|
|
707
|
+
const stagedSchemaPath = path.join(stagingDir, 'schema.yaml');
|
|
708
|
+
const schemaContent = fs.readFileSync(stagedSchemaPath, 'utf-8');
|
|
709
|
+
const doc = parseDocument(schemaContent);
|
|
710
|
+
doc.set('name', destinationName);
|
|
711
|
+
fs.writeFileSync(stagedSchemaPath, doc.toString());
|
|
712
|
+
// Authoritative gate: validate the COMPLETED staged schema — the exact
|
|
713
|
+
// bytes we are about to install — not just the source at the pre-check.
|
|
714
|
+
// The source files copyDirRecursive reads can change mid-copy, so a
|
|
715
|
+
// source that was valid up front can still produce an invalid staged
|
|
716
|
+
// fork. Validating here, before ANY destructive step, guarantees we
|
|
717
|
+
// never install an invalid fork or delete a valid destination for one.
|
|
731
718
|
try {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
719
|
+
parseSchema(fs.readFileSync(stagedSchemaPath, 'utf-8'));
|
|
720
|
+
}
|
|
721
|
+
catch (validationError) {
|
|
722
|
+
throw new Error(`The staged fork of '${source}' is not a valid schema (the source may have changed during copy); ` +
|
|
723
|
+
`aborted, '${destinationName}' was not modified.`, { cause: validationError });
|
|
724
|
+
}
|
|
725
|
+
// Swap the staged fork into place. When a destination already exists,
|
|
726
|
+
// move it aside to a sibling backup FIRST, then install the staged
|
|
727
|
+
// fork; only once the install succeeds is the backup discarded. If the
|
|
728
|
+
// install rename itself fails (e.g. a Windows lock), the backup is
|
|
729
|
+
// moved back so the user's original destination is never lost.
|
|
730
|
+
if (destinationExists) {
|
|
731
|
+
if (spinner)
|
|
732
|
+
spinner.text = `Replacing existing schema '${destinationName}'...`;
|
|
733
|
+
// Revalidate immediately before the destructive move: if the
|
|
734
|
+
// destination changed on disk while we were staging (or was removed),
|
|
735
|
+
// its fingerprint no longer matches what the user authorized. Abort
|
|
736
|
+
// WITHOUT touching it, so the concurrent changes are preserved. The
|
|
737
|
+
// outer catch cleans up staging.
|
|
738
|
+
const currentFingerprint = fs.existsSync(destinationDir)
|
|
739
|
+
? fingerprintDir(destinationDir)
|
|
740
|
+
: null;
|
|
741
|
+
if (currentFingerprint !== authorizedDestinationFingerprint) {
|
|
742
|
+
throw new Error(`Schema '${destinationName}' at ${destinationDir} changed on disk while the fork was being prepared. ` +
|
|
743
|
+
`Aborted to preserve those concurrent changes; nothing was overwritten. Re-run the fork to overwrite the current contents.`);
|
|
749
744
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
745
|
+
const backupDir = `${destinationDir}.fork-backup-${process.pid}-${Date.now()}`;
|
|
746
|
+
fs.renameSync(destinationDir, backupDir);
|
|
747
|
+
try {
|
|
748
|
+
fs.renameSync(stagingDir, destinationDir);
|
|
753
749
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (spinner)
|
|
761
|
-
spinner.text = `Replacing existing schema '${destinationName}'...`;
|
|
762
|
-
// Revalidate immediately before the destructive move: if the
|
|
763
|
-
// destination changed on disk while we were staging (or was removed),
|
|
764
|
-
// its fingerprint no longer matches what the user authorized. Abort
|
|
765
|
-
// WITHOUT touching it, so the concurrent changes are preserved. The
|
|
766
|
-
// outer catch cleans up staging.
|
|
767
|
-
const currentFingerprint = fs.existsSync(destinationDir)
|
|
768
|
-
? fingerprintDir(destinationDir)
|
|
769
|
-
: null;
|
|
770
|
-
if (currentFingerprint !== authorizedDestinationFingerprint) {
|
|
771
|
-
throw new Error(`Schema '${destinationName}' at ${destinationDir} changed on disk while the fork was being prepared. ` +
|
|
772
|
-
`Aborted to preserve those concurrent changes; nothing was overwritten. Re-run the fork to overwrite the current contents.`);
|
|
773
|
-
}
|
|
774
|
-
const backupDir = `${destinationDir}.fork-backup-${process.pid}-${Date.now()}`;
|
|
775
|
-
fs.renameSync(destinationDir, backupDir);
|
|
750
|
+
catch (installError) {
|
|
751
|
+
// Install failed after the original was moved aside. Try to move
|
|
752
|
+
// it back. If that restore ALSO fails, the original is stranded in
|
|
753
|
+
// the backup dir — surface an error naming both the backup and the
|
|
754
|
+
// destination so the user can recover manually, and attach the
|
|
755
|
+
// original install error as the cause. Never swallow this.
|
|
776
756
|
try {
|
|
777
|
-
fs.renameSync(
|
|
757
|
+
fs.renameSync(backupDir, destinationDir);
|
|
778
758
|
}
|
|
779
|
-
catch (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
// destination so the user can recover manually, and attach the
|
|
784
|
-
// original install error as the cause. Never swallow this.
|
|
785
|
-
try {
|
|
786
|
-
fs.renameSync(backupDir, destinationDir);
|
|
787
|
-
}
|
|
788
|
-
catch (restoreError) {
|
|
789
|
-
throw new Error(`Failed to install the forked schema and could not restore the previous '${destinationName}'. ` +
|
|
790
|
-
`Your previous schema is preserved at ${backupDir}; move it back to ${destinationDir} to restore. ` +
|
|
791
|
-
`Restore error: ${restoreError.message}`, { cause: installError });
|
|
792
|
-
}
|
|
793
|
-
throw installError;
|
|
759
|
+
catch (restoreError) {
|
|
760
|
+
throw new Error(`Failed to install the forked schema and could not restore the previous '${destinationName}'. ` +
|
|
761
|
+
`Your previous schema is preserved at ${backupDir}; move it back to ${destinationDir} to restore. ` +
|
|
762
|
+
`Restore error: ${restoreError.message}`, { cause: installError });
|
|
794
763
|
}
|
|
795
|
-
|
|
796
|
-
// still byte-for-byte the original destination we moved aside. If it
|
|
797
|
-
// changed during the install window (a concurrent write to the
|
|
798
|
-
// moved-aside directory), do NOT delete it — leave it in place and
|
|
799
|
-
// surface where it is so nothing is lost.
|
|
800
|
-
if (fingerprintDir(backupDir) === authorizedDestinationFingerprint) {
|
|
801
|
-
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
console.error(`Warning: the previous '${destinationName}' changed during the fork and was NOT deleted; ` +
|
|
805
|
-
`its pre-fork copy is preserved at ${backupDir}.`);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
else {
|
|
809
|
-
fs.renameSync(stagingDir, destinationDir);
|
|
764
|
+
throw installError;
|
|
810
765
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
try {
|
|
819
|
-
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
766
|
+
// Revalidate before discarding the backup: only delete it if it is
|
|
767
|
+
// still byte-for-byte the original destination we moved aside. If it
|
|
768
|
+
// changed during the install window (a concurrent write to the
|
|
769
|
+
// moved-aside directory), do NOT delete it — leave it in place and
|
|
770
|
+
// surface where it is so nothing is lost.
|
|
771
|
+
if (fingerprintDir(backupDir) === authorizedDestinationFingerprint) {
|
|
772
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
820
773
|
}
|
|
821
|
-
|
|
822
|
-
|
|
774
|
+
else {
|
|
775
|
+
console.error(`Warning: the previous '${destinationName}' changed during the fork and was NOT deleted; ` +
|
|
776
|
+
`its pre-fork copy is preserved at ${backupDir}.`);
|
|
823
777
|
}
|
|
824
|
-
throw error;
|
|
825
|
-
}
|
|
826
|
-
if (spinner)
|
|
827
|
-
spinner.succeed(`Forked '${source}' to '${destinationName}'`);
|
|
828
|
-
if (options?.json) {
|
|
829
|
-
console.log(JSON.stringify({
|
|
830
|
-
forked: true,
|
|
831
|
-
source,
|
|
832
|
-
sourcePath: sourceDir,
|
|
833
|
-
sourceLocation,
|
|
834
|
-
destination: destinationName,
|
|
835
|
-
destinationPath: destinationDir,
|
|
836
|
-
}, null, 2));
|
|
837
778
|
}
|
|
838
779
|
else {
|
|
839
|
-
|
|
840
|
-
console.log(`Destination: ${destinationDir}`);
|
|
841
|
-
console.log(`\nYou can now customize the schema at:`);
|
|
842
|
-
console.log(` ${destinationDir}/schema.yaml`);
|
|
780
|
+
fs.renameSync(stagingDir, destinationDir);
|
|
843
781
|
}
|
|
844
782
|
}
|
|
845
783
|
catch (error) {
|
|
846
|
-
|
|
847
|
-
|
|
784
|
+
// Remove only the staging directory we created this run; the source
|
|
785
|
+
// and any existing destination are left exactly as we found them.
|
|
786
|
+
// Guard the cleanup in its own try/catch so a failed removal (e.g. a
|
|
787
|
+
// locked file on Windows) can never mask the original error, then
|
|
788
|
+
// rethrow so the real failure still drives the JSON/exit-code report.
|
|
789
|
+
try {
|
|
790
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
// Best-effort cleanup; the original error below is what matters.
|
|
794
|
+
}
|
|
795
|
+
throw error;
|
|
796
|
+
}
|
|
797
|
+
if (spinner)
|
|
798
|
+
spinner.succeed(`Forked '${source}' to '${destinationName}'`);
|
|
799
|
+
if (options?.json) {
|
|
800
|
+
console.log(JSON.stringify({
|
|
801
|
+
forked: true,
|
|
802
|
+
source,
|
|
803
|
+
sourcePath: sourceDir,
|
|
804
|
+
sourceLocation,
|
|
805
|
+
destination: destinationName,
|
|
806
|
+
destinationPath: destinationDir,
|
|
807
|
+
}, null, 2));
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
console.log(`\nSource: ${sourceDir} (${sourceLocation})`);
|
|
811
|
+
console.log(`Destination: ${destinationDir}`);
|
|
812
|
+
console.log(`\nYou can now customize the schema at:`);
|
|
813
|
+
console.log(` ${destinationDir}/schema.yaml`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
if (spinner)
|
|
818
|
+
spinner.fail(`Fork failed`);
|
|
819
|
+
if (options?.json) {
|
|
820
|
+
console.log(JSON.stringify({
|
|
821
|
+
forked: false,
|
|
822
|
+
error: error.message,
|
|
823
|
+
}, null, 2));
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
console.error(`Error: ${error.message}`);
|
|
827
|
+
}
|
|
828
|
+
process.exitCode = 1;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
export async function schemaInitCommand(name, options) {
|
|
832
|
+
const spinner = options?.json ? null : ora();
|
|
833
|
+
try {
|
|
834
|
+
const projectRoot = process.cwd();
|
|
835
|
+
// Validate name
|
|
836
|
+
if (!isValidSchemaName(name)) {
|
|
848
837
|
if (options?.json) {
|
|
849
838
|
console.log(JSON.stringify({
|
|
850
|
-
|
|
851
|
-
error:
|
|
839
|
+
created: false,
|
|
840
|
+
error: `Invalid schema name '${name}'. Use kebab-case (e.g., my-workflow)`,
|
|
852
841
|
}, null, 2));
|
|
853
842
|
}
|
|
854
843
|
else {
|
|
855
|
-
console.error(`Error: ${
|
|
844
|
+
console.error(`Error: Invalid schema name '${name}'`);
|
|
845
|
+
console.error('Schema names must be kebab-case (e.g., my-workflow)');
|
|
856
846
|
}
|
|
857
847
|
process.exitCode = 1;
|
|
848
|
+
return;
|
|
858
849
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
.option('--json', 'Output as JSON')
|
|
865
|
-
.option('--description <text>', 'Schema description')
|
|
866
|
-
.option('--artifacts <list>', 'Comma-separated artifact IDs (proposal,specs,design,tasks)')
|
|
867
|
-
.option('--default', 'Set as project default schema')
|
|
868
|
-
.option('--no-default', 'Do not prompt to set as default')
|
|
869
|
-
.option('--force', 'Overwrite existing schema')
|
|
870
|
-
.action(async (name, options) => {
|
|
871
|
-
const spinner = options?.json ? null : ora();
|
|
872
|
-
try {
|
|
873
|
-
const projectRoot = process.cwd();
|
|
874
|
-
// Validate name
|
|
875
|
-
if (!isValidSchemaName(name)) {
|
|
850
|
+
const schemaDir = path.join(getProjectSchemasDir(projectRoot), name);
|
|
851
|
+
// Check overwrite permission without mutating the destination
|
|
852
|
+
const schemaExists = fs.existsSync(schemaDir);
|
|
853
|
+
if (schemaExists) {
|
|
854
|
+
if (!options?.force) {
|
|
876
855
|
if (options?.json) {
|
|
877
856
|
console.log(JSON.stringify({
|
|
878
857
|
created: false,
|
|
879
|
-
error: `
|
|
858
|
+
error: `Schema '${name}' already exists`,
|
|
859
|
+
suggestion: 'Use --force to overwrite or "openspec schema fork" to copy',
|
|
880
860
|
}, null, 2));
|
|
881
861
|
}
|
|
882
862
|
else {
|
|
883
|
-
console.error(`Error:
|
|
884
|
-
console.error('
|
|
863
|
+
console.error(`Error: Schema '${name}' already exists at ${schemaDir}`);
|
|
864
|
+
console.error('Use --force to overwrite or "openspec schema fork" to copy');
|
|
885
865
|
}
|
|
886
866
|
process.exitCode = 1;
|
|
887
867
|
return;
|
|
888
868
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const isInteractive = !options?.json && !hasExplicitOptions && process.stdout.isTTY;
|
|
915
|
-
if (isInteractive) {
|
|
916
|
-
// Interactive mode
|
|
917
|
-
const { input, checkbox, confirm } = await import('@inquirer/prompts');
|
|
918
|
-
description = await input({
|
|
919
|
-
message: 'Schema description:',
|
|
920
|
-
default: `Custom workflow schema for ${name}`,
|
|
921
|
-
});
|
|
922
|
-
const artifactChoices = DEFAULT_ARTIFACTS.map((a) => ({
|
|
923
|
-
name: a.id,
|
|
924
|
-
value: a.id,
|
|
925
|
-
checked: true,
|
|
926
|
-
}));
|
|
927
|
-
selectedArtifactIds = await checkbox({
|
|
928
|
-
message: 'Select artifacts to include:',
|
|
929
|
-
theme: {
|
|
930
|
-
icon: {
|
|
931
|
-
checked: '[x]',
|
|
932
|
-
unchecked: '[ ]',
|
|
933
|
-
},
|
|
869
|
+
}
|
|
870
|
+
// Determine artifacts and description
|
|
871
|
+
let description;
|
|
872
|
+
let selectedArtifactIds;
|
|
873
|
+
// Check if we have explicit flags (non-interactive mode)
|
|
874
|
+
const hasExplicitOptions = options?.description !== undefined || options?.artifacts !== undefined;
|
|
875
|
+
const isInteractive = !options?.json && !hasExplicitOptions && process.stdout.isTTY;
|
|
876
|
+
if (isInteractive) {
|
|
877
|
+
// Interactive mode
|
|
878
|
+
const { input, checkbox, confirm } = await import('@inquirer/prompts');
|
|
879
|
+
description = await input({
|
|
880
|
+
message: 'Schema description:',
|
|
881
|
+
default: `Custom workflow schema for ${name}`,
|
|
882
|
+
});
|
|
883
|
+
const artifactChoices = DEFAULT_ARTIFACTS.map((a) => ({
|
|
884
|
+
name: a.id,
|
|
885
|
+
value: a.id,
|
|
886
|
+
checked: true,
|
|
887
|
+
}));
|
|
888
|
+
selectedArtifactIds = await checkbox({
|
|
889
|
+
message: 'Select artifacts to include:',
|
|
890
|
+
theme: {
|
|
891
|
+
icon: {
|
|
892
|
+
checked: '[x]',
|
|
893
|
+
unchecked: '[ ]',
|
|
934
894
|
},
|
|
935
|
-
|
|
895
|
+
},
|
|
896
|
+
choices: artifactChoices,
|
|
897
|
+
});
|
|
898
|
+
if (selectedArtifactIds.length === 0) {
|
|
899
|
+
console.error('Error: At least one artifact must be selected');
|
|
900
|
+
process.exitCode = 1;
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
// Ask about setting as default (unless --no-default was passed)
|
|
904
|
+
if (options?.default === undefined) {
|
|
905
|
+
const setAsDefault = await confirm({
|
|
906
|
+
message: 'Set as project default schema?',
|
|
907
|
+
default: false,
|
|
936
908
|
});
|
|
937
|
-
if (
|
|
938
|
-
|
|
939
|
-
process.exitCode = 1;
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
// Ask about setting as default (unless --no-default was passed)
|
|
943
|
-
if (options?.default === undefined) {
|
|
944
|
-
const setAsDefault = await confirm({
|
|
945
|
-
message: 'Set as project default schema?',
|
|
946
|
-
default: false,
|
|
947
|
-
});
|
|
948
|
-
if (setAsDefault) {
|
|
949
|
-
options = { ...options, default: true };
|
|
950
|
-
}
|
|
909
|
+
if (setAsDefault) {
|
|
910
|
+
options = { ...options, default: true };
|
|
951
911
|
}
|
|
952
912
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
}
|
|
969
|
-
else {
|
|
970
|
-
console.error(`Error: Unknown artifact '${id}'`);
|
|
971
|
-
console.error(`Valid artifacts: ${validIds.join(', ')}`);
|
|
972
|
-
}
|
|
973
|
-
process.exitCode = 1;
|
|
974
|
-
return;
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
// Non-interactive mode
|
|
916
|
+
description = options?.description || `Custom workflow schema for ${name}`;
|
|
917
|
+
if (options?.artifacts) {
|
|
918
|
+
selectedArtifactIds = options.artifacts.split(',').map((a) => a.trim());
|
|
919
|
+
// Validate artifact IDs
|
|
920
|
+
const validIds = DEFAULT_ARTIFACTS.map((a) => a.id);
|
|
921
|
+
for (const id of selectedArtifactIds) {
|
|
922
|
+
if (!validIds.includes(id)) {
|
|
923
|
+
if (options?.json) {
|
|
924
|
+
console.log(JSON.stringify({
|
|
925
|
+
created: false,
|
|
926
|
+
error: `Unknown artifact '${id}'`,
|
|
927
|
+
valid: validIds,
|
|
928
|
+
}, null, 2));
|
|
975
929
|
}
|
|
930
|
+
else {
|
|
931
|
+
console.error(`Error: Unknown artifact '${id}'`);
|
|
932
|
+
console.error(`Valid artifacts: ${validIds.join(', ')}`);
|
|
933
|
+
}
|
|
934
|
+
process.exitCode = 1;
|
|
935
|
+
return;
|
|
976
936
|
}
|
|
977
937
|
}
|
|
978
|
-
else {
|
|
979
|
-
// Default to all artifacts
|
|
980
|
-
selectedArtifactIds = DEFAULT_ARTIFACTS.map((a) => a.id);
|
|
981
|
-
}
|
|
982
938
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
else if (id === 'design' && selectedArtifactIds.includes('specs')) {
|
|
998
|
-
artifact.requires = ['specs'];
|
|
999
|
-
}
|
|
1000
|
-
else if (id === 'tasks') {
|
|
1001
|
-
const requires = [];
|
|
1002
|
-
if (selectedArtifactIds.includes('design'))
|
|
1003
|
-
requires.push('design');
|
|
1004
|
-
else if (selectedArtifactIds.includes('specs'))
|
|
1005
|
-
requires.push('specs');
|
|
1006
|
-
artifact.requires = requires;
|
|
1007
|
-
}
|
|
1008
|
-
return artifact;
|
|
1009
|
-
});
|
|
1010
|
-
// Create schema.yaml
|
|
1011
|
-
const schema = {
|
|
1012
|
-
name,
|
|
1013
|
-
version: 1,
|
|
1014
|
-
description,
|
|
1015
|
-
artifacts: selectedArtifacts,
|
|
939
|
+
else {
|
|
940
|
+
// Default to all artifacts
|
|
941
|
+
selectedArtifactIds = DEFAULT_ARTIFACTS.map((a) => a.id);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// Build artifacts array with proper dependencies
|
|
945
|
+
const selectedArtifacts = selectedArtifactIds.map((id) => {
|
|
946
|
+
const template = DEFAULT_ARTIFACTS.find((a) => a.id === id);
|
|
947
|
+
const artifact = {
|
|
948
|
+
id: template.id,
|
|
949
|
+
generates: template.generates,
|
|
950
|
+
description: template.description,
|
|
951
|
+
template: template.template,
|
|
952
|
+
requires: [],
|
|
1016
953
|
};
|
|
1017
|
-
//
|
|
1018
|
-
if (selectedArtifactIds.includes('
|
|
1019
|
-
|
|
1020
|
-
requires: ['tasks'],
|
|
1021
|
-
tracks: 'tasks.md',
|
|
1022
|
-
};
|
|
954
|
+
// Set up dependencies based on typical workflow
|
|
955
|
+
if (id === 'specs' && selectedArtifactIds.includes('proposal')) {
|
|
956
|
+
artifact.requires = ['proposal'];
|
|
1023
957
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
958
|
+
else if (id === 'design' && selectedArtifactIds.includes('specs')) {
|
|
959
|
+
artifact.requires = ['specs'];
|
|
960
|
+
}
|
|
961
|
+
else if (id === 'tasks') {
|
|
962
|
+
const requires = [];
|
|
963
|
+
if (selectedArtifactIds.includes('design'))
|
|
964
|
+
requires.push('design');
|
|
965
|
+
else if (selectedArtifactIds.includes('specs'))
|
|
966
|
+
requires.push('specs');
|
|
967
|
+
artifact.requires = requires;
|
|
968
|
+
}
|
|
969
|
+
return artifact;
|
|
970
|
+
});
|
|
971
|
+
// Create schema.yaml
|
|
972
|
+
const schema = {
|
|
973
|
+
name,
|
|
974
|
+
version: 1,
|
|
975
|
+
description,
|
|
976
|
+
artifacts: selectedArtifacts,
|
|
977
|
+
};
|
|
978
|
+
// Add apply phase if tasks is included
|
|
979
|
+
if (selectedArtifactIds.includes('tasks')) {
|
|
980
|
+
schema.apply = {
|
|
981
|
+
requires: ['tasks'],
|
|
982
|
+
tracks: 'tasks.md',
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
// Parse and serialize the config before staging any schema files. This
|
|
986
|
+
// makes malformed, non-object, linked, and read-only configs fail before
|
|
987
|
+
// an existing schema can be moved or a new one can appear.
|
|
988
|
+
const preparedConfig = options?.default
|
|
989
|
+
? await prepareDefaultConfigUpdate(projectRoot, name)
|
|
990
|
+
: null;
|
|
991
|
+
const schemasDir = getProjectSchemasDir(projectRoot);
|
|
992
|
+
FileSystemUtils.assertProjectArtifactPath(projectRoot, schemaDir);
|
|
993
|
+
const authorizedSchemaFingerprint = schemaExists
|
|
994
|
+
? fingerprintDir(schemaDir)
|
|
995
|
+
: null;
|
|
996
|
+
if (spinner)
|
|
997
|
+
spinner.start(`Creating schema '${name}'...`);
|
|
998
|
+
fs.mkdirSync(schemasDir, { recursive: true });
|
|
999
|
+
const schemaStagingDir = fs.mkdtempSync(path.join(schemasDir, '.init-staging-'));
|
|
1000
|
+
let configStagingDir = null;
|
|
1001
|
+
let stagedConfigPath = null;
|
|
1002
|
+
try {
|
|
1003
|
+
fs.writeFileSync(path.join(schemaStagingDir, 'schema.yaml'), stringifyYaml(schema));
|
|
1004
|
+
const templatesDir = path.join(schemaStagingDir, 'templates');
|
|
1005
|
+
for (const artifact of selectedArtifacts) {
|
|
1006
|
+
const templatePath = path.join(templatesDir, artifact.template);
|
|
1007
|
+
fs.mkdirSync(path.dirname(templatePath), { recursive: true });
|
|
1008
|
+
fs.writeFileSync(templatePath, createDefaultTemplate(artifact.id));
|
|
1009
|
+
}
|
|
1010
|
+
const validation = validateSchema(schemaStagingDir);
|
|
1011
|
+
if (!validation.valid) {
|
|
1012
|
+
throw new Error(`Generated schema failed validation: ${validation.issues
|
|
1013
|
+
.map((issue) => issue.message)
|
|
1014
|
+
.join('; ')}`);
|
|
1015
|
+
}
|
|
1016
|
+
if (preparedConfig) {
|
|
1017
|
+
const configDir = path.dirname(preparedConfig.path);
|
|
1018
|
+
configStagingDir = fs.mkdtempSync(path.join(configDir, '.schema-init-config-'));
|
|
1019
|
+
stagedConfigPath = path.join(configStagingDir, path.basename(preparedConfig.path));
|
|
1020
|
+
fs.writeFileSync(stagedConfigPath, preparedConfig.content);
|
|
1021
|
+
if (preparedConfig.originalMode !== null) {
|
|
1022
|
+
fs.chmodSync(stagedConfigPath, preparedConfig.originalMode);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// Re-resolve both destinations immediately before the first move so
|
|
1026
|
+
// a parent symlink swap during staging cannot redirect the commit.
|
|
1031
1027
|
FileSystemUtils.assertProjectArtifactPath(projectRoot, schemaDir);
|
|
1032
|
-
|
|
1028
|
+
if (preparedConfig) {
|
|
1029
|
+
FileSystemUtils.assertProjectArtifactPath(projectRoot, preparedConfig.path);
|
|
1030
|
+
}
|
|
1031
|
+
const currentSchemaFingerprint = fs.existsSync(schemaDir)
|
|
1033
1032
|
? fingerprintDir(schemaDir)
|
|
1034
1033
|
: null;
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1034
|
+
if (currentSchemaFingerprint !== authorizedSchemaFingerprint) {
|
|
1035
|
+
throw new Error(`Schema '${name}' changed on disk while initialization was being prepared. ` +
|
|
1036
|
+
'Aborted to preserve those concurrent changes.');
|
|
1037
|
+
}
|
|
1038
|
+
if (preparedConfig && !configMatchesPreparedState(preparedConfig)) {
|
|
1039
|
+
throw new Error(`${path.basename(preparedConfig.path)} changed on disk while initialization was being prepared. ` +
|
|
1040
|
+
'Aborted to preserve those concurrent changes.');
|
|
1041
|
+
}
|
|
1042
|
+
const token = `${process.pid}-${Date.now()}`;
|
|
1043
|
+
const schemaBackup = `${schemaDir}.init-backup-${token}`;
|
|
1044
|
+
const configBackup = preparedConfig
|
|
1045
|
+
? `${preparedConfig.path}.init-backup-${token}`
|
|
1046
|
+
: null;
|
|
1047
|
+
let schemaBackedUp = false;
|
|
1048
|
+
let configBackedUp = false;
|
|
1049
|
+
let schemaInstalled = false;
|
|
1050
|
+
let configInstalled = false;
|
|
1041
1051
|
try {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const templatePath = path.join(templatesDir, artifact.template);
|
|
1046
|
-
fs.mkdirSync(path.dirname(templatePath), { recursive: true });
|
|
1047
|
-
fs.writeFileSync(templatePath, createDefaultTemplate(artifact.id));
|
|
1048
|
-
}
|
|
1049
|
-
const validation = validateSchema(schemaStagingDir);
|
|
1050
|
-
if (!validation.valid) {
|
|
1051
|
-
throw new Error(`Generated schema failed validation: ${validation.issues
|
|
1052
|
-
.map((issue) => issue.message)
|
|
1053
|
-
.join('; ')}`);
|
|
1054
|
-
}
|
|
1055
|
-
if (preparedConfig) {
|
|
1056
|
-
const configDir = path.dirname(preparedConfig.path);
|
|
1057
|
-
configStagingDir = fs.mkdtempSync(path.join(configDir, '.schema-init-config-'));
|
|
1058
|
-
stagedConfigPath = path.join(configStagingDir, path.basename(preparedConfig.path));
|
|
1059
|
-
fs.writeFileSync(stagedConfigPath, preparedConfig.content);
|
|
1060
|
-
if (preparedConfig.originalMode !== null) {
|
|
1061
|
-
fs.chmodSync(stagedConfigPath, preparedConfig.originalMode);
|
|
1062
|
-
}
|
|
1052
|
+
if (schemaExists) {
|
|
1053
|
+
schemaInitFileOperations.renameSync(schemaDir, schemaBackup);
|
|
1054
|
+
schemaBackedUp = true;
|
|
1063
1055
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
if (preparedConfig) {
|
|
1068
|
-
FileSystemUtils.assertProjectArtifactPath(projectRoot, preparedConfig.path);
|
|
1056
|
+
if (preparedConfig && preparedConfig.originalContent !== null) {
|
|
1057
|
+
schemaInitFileOperations.renameSync(preparedConfig.path, configBackup);
|
|
1058
|
+
configBackedUp = true;
|
|
1069
1059
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
'Aborted to preserve those concurrent changes.');
|
|
1076
|
-
}
|
|
1077
|
-
if (preparedConfig && !configMatchesPreparedState(preparedConfig)) {
|
|
1078
|
-
throw new Error(`${path.basename(preparedConfig.path)} changed on disk while initialization was being prepared. ` +
|
|
1079
|
-
'Aborted to preserve those concurrent changes.');
|
|
1060
|
+
schemaInitFileOperations.renameSync(schemaStagingDir, schemaDir);
|
|
1061
|
+
schemaInstalled = true;
|
|
1062
|
+
if (preparedConfig && stagedConfigPath) {
|
|
1063
|
+
schemaInitFileOperations.renameSync(stagedConfigPath, preparedConfig.path);
|
|
1064
|
+
configInstalled = true;
|
|
1080
1065
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
? `${preparedConfig.path}.init-backup-${token}`
|
|
1085
|
-
: null;
|
|
1086
|
-
let schemaBackedUp = false;
|
|
1087
|
-
let configBackedUp = false;
|
|
1088
|
-
let schemaInstalled = false;
|
|
1089
|
-
let configInstalled = false;
|
|
1066
|
+
}
|
|
1067
|
+
catch (installError) {
|
|
1068
|
+
const rollbackErrors = [];
|
|
1090
1069
|
try {
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
schemaBackedUp = true;
|
|
1094
|
-
}
|
|
1095
|
-
if (preparedConfig && preparedConfig.originalContent !== null) {
|
|
1096
|
-
schemaInitFileOperations.renameSync(preparedConfig.path, configBackup);
|
|
1097
|
-
configBackedUp = true;
|
|
1070
|
+
if (configInstalled && preparedConfig) {
|
|
1071
|
+
fs.rmSync(preparedConfig.path, { force: true });
|
|
1098
1072
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
if (preparedConfig && stagedConfigPath) {
|
|
1102
|
-
schemaInitFileOperations.renameSync(stagedConfigPath, preparedConfig.path);
|
|
1103
|
-
configInstalled = true;
|
|
1073
|
+
if (configBackedUp && preparedConfig && configBackup) {
|
|
1074
|
+
schemaInitFileOperations.renameSync(configBackup, preparedConfig.path);
|
|
1104
1075
|
}
|
|
1105
1076
|
}
|
|
1106
|
-
catch (
|
|
1107
|
-
|
|
1108
|
-
try {
|
|
1109
|
-
if (configInstalled && preparedConfig) {
|
|
1110
|
-
fs.rmSync(preparedConfig.path, { force: true });
|
|
1111
|
-
}
|
|
1112
|
-
if (configBackedUp && preparedConfig && configBackup) {
|
|
1113
|
-
schemaInitFileOperations.renameSync(configBackup, preparedConfig.path);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
catch (rollbackError) {
|
|
1117
|
-
rollbackErrors.push(`config: ${rollbackError.message}`);
|
|
1118
|
-
}
|
|
1119
|
-
try {
|
|
1120
|
-
if (schemaInstalled) {
|
|
1121
|
-
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
1122
|
-
}
|
|
1123
|
-
if (schemaBackedUp) {
|
|
1124
|
-
schemaInitFileOperations.renameSync(schemaBackup, schemaDir);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
catch (rollbackError) {
|
|
1128
|
-
rollbackErrors.push(`schema: ${rollbackError.message}`);
|
|
1129
|
-
}
|
|
1130
|
-
if (rollbackErrors.length > 0) {
|
|
1131
|
-
throw new Error(`Schema initialization failed and rollback was incomplete (${rollbackErrors.join(', ')}). ` +
|
|
1132
|
-
`Recovery backups may remain beside ${schemaDir} and ${preparedConfig?.path ?? 'the config file'}.`, { cause: installError });
|
|
1133
|
-
}
|
|
1134
|
-
throw installError;
|
|
1077
|
+
catch (rollbackError) {
|
|
1078
|
+
rollbackErrors.push(`config: ${rollbackError.message}`);
|
|
1135
1079
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
for (const backup of [
|
|
1140
|
-
schemaBackedUp ? schemaBackup : null,
|
|
1141
|
-
configBackedUp ? configBackup : null,
|
|
1142
|
-
]) {
|
|
1143
|
-
if (!backup)
|
|
1144
|
-
continue;
|
|
1145
|
-
try {
|
|
1146
|
-
fs.rmSync(backup, { recursive: true, force: true });
|
|
1080
|
+
try {
|
|
1081
|
+
if (schemaInstalled) {
|
|
1082
|
+
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
1147
1083
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1084
|
+
if (schemaBackedUp) {
|
|
1085
|
+
schemaInitFileOperations.renameSync(schemaBackup, schemaDir);
|
|
1150
1086
|
}
|
|
1151
1087
|
}
|
|
1088
|
+
catch (rollbackError) {
|
|
1089
|
+
rollbackErrors.push(`schema: ${rollbackError.message}`);
|
|
1090
|
+
}
|
|
1091
|
+
if (rollbackErrors.length > 0) {
|
|
1092
|
+
throw new Error(`Schema initialization failed and rollback was incomplete (${rollbackErrors.join(', ')}). ` +
|
|
1093
|
+
`Recovery backups may remain beside ${schemaDir} and ${preparedConfig?.path ?? 'the config file'}.`, { cause: installError });
|
|
1094
|
+
}
|
|
1095
|
+
throw installError;
|
|
1152
1096
|
}
|
|
1153
|
-
|
|
1097
|
+
// The transaction is committed. Cleanup cannot turn success into a
|
|
1098
|
+
// false failure, so leave a recoverable backup and warn if removal is
|
|
1099
|
+
// blocked instead of reporting that initialization failed.
|
|
1100
|
+
for (const backup of [
|
|
1101
|
+
schemaBackedUp ? schemaBackup : null,
|
|
1102
|
+
configBackedUp ? configBackup : null,
|
|
1103
|
+
]) {
|
|
1104
|
+
if (!backup)
|
|
1105
|
+
continue;
|
|
1154
1106
|
try {
|
|
1155
|
-
fs.rmSync(
|
|
1107
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
1156
1108
|
}
|
|
1157
|
-
catch {
|
|
1158
|
-
|
|
1109
|
+
catch (cleanupError) {
|
|
1110
|
+
console.error(`Warning: initialization succeeded, but the backup at ${backup} could not be removed: ${cleanupError.message}`);
|
|
1159
1111
|
}
|
|
1160
|
-
throw error;
|
|
1161
1112
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
catch {
|
|
1168
|
-
// Best-effort cleanup. A committed config has already moved out.
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1113
|
+
}
|
|
1114
|
+
catch (error) {
|
|
1115
|
+
try {
|
|
1116
|
+
fs.rmSync(schemaStagingDir, { recursive: true, force: true });
|
|
1171
1117
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if (options?.json) {
|
|
1175
|
-
console.log(JSON.stringify({
|
|
1176
|
-
created: true,
|
|
1177
|
-
path: schemaDir,
|
|
1178
|
-
schema: name,
|
|
1179
|
-
artifacts: selectedArtifactIds,
|
|
1180
|
-
setAsDefault: options?.default || false,
|
|
1181
|
-
}, null, 2));
|
|
1118
|
+
catch {
|
|
1119
|
+
// Best-effort cleanup must not hide the operation's real error.
|
|
1182
1120
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1121
|
+
throw error;
|
|
1122
|
+
}
|
|
1123
|
+
finally {
|
|
1124
|
+
if (configStagingDir) {
|
|
1125
|
+
try {
|
|
1126
|
+
fs.rmSync(configStagingDir, { recursive: true, force: true });
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
// Best-effort cleanup. A committed config has already moved out.
|
|
1188
1130
|
}
|
|
1189
|
-
console.log(`\nNext steps:`);
|
|
1190
|
-
console.log(` 1. Edit ${schemaDir}/schema.yaml to customize artifacts`);
|
|
1191
|
-
console.log(` 2. Modify templates in the schema directory`);
|
|
1192
|
-
console.log(` 3. Use with: openspec new --schema ${name}`);
|
|
1193
1131
|
}
|
|
1194
1132
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1133
|
+
if (spinner)
|
|
1134
|
+
spinner.succeed(`Created schema '${name}'`);
|
|
1135
|
+
if (options?.json) {
|
|
1136
|
+
console.log(JSON.stringify({
|
|
1137
|
+
created: true,
|
|
1138
|
+
path: schemaDir,
|
|
1139
|
+
schema: name,
|
|
1140
|
+
artifacts: selectedArtifactIds,
|
|
1141
|
+
setAsDefault: options?.default || false,
|
|
1142
|
+
}, null, 2));
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
console.log(`\nSchema created at: ${schemaDir}`);
|
|
1146
|
+
console.log(`\nArtifacts: ${selectedArtifactIds.join(', ')}`);
|
|
1147
|
+
if (options?.default) {
|
|
1148
|
+
console.log(`\nSet as project default schema.`);
|
|
1206
1149
|
}
|
|
1207
|
-
|
|
1150
|
+
console.log(`\nNext steps:`);
|
|
1151
|
+
console.log(` 1. Edit ${schemaDir}/schema.yaml to customize artifacts`);
|
|
1152
|
+
console.log(` 2. Modify templates in the schema directory`);
|
|
1153
|
+
console.log(` 3. Use with: openspec new --schema ${name}`);
|
|
1208
1154
|
}
|
|
1209
|
-
}
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
if (spinner)
|
|
1158
|
+
spinner.fail(`Creation failed`);
|
|
1159
|
+
if (options?.json) {
|
|
1160
|
+
console.log(JSON.stringify({
|
|
1161
|
+
created: false,
|
|
1162
|
+
error: error.message,
|
|
1163
|
+
}, null, 2));
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
console.error(`Error: ${error.message}`);
|
|
1167
|
+
}
|
|
1168
|
+
process.exitCode = 1;
|
|
1169
|
+
}
|
|
1210
1170
|
}
|
|
1211
1171
|
/**
|
|
1212
1172
|
* Create default template content for an artifact.
|