lightspec 0.1.1
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/LICENSE +22 -0
- package/README.md +435 -0
- package/bin/lightspec.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +361 -0
- package/dist/commands/change.d.ts +35 -0
- package/dist/commands/change.js +277 -0
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +257 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +198 -0
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +183 -0
- package/dist/commands/show.d.ts +14 -0
- package/dist/commands/show.js +132 -0
- package/dist/commands/spec.d.ts +15 -0
- package/dist/commands/spec.js +225 -0
- package/dist/commands/validate.d.ts +24 -0
- package/dist/commands/validate.js +294 -0
- package/dist/core/archive.d.ts +11 -0
- package/dist/core/archive.js +280 -0
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +456 -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 +64 -0
- package/dist/core/completions/factory.js +75 -0
- package/dist/core/completions/generators/bash-generator.d.ts +32 -0
- package/dist/core/completions/generators/bash-generator.js +174 -0
- package/dist/core/completions/generators/fish-generator.d.ts +32 -0
- package/dist/core/completions/generators/fish-generator.js +157 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
- package/dist/core/completions/generators/powershell-generator.js +207 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +44 -0
- package/dist/core/completions/generators/zsh-generator.js +250 -0
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +318 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +143 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
- package/dist/core/completions/installers/powershell-installer.js +327 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +125 -0
- package/dist/core/completions/installers/zsh-installer.js +449 -0
- package/dist/core/completions/templates/bash-templates.d.ts +6 -0
- package/dist/core/completions/templates/bash-templates.js +24 -0
- package/dist/core/completions/templates/fish-templates.d.ts +7 -0
- package/dist/core/completions/templates/fish-templates.js +39 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
- package/dist/core/completions/templates/powershell-templates.js +25 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
- package/dist/core/completions/templates/zsh-templates.js +36 -0
- package/dist/core/completions/types.d.ts +79 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +34 -0
- package/dist/core/config-schema.d.ts +76 -0
- package/dist/core/config-schema.js +200 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +30 -0
- package/dist/core/configurators/agents.d.ts +8 -0
- package/dist/core/configurators/agents.js +15 -0
- package/dist/core/configurators/base.d.ts +7 -0
- package/dist/core/configurators/base.js +2 -0
- package/dist/core/configurators/claude.d.ts +8 -0
- package/dist/core/configurators/claude.js +15 -0
- package/dist/core/configurators/cline.d.ts +8 -0
- package/dist/core/configurators/cline.js +15 -0
- package/dist/core/configurators/codebuddy.d.ts +8 -0
- package/dist/core/configurators/codebuddy.js +15 -0
- package/dist/core/configurators/costrict.d.ts +8 -0
- package/dist/core/configurators/costrict.js +15 -0
- package/dist/core/configurators/iflow.d.ts +8 -0
- package/dist/core/configurators/iflow.js +15 -0
- package/dist/core/configurators/qoder.d.ts +30 -0
- package/dist/core/configurators/qoder.js +42 -0
- package/dist/core/configurators/qwen.d.ts +24 -0
- package/dist/core/configurators/qwen.js +37 -0
- package/dist/core/configurators/registry.d.ts +9 -0
- package/dist/core/configurators/registry.js +43 -0
- package/dist/core/configurators/slash/amazon-q.d.ts +9 -0
- package/dist/core/configurators/slash/amazon-q.js +46 -0
- package/dist/core/configurators/slash/antigravity.d.ts +9 -0
- package/dist/core/configurators/slash/antigravity.js +23 -0
- package/dist/core/configurators/slash/auggie.d.ts +9 -0
- package/dist/core/configurators/slash/auggie.js +31 -0
- package/dist/core/configurators/slash/base.d.ts +19 -0
- package/dist/core/configurators/slash/base.js +69 -0
- package/dist/core/configurators/slash/claude.d.ts +9 -0
- package/dist/core/configurators/slash/claude.js +37 -0
- package/dist/core/configurators/slash/cline.d.ts +9 -0
- package/dist/core/configurators/slash/cline.js +23 -0
- package/dist/core/configurators/slash/codebuddy.d.ts +9 -0
- package/dist/core/configurators/slash/codebuddy.js +34 -0
- package/dist/core/configurators/slash/codex.d.ts +14 -0
- package/dist/core/configurators/slash/codex.js +109 -0
- package/dist/core/configurators/slash/continue.d.ts +9 -0
- package/dist/core/configurators/slash/continue.js +46 -0
- package/dist/core/configurators/slash/costrict.d.ts +9 -0
- package/dist/core/configurators/slash/costrict.js +31 -0
- package/dist/core/configurators/slash/crush.d.ts +9 -0
- package/dist/core/configurators/slash/crush.js +37 -0
- package/dist/core/configurators/slash/cursor.d.ts +9 -0
- package/dist/core/configurators/slash/cursor.js +37 -0
- package/dist/core/configurators/slash/factory.d.ts +10 -0
- package/dist/core/configurators/slash/factory.js +35 -0
- package/dist/core/configurators/slash/gemini.d.ts +9 -0
- package/dist/core/configurators/slash/gemini.js +22 -0
- package/dist/core/configurators/slash/github-copilot.d.ts +9 -0
- package/dist/core/configurators/slash/github-copilot.js +34 -0
- package/dist/core/configurators/slash/iflow.d.ts +9 -0
- package/dist/core/configurators/slash/iflow.js +37 -0
- package/dist/core/configurators/slash/kilocode.d.ts +9 -0
- package/dist/core/configurators/slash/kilocode.js +17 -0
- package/dist/core/configurators/slash/opencode.d.ts +12 -0
- package/dist/core/configurators/slash/opencode.js +72 -0
- package/dist/core/configurators/slash/qoder.d.ts +35 -0
- package/dist/core/configurators/slash/qoder.js +76 -0
- package/dist/core/configurators/slash/qwen.d.ts +32 -0
- package/dist/core/configurators/slash/qwen.js +49 -0
- package/dist/core/configurators/slash/registry.d.ts +8 -0
- package/dist/core/configurators/slash/registry.js +78 -0
- package/dist/core/configurators/slash/roocode.d.ts +9 -0
- package/dist/core/configurators/slash/roocode.js +23 -0
- package/dist/core/configurators/slash/toml-base.d.ts +10 -0
- package/dist/core/configurators/slash/toml-base.js +53 -0
- package/dist/core/configurators/slash/windsurf.d.ts +9 -0
- package/dist/core/configurators/slash/windsurf.js +23 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/global-config.d.ts +39 -0
- package/dist/core/global-config.js +115 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/init.d.ts +52 -0
- package/dist/core/init.js +644 -0
- package/dist/core/list.d.ts +9 -0
- package/dist/core/list.js +171 -0
- package/dist/core/parsers/change-parser.d.ts +13 -0
- package/dist/core/parsers/change-parser.js +193 -0
- package/dist/core/parsers/markdown-parser.d.ts +22 -0
- package/dist/core/parsers/markdown-parser.js +187 -0
- package/dist/core/parsers/requirement-blocks.d.ts +37 -0
- package/dist/core/parsers/requirement-blocks.js +201 -0
- package/dist/core/project-config.d.ts +64 -0
- package/dist/core/project-config.js +223 -0
- package/dist/core/schemas/base.schema.d.ts +13 -0
- package/dist/core/schemas/base.schema.js +13 -0
- package/dist/core/schemas/change.schema.d.ts +73 -0
- package/dist/core/schemas/change.schema.js +31 -0
- package/dist/core/schemas/index.d.ts +4 -0
- package/dist/core/schemas/index.js +4 -0
- package/dist/core/schemas/spec.schema.d.ts +18 -0
- package/dist/core/schemas/spec.schema.js +15 -0
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +384 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/agents-root-stub.d.ts +2 -0
- package/dist/core/templates/agents-root-stub.js +17 -0
- package/dist/core/templates/agents-template.d.ts +2 -0
- package/dist/core/templates/agents-template.js +458 -0
- package/dist/core/templates/claude-template.d.ts +2 -0
- package/dist/core/templates/claude-template.js +2 -0
- package/dist/core/templates/cline-template.d.ts +2 -0
- package/dist/core/templates/cline-template.js +2 -0
- package/dist/core/templates/costrict-template.d.ts +2 -0
- package/dist/core/templates/costrict-template.js +2 -0
- package/dist/core/templates/index.d.ts +17 -0
- package/dist/core/templates/index.js +37 -0
- package/dist/core/templates/project-template.d.ts +8 -0
- package/dist/core/templates/project-template.js +32 -0
- package/dist/core/templates/slash-command-templates.d.ts +4 -0
- package/dist/core/templates/slash-command-templates.js +49 -0
- package/dist/core/update.d.ts +4 -0
- package/dist/core/update.js +88 -0
- package/dist/core/validation/constants.d.ts +34 -0
- package/dist/core/validation/constants.js +40 -0
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.js +2 -0
- package/dist/core/validation/validator.d.ts +33 -0
- package/dist/core/validation/validator.js +409 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +168 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/telemetry/config.d.ts +32 -0
- package/dist/telemetry/config.js +68 -0
- package/dist/telemetry/index.d.ts +31 -0
- package/dist/telemetry/index.js +103 -0
- package/dist/utils/file-system.d.ts +25 -0
- package/dist/utils/file-system.js +218 -0
- package/dist/utils/interactive.d.ts +18 -0
- package/dist/utils/interactive.js +21 -0
- package/dist/utils/item-discovery.d.ts +4 -0
- package/dist/utils/item-discovery.js +72 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/dist/utils/task-progress.d.ts +8 -0
- package/dist/utils/task-progress.js +36 -0
- package/package.json +82 -0
- package/scripts/postinstall.js +147 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Validator } from '../core/validation/validator.js';
|
|
4
|
+
import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
|
|
5
|
+
import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
|
|
6
|
+
import { nearestMatches } from '../utils/match.js';
|
|
7
|
+
export class ValidateCommand {
|
|
8
|
+
async execute(itemName, options = {}) {
|
|
9
|
+
const interactive = isInteractive(options);
|
|
10
|
+
// Handle bulk flags first
|
|
11
|
+
if (options.all || options.changes || options.specs) {
|
|
12
|
+
await this.runBulkValidation({
|
|
13
|
+
changes: !!options.all || !!options.changes,
|
|
14
|
+
specs: !!options.all || !!options.specs,
|
|
15
|
+
}, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
// No item and no flags
|
|
19
|
+
if (!itemName) {
|
|
20
|
+
if (interactive) {
|
|
21
|
+
await this.runInteractiveSelector({ strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.printNonInteractiveHint();
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Direct item validation with type detection or override
|
|
29
|
+
const typeOverride = this.normalizeType(options.type);
|
|
30
|
+
await this.validateDirectItem(itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
|
|
31
|
+
}
|
|
32
|
+
normalizeType(value) {
|
|
33
|
+
if (!value)
|
|
34
|
+
return undefined;
|
|
35
|
+
const v = value.toLowerCase();
|
|
36
|
+
if (v === 'change' || v === 'spec')
|
|
37
|
+
return v;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
async runInteractiveSelector(opts) {
|
|
41
|
+
const { select } = await import('@inquirer/prompts');
|
|
42
|
+
const choice = await select({
|
|
43
|
+
message: 'What would you like to validate?',
|
|
44
|
+
choices: [
|
|
45
|
+
{ name: 'All (changes + specs)', value: 'all' },
|
|
46
|
+
{ name: 'All changes', value: 'changes' },
|
|
47
|
+
{ name: 'All specs', value: 'specs' },
|
|
48
|
+
{ name: 'Pick a specific change or spec', value: 'one' },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
if (choice === 'all')
|
|
52
|
+
return this.runBulkValidation({ changes: true, specs: true }, opts);
|
|
53
|
+
if (choice === 'changes')
|
|
54
|
+
return this.runBulkValidation({ changes: true, specs: false }, opts);
|
|
55
|
+
if (choice === 'specs')
|
|
56
|
+
return this.runBulkValidation({ changes: false, specs: true }, opts);
|
|
57
|
+
// one
|
|
58
|
+
const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
|
|
59
|
+
const items = [];
|
|
60
|
+
items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
|
|
61
|
+
items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec', id } })));
|
|
62
|
+
if (items.length === 0) {
|
|
63
|
+
console.error('No items found to validate.');
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const picked = await select({ message: 'Pick an item', choices: items });
|
|
68
|
+
await this.validateByType(picked.type, picked.id, opts);
|
|
69
|
+
}
|
|
70
|
+
printNonInteractiveHint() {
|
|
71
|
+
console.error('Nothing to validate. Try one of:');
|
|
72
|
+
console.error(' lightspec validate --all');
|
|
73
|
+
console.error(' lightspec validate --changes');
|
|
74
|
+
console.error(' lightspec validate --specs');
|
|
75
|
+
console.error(' lightspec validate <item-name>');
|
|
76
|
+
console.error('Or run in an interactive terminal.');
|
|
77
|
+
}
|
|
78
|
+
async validateDirectItem(itemName, opts) {
|
|
79
|
+
const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
|
|
80
|
+
const isChange = changes.includes(itemName);
|
|
81
|
+
const isSpec = specs.includes(itemName);
|
|
82
|
+
const type = opts.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
|
|
83
|
+
if (!type) {
|
|
84
|
+
console.error(`Unknown item '${itemName}'`);
|
|
85
|
+
const suggestions = nearestMatches(itemName, [...changes, ...specs]);
|
|
86
|
+
if (suggestions.length)
|
|
87
|
+
console.error(`Did you mean: ${suggestions.join(', ')}?`);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!opts.typeOverride && isChange && isSpec) {
|
|
92
|
+
console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
|
|
93
|
+
console.error('Pass --type change|spec, or use: lightspec change validate / lightspec spec validate');
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await this.validateByType(type, itemName, opts);
|
|
98
|
+
}
|
|
99
|
+
async validateByType(type, id, opts) {
|
|
100
|
+
const validator = new Validator(opts.strict);
|
|
101
|
+
if (type === 'change') {
|
|
102
|
+
const changeDir = path.join(process.cwd(), 'lightspec', 'changes', id);
|
|
103
|
+
const start = Date.now();
|
|
104
|
+
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
|
105
|
+
const durationMs = Date.now() - start;
|
|
106
|
+
this.printReport('change', id, report, durationMs, opts.json);
|
|
107
|
+
// Non-zero exit if invalid (keeps enriched output test semantics)
|
|
108
|
+
process.exitCode = report.valid ? 0 : 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const file = path.join(process.cwd(), 'lightspec', 'specs', id, 'spec.md');
|
|
112
|
+
const start = Date.now();
|
|
113
|
+
const report = await validator.validateSpec(file);
|
|
114
|
+
const durationMs = Date.now() - start;
|
|
115
|
+
this.printReport('spec', id, report, durationMs, opts.json);
|
|
116
|
+
process.exitCode = report.valid ? 0 : 1;
|
|
117
|
+
}
|
|
118
|
+
printReport(type, id, report, durationMs, json) {
|
|
119
|
+
if (json) {
|
|
120
|
+
const out = { items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }], summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } }, version: '1.0' };
|
|
121
|
+
console.log(JSON.stringify(out, null, 2));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (report.valid) {
|
|
125
|
+
console.log(`${type === 'change' ? 'Change' : 'Specification'} '${id}' is valid`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
|
|
129
|
+
for (const issue of report.issues) {
|
|
130
|
+
const label = issue.level === 'ERROR' ? 'ERROR' : issue.level;
|
|
131
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
132
|
+
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
|
133
|
+
}
|
|
134
|
+
this.printNextSteps(type);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
printNextSteps(type) {
|
|
138
|
+
const bullets = [];
|
|
139
|
+
if (type === 'change') {
|
|
140
|
+
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
|
141
|
+
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
|
142
|
+
bullets.push('- Debug parsed deltas: lightspec change show <id> --json --deltas-only');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
bullets.push('- Ensure spec includes ## Purpose and ## Requirements sections');
|
|
146
|
+
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
|
147
|
+
bullets.push('- Re-run with --json to see structured report');
|
|
148
|
+
}
|
|
149
|
+
console.error('Next steps:');
|
|
150
|
+
bullets.forEach(b => console.error(` ${b}`));
|
|
151
|
+
}
|
|
152
|
+
async runBulkValidation(scope, opts) {
|
|
153
|
+
const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
|
|
154
|
+
const [changeIds, specIds] = await Promise.all([
|
|
155
|
+
scope.changes ? getActiveChangeIds() : Promise.resolve([]),
|
|
156
|
+
scope.specs ? getSpecIds() : Promise.resolve([]),
|
|
157
|
+
]);
|
|
158
|
+
const DEFAULT_CONCURRENCY = 6;
|
|
159
|
+
const maxSuggestions = 5; // used by nearestMatches
|
|
160
|
+
const concurrency = normalizeConcurrency(opts.concurrency) ?? normalizeConcurrency(process.env.LIGHTSPEC_CONCURRENCY) ?? DEFAULT_CONCURRENCY;
|
|
161
|
+
const validator = new Validator(opts.strict);
|
|
162
|
+
const queue = [];
|
|
163
|
+
for (const id of changeIds) {
|
|
164
|
+
queue.push(async () => {
|
|
165
|
+
const start = Date.now();
|
|
166
|
+
const changeDir = path.join(process.cwd(), 'lightspec', 'changes', id);
|
|
167
|
+
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
|
168
|
+
const durationMs = Date.now() - start;
|
|
169
|
+
return { id, type: 'change', valid: report.valid, issues: report.issues, durationMs };
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
for (const id of specIds) {
|
|
173
|
+
queue.push(async () => {
|
|
174
|
+
const start = Date.now();
|
|
175
|
+
const file = path.join(process.cwd(), 'lightspec', 'specs', id, 'spec.md');
|
|
176
|
+
const report = await validator.validateSpec(file);
|
|
177
|
+
const durationMs = Date.now() - start;
|
|
178
|
+
return { id, type: 'spec', valid: report.valid, issues: report.issues, durationMs };
|
|
179
|
+
});
|
|
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
|
+
}
|
|
200
|
+
const results = [];
|
|
201
|
+
let index = 0;
|
|
202
|
+
let running = 0;
|
|
203
|
+
let passed = 0;
|
|
204
|
+
let failed = 0;
|
|
205
|
+
await new Promise((resolve) => {
|
|
206
|
+
const next = () => {
|
|
207
|
+
while (running < concurrency && index < queue.length) {
|
|
208
|
+
const currentIndex = index++;
|
|
209
|
+
const task = queue[currentIndex];
|
|
210
|
+
running++;
|
|
211
|
+
if (spinner)
|
|
212
|
+
spinner.text = `Validating (${currentIndex + 1}/${queue.length})...`;
|
|
213
|
+
task()
|
|
214
|
+
.then(res => {
|
|
215
|
+
results.push(res);
|
|
216
|
+
if (res.valid)
|
|
217
|
+
passed++;
|
|
218
|
+
else
|
|
219
|
+
failed++;
|
|
220
|
+
})
|
|
221
|
+
.catch((error) => {
|
|
222
|
+
const message = error?.message || 'Unknown error';
|
|
223
|
+
const res = { id: getPlannedId(currentIndex, changeIds, specIds) ?? 'unknown', type: getPlannedType(currentIndex, changeIds, specIds) ?? 'change', valid: false, issues: [{ level: 'ERROR', path: 'file', message }], durationMs: 0 };
|
|
224
|
+
results.push(res);
|
|
225
|
+
failed++;
|
|
226
|
+
})
|
|
227
|
+
.finally(() => {
|
|
228
|
+
running--;
|
|
229
|
+
if (index >= queue.length && running === 0)
|
|
230
|
+
resolve();
|
|
231
|
+
else
|
|
232
|
+
next();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
next();
|
|
237
|
+
});
|
|
238
|
+
spinner?.stop();
|
|
239
|
+
results.sort((a, b) => a.id.localeCompare(b.id));
|
|
240
|
+
const summary = {
|
|
241
|
+
totals: { items: results.length, passed, failed },
|
|
242
|
+
byType: {
|
|
243
|
+
...(scope.changes ? { change: summarizeType(results, 'change') } : {}),
|
|
244
|
+
...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
if (opts.json) {
|
|
248
|
+
const out = { items: results, summary, version: '1.0' };
|
|
249
|
+
console.log(JSON.stringify(out, null, 2));
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
for (const res of results) {
|
|
253
|
+
if (res.valid)
|
|
254
|
+
console.log(`✓ ${res.type}/${res.id}`);
|
|
255
|
+
else
|
|
256
|
+
console.error(`✗ ${res.type}/${res.id}`);
|
|
257
|
+
}
|
|
258
|
+
console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
|
|
259
|
+
}
|
|
260
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function summarizeType(results, type) {
|
|
264
|
+
const filtered = results.filter(r => r.type === type);
|
|
265
|
+
const items = filtered.length;
|
|
266
|
+
const passed = filtered.filter(r => r.valid).length;
|
|
267
|
+
const failed = items - passed;
|
|
268
|
+
return { items, passed, failed };
|
|
269
|
+
}
|
|
270
|
+
function normalizeConcurrency(value) {
|
|
271
|
+
if (!value)
|
|
272
|
+
return undefined;
|
|
273
|
+
const n = parseInt(value, 10);
|
|
274
|
+
if (Number.isNaN(n) || n <= 0)
|
|
275
|
+
return undefined;
|
|
276
|
+
return n;
|
|
277
|
+
}
|
|
278
|
+
function getPlannedId(index, changeIds, specIds) {
|
|
279
|
+
const totalChanges = changeIds.length;
|
|
280
|
+
if (index < totalChanges)
|
|
281
|
+
return changeIds[index];
|
|
282
|
+
const specIndex = index - totalChanges;
|
|
283
|
+
return specIds[specIndex];
|
|
284
|
+
}
|
|
285
|
+
function getPlannedType(index, changeIds, specIds) {
|
|
286
|
+
const totalChanges = changeIds.length;
|
|
287
|
+
if (index < totalChanges)
|
|
288
|
+
return 'change';
|
|
289
|
+
const specIndex = index - totalChanges;
|
|
290
|
+
if (specIndex >= 0 && specIndex < specIds.length)
|
|
291
|
+
return 'spec';
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class ArchiveCommand {
|
|
2
|
+
execute(changeName?: string, options?: {
|
|
3
|
+
yes?: boolean;
|
|
4
|
+
skipSpecs?: boolean;
|
|
5
|
+
noValidate?: boolean;
|
|
6
|
+
validate?: boolean;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
private selectChange;
|
|
9
|
+
private getArchiveDate;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
|
+
import { Validator } from './validation/validator.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
|
|
7
|
+
export class ArchiveCommand {
|
|
8
|
+
async execute(changeName, options = {}) {
|
|
9
|
+
const targetPath = '.';
|
|
10
|
+
const changesDir = path.join(targetPath, 'lightspec', 'changes');
|
|
11
|
+
const archiveDir = path.join(changesDir, 'archive');
|
|
12
|
+
const mainSpecsDir = path.join(targetPath, 'lightspec', 'specs');
|
|
13
|
+
// Check if changes directory exists
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(changesDir);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error("No LightSpec changes directory found. Run 'lightspec init' first.");
|
|
19
|
+
}
|
|
20
|
+
// Get change name interactively if not provided
|
|
21
|
+
if (!changeName) {
|
|
22
|
+
const selectedChange = await this.selectChange(changesDir);
|
|
23
|
+
if (!selectedChange) {
|
|
24
|
+
console.log('No change selected. Aborting.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
changeName = selectedChange;
|
|
28
|
+
}
|
|
29
|
+
const changeDir = path.join(changesDir, changeName);
|
|
30
|
+
// Verify change exists
|
|
31
|
+
try {
|
|
32
|
+
const stat = await fs.stat(changeDir);
|
|
33
|
+
if (!stat.isDirectory()) {
|
|
34
|
+
throw new Error(`Change '${changeName}' not found.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Change '${changeName}' not found.`);
|
|
39
|
+
}
|
|
40
|
+
const skipValidation = options.validate === false || options.noValidate === true;
|
|
41
|
+
// Validate specs and change before archiving
|
|
42
|
+
if (!skipValidation) {
|
|
43
|
+
const validator = new Validator();
|
|
44
|
+
let hasValidationErrors = false;
|
|
45
|
+
// Validate proposal.md (non-blocking unless strict mode desired in future)
|
|
46
|
+
const changeFile = path.join(changeDir, 'proposal.md');
|
|
47
|
+
try {
|
|
48
|
+
await fs.access(changeFile);
|
|
49
|
+
const changeReport = await validator.validateChange(changeFile);
|
|
50
|
+
// Proposal validation is informative only (do not block archive)
|
|
51
|
+
if (!changeReport.valid) {
|
|
52
|
+
console.log(chalk.yellow(`\nProposal warnings in proposal.md (non-blocking):`));
|
|
53
|
+
for (const issue of changeReport.issues) {
|
|
54
|
+
const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
|
|
55
|
+
console.log(chalk.yellow(` ${symbol} ${issue.message}`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Change file doesn't exist, skip validation
|
|
61
|
+
}
|
|
62
|
+
// Validate delta-formatted spec files under the change directory if present
|
|
63
|
+
const changeSpecsDir = path.join(changeDir, 'specs');
|
|
64
|
+
let hasDeltaSpecs = false;
|
|
65
|
+
try {
|
|
66
|
+
const candidates = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
|
67
|
+
for (const c of candidates) {
|
|
68
|
+
if (c.isDirectory()) {
|
|
69
|
+
try {
|
|
70
|
+
const candidatePath = path.join(changeSpecsDir, c.name, 'spec.md');
|
|
71
|
+
await fs.access(candidatePath);
|
|
72
|
+
const content = await fs.readFile(candidatePath, 'utf-8');
|
|
73
|
+
if (/^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements/m.test(content)) {
|
|
74
|
+
hasDeltaSpecs = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
if (hasDeltaSpecs) {
|
|
84
|
+
const deltaReport = await validator.validateChangeDeltaSpecs(changeDir);
|
|
85
|
+
if (!deltaReport.valid) {
|
|
86
|
+
hasValidationErrors = true;
|
|
87
|
+
console.log(chalk.red(`\nValidation errors in change delta specs:`));
|
|
88
|
+
for (const issue of deltaReport.issues) {
|
|
89
|
+
if (issue.level === 'ERROR') {
|
|
90
|
+
console.log(chalk.red(` ✗ ${issue.message}`));
|
|
91
|
+
}
|
|
92
|
+
else if (issue.level === 'WARNING') {
|
|
93
|
+
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (hasValidationErrors) {
|
|
99
|
+
console.log(chalk.red('\nValidation failed. Please fix the errors before archiving.'));
|
|
100
|
+
console.log(chalk.yellow('To skip validation (not recommended), use --no-validate flag.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Log warning when validation is skipped
|
|
106
|
+
const timestamp = new Date().toISOString();
|
|
107
|
+
if (!options.yes) {
|
|
108
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
109
|
+
const proceed = await confirm({
|
|
110
|
+
message: chalk.yellow('⚠️ WARNING: Skipping validation may archive invalid specs. Continue? (y/N)'),
|
|
111
|
+
default: false
|
|
112
|
+
});
|
|
113
|
+
if (!proceed) {
|
|
114
|
+
console.log('Archive cancelled.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(chalk.yellow(`\n⚠️ WARNING: Skipping validation may archive invalid specs.`));
|
|
120
|
+
}
|
|
121
|
+
console.log(chalk.yellow(`[${timestamp}] Validation skipped for change: ${changeName}`));
|
|
122
|
+
console.log(chalk.yellow(`Affected files: ${changeDir}`));
|
|
123
|
+
}
|
|
124
|
+
// Show progress and check for incomplete tasks
|
|
125
|
+
const progress = await getTaskProgressForChange(changesDir, changeName);
|
|
126
|
+
const status = formatTaskStatus(progress);
|
|
127
|
+
console.log(`Task status: ${status}`);
|
|
128
|
+
const incompleteTasks = Math.max(progress.total - progress.completed, 0);
|
|
129
|
+
if (incompleteTasks > 0) {
|
|
130
|
+
if (!options.yes) {
|
|
131
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
132
|
+
const proceed = await confirm({
|
|
133
|
+
message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
|
|
134
|
+
default: false
|
|
135
|
+
});
|
|
136
|
+
if (!proceed) {
|
|
137
|
+
console.log('Archive cancelled.');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(`Warning: ${incompleteTasks} incomplete task(s) found. Continuing due to --yes flag.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Handle spec updates unless skipSpecs flag is set
|
|
146
|
+
if (options.skipSpecs) {
|
|
147
|
+
console.log('Skipping spec updates (--skip-specs flag provided).');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Find specs to update
|
|
151
|
+
const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
152
|
+
if (specUpdates.length > 0) {
|
|
153
|
+
console.log('\nSpecs to update:');
|
|
154
|
+
for (const update of specUpdates) {
|
|
155
|
+
const status = update.exists ? 'update' : 'create';
|
|
156
|
+
const capability = path.basename(path.dirname(update.target));
|
|
157
|
+
console.log(` ${capability}: ${status}`);
|
|
158
|
+
}
|
|
159
|
+
let shouldUpdateSpecs = true;
|
|
160
|
+
if (!options.yes) {
|
|
161
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
162
|
+
shouldUpdateSpecs = await confirm({
|
|
163
|
+
message: 'Proceed with spec updates?',
|
|
164
|
+
default: true
|
|
165
|
+
});
|
|
166
|
+
if (!shouldUpdateSpecs) {
|
|
167
|
+
console.log('Skipping spec updates. Proceeding with archive.');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (shouldUpdateSpecs) {
|
|
171
|
+
// Prepare all updates first (validation pass, no writes)
|
|
172
|
+
const prepared = [];
|
|
173
|
+
try {
|
|
174
|
+
for (const update of specUpdates) {
|
|
175
|
+
const built = await buildUpdatedSpec(update, changeName);
|
|
176
|
+
prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.log(String(err.message || err));
|
|
181
|
+
console.log('Aborted. No files were changed.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// All validations passed; pre-validate rebuilt full spec and then write files and display counts
|
|
185
|
+
let totals = { added: 0, modified: 0, removed: 0, renamed: 0 };
|
|
186
|
+
for (const p of prepared) {
|
|
187
|
+
const specName = path.basename(path.dirname(p.update.target));
|
|
188
|
+
if (!skipValidation) {
|
|
189
|
+
const report = await new Validator().validateSpecContent(specName, p.rebuilt);
|
|
190
|
+
if (!report.valid) {
|
|
191
|
+
console.log(chalk.red(`\nValidation errors in rebuilt spec for ${specName} (will not write changes):`));
|
|
192
|
+
for (const issue of report.issues) {
|
|
193
|
+
if (issue.level === 'ERROR')
|
|
194
|
+
console.log(chalk.red(` ✗ ${issue.message}`));
|
|
195
|
+
else if (issue.level === 'WARNING')
|
|
196
|
+
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
|
197
|
+
}
|
|
198
|
+
console.log('Aborted. No files were changed.');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
await writeUpdatedSpec(p.update, p.rebuilt, p.counts);
|
|
203
|
+
totals.added += p.counts.added;
|
|
204
|
+
totals.modified += p.counts.modified;
|
|
205
|
+
totals.removed += p.counts.removed;
|
|
206
|
+
totals.renamed += p.counts.renamed;
|
|
207
|
+
}
|
|
208
|
+
console.log(`Totals: + ${totals.added}, ~ ${totals.modified}, - ${totals.removed}, → ${totals.renamed}`);
|
|
209
|
+
console.log('Specs updated successfully.');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Create archive directory with date prefix
|
|
214
|
+
const archiveName = `${this.getArchiveDate()}-${changeName}`;
|
|
215
|
+
const archivePath = path.join(archiveDir, archiveName);
|
|
216
|
+
// Check if archive already exists
|
|
217
|
+
try {
|
|
218
|
+
await fs.access(archivePath);
|
|
219
|
+
throw new Error(`Archive '${archiveName}' already exists.`);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error.code !== 'ENOENT') {
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Create archive directory if needed
|
|
227
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
228
|
+
// Move change to archive
|
|
229
|
+
await fs.rename(changeDir, archivePath);
|
|
230
|
+
console.log(`Change '${changeName}' archived as '${archiveName}'.`);
|
|
231
|
+
}
|
|
232
|
+
async selectChange(changesDir) {
|
|
233
|
+
const { select } = await import('@inquirer/prompts');
|
|
234
|
+
// Get all directories in changes (excluding archive)
|
|
235
|
+
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
236
|
+
const changeDirs = entries
|
|
237
|
+
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
|
238
|
+
.map(entry => entry.name)
|
|
239
|
+
.sort();
|
|
240
|
+
if (changeDirs.length === 0) {
|
|
241
|
+
console.log('No active changes found.');
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
// Build choices with progress inline to avoid duplicate lists
|
|
245
|
+
let choices = changeDirs.map(name => ({ name, value: name }));
|
|
246
|
+
try {
|
|
247
|
+
const progressList = [];
|
|
248
|
+
for (const id of changeDirs) {
|
|
249
|
+
const progress = await getTaskProgressForChange(changesDir, id);
|
|
250
|
+
const status = formatTaskStatus(progress);
|
|
251
|
+
progressList.push({ id, status });
|
|
252
|
+
}
|
|
253
|
+
const nameWidth = Math.max(...progressList.map(p => p.id.length));
|
|
254
|
+
choices = progressList.map(p => ({
|
|
255
|
+
name: `${p.id.padEnd(nameWidth)} ${p.status}`,
|
|
256
|
+
value: p.id
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// If anything fails, fall back to simple names
|
|
261
|
+
choices = changeDirs.map(name => ({ name, value: name }));
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const answer = await select({
|
|
265
|
+
message: 'Select a change to archive',
|
|
266
|
+
choices
|
|
267
|
+
});
|
|
268
|
+
return answer;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
// User cancelled (Ctrl+C)
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
getArchiveDate() {
|
|
276
|
+
// Returns date in YYYY-MM-DD format
|
|
277
|
+
return new Date().toISOString().split('T')[0];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CommandDefinition } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Registry of all LightSpec 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
|