@fission-ai/openspec 1.3.0 → 1.4.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 +9 -6
- package/bin/openspec.js +3 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +46 -2
- package/dist/commands/completion.js +7 -0
- package/dist/commands/config.js +64 -5
- package/dist/commands/context-store.d.ts +3 -0
- package/dist/commands/context-store.js +475 -0
- package/dist/commands/initiative.d.ts +13 -0
- package/dist/commands/initiative.js +318 -0
- package/dist/commands/workflow/index.d.ts +2 -0
- package/dist/commands/workflow/index.js +1 -0
- package/dist/commands/workflow/initiative-link.d.ts +24 -0
- package/dist/commands/workflow/initiative-link.js +47 -0
- package/dist/commands/workflow/instructions.d.ts +1 -1
- package/dist/commands/workflow/instructions.js +46 -83
- package/dist/commands/workflow/new-change.d.ts +6 -0
- package/dist/commands/workflow/new-change.js +114 -17
- package/dist/commands/workflow/set-change.d.ts +13 -0
- package/dist/commands/workflow/set-change.js +87 -0
- package/dist/commands/workflow/shared.d.ts +5 -3
- package/dist/commands/workflow/shared.js +6 -6
- package/dist/commands/workflow/status.js +24 -9
- package/dist/commands/workflow/templates.js +5 -4
- package/dist/commands/workspace/context-status.d.ts +4 -0
- package/dist/commands/workspace/context-status.js +59 -0
- package/dist/commands/workspace/open-target-selection.d.ts +13 -0
- package/dist/commands/workspace/open-target-selection.js +146 -0
- package/dist/commands/workspace/open-view.d.ts +62 -0
- package/dist/commands/workspace/open-view.js +249 -0
- package/dist/commands/workspace/open.d.ts +37 -0
- package/dist/commands/workspace/open.js +110 -0
- package/dist/commands/workspace/opener-selection.d.ts +11 -0
- package/dist/commands/workspace/opener-selection.js +98 -0
- package/dist/commands/workspace/operations.d.ts +29 -0
- package/dist/commands/workspace/operations.js +543 -0
- package/dist/commands/workspace/prompt-theme.d.ts +29 -0
- package/dist/commands/workspace/prompt-theme.js +24 -0
- package/dist/commands/workspace/registration.d.ts +13 -0
- package/dist/commands/workspace/registration.js +84 -0
- package/dist/commands/workspace/selection.d.ts +9 -0
- package/dist/commands/workspace/selection.js +115 -0
- package/dist/commands/workspace/setup-prompts.d.ts +13 -0
- package/dist/commands/workspace/setup-prompts.js +121 -0
- package/dist/commands/workspace/types.d.ts +103 -0
- package/dist/commands/workspace/types.js +36 -0
- package/dist/commands/workspace.d.ts +6 -0
- package/dist/commands/workspace.js +587 -0
- package/dist/core/artifact-graph/index.d.ts +3 -1
- package/dist/core/artifact-graph/index.js +1 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +41 -1
- package/dist/core/artifact-graph/instruction-loader.js +51 -9
- 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/artifact-graph/types.d.ts +0 -5
- package/dist/core/artifact-graph/types.js +0 -14
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +18 -0
- package/dist/core/change-metadata/schema.js +28 -0
- package/dist/core/change-status-policy.d.ts +50 -0
- package/dist/core/change-status-policy.js +70 -0
- package/dist/core/collections/index.d.ts +3 -0
- package/dist/core/collections/index.js +3 -0
- package/dist/core/collections/initiatives/collection.d.ts +4 -0
- package/dist/core/collections/initiatives/collection.js +17 -0
- package/dist/core/collections/initiatives/index.d.ts +6 -0
- package/dist/core/collections/initiatives/index.js +6 -0
- package/dist/core/collections/initiatives/operations.d.ts +49 -0
- package/dist/core/collections/initiatives/operations.js +175 -0
- package/dist/core/collections/initiatives/resolution.d.ts +87 -0
- package/dist/core/collections/initiatives/resolution.js +374 -0
- package/dist/core/collections/initiatives/schema.d.ts +41 -0
- package/dist/core/collections/initiatives/schema.js +134 -0
- package/dist/core/collections/initiatives/templates.d.ts +12 -0
- package/dist/core/collections/initiatives/templates.js +90 -0
- package/dist/core/collections/runtime.d.ts +46 -0
- package/dist/core/collections/runtime.js +194 -0
- package/dist/core/completions/command-registry.d.ts +1 -5
- package/dist/core/completions/command-registry.js +532 -32
- package/dist/core/completions/completion-provider.d.ts +11 -0
- package/dist/core/completions/completion-provider.js +27 -0
- package/dist/core/completions/generators/bash-generator.d.ts +3 -0
- package/dist/core/completions/generators/bash-generator.js +60 -4
- package/dist/core/completions/generators/fish-generator.js +3 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +3 -0
- package/dist/core/completions/generators/powershell-generator.js +63 -4
- package/dist/core/completions/generators/zsh-generator.d.ts +3 -0
- package/dist/core/completions/generators/zsh-generator.js +46 -22
- package/dist/core/completions/installers/zsh-installer.d.ts +0 -8
- package/dist/core/completions/installers/zsh-installer.js +3 -31
- package/dist/core/completions/shared-flags.d.ts +12 -0
- package/dist/core/completions/shared-flags.js +28 -0
- package/dist/core/completions/templates/bash-templates.d.ts +1 -1
- package/dist/core/completions/templates/bash-templates.js +6 -0
- package/dist/core/completions/templates/fish-templates.d.ts +1 -1
- package/dist/core/completions/templates/fish-templates.js +6 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +1 -1
- package/dist/core/completions/templates/powershell-templates.js +9 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +1 -1
- package/dist/core/completions/templates/zsh-templates.js +9 -0
- package/dist/core/completions/types.d.ts +23 -1
- package/dist/core/config.js +3 -1
- package/dist/core/context-store/binding.d.ts +53 -0
- package/dist/core/context-store/binding.js +197 -0
- package/dist/core/context-store/errors.d.ts +20 -0
- package/dist/core/context-store/errors.js +22 -0
- package/dist/core/context-store/foundation.d.ts +55 -0
- package/dist/core/context-store/foundation.js +321 -0
- package/dist/core/context-store/index.d.ts +6 -0
- package/dist/core/context-store/index.js +6 -0
- package/dist/core/context-store/operations.d.ts +85 -0
- package/dist/core/context-store/operations.js +528 -0
- package/dist/core/context-store/registry.d.ts +45 -0
- package/dist/core/context-store/registry.js +229 -0
- package/dist/core/global-config.d.ts +6 -1
- package/dist/core/global-config.js +15 -16
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +4 -0
- 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/requirement-blocks.js +5 -5
- package/dist/core/parsers/spec-structure.d.ts +9 -0
- package/dist/core/parsers/spec-structure.js +88 -0
- package/dist/core/planning-home.d.ts +21 -0
- package/dist/core/planning-home.js +108 -0
- package/dist/core/profiles.d.ts +1 -1
- package/dist/core/profiles.js +1 -1
- package/dist/core/specs-apply.js +9 -1
- package/dist/core/templates/workflows/apply-change.js +10 -4
- package/dist/core/templates/workflows/archive-change.js +21 -15
- package/dist/core/templates/workflows/bulk-archive-change.js +14 -10
- package/dist/core/templates/workflows/continue-change.js +6 -4
- package/dist/core/templates/workflows/explore.js +8 -10
- package/dist/core/templates/workflows/ff-change.js +8 -6
- package/dist/core/templates/workflows/new-change.js +6 -6
- package/dist/core/templates/workflows/onboard.js +13 -15
- package/dist/core/templates/workflows/propose.js +8 -6
- package/dist/core/templates/workflows/sync-specs.js +28 -10
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +5 -0
- package/dist/core/update.js +20 -0
- package/dist/core/validation/validator.d.ts +11 -0
- package/dist/core/validation/validator.js +28 -2
- package/dist/core/workspace/foundation.d.ts +67 -0
- package/dist/core/workspace/foundation.js +295 -0
- package/dist/core/workspace/index.d.ts +8 -0
- package/dist/core/workspace/index.js +8 -0
- package/dist/core/workspace/legacy-state.d.ts +28 -0
- package/dist/core/workspace/legacy-state.js +200 -0
- package/dist/core/workspace/link-input.d.ts +9 -0
- package/dist/core/workspace/link-input.js +32 -0
- package/dist/core/workspace/open-surface.d.ts +45 -0
- package/dist/core/workspace/open-surface.js +215 -0
- package/dist/core/workspace/openers.d.ts +21 -0
- package/dist/core/workspace/openers.js +124 -0
- package/dist/core/workspace/registry.d.ts +24 -0
- package/dist/core/workspace/registry.js +146 -0
- package/dist/core/workspace/skills.d.ts +57 -0
- package/dist/core/workspace/skills.js +334 -0
- package/dist/core/workspace/state-io.d.ts +10 -0
- package/dist/core/workspace/state-io.js +119 -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/change-metadata.d.ts +5 -2
- package/dist/utils/change-metadata.js +7 -13
- package/dist/utils/change-utils.d.ts +9 -0
- package/dist/utils/change-utils.js +7 -5
- package/dist/utils/file-system.d.ts +5 -0
- package/dist/utils/file-system.js +21 -1
- package/package.json +2 -1
- package/schemas/workspace-planning/schema.yaml +72 -0
- package/schemas/workspace-planning/templates/design.md +33 -0
- package/schemas/workspace-planning/templates/proposal.md +28 -0
- package/schemas/workspace-planning/templates/spec.md +9 -0
- package/schemas/workspace-planning/templates/tasks.md +15 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { getContextStoreMetadataPath, getContextStoreMetadataDir, listContextStoreRegistryEntries, readContextStoreRegistryState, readOptionalContextStoreMetadataState, resolveGitContextStoreBackendConfig, updateContextStoreRegistryState, validateContextStoreId, writeContextStoreMetadataState, } from './foundation.js';
|
|
3
|
+
import { ContextStoreError } from './errors.js';
|
|
4
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
5
|
+
export function getStoreRootForBackend(backend) {
|
|
6
|
+
switch (backend.type) {
|
|
7
|
+
case 'git':
|
|
8
|
+
return backend.local_path;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function normalizePathForComparison(targetPath) {
|
|
12
|
+
try {
|
|
13
|
+
return FileSystemUtils.canonicalizeExistingPath(targetPath);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return targetPath;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function assertNoRegisteredStoreConflict(registry, id, backend) {
|
|
20
|
+
const nextPath = normalizePathForComparison(getStoreRootForBackend(backend));
|
|
21
|
+
for (const entry of listContextStoreRegistryEntries(registry ?? { version: 1, stores: {} })) {
|
|
22
|
+
const entryPath = normalizePathForComparison(getStoreRootForBackend(entry.backend));
|
|
23
|
+
if (entry.id === id && entryPath === nextPath) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (entry.id === id) {
|
|
27
|
+
throw new ContextStoreError(`Context store '${id}' is already registered at ${getStoreRootForBackend(entry.backend)}.`, 'context_store_id_conflict', {
|
|
28
|
+
target: 'context_store.id',
|
|
29
|
+
fix: 'Use the existing registration or choose a different context store id.',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (entryPath === nextPath) {
|
|
33
|
+
throw new ContextStoreError(`Context store path is already registered as '${entry.id}'.`, 'context_store_path_conflict', {
|
|
34
|
+
target: 'context_store.root',
|
|
35
|
+
fix: `Use the existing '${entry.id}' registration or choose a different path.`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function withRegisteredStore(registry, id, backend) {
|
|
41
|
+
assertNoRegisteredStoreConflict(registry, id, backend);
|
|
42
|
+
const stores = {
|
|
43
|
+
...(registry?.stores ?? {}),
|
|
44
|
+
[id]: {
|
|
45
|
+
backend,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
version: 1,
|
|
50
|
+
stores: Object.fromEntries(Object.entries(stores).sort(([leftId], [rightId]) => leftId.localeCompare(rightId))),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function getRegisteredStoreOrThrow(registry, id) {
|
|
54
|
+
const entry = registry?.stores[id];
|
|
55
|
+
if (!entry) {
|
|
56
|
+
throw new ContextStoreError(`Unknown context store '${id}'`, 'context_store_not_found', {
|
|
57
|
+
target: 'context_store.id',
|
|
58
|
+
fix: 'Run openspec context-store list to see registered stores.',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
backend: entry.backend,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function contextStoreBackendsMatch(actual, expected) {
|
|
67
|
+
return (actual.type === expected.type &&
|
|
68
|
+
normalizePathForComparison(actual.local_path) ===
|
|
69
|
+
normalizePathForComparison(expected.local_path) &&
|
|
70
|
+
actual.remote === expected.remote &&
|
|
71
|
+
actual.branch === expected.branch);
|
|
72
|
+
}
|
|
73
|
+
function assertExpectedRegisteredBackend(id, actual, expected) {
|
|
74
|
+
if (!expected || contextStoreBackendsMatch(actual, expected))
|
|
75
|
+
return;
|
|
76
|
+
throw new ContextStoreError(`Context store '${id}' changed before cleanup completed.`, 'context_store_registry_changed', {
|
|
77
|
+
target: 'context_store.registry',
|
|
78
|
+
fix: 'Retry the cleanup command after reviewing the current context-store registration.',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function withoutRegisteredStore(registry, id, expectedBackend) {
|
|
82
|
+
const removed = getRegisteredStoreOrThrow(registry, id);
|
|
83
|
+
assertExpectedRegisteredBackend(id, removed.backend, expectedBackend);
|
|
84
|
+
const stores = { ...(registry?.stores ?? {}) };
|
|
85
|
+
delete stores[id];
|
|
86
|
+
return {
|
|
87
|
+
removed,
|
|
88
|
+
next: {
|
|
89
|
+
version: 1,
|
|
90
|
+
stores: Object.fromEntries(Object.entries(stores).sort(([leftId], [rightId]) => leftId.localeCompare(rightId))),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function ensureStoreMetadata(storeRoot, id, options) {
|
|
95
|
+
const metadata = await readOptionalContextStoreMetadataState(storeRoot);
|
|
96
|
+
if (!metadata) {
|
|
97
|
+
if (!options.writeIfMissing) {
|
|
98
|
+
throw new ContextStoreError(`Registered context store '${id}' is missing metadata at ${getContextStoreMetadataPath(storeRoot)}`, 'context_store_metadata_missing', {
|
|
99
|
+
target: 'context_store.metadata',
|
|
100
|
+
fix: `Create ${getContextStoreMetadataPath(storeRoot)} or rerun context-store register.`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
await writeContextStoreMetadataState(storeRoot, {
|
|
104
|
+
version: 1,
|
|
105
|
+
id,
|
|
106
|
+
});
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (metadata.id !== id) {
|
|
110
|
+
throw new ContextStoreError(`Context store metadata id '${metadata.id}' does not match registered id '${id}'`, 'context_store_metadata_id_mismatch', {
|
|
111
|
+
target: 'context_store.metadata',
|
|
112
|
+
fix: 'Repair the local registry or store metadata so the ids match.',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
export async function commitContextStoreRegistration(input) {
|
|
118
|
+
const id = validateContextStoreId(input.id);
|
|
119
|
+
const backend = input.backend;
|
|
120
|
+
const storeRoot = getStoreRootForBackend(backend);
|
|
121
|
+
let metadataCreated = false;
|
|
122
|
+
try {
|
|
123
|
+
metadataCreated = await ensureStoreMetadata(storeRoot, id, {
|
|
124
|
+
writeIfMissing: input.writeMetadataIfMissing,
|
|
125
|
+
});
|
|
126
|
+
await updateContextStoreRegistryState((registry) => withRegisteredStore(registry, id, backend), { globalDataDir: input.globalDataDir });
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (metadataCreated) {
|
|
130
|
+
await fs.rm(getContextStoreMetadataPath(storeRoot), { force: true });
|
|
131
|
+
await fs.rmdir(getContextStoreMetadataDir(storeRoot)).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
storeRoot,
|
|
138
|
+
backend,
|
|
139
|
+
metadataCreated,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export async function registerContextStore(input) {
|
|
143
|
+
const id = validateContextStoreId(input.id);
|
|
144
|
+
const backend = await resolveGitContextStoreBackendConfig({
|
|
145
|
+
localPath: input.localPath,
|
|
146
|
+
...(input.remote !== undefined ? { remote: input.remote } : {}),
|
|
147
|
+
...(input.branch !== undefined ? { branch: input.branch } : {}),
|
|
148
|
+
}, input.cwd);
|
|
149
|
+
const storeRoot = getStoreRootForBackend(backend);
|
|
150
|
+
const committed = await commitContextStoreRegistration({
|
|
151
|
+
id,
|
|
152
|
+
backend,
|
|
153
|
+
writeMetadataIfMissing: true,
|
|
154
|
+
...(input.globalDataDir ? { globalDataDir: input.globalDataDir } : {}),
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
id: committed.id,
|
|
158
|
+
storeRoot: committed.storeRoot,
|
|
159
|
+
backend: committed.backend,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export async function listRegisteredContextStores(options = {}) {
|
|
163
|
+
const registry = await readContextStoreRegistryState(options);
|
|
164
|
+
if (!registry) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
return listContextStoreRegistryEntries(registry).map((entry) => ({
|
|
168
|
+
...entry,
|
|
169
|
+
storeRoot: getStoreRootForBackend(entry.backend),
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
export async function getRegisteredContextStore(input) {
|
|
173
|
+
const id = validateContextStoreId(input.id);
|
|
174
|
+
const registry = await readContextStoreRegistryState({
|
|
175
|
+
globalDataDir: input.globalDataDir,
|
|
176
|
+
});
|
|
177
|
+
const entry = getRegisteredStoreOrThrow(registry, id);
|
|
178
|
+
assertExpectedRegisteredBackend(id, entry.backend, input.expectedBackend);
|
|
179
|
+
return {
|
|
180
|
+
...entry,
|
|
181
|
+
storeRoot: getStoreRootForBackend(entry.backend),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export async function unregisterContextStoreRegistration(input) {
|
|
185
|
+
const id = validateContextStoreId(input.id);
|
|
186
|
+
let removed;
|
|
187
|
+
await updateContextStoreRegistryState(async (registry) => {
|
|
188
|
+
const result = withoutRegisteredStore(registry, id, input.expectedBackend);
|
|
189
|
+
const removedEntry = {
|
|
190
|
+
...result.removed,
|
|
191
|
+
storeRoot: getStoreRootForBackend(result.removed.backend),
|
|
192
|
+
};
|
|
193
|
+
await input.beforeCommit?.(removedEntry);
|
|
194
|
+
removed = result.removed;
|
|
195
|
+
return result.next;
|
|
196
|
+
}, { globalDataDir: input.globalDataDir });
|
|
197
|
+
if (!removed) {
|
|
198
|
+
throw new ContextStoreError(`Unknown context store '${id}'`, 'context_store_not_found', {
|
|
199
|
+
target: 'context_store.id',
|
|
200
|
+
fix: 'Run openspec context-store list to see registered stores.',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
...removed,
|
|
205
|
+
storeRoot: getStoreRootForBackend(removed.backend),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export async function resolveRegisteredContextStore(input) {
|
|
209
|
+
const id = validateContextStoreId(input.id);
|
|
210
|
+
const registry = await readContextStoreRegistryState({
|
|
211
|
+
globalDataDir: input.globalDataDir,
|
|
212
|
+
});
|
|
213
|
+
if (!registry) {
|
|
214
|
+
throw new ContextStoreError('No context store registry found', 'no_context_store_registry', {
|
|
215
|
+
target: 'context_store.id',
|
|
216
|
+
fix: 'Register a context store before using --store, or pass --store-path <path>.',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const entry = getRegisteredStoreOrThrow(registry, id);
|
|
220
|
+
const backend = entry.backend;
|
|
221
|
+
const storeRoot = getStoreRootForBackend(backend);
|
|
222
|
+
await ensureStoreMetadata(storeRoot, id, { writeIfMissing: false });
|
|
223
|
+
return {
|
|
224
|
+
id,
|
|
225
|
+
storeRoot,
|
|
226
|
+
backend,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -25,7 +25,12 @@ export declare function getGlobalConfigDir(): string;
|
|
|
25
25
|
* - Unix/macOS fallback: ~/.local/share/openspec/
|
|
26
26
|
* - Windows fallback: %LOCALAPPDATA%/openspec/
|
|
27
27
|
*/
|
|
28
|
-
export
|
|
28
|
+
export interface GlobalDataDirOptions {
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
platform?: NodeJS.Platform;
|
|
31
|
+
homedir?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function getGlobalDataDir(options?: GlobalDataDirOptions): string;
|
|
29
34
|
/**
|
|
30
35
|
* Gets the path to the global config file.
|
|
31
36
|
*/
|
|
@@ -36,32 +36,31 @@ export function getGlobalConfigDir() {
|
|
|
36
36
|
// Unix/macOS fallback: ~/.config
|
|
37
37
|
return path.join(os.homedir(), '.config', GLOBAL_CONFIG_DIR_NAME);
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export function getGlobalDataDir() {
|
|
39
|
+
function joinGlobalDataPath(platform, ...segments) {
|
|
40
|
+
return platform === 'win32'
|
|
41
|
+
? path.win32.join(...segments)
|
|
42
|
+
: path.posix.join(...segments);
|
|
43
|
+
}
|
|
44
|
+
export function getGlobalDataDir(options = {}) {
|
|
45
|
+
const env = options.env ?? process.env;
|
|
46
|
+
const platform = options.platform ?? os.platform();
|
|
48
47
|
// XDG_DATA_HOME takes precedence on all platforms when explicitly set
|
|
49
|
-
const xdgDataHome =
|
|
48
|
+
const xdgDataHome = env.XDG_DATA_HOME;
|
|
50
49
|
if (xdgDataHome) {
|
|
51
|
-
return
|
|
50
|
+
return joinGlobalDataPath(platform, xdgDataHome, GLOBAL_DATA_DIR_NAME);
|
|
52
51
|
}
|
|
53
|
-
const
|
|
52
|
+
const homedir = options.homedir ?? os.homedir();
|
|
54
53
|
if (platform === 'win32') {
|
|
55
54
|
// Windows: use %LOCALAPPDATA%
|
|
56
|
-
const localAppData =
|
|
55
|
+
const localAppData = env.LOCALAPPDATA;
|
|
57
56
|
if (localAppData) {
|
|
58
|
-
return
|
|
57
|
+
return joinGlobalDataPath(platform, localAppData, GLOBAL_DATA_DIR_NAME);
|
|
59
58
|
}
|
|
60
59
|
// Fallback for Windows if LOCALAPPDATA is not set
|
|
61
|
-
return
|
|
60
|
+
return joinGlobalDataPath(platform, homedir, 'AppData', 'Local', GLOBAL_DATA_DIR_NAME);
|
|
62
61
|
}
|
|
63
62
|
// Unix/macOS fallback: ~/.local/share
|
|
64
|
-
return
|
|
63
|
+
return joinGlobalDataPath(platform, homedir, '.local', 'share', GLOBAL_DATA_DIR_NAME);
|
|
65
64
|
}
|
|
66
65
|
/**
|
|
67
66
|
* Gets the path to the global config file.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
|
|
1
|
+
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, type GlobalDataDirOptions, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
|
|
2
|
+
export * from './workspace/index.js';
|
|
3
|
+
export * from './context-store/index.js';
|
|
4
|
+
export * from './collections/index.js';
|
|
5
|
+
export * from './planning-home.js';
|
|
2
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
// Core OpenSpec logic will be implemented here
|
|
2
2
|
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
|
|
3
|
+
export * from './workspace/index.js';
|
|
4
|
+
export * from './context-store/index.js';
|
|
5
|
+
export * from './collections/index.js';
|
|
6
|
+
export * from './planning-home.js';
|
|
3
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -148,15 +148,19 @@ export class ChangeParser extends MarkdownParser {
|
|
|
148
148
|
parseSectionsFromContent(content) {
|
|
149
149
|
const normalizedContent = ChangeParser.normalizeContent(content);
|
|
150
150
|
const lines = normalizedContent.split('\n');
|
|
151
|
+
const codeFenceLineMask = ChangeParser.buildCodeFenceMask(lines);
|
|
151
152
|
const sections = [];
|
|
152
153
|
const stack = [];
|
|
153
154
|
for (let i = 0; i < lines.length; i++) {
|
|
154
155
|
const line = lines[i];
|
|
156
|
+
if (codeFenceLineMask[i]) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
155
159
|
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
156
160
|
if (headerMatch) {
|
|
157
161
|
const level = headerMatch[1].length;
|
|
158
162
|
const title = headerMatch[2].trim();
|
|
159
|
-
const contentLines = this.getContentUntilNextHeaderFromLines(lines, i + 1, level);
|
|
163
|
+
const contentLines = this.getContentUntilNextHeaderFromLines(lines, codeFenceLineMask, i + 1, level);
|
|
160
164
|
const section = {
|
|
161
165
|
level,
|
|
162
166
|
title,
|
|
@@ -177,11 +181,11 @@ export class ChangeParser extends MarkdownParser {
|
|
|
177
181
|
}
|
|
178
182
|
return sections;
|
|
179
183
|
}
|
|
180
|
-
getContentUntilNextHeaderFromLines(lines, startLine, currentLevel) {
|
|
184
|
+
getContentUntilNextHeaderFromLines(lines, codeFenceLineMask, startLine, currentLevel) {
|
|
181
185
|
const contentLines = [];
|
|
182
186
|
for (let i = startLine; i < lines.length; i++) {
|
|
183
187
|
const line = lines[i];
|
|
184
|
-
const headerMatch = line.match(/^(#{1,6})\s+/);
|
|
188
|
+
const headerMatch = codeFenceLineMask[i] ? null : line.match(/^(#{1,6})\s+/);
|
|
185
189
|
if (headerMatch && headerMatch[1].length <= currentLevel) {
|
|
186
190
|
break;
|
|
187
191
|
}
|
|
@@ -7,9 +7,13 @@ export interface Section {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class MarkdownParser {
|
|
9
9
|
private lines;
|
|
10
|
+
private codeFenceLineMask;
|
|
10
11
|
private currentLine;
|
|
11
12
|
constructor(content: string);
|
|
12
13
|
protected static normalizeContent(content: string): string;
|
|
14
|
+
protected static buildCodeFenceMask(lines: string[]): boolean[];
|
|
15
|
+
private static getFenceMarker;
|
|
16
|
+
private static isClosingFence;
|
|
13
17
|
parseSpec(name: string): Spec;
|
|
14
18
|
parseChange(name: string): Change;
|
|
15
19
|
protected parseSections(): Section[];
|
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
export class MarkdownParser {
|
|
2
2
|
lines;
|
|
3
|
+
codeFenceLineMask;
|
|
3
4
|
currentLine;
|
|
4
5
|
constructor(content) {
|
|
5
6
|
const normalized = MarkdownParser.normalizeContent(content);
|
|
6
7
|
this.lines = normalized.split('\n');
|
|
8
|
+
this.codeFenceLineMask = MarkdownParser.buildCodeFenceMask(this.lines);
|
|
7
9
|
this.currentLine = 0;
|
|
8
10
|
}
|
|
9
11
|
static normalizeContent(content) {
|
|
10
12
|
return content.replace(/\r\n?/g, '\n');
|
|
11
13
|
}
|
|
14
|
+
static buildCodeFenceMask(lines) {
|
|
15
|
+
const mask = new Array(lines.length).fill(false);
|
|
16
|
+
let activeFence = null;
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
const fence = MarkdownParser.getFenceMarker(lines[i]);
|
|
19
|
+
if (!activeFence) {
|
|
20
|
+
if (fence) {
|
|
21
|
+
activeFence = fence;
|
|
22
|
+
mask[i] = true;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
mask[i] = true;
|
|
27
|
+
if (MarkdownParser.isClosingFence(lines[i], activeFence)) {
|
|
28
|
+
activeFence = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return mask;
|
|
32
|
+
}
|
|
33
|
+
static getFenceMarker(line) {
|
|
34
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
|
|
35
|
+
if (!fenceMatch) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
marker: fenceMatch[1][0],
|
|
40
|
+
length: fenceMatch[1].length,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
static isClosingFence(line, activeFence) {
|
|
44
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
45
|
+
return Boolean(fenceMatch &&
|
|
46
|
+
fenceMatch[1][0] === activeFence.marker &&
|
|
47
|
+
fenceMatch[1].length >= activeFence.length);
|
|
48
|
+
}
|
|
12
49
|
parseSpec(name) {
|
|
13
50
|
const sections = this.parseSections();
|
|
14
51
|
const purpose = this.findSection(sections, 'Purpose')?.content || '';
|
|
@@ -57,6 +94,9 @@ export class MarkdownParser {
|
|
|
57
94
|
const stack = [];
|
|
58
95
|
for (let i = 0; i < this.lines.length; i++) {
|
|
59
96
|
const line = this.lines[i];
|
|
97
|
+
if (this.codeFenceLineMask[i]) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
60
100
|
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
61
101
|
if (headerMatch) {
|
|
62
102
|
const level = headerMatch[1].length;
|
|
@@ -86,7 +126,7 @@ export class MarkdownParser {
|
|
|
86
126
|
const contentLines = [];
|
|
87
127
|
for (let i = startLine; i < this.lines.length; i++) {
|
|
88
128
|
const line = this.lines[i];
|
|
89
|
-
const headerMatch = line.match(/^(#{1,6})\s+/);
|
|
129
|
+
const headerMatch = this.codeFenceLineMask[i] ? null : line.match(/^(#{1,6})\s+/);
|
|
90
130
|
if (headerMatch && headerMatch[1].length <= currentLevel) {
|
|
91
131
|
break;
|
|
92
132
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function normalizeRequirementName(name) {
|
|
2
2
|
return name.trim();
|
|
3
3
|
}
|
|
4
|
-
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s
|
|
4
|
+
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
5
5
|
/**
|
|
6
6
|
* Extracts the Requirements section from a spec file and parses requirement blocks.
|
|
7
7
|
*/
|
|
@@ -37,7 +37,7 @@ export function extractRequirementsSection(content) {
|
|
|
37
37
|
let cursor = 0;
|
|
38
38
|
let preambleLines = [];
|
|
39
39
|
// Collect preamble lines until first requirement header
|
|
40
|
-
while (cursor < sectionBodyLines.length &&
|
|
40
|
+
while (cursor < sectionBodyLines.length && !REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor])) {
|
|
41
41
|
preambleLines.push(sectionBodyLines[cursor]);
|
|
42
42
|
cursor++;
|
|
43
43
|
}
|
|
@@ -54,7 +54,7 @@ export function extractRequirementsSection(content) {
|
|
|
54
54
|
cursor++;
|
|
55
55
|
// Gather lines until next requirement header or end of section
|
|
56
56
|
const bodyLines = [headerLineCandidate];
|
|
57
|
-
while (cursor < sectionBodyLines.length &&
|
|
57
|
+
while (cursor < sectionBodyLines.length && !REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor]) && !/^##\s+/.test(sectionBodyLines[cursor])) {
|
|
58
58
|
bodyLines.push(sectionBodyLines[cursor]);
|
|
59
59
|
cursor++;
|
|
60
60
|
}
|
|
@@ -136,7 +136,7 @@ function parseRequirementBlocksFromSection(sectionBody) {
|
|
|
136
136
|
let i = 0;
|
|
137
137
|
while (i < lines.length) {
|
|
138
138
|
// Seek next requirement header
|
|
139
|
-
while (i < lines.length &&
|
|
139
|
+
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i]))
|
|
140
140
|
i++;
|
|
141
141
|
if (i >= lines.length)
|
|
142
142
|
break;
|
|
@@ -149,7 +149,7 @@ function parseRequirementBlocksFromSection(sectionBody) {
|
|
|
149
149
|
const name = normalizeRequirementName(m[1]);
|
|
150
150
|
const buf = [headerLine];
|
|
151
151
|
i++;
|
|
152
|
-
while (i < lines.length &&
|
|
152
|
+
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i]) && !/^##\s+/.test(lines[i])) {
|
|
153
153
|
buf.push(lines[i]);
|
|
154
154
|
i++;
|
|
155
155
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface MainSpecStructureIssue {
|
|
2
|
+
kind: 'delta-header' | 'requirement-outside-requirements';
|
|
3
|
+
line: number;
|
|
4
|
+
header: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function findMainSpecStructureIssues(content: string): MainSpecStructureIssue[];
|
|
8
|
+
export declare function stripFencedCodeBlocksPreservingLines(content: string): string;
|
|
9
|
+
//# sourceMappingURL=spec-structure.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
|
|
2
|
+
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
|
|
3
|
+
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
4
|
+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/i;
|
|
5
|
+
export function findMainSpecStructureIssues(content) {
|
|
6
|
+
const normalized = content.replace(/\r\n?/g, '\n');
|
|
7
|
+
const stripped = stripFencedCodeBlocksPreservingLines(normalized);
|
|
8
|
+
const lines = stripped.split('\n');
|
|
9
|
+
const issues = [];
|
|
10
|
+
const requirementsHeaderIndex = lines.findIndex(line => REQUIREMENTS_SECTION_HEADER.test(line));
|
|
11
|
+
let requirementsEndIndex = lines.length;
|
|
12
|
+
if (requirementsHeaderIndex !== -1) {
|
|
13
|
+
for (let i = requirementsHeaderIndex + 1; i < lines.length; i++) {
|
|
14
|
+
if (TOP_LEVEL_SECTION_HEADER.test(lines[i])) {
|
|
15
|
+
requirementsEndIndex = i;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (DELTA_HEADER.test(line)) {
|
|
27
|
+
issues.push({
|
|
28
|
+
kind: 'delta-header',
|
|
29
|
+
line: i + 1,
|
|
30
|
+
header: trimmed,
|
|
31
|
+
message: `Main spec contains delta header "${trimmed}". ` +
|
|
32
|
+
'Delta headers are only valid inside openspec/changes/<name>/specs/<capability>/spec.md ' +
|
|
33
|
+
'and truncate the parsed ## Requirements section.',
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const requirementMatch = line.match(REQUIREMENT_HEADER);
|
|
38
|
+
if (!requirementMatch) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const insideRequirements = requirementsHeaderIndex !== -1 &&
|
|
42
|
+
i > requirementsHeaderIndex &&
|
|
43
|
+
i < requirementsEndIndex;
|
|
44
|
+
if (!insideRequirements) {
|
|
45
|
+
issues.push({
|
|
46
|
+
kind: 'requirement-outside-requirements',
|
|
47
|
+
line: i + 1,
|
|
48
|
+
header: trimmed,
|
|
49
|
+
message: `Requirement header "${trimmed}" appears outside the main ## Requirements section. ` +
|
|
50
|
+
'Main specs only parse requirements inside that section, so this requirement is currently invisible to validate, list, and archive.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
56
|
+
export function stripFencedCodeBlocksPreservingLines(content) {
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
const output = [];
|
|
59
|
+
let activeFence = null;
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
62
|
+
if (!activeFence) {
|
|
63
|
+
if (fenceMatch) {
|
|
64
|
+
activeFence = {
|
|
65
|
+
marker: fenceMatch[1][0],
|
|
66
|
+
length: fenceMatch[1].length,
|
|
67
|
+
};
|
|
68
|
+
output.push('');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
output.push(line);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
output.push('');
|
|
76
|
+
if (isClosingFence(line, activeFence)) {
|
|
77
|
+
activeFence = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return output.join('\n');
|
|
81
|
+
}
|
|
82
|
+
function isClosingFence(line, activeFence) {
|
|
83
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
84
|
+
return Boolean(fenceMatch &&
|
|
85
|
+
fenceMatch[1][0] === activeFence.marker &&
|
|
86
|
+
fenceMatch[1].length >= activeFence.length);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=spec-structure.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type PlanningHomeKind = 'repo' | 'workspace';
|
|
2
|
+
export interface PlanningHome {
|
|
3
|
+
kind: PlanningHomeKind;
|
|
4
|
+
root: string;
|
|
5
|
+
changesDir: string;
|
|
6
|
+
defaultSchema: string;
|
|
7
|
+
workspace?: {
|
|
8
|
+
name: string;
|
|
9
|
+
links: string[];
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface ResolvePlanningHomeOptions {
|
|
13
|
+
startPath?: string;
|
|
14
|
+
allowImplicitRepoRoot?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function findWorkspacePlanningRootSync(startPath?: string): string | null;
|
|
17
|
+
export declare function findRepoPlanningRootSync(startPath?: string): string | null;
|
|
18
|
+
export declare function resolveCurrentPlanningHomeSync(options?: ResolvePlanningHomeOptions): PlanningHome;
|
|
19
|
+
export declare function getChangeDir(planningHome: PlanningHome, changeName: string): string;
|
|
20
|
+
export declare function formatChangeLocation(planningHome: PlanningHome, changeName: string): string;
|
|
21
|
+
//# sourceMappingURL=planning-home.d.ts.map
|