@fission-ai/openspec 1.6.0-beta.1 → 1.7.0
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 +28 -1
- package/dist/cli/index.js +73 -7
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +90 -50
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +21 -11
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +20 -24
- package/dist/commands/validate.js +18 -3
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +89 -6
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +7 -2
- package/dist/core/archive.d.ts +0 -1
- package/dist/core/archive.js +96 -33
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
- package/dist/core/artifact-graph/instruction-loader.js +55 -7
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +34 -6
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +6 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +8 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +7 -1
- package/dist/core/config-schema.js +40 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +21 -3
- package/dist/core/global-config.d.ts +5 -0
- package/dist/core/init.d.ts +35 -0
- package/dist/core/init.js +225 -39
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +249 -22
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +71 -1
- package/dist/core/migration.js +294 -1
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +8 -0
- package/dist/core/parsers/requirement-blocks.js +60 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.js +3 -29
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +9 -45
- package/dist/core/project-config.d.ts +25 -6
- package/dist/core/project-config.js +77 -7
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/shared/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +105 -6
- package/dist/core/specs-apply.d.ts +8 -33
- package/dist/core/specs-apply.js +252 -157
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/workflows/apply-change.js +54 -4
- package/dist/core/templates/workflows/archive-change.js +166 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
- package/dist/core/templates/workflows/continue-change.js +32 -40
- package/dist/core/templates/workflows/explore.js +12 -0
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/templates/workflows/propose.js +50 -32
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +172 -22
- package/dist/core/templates/workflows/update-change.js +20 -12
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +39 -5
- package/dist/core/update.js +288 -71
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +1 -7
- package/dist/core/validation/validator.js +92 -36
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +736 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +55 -33
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +30 -0
- package/dist/utils/change-metadata.js +101 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/command-references.d.ts +80 -6
- package/dist/utils/command-references.js +138 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +102 -0
- package/package.json +29 -30
- package/schemas/spec-driven/schema.yaml +60 -5
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +5 -1
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
package/dist/core/references.js
CHANGED
|
@@ -53,6 +53,46 @@ function registerFix(id, remote) {
|
|
|
53
53
|
}
|
|
54
54
|
return `Get a checkout from a teammate and run: openspec store register <path> --id ${id}`;
|
|
55
55
|
}
|
|
56
|
+
const WHITESPACE = /\s/;
|
|
57
|
+
/**
|
|
58
|
+
* Drop a CommonMark closing sequence (`## Purpose ##`). The closing run only
|
|
59
|
+
* counts when whitespace precedes it, so `Purpose###` keeps its hashes.
|
|
60
|
+
* Scans from the end so the cost stays linear in the title length.
|
|
61
|
+
*/
|
|
62
|
+
function stripClosingSequence(title) {
|
|
63
|
+
let end = title.length;
|
|
64
|
+
while (end > 0 && WHITESPACE.test(title[end - 1])) {
|
|
65
|
+
end--;
|
|
66
|
+
}
|
|
67
|
+
const hashEnd = end;
|
|
68
|
+
while (end > 0 && title[end - 1] === '#') {
|
|
69
|
+
end--;
|
|
70
|
+
}
|
|
71
|
+
const noClosingRun = end === hashEnd;
|
|
72
|
+
const missingLeadingSpace = end === 0 || !WHITESPACE.test(title[end - 1]);
|
|
73
|
+
if (noClosingRun || missingLeadingSpace) {
|
|
74
|
+
return title.trim();
|
|
75
|
+
}
|
|
76
|
+
return title.slice(0, end).trim();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Heading title, or null when the line is not an ATX heading. Hand-rolled
|
|
80
|
+
* rather than a regex so a title padded with whitespace cannot backtrack.
|
|
81
|
+
*/
|
|
82
|
+
function parseHeadingTitle(line) {
|
|
83
|
+
let level = 0;
|
|
84
|
+
while (level < 6 && level < line.length && line[level] === '#') {
|
|
85
|
+
level++;
|
|
86
|
+
}
|
|
87
|
+
if (level === 0 || level >= line.length || !WHITESPACE.test(line[level])) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
let start = level;
|
|
91
|
+
while (start < line.length && WHITESPACE.test(line[start])) {
|
|
92
|
+
start++;
|
|
93
|
+
}
|
|
94
|
+
return stripClosingSequence(line.slice(start));
|
|
95
|
+
}
|
|
56
96
|
/**
|
|
57
97
|
* Tolerant first-Purpose-line extraction. parseSpec() throws on specs
|
|
58
98
|
* without Purpose/Requirements sections; the index must never fail on an
|
|
@@ -79,12 +119,11 @@ export function extractFirstPurposeLine(markdown) {
|
|
|
79
119
|
if (fenceMarker !== null) {
|
|
80
120
|
continue;
|
|
81
121
|
}
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
122
|
+
const title = parseHeadingTitle(line);
|
|
123
|
+
if (title !== null) {
|
|
84
124
|
if (inPurpose) {
|
|
85
125
|
return '';
|
|
86
126
|
}
|
|
87
|
-
const title = heading[2].replace(/\s+#+\s*$/, '').trim();
|
|
88
127
|
inPurpose = title.toLowerCase() === 'purpose';
|
|
89
128
|
continue;
|
|
90
129
|
}
|
|
@@ -152,7 +191,6 @@ export function renderReferencedStoresSection(entries) {
|
|
|
152
191
|
* let hostile content forge instruction lines (slice 6.1 hardening).
|
|
153
192
|
*/
|
|
154
193
|
export function sanitizeInline(value, maxLength = 300) {
|
|
155
|
-
// eslint-disable-next-line no-control-regex
|
|
156
194
|
const flattened = value.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim();
|
|
157
195
|
return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}…` : flattened;
|
|
158
196
|
}
|
|
@@ -26,6 +26,10 @@ export interface RelationshipHealth {
|
|
|
26
26
|
remote?: string;
|
|
27
27
|
};
|
|
28
28
|
origin_url?: string;
|
|
29
|
+
drift?: {
|
|
30
|
+
ahead: number;
|
|
31
|
+
behind: number;
|
|
32
|
+
};
|
|
29
33
|
status: StoreDiagnostic[];
|
|
30
34
|
} | null;
|
|
31
35
|
references: ReferenceIndexEntry[];
|
|
@@ -42,6 +46,10 @@ export interface InspectRelationshipsInput {
|
|
|
42
46
|
metadataValid: boolean;
|
|
43
47
|
canonicalRemote?: string;
|
|
44
48
|
originUrl?: string;
|
|
49
|
+
drift?: {
|
|
50
|
+
ahead: number;
|
|
51
|
+
behind: number;
|
|
52
|
+
};
|
|
45
53
|
};
|
|
46
54
|
referenceEntries: ReferenceIndexEntry[];
|
|
47
55
|
registryUnreadable: boolean;
|
|
@@ -37,6 +37,18 @@ export function inspectRelationships(input) {
|
|
|
37
37
|
input.storeFacts.canonicalRemote !== input.storeFacts.originUrl) {
|
|
38
38
|
storeStatus.push(makeStoreDiagnostic('info', 'store_remote_divergence', `The store.yaml remote (${sanitizeInline(input.storeFacts.canonicalRemote, 200)}) differs from the checkout's origin (${sanitizeInline(input.storeFacts.originUrl, 200)}).`, { target: 'store.metadata' }));
|
|
39
39
|
}
|
|
40
|
+
// Checkout behind its upstream tracking ref: a read-only staleness
|
|
41
|
+
// signal, not a version pin — OpenSpec never syncs stores, so this
|
|
42
|
+
// compares against the local upstream ref, not the live remote.
|
|
43
|
+
// Behind means teammates on newer commits may resolve different specs.
|
|
44
|
+
// Ahead-only is normal (OpenSpec never pushes stores), so it stays quiet.
|
|
45
|
+
const drift = input.storeFacts.drift;
|
|
46
|
+
if (drift && drift.behind > 0) {
|
|
47
|
+
const behindCommits = `${drift.behind} commit${drift.behind === 1 ? '' : 's'}`;
|
|
48
|
+
storeStatus.push(makeStoreDiagnostic('info', 'store_checkout_drift', drift.ahead > 0
|
|
49
|
+
? `This store checkout has diverged from its upstream tracking branch (${drift.behind} behind, ${drift.ahead} ahead); teammates on newer commits may resolve different specs.`
|
|
50
|
+
: `This store checkout is ${behindCommits} behind its upstream tracking branch; teammates on newer commits may resolve different specs.`, { target: 'store.git' }));
|
|
51
|
+
}
|
|
40
52
|
store = {
|
|
41
53
|
id: input.storeFacts.id,
|
|
42
54
|
metadata: {
|
|
@@ -47,6 +59,7 @@ export function inspectRelationships(input) {
|
|
|
47
59
|
: {}),
|
|
48
60
|
},
|
|
49
61
|
...(input.storeFacts.originUrl ? { origin_url: input.storeFacts.originUrl } : {}),
|
|
62
|
+
...(drift ? { drift } : {}),
|
|
50
63
|
status: storeStatus,
|
|
51
64
|
};
|
|
52
65
|
}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* - `--store <id>` selects a registered store's root.
|
|
8
8
|
* - Without `--store`, the nearest ancestor containing `openspec/` wins.
|
|
9
9
|
* Leftover workspace view state is never considered a root here.
|
|
10
|
-
* - With no nearest root,
|
|
11
|
-
*
|
|
10
|
+
* - With no nearest root, a global `defaultStore` (if set) is the last
|
|
11
|
+
* machine-level fallback before the selection hint error.
|
|
12
|
+
* - With no nearest root and no default, registered stores produce a
|
|
13
|
+
* selection hint error; otherwise commands may treat the current
|
|
14
|
+
* directory as an implicit root.
|
|
12
15
|
*
|
|
13
16
|
* Diagnostic codes reuse the store taxonomy where an error passes
|
|
14
17
|
* through unchanged (`invalid_store_id`, metadata parse failures);
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
* `no_openspec_root`).
|
|
20
23
|
*/
|
|
21
24
|
import { type PlanningHome } from './planning-home.js';
|
|
22
|
-
export type OpenSpecRootSource = 'store' | 'declared' | 'nearest' | 'implicit';
|
|
25
|
+
export type OpenSpecRootSource = 'store' | 'declared' | 'global_default' | 'nearest' | 'implicit';
|
|
23
26
|
export interface StoreSelectorOptions {
|
|
24
27
|
store?: string;
|
|
25
28
|
storePath?: string;
|
|
@@ -84,9 +87,9 @@ export interface RootOutput {
|
|
|
84
87
|
}
|
|
85
88
|
export declare function toRootOutput(root: ResolvedOpenSpecRoot): RootOutput;
|
|
86
89
|
/**
|
|
87
|
-
* A store-selected root — explicit `--store
|
|
88
|
-
* Cross-root behavior (absolute paths, --store hints,
|
|
89
|
-
* noun-form suggestions) keys on this, never on `source` directly.
|
|
90
|
+
* A store-selected root — explicit `--store`, a declared pointer, or the
|
|
91
|
+
* global default. Cross-root behavior (absolute paths, --store hints,
|
|
92
|
+
* suppressed noun-form suggestions) keys on this, never on `source` directly.
|
|
90
93
|
*/
|
|
91
94
|
export declare function isStoreSelectedRoot(root: ResolvedOpenSpecRoot): root is ResolvedOpenSpecRoot & {
|
|
92
95
|
storeId: string;
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* - `--store <id>` selects a registered store's root.
|
|
8
8
|
* - Without `--store`, the nearest ancestor containing `openspec/` wins.
|
|
9
9
|
* Leftover workspace view state is never considered a root here.
|
|
10
|
-
* - With no nearest root,
|
|
11
|
-
*
|
|
10
|
+
* - With no nearest root, a global `defaultStore` (if set) is the last
|
|
11
|
+
* machine-level fallback before the selection hint error.
|
|
12
|
+
* - With no nearest root and no default, registered stores produce a
|
|
13
|
+
* selection hint error; otherwise commands may treat the current
|
|
14
|
+
* directory as an implicit root.
|
|
12
15
|
*
|
|
13
16
|
* Diagnostic codes reuse the store taxonomy where an error passes
|
|
14
17
|
* through unchanged (`invalid_store_id`, metadata parse failures);
|
|
@@ -26,6 +29,7 @@ import { getStoreRootForBackend } from './store/registry.js';
|
|
|
26
29
|
import { inspectOpenSpecRoot } from './openspec-root.js';
|
|
27
30
|
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
28
31
|
import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
|
|
32
|
+
import { getGlobalConfig } from './global-config.js';
|
|
29
33
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
30
34
|
export class RootSelectionError extends Error {
|
|
31
35
|
diagnostic;
|
|
@@ -221,6 +225,32 @@ async function resolveNearestOrDeclaredRoot(nearestRoot, globalDataDir) {
|
|
|
221
225
|
throw error;
|
|
222
226
|
}
|
|
223
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* The machine-level fallback: the global `defaultStore` resolved as a root,
|
|
230
|
+
* with its own provenance (`global_default`) so JSON surfaces can tell a
|
|
231
|
+
* machine-wide default from a repo's `store:` pointer. Mirrors the
|
|
232
|
+
* declared-pointer catch — a stale or unregistered id degrades to the
|
|
233
|
+
* underlying error, reshaped to point at clearing the global default
|
|
234
|
+
* rather than passing --store.
|
|
235
|
+
*/
|
|
236
|
+
async function resolveDefaultStoreRoot(id, globalDataDir) {
|
|
237
|
+
try {
|
|
238
|
+
return await resolveStoreRoot(id, globalDataDir, 'global_default');
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error instanceof RootSelectionError) {
|
|
242
|
+
const staleFix = error.diagnostic.code === 'unknown_store' ||
|
|
243
|
+
error.diagnostic.code === 'no_registered_stores'
|
|
244
|
+
? `Register the store (openspec store register <path> --id ${id}) or clear the stale global default (openspec config unset defaultStore).`
|
|
245
|
+
: error.diagnostic.fix;
|
|
246
|
+
throw new RootSelectionError(`Global defaultStore '${id}': ${error.message}`, error.diagnostic.code, {
|
|
247
|
+
...(error.diagnostic.target ? { target: error.diagnostic.target } : {}),
|
|
248
|
+
...(staleFix ? { fix: staleFix } : {}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
224
254
|
export async function resolveOpenSpecRoot(options = {}) {
|
|
225
255
|
if (options.storePath !== undefined) {
|
|
226
256
|
throw new RootSelectionError('--store-path is not supported. Register the path with openspec store register <path>, then select it with --store <id>.', 'store_path_not_supported', {
|
|
@@ -236,6 +266,13 @@ export async function resolveOpenSpecRoot(options = {}) {
|
|
|
236
266
|
if (nearestRoot) {
|
|
237
267
|
return resolveNearestOrDeclaredRoot(nearestRoot, options.globalDataDir);
|
|
238
268
|
}
|
|
269
|
+
// Machine-level fallback: a global defaultStore is consulted only after
|
|
270
|
+
// --store, the nearest local root, and project-level pointers have all
|
|
271
|
+
// failed to resolve — it changes the failure path, never the precedence.
|
|
272
|
+
const defaultStore = getGlobalConfig().defaultStore;
|
|
273
|
+
if (defaultStore) {
|
|
274
|
+
return resolveDefaultStoreRoot(defaultStore, options.globalDataDir);
|
|
275
|
+
}
|
|
239
276
|
let registry;
|
|
240
277
|
try {
|
|
241
278
|
registry = await readStoreRegistryState(options.globalDataDir ? { globalDataDir: options.globalDataDir } : {});
|
|
@@ -265,9 +302,9 @@ export function toRootOutput(root) {
|
|
|
265
302
|
};
|
|
266
303
|
}
|
|
267
304
|
/**
|
|
268
|
-
* A store-selected root — explicit `--store
|
|
269
|
-
* Cross-root behavior (absolute paths, --store hints,
|
|
270
|
-
* noun-form suggestions) keys on this, never on `source` directly.
|
|
305
|
+
* A store-selected root — explicit `--store`, a declared pointer, or the
|
|
306
|
+
* global default. Cross-root behavior (absolute paths, --store hints,
|
|
307
|
+
* suppressed noun-form suggestions) keys on this, never on `source` directly.
|
|
271
308
|
*/
|
|
272
309
|
export function isStoreSelectedRoot(root) {
|
|
273
310
|
return root.storeId !== undefined;
|
|
@@ -32,9 +32,13 @@ export interface ToolVersionStatus {
|
|
|
32
32
|
toolId: string;
|
|
33
33
|
/** The tool's display name */
|
|
34
34
|
toolName: string;
|
|
35
|
-
/** Whether the tool has any skills configured */
|
|
35
|
+
/** Whether the tool has any skills or commands configured */
|
|
36
36
|
configured: boolean;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The generatedBy version recorded in the tool's skill files. For a tool that
|
|
39
|
+
* has commands but no skills, the current version when the command files match
|
|
40
|
+
* what would be generated now. Null when neither says the files are current.
|
|
41
|
+
*/
|
|
38
42
|
generatedByVersion: string | null;
|
|
39
43
|
/** Whether the tool needs updating (version mismatch or missing) */
|
|
40
44
|
needsUpdate: boolean;
|
|
@@ -47,6 +51,19 @@ export declare function getToolsWithSkillsDir(): string[];
|
|
|
47
51
|
* Checks which skill files exist for a tool.
|
|
48
52
|
*/
|
|
49
53
|
export declare function getToolSkillStatus(projectRoot: string, toolId: string): ToolSkillStatus;
|
|
54
|
+
/**
|
|
55
|
+
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
56
|
+
*/
|
|
57
|
+
export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Checks whether command files for a tool on disk match current generated command contents.
|
|
60
|
+
*
|
|
61
|
+
* Command files carry no version stamp, so content equality is the only available
|
|
62
|
+
* "is this current?" signal for a commands-only install.
|
|
63
|
+
*/
|
|
64
|
+
export declare function areCommandFilesUpToDate(projectRoot: string, toolId: string, options?: {
|
|
65
|
+
workflows?: readonly string[];
|
|
66
|
+
}): boolean;
|
|
50
67
|
/**
|
|
51
68
|
* Gets the skill status for all tools with skillsDir configured.
|
|
52
69
|
*/
|
|
@@ -57,11 +74,14 @@ export declare function getToolStates(projectRoot: string): Map<string, ToolSkil
|
|
|
57
74
|
*/
|
|
58
75
|
export declare function extractGeneratedByVersion(skillFilePath: string): string | null;
|
|
59
76
|
/**
|
|
60
|
-
* Gets version status for a tool by reading
|
|
77
|
+
* Gets version status for a tool by reading its skill files, falling back to a
|
|
78
|
+
* command-content fingerprint for installs that have commands but no skills.
|
|
61
79
|
*/
|
|
62
|
-
export declare function getToolVersionStatus(projectRoot: string, toolId: string, currentVersion: string
|
|
80
|
+
export declare function getToolVersionStatus(projectRoot: string, toolId: string, currentVersion: string, options?: {
|
|
81
|
+
workflows?: readonly string[];
|
|
82
|
+
}): ToolVersionStatus;
|
|
63
83
|
/**
|
|
64
|
-
* Gets all configured tools in the project.
|
|
84
|
+
* Gets all configured tools in the project (configured via skills or commands).
|
|
65
85
|
*/
|
|
66
86
|
export declare function getConfiguredTools(projectRoot: string): string[];
|
|
67
87
|
/**
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import { AI_TOOLS } from '../config.js';
|
|
9
|
+
import { CommandAdapterRegistry, generateCommands } from '../command-generation/index.js';
|
|
10
|
+
import { getCommandContents } from './skill-generation.js';
|
|
11
|
+
import { getGlobalConfig } from '../global-config.js';
|
|
12
|
+
import { getProfileWorkflows, ALL_WORKFLOWS } from '../profiles.js';
|
|
9
13
|
/**
|
|
10
14
|
* Names of skill directories created by openspec init.
|
|
11
15
|
*/
|
|
@@ -68,6 +72,88 @@ export function getToolSkillStatus(projectRoot, toolId) {
|
|
|
68
72
|
skillCount,
|
|
69
73
|
};
|
|
70
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
77
|
+
*/
|
|
78
|
+
export function toolHasAnyConfiguredCommand(projectPath, toolId) {
|
|
79
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
80
|
+
if (!adapter)
|
|
81
|
+
return false;
|
|
82
|
+
for (const commandId of COMMAND_IDS) {
|
|
83
|
+
const cmdPath = adapter.getFilePath(commandId);
|
|
84
|
+
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
85
|
+
if (fs.existsSync(fullPath)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Normalizes checkout artifacts that are not real content drift: a UTF-8 BOM and
|
|
93
|
+
* CRLF line endings, which a Windows clone with `core.autocrlf` reintroduces on
|
|
94
|
+
* every checkout of committed command files.
|
|
95
|
+
*/
|
|
96
|
+
function normalizeCommandContent(content) {
|
|
97
|
+
return content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Checks whether command files for a tool on disk match current generated command contents.
|
|
101
|
+
*
|
|
102
|
+
* Command files carry no version stamp, so content equality is the only available
|
|
103
|
+
* "is this current?" signal for a commands-only install.
|
|
104
|
+
*/
|
|
105
|
+
export function areCommandFilesUpToDate(projectRoot, toolId, options) {
|
|
106
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
107
|
+
if (!adapter)
|
|
108
|
+
return false;
|
|
109
|
+
let workflows;
|
|
110
|
+
if (options?.workflows) {
|
|
111
|
+
workflows = options.workflows;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
try {
|
|
115
|
+
const globalCfg = getGlobalConfig();
|
|
116
|
+
const profile = globalCfg.profile ?? 'core';
|
|
117
|
+
workflows = getProfileWorkflows(profile, globalCfg.workflows);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
workflows = ALL_WORKFLOWS;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const knownWorkflows = workflows.filter((w) => ALL_WORKFLOWS.includes(w));
|
|
124
|
+
const commandContents = getCommandContents(knownWorkflows);
|
|
125
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
126
|
+
if (generatedCommands.length === 0) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
for (const cmd of generatedCommands) {
|
|
130
|
+
const cmdPath = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectRoot, cmd.path);
|
|
131
|
+
if (!fs.existsSync(cmdPath)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const existingContent = fs.readFileSync(cmdPath, 'utf-8');
|
|
136
|
+
if (normalizeCommandContent(existingContent) !== normalizeCommandContent(cmd.fileContent)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Also check no extra command files exist for deselected workflows
|
|
145
|
+
const desiredWorkflowSet = new Set(knownWorkflows);
|
|
146
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
147
|
+
if (desiredWorkflowSet.has(workflow))
|
|
148
|
+
continue;
|
|
149
|
+
const cmdPath = adapter.getFilePath(workflow);
|
|
150
|
+
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectRoot, cmdPath);
|
|
151
|
+
if (fs.existsSync(fullPath)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
71
157
|
/**
|
|
72
158
|
* Gets the skill status for all tools with skillsDir configured.
|
|
73
159
|
*/
|
|
@@ -109,9 +195,10 @@ export function extractGeneratedByVersion(skillFilePath) {
|
|
|
109
195
|
}
|
|
110
196
|
}
|
|
111
197
|
/**
|
|
112
|
-
* Gets version status for a tool by reading
|
|
198
|
+
* Gets version status for a tool by reading its skill files, falling back to a
|
|
199
|
+
* command-content fingerprint for installs that have commands but no skills.
|
|
113
200
|
*/
|
|
114
|
-
export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
|
|
201
|
+
export function getToolVersionStatus(projectRoot, toolId, currentVersion, options) {
|
|
115
202
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
116
203
|
if (!tool?.skillsDir) {
|
|
117
204
|
return {
|
|
@@ -124,7 +211,7 @@ export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
|
|
|
124
211
|
}
|
|
125
212
|
const skillsDir = path.join(projectRoot, tool.skillsDir, 'skills');
|
|
126
213
|
let generatedByVersion = null;
|
|
127
|
-
// Find the first skill file that exists and read its version
|
|
214
|
+
// 1. Find the first skill file that exists and read its version
|
|
128
215
|
for (const skillName of SKILL_NAMES) {
|
|
129
216
|
const skillFile = path.join(skillsDir, skillName, 'SKILL.md');
|
|
130
217
|
if (fs.existsSync(skillFile)) {
|
|
@@ -132,7 +219,15 @@ export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
|
|
|
132
219
|
break;
|
|
133
220
|
}
|
|
134
221
|
}
|
|
135
|
-
const
|
|
222
|
+
const skillConfigured = getToolSkillStatus(projectRoot, toolId).configured;
|
|
223
|
+
const commandConfigured = toolHasAnyConfiguredCommand(projectRoot, toolId);
|
|
224
|
+
const configured = skillConfigured || commandConfigured;
|
|
225
|
+
// 2. Commands-only installs have no skill file to read a version from, so fall
|
|
226
|
+
// back to comparing the generated command content. Deliberately skipped when
|
|
227
|
+
// skill files exist: an unreadable version there must still force a rewrite.
|
|
228
|
+
if (!skillConfigured && commandConfigured && areCommandFilesUpToDate(projectRoot, toolId, options)) {
|
|
229
|
+
generatedByVersion = currentVersion;
|
|
230
|
+
}
|
|
136
231
|
const needsUpdate = configured && (generatedByVersion === null || generatedByVersion !== currentVersion);
|
|
137
232
|
return {
|
|
138
233
|
toolId,
|
|
@@ -143,11 +238,15 @@ export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
|
|
|
143
238
|
};
|
|
144
239
|
}
|
|
145
240
|
/**
|
|
146
|
-
* Gets all configured tools in the project.
|
|
241
|
+
* Gets all configured tools in the project (configured via skills or commands).
|
|
147
242
|
*/
|
|
148
243
|
export function getConfiguredTools(projectRoot) {
|
|
149
244
|
return AI_TOOLS
|
|
150
|
-
.filter((t) =>
|
|
245
|
+
.filter((t) => {
|
|
246
|
+
if (!t.skillsDir)
|
|
247
|
+
return false;
|
|
248
|
+
return getToolSkillStatus(projectRoot, t.value).configured || toolHasAnyConfiguredCommand(projectRoot, t.value);
|
|
249
|
+
})
|
|
151
250
|
.map((t) => t.value);
|
|
152
251
|
}
|
|
153
252
|
/**
|
|
@@ -5,28 +5,12 @@
|
|
|
5
5
|
* Applies delta specs from a change to main specs without archiving.
|
|
6
6
|
*/
|
|
7
7
|
export interface SpecUpdate {
|
|
8
|
+
/** Capability id relative to the specs root, forward-slash separated (e.g. "web" or "platform/session-layout"). */
|
|
9
|
+
id: string;
|
|
8
10
|
source: string;
|
|
9
11
|
target: string;
|
|
10
12
|
exists: boolean;
|
|
11
13
|
}
|
|
12
|
-
export interface ApplyResult {
|
|
13
|
-
capability: string;
|
|
14
|
-
added: number;
|
|
15
|
-
modified: number;
|
|
16
|
-
removed: number;
|
|
17
|
-
renamed: number;
|
|
18
|
-
}
|
|
19
|
-
export interface SpecsApplyOutput {
|
|
20
|
-
changeName: string;
|
|
21
|
-
capabilities: ApplyResult[];
|
|
22
|
-
totals: {
|
|
23
|
-
added: number;
|
|
24
|
-
modified: number;
|
|
25
|
-
removed: number;
|
|
26
|
-
renamed: number;
|
|
27
|
-
};
|
|
28
|
-
noChanges: boolean;
|
|
29
|
-
}
|
|
30
14
|
/**
|
|
31
15
|
* Find all delta spec files that need to be applied from a change.
|
|
32
16
|
*/
|
|
@@ -45,6 +29,7 @@ export declare function buildUpdatedSpec(update: SpecUpdate, changeName: string,
|
|
|
45
29
|
removed: number;
|
|
46
30
|
renamed: number;
|
|
47
31
|
};
|
|
32
|
+
warnings: string[];
|
|
48
33
|
}>;
|
|
49
34
|
/**
|
|
50
35
|
* Write an updated spec to disk.
|
|
@@ -59,20 +44,10 @@ export declare function writeUpdatedSpec(update: SpecUpdate, rebuilt: string, co
|
|
|
59
44
|
displayPath?: string;
|
|
60
45
|
}): Promise<void>;
|
|
61
46
|
/**
|
|
62
|
-
* Build a skeleton spec for new capabilities.
|
|
47
|
+
* Build a skeleton spec for new capabilities. When the delta spec authored a
|
|
48
|
+
* `## Purpose`, carry it over instead of the TBD placeholder (#1413) - archive
|
|
49
|
+
* invents the Purpose for a brand-new main spec either way, and the author's
|
|
50
|
+
* own wording beats a placeholder they then have to hand-edit.
|
|
63
51
|
*/
|
|
64
|
-
export declare function buildSpecSkeleton(specFolderName: string, changeName: string): string;
|
|
65
|
-
/**
|
|
66
|
-
* Apply all delta specs from a change to main specs.
|
|
67
|
-
*
|
|
68
|
-
* @param projectRoot - The project root directory
|
|
69
|
-
* @param changeName - The name of the change to apply
|
|
70
|
-
* @param options - Options for the operation
|
|
71
|
-
* @returns Result of the operation with counts
|
|
72
|
-
*/
|
|
73
|
-
export declare function applySpecs(projectRoot: string, changeName: string, options?: {
|
|
74
|
-
dryRun?: boolean;
|
|
75
|
-
skipValidation?: boolean;
|
|
76
|
-
silent?: boolean;
|
|
77
|
-
}): Promise<SpecsApplyOutput>;
|
|
52
|
+
export declare function buildSpecSkeleton(specFolderName: string, changeName: string, purpose?: string): string;
|
|
78
53
|
//# sourceMappingURL=specs-apply.d.ts.map
|