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,183 @@
|
|
|
1
|
+
import { execSync, execFileSync } from 'child_process';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
/**
|
|
6
|
+
* Check if gh CLI is installed and available in PATH
|
|
7
|
+
* Uses platform-appropriate command: 'where' on Windows, 'which' on Unix/macOS
|
|
8
|
+
*/
|
|
9
|
+
function isGhInstalled() {
|
|
10
|
+
try {
|
|
11
|
+
const command = process.platform === 'win32' ? 'where gh' : 'which gh';
|
|
12
|
+
execSync(command, { stdio: 'pipe' });
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if gh CLI is authenticated
|
|
21
|
+
*/
|
|
22
|
+
function isGhAuthenticated() {
|
|
23
|
+
try {
|
|
24
|
+
execSync('gh auth status', { stdio: 'pipe' });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get LightSpec version from package.json
|
|
33
|
+
*/
|
|
34
|
+
function getVersion() {
|
|
35
|
+
try {
|
|
36
|
+
const { version } = require('../../package.json');
|
|
37
|
+
return version;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get platform name
|
|
45
|
+
*/
|
|
46
|
+
function getPlatform() {
|
|
47
|
+
return os.platform();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get current timestamp in ISO format
|
|
51
|
+
*/
|
|
52
|
+
function getTimestamp() {
|
|
53
|
+
return new Date().toISOString();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generate metadata footer for feedback
|
|
57
|
+
*/
|
|
58
|
+
function generateMetadata() {
|
|
59
|
+
const version = getVersion();
|
|
60
|
+
const platform = getPlatform();
|
|
61
|
+
const timestamp = getTimestamp();
|
|
62
|
+
return `---
|
|
63
|
+
Submitted via LightSpec CLI
|
|
64
|
+
- Version: ${version}
|
|
65
|
+
- Platform: ${platform}
|
|
66
|
+
- Timestamp: ${timestamp}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Format the feedback title
|
|
70
|
+
*/
|
|
71
|
+
function formatTitle(message) {
|
|
72
|
+
return `Feedback: ${message}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format the full feedback body
|
|
76
|
+
*/
|
|
77
|
+
function formatBody(bodyText) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (bodyText) {
|
|
80
|
+
parts.push(bodyText);
|
|
81
|
+
parts.push(''); // Empty line before metadata
|
|
82
|
+
}
|
|
83
|
+
parts.push(generateMetadata());
|
|
84
|
+
return parts.join('\n');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Generate a pre-filled GitHub issue URL for manual submission
|
|
88
|
+
*/
|
|
89
|
+
function generateManualSubmissionUrl(title, body) {
|
|
90
|
+
const repo = 'Fission-AI/LightSpec';
|
|
91
|
+
const encodedTitle = encodeURIComponent(title);
|
|
92
|
+
const encodedBody = encodeURIComponent(body);
|
|
93
|
+
const encodedLabels = encodeURIComponent('feedback');
|
|
94
|
+
return `https://github.com/${repo}/issues/new?title=${encodedTitle}&body=${encodedBody}&labels=${encodedLabels}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Display formatted feedback content for manual submission
|
|
98
|
+
*/
|
|
99
|
+
function displayFormattedFeedback(title, body) {
|
|
100
|
+
console.log('\n--- FORMATTED FEEDBACK ---');
|
|
101
|
+
console.log(`Title: ${title}`);
|
|
102
|
+
console.log(`Labels: feedback`);
|
|
103
|
+
console.log('\nBody:');
|
|
104
|
+
console.log(body);
|
|
105
|
+
console.log('--- END FEEDBACK ---\n');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Submit feedback via gh CLI
|
|
109
|
+
* Uses execFileSync to prevent shell injection vulnerabilities
|
|
110
|
+
*/
|
|
111
|
+
function submitViaGhCli(title, body) {
|
|
112
|
+
try {
|
|
113
|
+
const result = execFileSync('gh', [
|
|
114
|
+
'issue',
|
|
115
|
+
'create',
|
|
116
|
+
'--repo',
|
|
117
|
+
'Fission-AI/LightSpec',
|
|
118
|
+
'--title',
|
|
119
|
+
title,
|
|
120
|
+
'--body',
|
|
121
|
+
body,
|
|
122
|
+
'--label',
|
|
123
|
+
'feedback',
|
|
124
|
+
], { encoding: 'utf-8', stdio: 'pipe' });
|
|
125
|
+
const issueUrl = result.trim();
|
|
126
|
+
console.log(`\n✓ Feedback submitted successfully!`);
|
|
127
|
+
console.log(`Issue URL: ${issueUrl}\n`);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// Display the error output from gh CLI
|
|
131
|
+
if (error.stderr) {
|
|
132
|
+
console.error(error.stderr.toString());
|
|
133
|
+
}
|
|
134
|
+
else if (error.message) {
|
|
135
|
+
console.error(error.message);
|
|
136
|
+
}
|
|
137
|
+
// Exit with the same code as gh CLI
|
|
138
|
+
process.exit(error.status ?? 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Handle fallback when gh CLI is not available or not authenticated
|
|
143
|
+
*/
|
|
144
|
+
function handleFallback(title, body, reason) {
|
|
145
|
+
if (reason === 'missing') {
|
|
146
|
+
console.log('⚠️ GitHub CLI not found. Manual submission required.');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.log('⚠️ GitHub authentication required. Manual submission required.');
|
|
150
|
+
}
|
|
151
|
+
displayFormattedFeedback(title, body);
|
|
152
|
+
const manualUrl = generateManualSubmissionUrl(title, body);
|
|
153
|
+
console.log('Please submit your feedback manually:');
|
|
154
|
+
console.log(manualUrl);
|
|
155
|
+
if (reason === 'unauthenticated') {
|
|
156
|
+
console.log('\nTo auto-submit in the future: gh auth login');
|
|
157
|
+
}
|
|
158
|
+
// Exit with success code (fallback is successful)
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Feedback command implementation
|
|
163
|
+
*/
|
|
164
|
+
export class FeedbackCommand {
|
|
165
|
+
async execute(message, options) {
|
|
166
|
+
// Format title and body once for all code paths
|
|
167
|
+
const title = formatTitle(message);
|
|
168
|
+
const body = formatBody(options?.body);
|
|
169
|
+
// Check if gh CLI is installed
|
|
170
|
+
if (!isGhInstalled()) {
|
|
171
|
+
handleFallback(title, body, 'missing');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Check if gh CLI is authenticated
|
|
175
|
+
if (!isGhAuthenticated()) {
|
|
176
|
+
handleFallback(title, body, 'unauthenticated');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Submit via gh CLI
|
|
180
|
+
submitViaGhCli(title, body);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=feedback.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class ShowCommand {
|
|
2
|
+
execute(itemName?: string, options?: {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
type?: string;
|
|
5
|
+
noInteractive?: boolean;
|
|
6
|
+
[k: string]: any;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
private normalizeType;
|
|
9
|
+
private runInteractiveByType;
|
|
10
|
+
private showDirect;
|
|
11
|
+
private printNonInteractiveHint;
|
|
12
|
+
private warnIrrelevantFlags;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=show.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { isInteractive } from '../utils/interactive.js';
|
|
2
|
+
import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
|
|
3
|
+
import { ChangeCommand } from './change.js';
|
|
4
|
+
import { SpecCommand } from './spec.js';
|
|
5
|
+
import { nearestMatches } from '../utils/match.js';
|
|
6
|
+
const CHANGE_FLAG_KEYS = new Set(['deltasOnly', 'requirementsOnly']);
|
|
7
|
+
const SPEC_FLAG_KEYS = new Set(['requirements', 'scenarios', 'requirement']);
|
|
8
|
+
export class ShowCommand {
|
|
9
|
+
async execute(itemName, options = {}) {
|
|
10
|
+
const interactive = isInteractive(options);
|
|
11
|
+
const typeOverride = this.normalizeType(options.type);
|
|
12
|
+
if (!itemName) {
|
|
13
|
+
if (interactive) {
|
|
14
|
+
const { select } = await import('@inquirer/prompts');
|
|
15
|
+
const type = await select({
|
|
16
|
+
message: 'What would you like to show?',
|
|
17
|
+
choices: [
|
|
18
|
+
{ name: 'Change', value: 'change' },
|
|
19
|
+
{ name: 'Spec', value: 'spec' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
await this.runInteractiveByType(type, options);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.printNonInteractiveHint();
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await this.showDirect(itemName, { typeOverride, options });
|
|
30
|
+
}
|
|
31
|
+
normalizeType(value) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return undefined;
|
|
34
|
+
const v = value.toLowerCase();
|
|
35
|
+
if (v === 'change' || v === 'spec')
|
|
36
|
+
return v;
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
async runInteractiveByType(type, options) {
|
|
40
|
+
const { select } = await import('@inquirer/prompts');
|
|
41
|
+
if (type === 'change') {
|
|
42
|
+
const changes = await getActiveChangeIds();
|
|
43
|
+
if (changes.length === 0) {
|
|
44
|
+
console.error('No changes found.');
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const picked = await select({ message: 'Pick a change', choices: changes.map(id => ({ name: id, value: id })) });
|
|
49
|
+
const cmd = new ChangeCommand();
|
|
50
|
+
await cmd.show(picked, options);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const specs = await getSpecIds();
|
|
54
|
+
if (specs.length === 0) {
|
|
55
|
+
console.error('No specs found.');
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const picked = await select({ message: 'Pick a spec', choices: specs.map(id => ({ name: id, value: id })) });
|
|
60
|
+
const cmd = new SpecCommand();
|
|
61
|
+
await cmd.show(picked, options);
|
|
62
|
+
}
|
|
63
|
+
async showDirect(itemName, params) {
|
|
64
|
+
// Optimize lookups when type is pre-specified
|
|
65
|
+
let isChange = false;
|
|
66
|
+
let isSpec = false;
|
|
67
|
+
let changes = [];
|
|
68
|
+
let specs = [];
|
|
69
|
+
if (params.typeOverride === 'change') {
|
|
70
|
+
changes = await getActiveChangeIds();
|
|
71
|
+
isChange = changes.includes(itemName);
|
|
72
|
+
}
|
|
73
|
+
else if (params.typeOverride === 'spec') {
|
|
74
|
+
specs = await getSpecIds();
|
|
75
|
+
isSpec = specs.includes(itemName);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
[changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
|
|
79
|
+
isChange = changes.includes(itemName);
|
|
80
|
+
isSpec = specs.includes(itemName);
|
|
81
|
+
}
|
|
82
|
+
const resolvedType = params.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
|
|
83
|
+
if (!resolvedType) {
|
|
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 (!params.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 show / lightspec spec show');
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.warnIrrelevantFlags(resolvedType, params.options);
|
|
98
|
+
if (resolvedType === 'change') {
|
|
99
|
+
const cmd = new ChangeCommand();
|
|
100
|
+
await cmd.show(itemName, params.options);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const cmd = new SpecCommand();
|
|
104
|
+
await cmd.show(itemName, params.options);
|
|
105
|
+
}
|
|
106
|
+
printNonInteractiveHint() {
|
|
107
|
+
console.error('Nothing to show. Try one of:');
|
|
108
|
+
console.error(' lightspec show <item>');
|
|
109
|
+
console.error(' lightspec change show');
|
|
110
|
+
console.error(' lightspec spec show');
|
|
111
|
+
console.error('Or run in an interactive terminal.');
|
|
112
|
+
}
|
|
113
|
+
warnIrrelevantFlags(type, options) {
|
|
114
|
+
const irrelevant = [];
|
|
115
|
+
if (type === 'change') {
|
|
116
|
+
for (const k of SPEC_FLAG_KEYS)
|
|
117
|
+
if (k in options)
|
|
118
|
+
irrelevant.push(k);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
for (const k of CHANGE_FLAG_KEYS)
|
|
122
|
+
if (k in options)
|
|
123
|
+
irrelevant.push(k);
|
|
124
|
+
}
|
|
125
|
+
if (irrelevant.length > 0) {
|
|
126
|
+
console.error(`Warning: Ignoring flags not applicable to ${type}: ${irrelevant.join(', ')}`);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=show.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { program } from 'commander';
|
|
2
|
+
interface ShowOptions {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
requirements?: boolean;
|
|
5
|
+
scenarios?: boolean;
|
|
6
|
+
requirement?: string;
|
|
7
|
+
noInteractive?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class SpecCommand {
|
|
10
|
+
private SPECS_DIR;
|
|
11
|
+
show(specId?: string, options?: ShowOptions): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function registerSpecCommand(rootProgram: typeof program): import("commander").Command;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=spec.d.ts.map
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { MarkdownParser } from '../core/parsers/markdown-parser.js';
|
|
4
|
+
import { Validator } from '../core/validation/validator.js';
|
|
5
|
+
import { isInteractive } from '../utils/interactive.js';
|
|
6
|
+
import { getSpecIds } from '../utils/item-discovery.js';
|
|
7
|
+
const SPECS_DIR = 'lightspec/specs';
|
|
8
|
+
function parseSpecFromFile(specPath, specId) {
|
|
9
|
+
const content = readFileSync(specPath, 'utf-8');
|
|
10
|
+
const parser = new MarkdownParser(content);
|
|
11
|
+
return parser.parseSpec(specId);
|
|
12
|
+
}
|
|
13
|
+
function validateRequirementIndex(spec, requirementOpt) {
|
|
14
|
+
if (!requirementOpt)
|
|
15
|
+
return undefined;
|
|
16
|
+
const index = Number.parseInt(requirementOpt, 10);
|
|
17
|
+
if (!Number.isInteger(index) || index < 1 || index > spec.requirements.length) {
|
|
18
|
+
throw new Error(`Requirement ${requirementOpt} not found`);
|
|
19
|
+
}
|
|
20
|
+
return index - 1; // convert to 0-based
|
|
21
|
+
}
|
|
22
|
+
function filterSpec(spec, options) {
|
|
23
|
+
const requirementIndex = validateRequirementIndex(spec, options.requirement);
|
|
24
|
+
const includeScenarios = options.scenarios !== false && !options.requirements;
|
|
25
|
+
const filteredRequirements = (requirementIndex !== undefined
|
|
26
|
+
? [spec.requirements[requirementIndex]]
|
|
27
|
+
: spec.requirements).map(req => ({
|
|
28
|
+
text: req.text,
|
|
29
|
+
scenarios: includeScenarios ? req.scenarios : [],
|
|
30
|
+
}));
|
|
31
|
+
const metadata = spec.metadata ?? { version: '1.0.0', format: 'lightspec' };
|
|
32
|
+
return {
|
|
33
|
+
name: spec.name,
|
|
34
|
+
overview: spec.overview,
|
|
35
|
+
requirements: filteredRequirements,
|
|
36
|
+
metadata,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Print the raw markdown content for a spec file without any formatting.
|
|
41
|
+
* Raw-first behavior ensures text mode is a passthrough for deterministic output.
|
|
42
|
+
*/
|
|
43
|
+
function printSpecTextRaw(specPath) {
|
|
44
|
+
const content = readFileSync(specPath, 'utf-8');
|
|
45
|
+
console.log(content);
|
|
46
|
+
}
|
|
47
|
+
export class SpecCommand {
|
|
48
|
+
SPECS_DIR = 'lightspec/specs';
|
|
49
|
+
async show(specId, options = {}) {
|
|
50
|
+
if (!specId) {
|
|
51
|
+
const canPrompt = isInteractive(options);
|
|
52
|
+
const specIds = await getSpecIds();
|
|
53
|
+
if (canPrompt && specIds.length > 0) {
|
|
54
|
+
const { select } = await import('@inquirer/prompts');
|
|
55
|
+
specId = await select({
|
|
56
|
+
message: 'Select a spec to show',
|
|
57
|
+
choices: specIds.map(id => ({ name: id, value: id })),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
throw new Error('Missing required argument <spec-id>');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const specPath = join(this.SPECS_DIR, specId, 'spec.md');
|
|
65
|
+
if (!existsSync(specPath)) {
|
|
66
|
+
throw new Error(`Spec '${specId}' not found at lightspec/specs/${specId}/spec.md`);
|
|
67
|
+
}
|
|
68
|
+
if (options.json) {
|
|
69
|
+
if (options.requirements && options.requirement) {
|
|
70
|
+
throw new Error('Options --requirements and --requirement cannot be used together');
|
|
71
|
+
}
|
|
72
|
+
const parsed = parseSpecFromFile(specPath, specId);
|
|
73
|
+
const filtered = filterSpec(parsed, options);
|
|
74
|
+
const output = {
|
|
75
|
+
id: specId,
|
|
76
|
+
title: parsed.name,
|
|
77
|
+
overview: parsed.overview,
|
|
78
|
+
requirementCount: filtered.requirements.length,
|
|
79
|
+
requirements: filtered.requirements,
|
|
80
|
+
metadata: parsed.metadata ?? { version: '1.0.0', format: 'lightspec' },
|
|
81
|
+
};
|
|
82
|
+
console.log(JSON.stringify(output, null, 2));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
printSpecTextRaw(specPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function registerSpecCommand(rootProgram) {
|
|
89
|
+
const specCommand = rootProgram
|
|
90
|
+
.command('spec')
|
|
91
|
+
.description('Manage and view LightSpec specifications');
|
|
92
|
+
// Deprecation notice for noun-based commands
|
|
93
|
+
specCommand.hook('preAction', () => {
|
|
94
|
+
console.error('Warning: The "lightspec spec ..." commands are deprecated. Prefer verb-first commands (e.g., "lightspec show", "lightspec validate --specs").');
|
|
95
|
+
});
|
|
96
|
+
specCommand
|
|
97
|
+
.command('show [spec-id]')
|
|
98
|
+
.description('Display a specific specification')
|
|
99
|
+
.option('--json', 'Output as JSON')
|
|
100
|
+
.option('--requirements', 'JSON only: Show only requirements (exclude scenarios)')
|
|
101
|
+
.option('--no-scenarios', 'JSON only: Exclude scenario content')
|
|
102
|
+
.option('-r, --requirement <id>', 'JSON only: Show specific requirement by ID (1-based)')
|
|
103
|
+
.option('--no-interactive', 'Disable interactive prompts')
|
|
104
|
+
.action(async (specId, options) => {
|
|
105
|
+
try {
|
|
106
|
+
const cmd = new SpecCommand();
|
|
107
|
+
await cmd.show(specId, options);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
specCommand
|
|
115
|
+
.command('list')
|
|
116
|
+
.description('List all available specifications')
|
|
117
|
+
.option('--json', 'Output as JSON')
|
|
118
|
+
.option('--long', 'Show id and title with counts')
|
|
119
|
+
.action((options) => {
|
|
120
|
+
try {
|
|
121
|
+
if (!existsSync(SPECS_DIR)) {
|
|
122
|
+
console.log('No items found');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const specs = readdirSync(SPECS_DIR, { withFileTypes: true })
|
|
126
|
+
.filter(dirent => dirent.isDirectory())
|
|
127
|
+
.map(dirent => {
|
|
128
|
+
const specPath = join(SPECS_DIR, dirent.name, 'spec.md');
|
|
129
|
+
if (existsSync(specPath)) {
|
|
130
|
+
try {
|
|
131
|
+
const spec = parseSpecFromFile(specPath, dirent.name);
|
|
132
|
+
return {
|
|
133
|
+
id: dirent.name,
|
|
134
|
+
title: spec.name,
|
|
135
|
+
requirementCount: spec.requirements.length
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return {
|
|
140
|
+
id: dirent.name,
|
|
141
|
+
title: dirent.name,
|
|
142
|
+
requirementCount: 0
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
})
|
|
148
|
+
.filter((spec) => spec !== null)
|
|
149
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
150
|
+
if (options.json) {
|
|
151
|
+
console.log(JSON.stringify(specs, null, 2));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
if (specs.length === 0) {
|
|
155
|
+
console.log('No items found');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!options.long) {
|
|
159
|
+
specs.forEach(spec => console.log(spec.id));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
specs.forEach(spec => {
|
|
163
|
+
console.log(`${spec.id}: ${spec.title} [requirements ${spec.requirementCount}]`);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
specCommand
|
|
173
|
+
.command('validate [spec-id]')
|
|
174
|
+
.description('Validate a specification structure')
|
|
175
|
+
.option('--strict', 'Enable strict validation mode')
|
|
176
|
+
.option('--json', 'Output validation report as JSON')
|
|
177
|
+
.option('--no-interactive', 'Disable interactive prompts')
|
|
178
|
+
.action(async (specId, options) => {
|
|
179
|
+
try {
|
|
180
|
+
if (!specId) {
|
|
181
|
+
const canPrompt = isInteractive(options);
|
|
182
|
+
const specIds = await getSpecIds();
|
|
183
|
+
if (canPrompt && specIds.length > 0) {
|
|
184
|
+
const { select } = await import('@inquirer/prompts');
|
|
185
|
+
specId = await select({
|
|
186
|
+
message: 'Select a spec to validate',
|
|
187
|
+
choices: specIds.map(id => ({ name: id, value: id })),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
throw new Error('Missing required argument <spec-id>');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const specPath = join(SPECS_DIR, specId, 'spec.md');
|
|
195
|
+
if (!existsSync(specPath)) {
|
|
196
|
+
throw new Error(`Spec '${specId}' not found at lightspec/specs/${specId}/spec.md`);
|
|
197
|
+
}
|
|
198
|
+
const validator = new Validator(options.strict);
|
|
199
|
+
const report = await validator.validateSpec(specPath);
|
|
200
|
+
if (options.json) {
|
|
201
|
+
console.log(JSON.stringify(report, null, 2));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
if (report.valid) {
|
|
205
|
+
console.log(`Specification '${specId}' is valid`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.error(`Specification '${specId}' has issues`);
|
|
209
|
+
report.issues.forEach(issue => {
|
|
210
|
+
const label = issue.level === 'ERROR' ? 'ERROR' : issue.level;
|
|
211
|
+
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
|
|
212
|
+
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
process.exitCode = report.valid ? 0 : 1;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return specCommand;
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface ExecuteOptions {
|
|
2
|
+
all?: boolean;
|
|
3
|
+
changes?: boolean;
|
|
4
|
+
specs?: boolean;
|
|
5
|
+
type?: string;
|
|
6
|
+
strict?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
noInteractive?: boolean;
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
concurrency?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class ValidateCommand {
|
|
13
|
+
execute(itemName: string | undefined, options?: ExecuteOptions): Promise<void>;
|
|
14
|
+
private normalizeType;
|
|
15
|
+
private runInteractiveSelector;
|
|
16
|
+
private printNonInteractiveHint;
|
|
17
|
+
private validateDirectItem;
|
|
18
|
+
private validateByType;
|
|
19
|
+
private printReport;
|
|
20
|
+
private printNextSteps;
|
|
21
|
+
private runBulkValidation;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=validate.d.ts.map
|