@fission-ai/openspec 1.2.0 → 1.3.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/README.md +8 -13
- package/dist/commands/workflow/instructions.js +18 -72
- package/dist/commands/workflow/shared.d.ts +6 -1
- package/dist/commands/workflow/shared.js +21 -16
- package/dist/commands/workflow/status.js +21 -4
- package/dist/commands/workflow/templates.js +5 -4
- package/dist/core/artifact-graph/index.d.ts +1 -0
- package/dist/core/artifact-graph/index.js +1 -0
- package/dist/core/artifact-graph/instruction-loader.js +6 -4
- package/dist/core/artifact-graph/outputs.d.ts +14 -0
- package/dist/core/artifact-graph/outputs.js +39 -0
- package/dist/core/artifact-graph/state.js +2 -25
- package/dist/core/available-tools.d.ts +3 -2
- package/dist/core/available-tools.js +15 -2
- package/dist/core/command-generation/adapters/bob.d.ts +14 -0
- package/dist/core/command-generation/adapters/bob.js +45 -0
- package/dist/core/command-generation/adapters/index.d.ts +3 -0
- package/dist/core/command-generation/adapters/index.js +3 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +26 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +1 -1
- package/dist/core/command-generation/adapters/opencode.js +2 -2
- package/dist/core/command-generation/adapters/pi.d.ts +4 -0
- package/dist/core/command-generation/adapters/pi.js +15 -1
- package/dist/core/command-generation/registry.js +6 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +14 -0
- package/dist/core/completions/installers/powershell-installer.js +69 -9
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +5 -1
- package/dist/core/init.js +6 -10
- package/dist/core/legacy-cleanup.d.ts +1 -1
- package/dist/core/legacy-cleanup.js +22 -10
- package/dist/core/parsers/change-parser.js +7 -3
- package/dist/core/parsers/markdown-parser.d.ts +4 -0
- package/dist/core/parsers/markdown-parser.js +41 -1
- package/dist/core/parsers/spec-structure.d.ts +9 -0
- package/dist/core/parsers/spec-structure.js +88 -0
- package/dist/core/specs-apply.js +8 -0
- package/dist/core/templates/workflows/apply-change.js +4 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +2 -2
- package/dist/core/templates/workflows/explore.js +25 -25
- package/dist/core/templates/workflows/onboard.js +24 -24
- package/dist/core/templates/workflows/verify-change.js +6 -6
- package/dist/core/update.js +2 -2
- package/dist/core/validation/validator.js +9 -0
- package/dist/telemetry/config.d.ts +7 -1
- package/dist/telemetry/config.js +87 -19
- package/dist/telemetry/index.js +19 -0
- package/dist/utils/file-system.d.ts +5 -0
- package/dist/utils/file-system.js +21 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -72
package/README.md
CHANGED
|
@@ -36,27 +36,20 @@ Our philosophy:
|
|
|
36
36
|
> [!TIP]
|
|
37
37
|
> **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
|
|
38
38
|
>
|
|
39
|
-
> Run `/opsx:
|
|
39
|
+
> Run `/opsx:propose "your idea"` to get started. → [Learn more here](docs/opsx.md)
|
|
40
40
|
|
|
41
41
|
<p align="center">
|
|
42
42
|
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
|
|
43
43
|
</p>
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Using OpenSpec in a team? [Email here](mailto:teams@openspec.dev) for access to our Slack channel.
|
|
48
|
-
|
|
49
|
-
<!-- TODO: Add GIF demo of /opsx:new → /opsx:archive workflow -->
|
|
45
|
+
<!-- TODO: Add GIF demo of /opsx:propose → /opsx:archive workflow -->
|
|
50
46
|
|
|
51
47
|
## See it in action
|
|
52
48
|
|
|
53
49
|
```text
|
|
54
|
-
You: /opsx:
|
|
50
|
+
You: /opsx:propose add-dark-mode
|
|
55
51
|
AI: Created openspec/changes/add-dark-mode/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
You: /opsx:ff # "fast-forward" - generate all planning docs
|
|
59
|
-
AI: ✓ proposal.md — why we're doing this, what's changing
|
|
52
|
+
✓ proposal.md — why we're doing this, what's changing
|
|
60
53
|
✓ specs/ — requirements and scenarios
|
|
61
54
|
✓ design.md — technical approach
|
|
62
55
|
✓ tasks.md — implementation checklist
|
|
@@ -101,10 +94,12 @@ cd your-project
|
|
|
101
94
|
openspec init
|
|
102
95
|
```
|
|
103
96
|
|
|
104
|
-
Now tell your AI: `/opsx:
|
|
97
|
+
Now tell your AI: `/opsx:propose <what-you-want-to-build>`
|
|
98
|
+
|
|
99
|
+
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
|
|
105
100
|
|
|
106
101
|
> [!NOTE]
|
|
107
|
-
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support
|
|
102
|
+
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
|
|
108
103
|
>
|
|
109
104
|
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
|
|
110
105
|
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
|
-
import { loadChangeContext, generateInstructions, resolveSchema, } from '../../core/artifact-graph/index.js';
|
|
10
|
+
import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
|
|
11
11
|
import { validateChangeExists, validateSchemaExists, } from './shared.js';
|
|
12
12
|
// -----------------------------------------------------------------------------
|
|
13
13
|
// Artifact Instructions Command
|
|
14
14
|
// -----------------------------------------------------------------------------
|
|
15
15
|
export async function instructionsCommand(artifactId, options) {
|
|
16
|
-
const spinner = ora('Generating instructions...').start();
|
|
16
|
+
const spinner = options.json ? undefined : ora('Generating instructions...').start();
|
|
17
17
|
try {
|
|
18
18
|
const projectRoot = process.cwd();
|
|
19
19
|
const changeName = await validateChangeExists(options.change, projectRoot);
|
|
@@ -24,19 +24,19 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
24
24
|
// loadChangeContext will auto-detect schema from metadata if not provided
|
|
25
25
|
const context = loadChangeContext(projectRoot, changeName, options.schema);
|
|
26
26
|
if (!artifactId) {
|
|
27
|
-
spinner
|
|
27
|
+
spinner?.stop();
|
|
28
28
|
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
|
29
29
|
throw new Error(`Missing required argument <artifact>. Valid artifacts:\n ${validIds.join('\n ')}`);
|
|
30
30
|
}
|
|
31
31
|
const artifact = context.graph.getArtifact(artifactId);
|
|
32
32
|
if (!artifact) {
|
|
33
|
-
spinner
|
|
33
|
+
spinner?.stop();
|
|
34
34
|
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
|
35
35
|
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`);
|
|
36
36
|
}
|
|
37
37
|
const instructions = generateInstructions(context, artifactId, projectRoot);
|
|
38
38
|
const isBlocked = instructions.dependencies.some((d) => !d.done);
|
|
39
|
-
spinner
|
|
39
|
+
spinner?.stop();
|
|
40
40
|
if (options.json) {
|
|
41
41
|
console.log(JSON.stringify(instructions, null, 2));
|
|
42
42
|
return;
|
|
@@ -44,7 +44,7 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
44
44
|
printInstructionsText(instructions, isBlocked);
|
|
45
45
|
}
|
|
46
46
|
catch (error) {
|
|
47
|
-
spinner
|
|
47
|
+
spinner?.stop();
|
|
48
48
|
throw error;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -161,63 +161,6 @@ function parseTasksFile(content) {
|
|
|
161
161
|
}
|
|
162
162
|
return tasks;
|
|
163
163
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Checks if an artifact output exists in the change directory.
|
|
166
|
-
* Supports glob patterns (e.g., "specs/*.md") by verifying at least one matching file exists.
|
|
167
|
-
*/
|
|
168
|
-
function artifactOutputExists(changeDir, generates) {
|
|
169
|
-
// Normalize the generates path to use platform-specific separators
|
|
170
|
-
const normalizedGenerates = generates.split('/').join(path.sep);
|
|
171
|
-
const fullPath = path.join(changeDir, normalizedGenerates);
|
|
172
|
-
// If it's a glob pattern (contains ** or *), check for matching files
|
|
173
|
-
if (generates.includes('*')) {
|
|
174
|
-
// Extract the directory part before the glob pattern
|
|
175
|
-
const parts = normalizedGenerates.split(path.sep);
|
|
176
|
-
const dirParts = [];
|
|
177
|
-
let patternPart = '';
|
|
178
|
-
for (const part of parts) {
|
|
179
|
-
if (part.includes('*')) {
|
|
180
|
-
patternPart = part;
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
dirParts.push(part);
|
|
184
|
-
}
|
|
185
|
-
const dirPath = path.join(changeDir, ...dirParts);
|
|
186
|
-
// Check if directory exists
|
|
187
|
-
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
// Extract expected extension from pattern (e.g., "*.md" -> ".md")
|
|
191
|
-
const extMatch = patternPart.match(/\*(\.[a-zA-Z0-9]+)$/);
|
|
192
|
-
const expectedExt = extMatch ? extMatch[1] : null;
|
|
193
|
-
// Recursively check for matching files
|
|
194
|
-
const hasMatchingFiles = (dir) => {
|
|
195
|
-
try {
|
|
196
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
197
|
-
for (const entry of entries) {
|
|
198
|
-
if (entry.isDirectory()) {
|
|
199
|
-
// For ** patterns, recurse into subdirectories
|
|
200
|
-
if (generates.includes('**') && hasMatchingFiles(path.join(dir, entry.name))) {
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
else if (entry.isFile()) {
|
|
205
|
-
// Check if file matches expected extension (or any file if no extension specified)
|
|
206
|
-
if (!expectedExt || entry.name.endsWith(expectedExt)) {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
return false;
|
|
216
|
-
};
|
|
217
|
-
return hasMatchingFiles(dirPath);
|
|
218
|
-
}
|
|
219
|
-
return fs.existsSync(fullPath);
|
|
220
|
-
}
|
|
221
164
|
/**
|
|
222
165
|
* Generates apply instructions for implementing tasks from a change.
|
|
223
166
|
* Schema-aware: reads apply phase configuration from schema to determine
|
|
@@ -226,7 +169,7 @@ function artifactOutputExists(changeDir, generates) {
|
|
|
226
169
|
export async function generateApplyInstructions(projectRoot, changeName, schemaName) {
|
|
227
170
|
// loadChangeContext will auto-detect schema from metadata if not provided
|
|
228
171
|
const context = loadChangeContext(projectRoot, changeName, schemaName);
|
|
229
|
-
const changeDir =
|
|
172
|
+
const changeDir = context.changeDir;
|
|
230
173
|
// Get the full schema to access the apply phase configuration
|
|
231
174
|
const schema = resolveSchema(context.schemaName, projectRoot);
|
|
232
175
|
const applyConfig = schema.apply;
|
|
@@ -239,15 +182,16 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
239
182
|
const missingArtifacts = [];
|
|
240
183
|
for (const artifactId of requiredArtifactIds) {
|
|
241
184
|
const artifact = schema.artifacts.find((a) => a.id === artifactId);
|
|
242
|
-
if (artifact &&
|
|
185
|
+
if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
|
|
243
186
|
missingArtifacts.push(artifactId);
|
|
244
187
|
}
|
|
245
188
|
}
|
|
246
189
|
// Build context files from all existing artifacts in schema
|
|
247
190
|
const contextFiles = {};
|
|
248
191
|
for (const artifact of schema.artifacts) {
|
|
249
|
-
|
|
250
|
-
|
|
192
|
+
const outputs = resolveArtifactOutputs(changeDir, artifact.generates);
|
|
193
|
+
if (outputs.length > 0) {
|
|
194
|
+
contextFiles[artifact.id] = outputs;
|
|
251
195
|
}
|
|
252
196
|
}
|
|
253
197
|
// Parse tasks if tracking file exists
|
|
@@ -310,7 +254,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
310
254
|
};
|
|
311
255
|
}
|
|
312
256
|
export async function applyInstructionsCommand(options) {
|
|
313
|
-
const spinner = ora('Generating apply instructions...').start();
|
|
257
|
+
const spinner = options.json ? undefined : ora('Generating apply instructions...').start();
|
|
314
258
|
try {
|
|
315
259
|
const projectRoot = process.cwd();
|
|
316
260
|
const changeName = await validateChangeExists(options.change, projectRoot);
|
|
@@ -320,7 +264,7 @@ export async function applyInstructionsCommand(options) {
|
|
|
320
264
|
}
|
|
321
265
|
// generateApplyInstructions uses loadChangeContext which auto-detects schema
|
|
322
266
|
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema);
|
|
323
|
-
spinner
|
|
267
|
+
spinner?.stop();
|
|
324
268
|
if (options.json) {
|
|
325
269
|
console.log(JSON.stringify(instructions, null, 2));
|
|
326
270
|
return;
|
|
@@ -328,7 +272,7 @@ export async function applyInstructionsCommand(options) {
|
|
|
328
272
|
printApplyInstructionsText(instructions);
|
|
329
273
|
}
|
|
330
274
|
catch (error) {
|
|
331
|
-
spinner
|
|
275
|
+
spinner?.stop();
|
|
332
276
|
throw error;
|
|
333
277
|
}
|
|
334
278
|
}
|
|
@@ -349,8 +293,10 @@ export function printApplyInstructionsText(instructions) {
|
|
|
349
293
|
const contextFileEntries = Object.entries(contextFiles);
|
|
350
294
|
if (contextFileEntries.length > 0) {
|
|
351
295
|
console.log('### Context Files');
|
|
352
|
-
for (const [artifactId,
|
|
353
|
-
|
|
296
|
+
for (const [artifactId, filePaths] of contextFileEntries) {
|
|
297
|
+
for (const filePath of filePaths) {
|
|
298
|
+
console.log(`- ${artifactId}: ${filePath}`);
|
|
299
|
+
}
|
|
354
300
|
}
|
|
355
301
|
console.log();
|
|
356
302
|
}
|
|
@@ -13,7 +13,7 @@ export interface ApplyInstructions {
|
|
|
13
13
|
changeName: string;
|
|
14
14
|
changeDir: string;
|
|
15
15
|
schemaName: string;
|
|
16
|
-
contextFiles: Record<string, string>;
|
|
16
|
+
contextFiles: Record<string, string[]>;
|
|
17
17
|
progress: {
|
|
18
18
|
total: number;
|
|
19
19
|
complete: number;
|
|
@@ -37,6 +37,11 @@ export declare function getStatusColor(status: 'done' | 'ready' | 'blocked'): (t
|
|
|
37
37
|
* Gets the status indicator for an artifact.
|
|
38
38
|
*/
|
|
39
39
|
export declare function getStatusIndicator(status: 'done' | 'ready' | 'blocked'): string;
|
|
40
|
+
/**
|
|
41
|
+
* Returns the list of available change directory names under openspec/changes/.
|
|
42
|
+
* Excludes the archive directory and hidden directories.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getAvailableChanges(projectRoot: string): Promise<string[]>;
|
|
40
45
|
/**
|
|
41
46
|
* Validates that a change exists and returns available changes if not.
|
|
42
47
|
* Checks directory existence directly to support scaffolded changes (without proposal.md).
|
|
@@ -52,26 +52,31 @@ export function getStatusIndicator(status) {
|
|
|
52
52
|
return color('[-]');
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns the list of available change directory names under openspec/changes/.
|
|
57
|
+
* Excludes the archive directory and hidden directories.
|
|
58
|
+
*/
|
|
59
|
+
export async function getAvailableChanges(projectRoot) {
|
|
60
|
+
const changesPath = path.join(projectRoot, 'openspec', 'changes');
|
|
61
|
+
try {
|
|
62
|
+
const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
|
|
63
|
+
return entries
|
|
64
|
+
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
|
|
65
|
+
.map((e) => e.name);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code === 'ENOENT')
|
|
69
|
+
return [];
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
55
73
|
/**
|
|
56
74
|
* Validates that a change exists and returns available changes if not.
|
|
57
75
|
* Checks directory existence directly to support scaffolded changes (without proposal.md).
|
|
58
76
|
*/
|
|
59
77
|
export async function validateChangeExists(changeName, projectRoot) {
|
|
60
|
-
const changesPath = path.join(projectRoot, 'openspec', 'changes');
|
|
61
|
-
// Get all change directories (not just those with proposal.md)
|
|
62
|
-
const getAvailableChanges = async () => {
|
|
63
|
-
try {
|
|
64
|
-
const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
|
|
65
|
-
return entries
|
|
66
|
-
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
|
|
67
|
-
.map((e) => e.name);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
78
|
if (!changeName) {
|
|
74
|
-
const available = await getAvailableChanges();
|
|
79
|
+
const available = await getAvailableChanges(projectRoot);
|
|
75
80
|
if (available.length === 0) {
|
|
76
81
|
throw new Error('No changes found. Create one with: openspec new change <name>');
|
|
77
82
|
}
|
|
@@ -83,10 +88,10 @@ export async function validateChangeExists(changeName, projectRoot) {
|
|
|
83
88
|
throw new Error(`Invalid change name '${changeName}': ${nameValidation.error}`);
|
|
84
89
|
}
|
|
85
90
|
// Check directory existence directly
|
|
86
|
-
const changePath = path.join(
|
|
91
|
+
const changePath = path.join(projectRoot, 'openspec', 'changes', changeName);
|
|
87
92
|
const exists = fs.existsSync(changePath) && fs.statSync(changePath).isDirectory();
|
|
88
93
|
if (!exists) {
|
|
89
|
-
const available = await getAvailableChanges();
|
|
94
|
+
const available = await getAvailableChanges(projectRoot);
|
|
90
95
|
if (available.length === 0) {
|
|
91
96
|
throw new Error(`Change '${changeName}' not found. No changes exist. Create one with: openspec new change <name>`);
|
|
92
97
|
}
|
|
@@ -6,14 +6,31 @@
|
|
|
6
6
|
import ora from 'ora';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
|
|
9
|
-
import { validateChangeExists, validateSchemaExists, getStatusIndicator, getStatusColor, } from './shared.js';
|
|
9
|
+
import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
|
|
10
10
|
// -----------------------------------------------------------------------------
|
|
11
11
|
// Command Implementation
|
|
12
12
|
// -----------------------------------------------------------------------------
|
|
13
13
|
export async function statusCommand(options) {
|
|
14
|
-
const spinner = ora('Loading change status...').start();
|
|
14
|
+
const spinner = options.json ? undefined : ora('Loading change status...').start();
|
|
15
15
|
try {
|
|
16
16
|
const projectRoot = process.cwd();
|
|
17
|
+
// Handle no-changes case gracefully — status is informational,
|
|
18
|
+
// so "no changes" is a valid state, not an error.
|
|
19
|
+
if (!options.change) {
|
|
20
|
+
const available = await getAvailableChanges(projectRoot);
|
|
21
|
+
if (available.length === 0) {
|
|
22
|
+
spinner?.stop();
|
|
23
|
+
if (options.json) {
|
|
24
|
+
console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log('No active changes. Create one with: openspec new change <name>');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Changes exist but --change not provided
|
|
31
|
+
spinner?.stop();
|
|
32
|
+
throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
|
|
33
|
+
}
|
|
17
34
|
const changeName = await validateChangeExists(options.change, projectRoot);
|
|
18
35
|
// Validate schema if explicitly provided
|
|
19
36
|
if (options.schema) {
|
|
@@ -22,7 +39,7 @@ export async function statusCommand(options) {
|
|
|
22
39
|
// loadChangeContext will auto-detect schema from metadata if not provided
|
|
23
40
|
const context = loadChangeContext(projectRoot, changeName, options.schema);
|
|
24
41
|
const status = formatChangeStatus(context);
|
|
25
|
-
spinner
|
|
42
|
+
spinner?.stop();
|
|
26
43
|
if (options.json) {
|
|
27
44
|
console.log(JSON.stringify(status, null, 2));
|
|
28
45
|
return;
|
|
@@ -30,7 +47,7 @@ export async function statusCommand(options) {
|
|
|
30
47
|
printStatusText(status);
|
|
31
48
|
}
|
|
32
49
|
catch (error) {
|
|
33
|
-
spinner
|
|
50
|
+
spinner?.stop();
|
|
34
51
|
throw error;
|
|
35
52
|
}
|
|
36
53
|
}
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
import ora from 'ora';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { resolveSchema, getSchemaDir, ArtifactGraph, } from '../../core/artifact-graph/index.js';
|
|
9
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
9
10
|
import { validateSchemaExists, DEFAULT_SCHEMA } from './shared.js';
|
|
10
11
|
// -----------------------------------------------------------------------------
|
|
11
12
|
// Command Implementation
|
|
12
13
|
// -----------------------------------------------------------------------------
|
|
13
14
|
export async function templatesCommand(options) {
|
|
14
|
-
const spinner = ora('Loading templates...').start();
|
|
15
|
+
const spinner = options.json ? undefined : ora('Loading templates...').start();
|
|
15
16
|
try {
|
|
16
17
|
const projectRoot = process.cwd();
|
|
17
18
|
const schemaName = validateSchemaExists(options.schema ?? DEFAULT_SCHEMA, projectRoot);
|
|
@@ -40,10 +41,10 @@ export async function templatesCommand(options) {
|
|
|
40
41
|
}
|
|
41
42
|
const templates = graph.getAllArtifacts().map((artifact) => ({
|
|
42
43
|
artifactId: artifact.id,
|
|
43
|
-
templatePath: path.join(schemaDir, 'templates', artifact.template),
|
|
44
|
+
templatePath: FileSystemUtils.canonicalizeExistingPath(path.join(schemaDir, 'templates', artifact.template)),
|
|
44
45
|
source,
|
|
45
46
|
}));
|
|
46
|
-
spinner
|
|
47
|
+
spinner?.stop();
|
|
47
48
|
if (options.json) {
|
|
48
49
|
const output = {};
|
|
49
50
|
for (const t of templates) {
|
|
@@ -61,7 +62,7 @@ export async function templatesCommand(options) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
catch (error) {
|
|
64
|
-
spinner
|
|
65
|
+
spinner?.stop();
|
|
65
66
|
throw error;
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -2,6 +2,7 @@ export { ArtifactSchema, SchemaYamlSchema, type Artifact, type SchemaYaml, type
|
|
|
2
2
|
export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
|
|
3
3
|
export { ArtifactGraph } from './graph.js';
|
|
4
4
|
export { detectCompleted } from './state.js';
|
|
5
|
+
export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
|
|
5
6
|
export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, type SchemaInfo, } from './resolver.js';
|
|
6
7
|
export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, type ChangeContext, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, } from './instruction-loader.js';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -6,6 +6,7 @@ export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
|
|
|
6
6
|
export { ArtifactGraph } from './graph.js';
|
|
7
7
|
// State detection
|
|
8
8
|
export { detectCompleted } from './state.js';
|
|
9
|
+
export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
|
|
9
10
|
// Schema resolution
|
|
10
11
|
export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, } from './resolver.js';
|
|
11
12
|
// Instruction loading
|
|
@@ -4,6 +4,7 @@ import { getSchemaDir, resolveSchema } from './resolver.js';
|
|
|
4
4
|
import { ArtifactGraph } from './graph.js';
|
|
5
5
|
import { detectCompleted } from './state.js';
|
|
6
6
|
import { resolveSchemaForChange } from '../../utils/change-metadata.js';
|
|
7
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
7
8
|
import { readProjectConfig, validateConfigRules } from '../project-config.js';
|
|
8
9
|
// Session-level cache for validation warnings (avoid repeating same warnings)
|
|
9
10
|
const shownWarnings = new Set();
|
|
@@ -32,10 +33,11 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
|
|
|
32
33
|
if (!schemaDir) {
|
|
33
34
|
throw new TemplateLoadError(`Schema '${schemaName}' not found`, templatePath);
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
if (!fs.existsSync(
|
|
37
|
-
throw new TemplateLoadError(`Template not found: ${
|
|
36
|
+
const templatePathOnDisk = path.join(schemaDir, 'templates', templatePath);
|
|
37
|
+
if (!fs.existsSync(templatePathOnDisk)) {
|
|
38
|
+
throw new TemplateLoadError(`Template not found: ${templatePathOnDisk}`, templatePathOnDisk);
|
|
38
39
|
}
|
|
40
|
+
const fullPath = FileSystemUtils.canonicalizeExistingPath(templatePathOnDisk);
|
|
39
41
|
try {
|
|
40
42
|
return fs.readFileSync(fullPath, 'utf-8');
|
|
41
43
|
}
|
|
@@ -58,7 +60,7 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
|
|
|
58
60
|
* @returns Change context with graph, completed set, and metadata
|
|
59
61
|
*/
|
|
60
62
|
export function loadChangeContext(projectRoot, changeName, schemaName) {
|
|
61
|
-
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
|
|
63
|
+
const changeDir = FileSystemUtils.canonicalizeExistingPath(path.join(projectRoot, 'openspec', 'changes', changeName));
|
|
62
64
|
// Resolve schema: explicit > metadata > default
|
|
63
65
|
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName);
|
|
64
66
|
const schema = resolveSchema(resolvedSchemaName, projectRoot);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a path contains glob pattern characters.
|
|
3
|
+
*/
|
|
4
|
+
export declare function isGlobPattern(pattern: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves an artifact's output path(s) to concrete files that currently exist.
|
|
7
|
+
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveArtifactOutputs(changeDir: string, generates: string): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Checks if an artifact has at least one resolved output file.
|
|
12
|
+
*/
|
|
13
|
+
export declare function artifactOutputExists(changeDir: string, generates: string): boolean;
|
|
14
|
+
//# sourceMappingURL=outputs.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a path contains glob pattern characters.
|
|
7
|
+
*/
|
|
8
|
+
export function isGlobPattern(pattern) {
|
|
9
|
+
return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolves an artifact's output path(s) to concrete files that currently exist.
|
|
13
|
+
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveArtifactOutputs(changeDir, generates) {
|
|
16
|
+
if (!isGlobPattern(generates)) {
|
|
17
|
+
const fullPath = path.join(changeDir, generates);
|
|
18
|
+
try {
|
|
19
|
+
return fs.statSync(fullPath).isFile()
|
|
20
|
+
? [FileSystemUtils.canonicalizeExistingPath(fullPath)]
|
|
21
|
+
: [];
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const normalizedPattern = FileSystemUtils.toPosixPath(generates);
|
|
28
|
+
const matches = fg
|
|
29
|
+
.sync(normalizedPattern, { cwd: changeDir, onlyFiles: true, absolute: true })
|
|
30
|
+
.map((match) => FileSystemUtils.canonicalizeExistingPath(path.normalize(match)));
|
|
31
|
+
return Array.from(new Set(matches)).sort();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Checks if an artifact has at least one resolved output file.
|
|
35
|
+
*/
|
|
36
|
+
export function artifactOutputExists(changeDir, generates) {
|
|
37
|
+
return resolveArtifactOutputs(changeDir, generates).length > 0;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=outputs.js.map
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
-
import
|
|
3
|
-
import fg from 'fast-glob';
|
|
4
|
-
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
2
|
+
import { artifactOutputExists } from './outputs.js';
|
|
5
3
|
/**
|
|
6
4
|
* Detects which artifacts are completed by checking file existence in the change directory.
|
|
7
5
|
* Returns a Set of completed artifact IDs.
|
|
@@ -28,27 +26,6 @@ export function detectCompleted(graph, changeDir) {
|
|
|
28
26
|
* Supports both simple paths and glob patterns.
|
|
29
27
|
*/
|
|
30
28
|
function isArtifactComplete(generates, changeDir) {
|
|
31
|
-
|
|
32
|
-
// Check if it's a glob pattern
|
|
33
|
-
if (isGlobPattern(generates)) {
|
|
34
|
-
return hasGlobMatches(fullPattern);
|
|
35
|
-
}
|
|
36
|
-
// Simple file path - check if file exists
|
|
37
|
-
return fs.existsSync(fullPattern);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Checks if a path contains glob pattern characters.
|
|
41
|
-
*/
|
|
42
|
-
function isGlobPattern(pattern) {
|
|
43
|
-
return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Checks if a glob pattern has any matches.
|
|
47
|
-
* Normalizes Windows backslashes to forward slashes for cross-platform glob compatibility.
|
|
48
|
-
*/
|
|
49
|
-
function hasGlobMatches(pattern) {
|
|
50
|
-
const normalizedPattern = FileSystemUtils.toPosixPath(pattern);
|
|
51
|
-
const matches = fg.sync(normalizedPattern, { onlyFiles: true });
|
|
52
|
-
return matches.length > 0;
|
|
29
|
+
return artifactOutputExists(changeDir, generates);
|
|
53
30
|
}
|
|
54
31
|
//# sourceMappingURL=state.js.map
|
|
@@ -9,8 +9,9 @@ import { type AIToolOption } from './config.js';
|
|
|
9
9
|
* Scans the project path for AI tool configuration directories and returns
|
|
10
10
|
* the tools that are present.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* For tools with `detectionPaths`, checks those specific paths (files or
|
|
13
|
+
* directories). Otherwise checks for the tool's `skillsDir` directory at
|
|
14
|
+
* the project root. Only tools with a `skillsDir` property are considered.
|
|
14
15
|
*/
|
|
15
16
|
export declare function getAvailableTools(projectPath: string): AIToolOption[];
|
|
16
17
|
//# sourceMappingURL=available-tools.d.ts.map
|
|
@@ -11,13 +11,26 @@ import { AI_TOOLS } from './config.js';
|
|
|
11
11
|
* Scans the project path for AI tool configuration directories and returns
|
|
12
12
|
* the tools that are present.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* For tools with `detectionPaths`, checks those specific paths (files or
|
|
15
|
+
* directories). Otherwise checks for the tool's `skillsDir` directory at
|
|
16
|
+
* the project root. Only tools with a `skillsDir` property are considered.
|
|
16
17
|
*/
|
|
17
18
|
export function getAvailableTools(projectPath) {
|
|
18
19
|
return AI_TOOLS.filter((tool) => {
|
|
19
20
|
if (!tool.skillsDir)
|
|
20
21
|
return false;
|
|
22
|
+
if (tool.detectionPaths && tool.detectionPaths.length > 0) {
|
|
23
|
+
// statSync without .isDirectory() — detection paths can be files or directories
|
|
24
|
+
return tool.detectionPaths.some((p) => {
|
|
25
|
+
try {
|
|
26
|
+
fs.statSync(path.join(projectPath, p));
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
21
34
|
const dirPath = path.join(projectPath, tool.skillsDir);
|
|
22
35
|
try {
|
|
23
36
|
return fs.statSync(dirPath).isDirectory();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bob Shell Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Bob Shell following its markdown specification.
|
|
5
|
+
* Commands are stored in .bob/commands/ directory.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolCommandAdapter } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Bob Shell adapter for command generation.
|
|
10
|
+
* File path: .bob/commands/opsx-<id>.md
|
|
11
|
+
* Frontmatter: description, argument-hint
|
|
12
|
+
*/
|
|
13
|
+
export declare const bobAdapter: ToolCommandAdapter;
|
|
14
|
+
//# sourceMappingURL=bob.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bob Shell Command Adapter
|
|
3
|
+
*
|
|
4
|
+
* Formats commands for Bob Shell following its markdown specification.
|
|
5
|
+
* Commands are stored in .bob/commands/ directory.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
|
9
|
+
/**
|
|
10
|
+
* Escapes a string value for safe YAML output.
|
|
11
|
+
* Quotes the string if it contains special YAML characters.
|
|
12
|
+
*/
|
|
13
|
+
function escapeYamlValue(value) {
|
|
14
|
+
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
|
|
15
|
+
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
|
|
16
|
+
if (needsQuoting) {
|
|
17
|
+
// Use double quotes and escape internal double quotes and backslashes
|
|
18
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
19
|
+
return `"${escaped}"`;
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Bob Shell adapter for command generation.
|
|
25
|
+
* File path: .bob/commands/opsx-<id>.md
|
|
26
|
+
* Frontmatter: description, argument-hint
|
|
27
|
+
*/
|
|
28
|
+
export const bobAdapter = {
|
|
29
|
+
toolId: 'bob',
|
|
30
|
+
getFilePath(commandId) {
|
|
31
|
+
return path.join('.bob', 'commands', `opsx-${commandId}.md`);
|
|
32
|
+
},
|
|
33
|
+
formatFile(content) {
|
|
34
|
+
// Transform command references from colon to hyphen format for Bob
|
|
35
|
+
const transformedBody = transformToHyphenCommands(content.body);
|
|
36
|
+
return `---
|
|
37
|
+
description: ${escapeYamlValue(content.description)}
|
|
38
|
+
argument-hint: command arguments
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
${transformedBody}
|
|
42
|
+
`;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=bob.js.map
|