@fission-ai/openspec 1.3.1 → 1.4.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 +9 -2
- package/bin/openspec.js +3 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +65 -3
- 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 +28 -11
- 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 +4 -2
- package/dist/commands/workflow/shared.js +6 -6
- package/dist/commands/workflow/status.js +19 -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 +8 -0
- package/dist/commands/workspace/selection.js +104 -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 +5 -0
- package/dist/commands/workspace.js +574 -0
- package/dist/core/artifact-graph/index.d.ts +2 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +41 -1
- package/dist/core/artifact-graph/instruction-loader.js +46 -6
- 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/requirement-blocks.js +5 -5
- package/dist/core/parsers/spec-structure.js +1 -1
- 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 +1 -1
- package/dist/core/templates/workflows/apply-change.js +6 -0
- 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 +10 -4
- 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 +19 -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 +121 -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/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
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
|
|
2
2
|
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
|
|
3
3
|
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
4
|
-
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s
|
|
4
|
+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/i;
|
|
5
5
|
export function findMainSpecStructureIssues(content) {
|
|
6
6
|
const normalized = content.replace(/\r\n?/g, '\n');
|
|
7
7
|
const stripped = stripFencedCodeBlocksPreservingLines(normalized);
|
|
@@ -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
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getWorkspaceChangesDir, readWorkspaceViewStateSync, workspaceStateFileExistsSync, } from './workspace/index.js';
|
|
4
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
5
|
+
const REPO_DEFAULT_SCHEMA = 'spec-driven';
|
|
6
|
+
const WORKSPACE_DEFAULT_SCHEMA = 'workspace-planning';
|
|
7
|
+
function pathExistsAsDirectory(candidatePath) {
|
|
8
|
+
try {
|
|
9
|
+
return fs.statSync(candidatePath).isDirectory();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function getSearchStartDirectory(startPath) {
|
|
16
|
+
const resolved = path.resolve(startPath);
|
|
17
|
+
try {
|
|
18
|
+
const stats = fs.statSync(resolved);
|
|
19
|
+
const searchStart = stats.isDirectory() ? resolved : path.dirname(resolved);
|
|
20
|
+
return FileSystemUtils.canonicalizeExistingPath(searchStart);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return resolved;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function findNearestAncestor(startPath, predicate) {
|
|
27
|
+
let currentDir = getSearchStartDirectory(startPath);
|
|
28
|
+
while (true) {
|
|
29
|
+
if (predicate(currentDir)) {
|
|
30
|
+
return FileSystemUtils.canonicalizeExistingPath(currentDir);
|
|
31
|
+
}
|
|
32
|
+
const parentDir = path.dirname(currentDir);
|
|
33
|
+
if (parentDir === currentDir) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
currentDir = parentDir;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function findWorkspacePlanningRootSync(startPath = process.cwd()) {
|
|
40
|
+
return findNearestAncestor(startPath, workspaceStateFileExistsSync);
|
|
41
|
+
}
|
|
42
|
+
export function findRepoPlanningRootSync(startPath = process.cwd()) {
|
|
43
|
+
return findNearestAncestor(startPath, (dirPath) => pathExistsAsDirectory(path.join(dirPath, 'openspec')));
|
|
44
|
+
}
|
|
45
|
+
function isSameOrDescendant(rootPath, candidatePath) {
|
|
46
|
+
const relative = path.relative(rootPath, candidatePath);
|
|
47
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
48
|
+
}
|
|
49
|
+
function countPathSegments(candidatePath) {
|
|
50
|
+
return path.resolve(candidatePath).split(path.sep).filter(Boolean).length;
|
|
51
|
+
}
|
|
52
|
+
function isWindowsLikePath(candidatePath) {
|
|
53
|
+
return /^[A-Za-z]:[\\/]/.test(candidatePath) || candidatePath.startsWith('\\\\');
|
|
54
|
+
}
|
|
55
|
+
function relativePlanningPath(fromPath, toPath) {
|
|
56
|
+
if (isWindowsLikePath(fromPath) || isWindowsLikePath(toPath)) {
|
|
57
|
+
return path.win32.relative(path.win32.normalize(fromPath), path.win32.normalize(toPath));
|
|
58
|
+
}
|
|
59
|
+
return path.posix.relative(fromPath.replace(/\\/g, '/'), toPath.replace(/\\/g, '/'));
|
|
60
|
+
}
|
|
61
|
+
function workspacePlanningHome(workspaceRoot) {
|
|
62
|
+
const viewState = readWorkspaceViewStateSync(workspaceRoot);
|
|
63
|
+
return {
|
|
64
|
+
kind: 'workspace',
|
|
65
|
+
root: workspaceRoot,
|
|
66
|
+
changesDir: getWorkspaceChangesDir(workspaceRoot),
|
|
67
|
+
defaultSchema: WORKSPACE_DEFAULT_SCHEMA,
|
|
68
|
+
workspace: {
|
|
69
|
+
name: viewState?.name ?? path.basename(workspaceRoot),
|
|
70
|
+
links: Object.keys(viewState?.links ?? {}).sort((a, b) => a.localeCompare(b)),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function repoPlanningHome(repoRoot) {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'repo',
|
|
77
|
+
root: repoRoot,
|
|
78
|
+
changesDir: path.join(repoRoot, 'openspec', 'changes'),
|
|
79
|
+
defaultSchema: REPO_DEFAULT_SCHEMA,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function resolveCurrentPlanningHomeSync(options = {}) {
|
|
83
|
+
const startPath = options.startPath ?? process.cwd();
|
|
84
|
+
const searchStart = getSearchStartDirectory(startPath);
|
|
85
|
+
const workspaceRoot = findWorkspacePlanningRootSync(searchStart);
|
|
86
|
+
const repoRoot = findRepoPlanningRootSync(searchStart);
|
|
87
|
+
if (workspaceRoot && isSameOrDescendant(workspaceRoot, searchStart)) {
|
|
88
|
+
if (!repoRoot || countPathSegments(workspaceRoot) >= countPathSegments(repoRoot)) {
|
|
89
|
+
return workspacePlanningHome(workspaceRoot);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (repoRoot) {
|
|
93
|
+
return repoPlanningHome(repoRoot);
|
|
94
|
+
}
|
|
95
|
+
if (options.allowImplicitRepoRoot === false) {
|
|
96
|
+
throw new Error('No OpenSpec planning home found from the current directory.');
|
|
97
|
+
}
|
|
98
|
+
return repoPlanningHome(FileSystemUtils.canonicalizeExistingPath(searchStart));
|
|
99
|
+
}
|
|
100
|
+
export function getChangeDir(planningHome, changeName) {
|
|
101
|
+
return FileSystemUtils.joinPath(planningHome.changesDir, changeName);
|
|
102
|
+
}
|
|
103
|
+
export function formatChangeLocation(planningHome, changeName) {
|
|
104
|
+
const changeDir = getChangeDir(planningHome, changeName);
|
|
105
|
+
const relative = relativePlanningPath(planningHome.root, changeDir);
|
|
106
|
+
return relative.length > 0 ? relative : changeDir;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=planning-home.js.map
|
package/dist/core/profiles.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Profile } from './global-config.js';
|
|
|
9
9
|
* Core workflows included in the 'core' profile.
|
|
10
10
|
* These provide the streamlined experience for new users.
|
|
11
11
|
*/
|
|
12
|
-
export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "archive"];
|
|
12
|
+
export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "sync", "archive"];
|
|
13
13
|
/**
|
|
14
14
|
* All available workflows in the system.
|
|
15
15
|
*/
|
package/dist/core/profiles.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Core workflows included in the 'core' profile.
|
|
9
9
|
* These provide the streamlined experience for new users.
|
|
10
10
|
*/
|
|
11
|
-
export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'archive'];
|
|
11
|
+
export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'sync', 'archive'];
|
|
12
12
|
/**
|
|
13
13
|
* All available workflows in the system.
|
|
14
14
|
*/
|
package/dist/core/specs-apply.js
CHANGED
|
@@ -211,7 +211,7 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
211
211
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
|
|
212
212
|
}
|
|
213
213
|
// Replace block with provided raw (ensure header line matches key)
|
|
214
|
-
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/);
|
|
214
|
+
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/i);
|
|
215
215
|
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
|
216
216
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`);
|
|
217
217
|
}
|
|
@@ -23,6 +23,7 @@ export function getApplyChangeSkillTemplate() {
|
|
|
23
23
|
\`\`\`
|
|
24
24
|
Parse the JSON to understand:
|
|
25
25
|
- \`schemaName\`: The workflow being used (e.g., "spec-driven")
|
|
26
|
+
- \`planningHome\`, \`changeRoot\`, and \`actionContext\`: planning scope and edit constraints
|
|
26
27
|
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
|
27
28
|
|
|
28
29
|
3. **Get apply instructions**
|
|
@@ -42,6 +43,8 @@ export function getApplyChangeSkillTemplate() {
|
|
|
42
43
|
- If \`state: "all_done"\`: congratulate, suggest archive
|
|
43
44
|
- Otherwise: proceed to implementation
|
|
44
45
|
|
|
46
|
+
**Workspace guard:** If status JSON reports \`actionContext.mode: "workspace-planning"\` and \`allowedEditRoots\` is empty, explain that full workspace apply is not supported in this slice. Treat linked repos and folders as read-only context, ask the user to select an affected area through an explicit implementation workflow, and STOP before editing files.
|
|
47
|
+
|
|
45
48
|
4. **Read context files**
|
|
46
49
|
|
|
47
50
|
Read every file path listed under \`contextFiles\` from the apply instructions output.
|
|
@@ -179,6 +182,7 @@ export function getOpsxApplyCommandTemplate() {
|
|
|
179
182
|
\`\`\`
|
|
180
183
|
Parse the JSON to understand:
|
|
181
184
|
- \`schemaName\`: The workflow being used (e.g., "spec-driven")
|
|
185
|
+
- \`planningHome\`, \`changeRoot\`, and \`actionContext\`: planning scope and edit constraints
|
|
182
186
|
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
|
183
187
|
|
|
184
188
|
3. **Get apply instructions**
|
|
@@ -198,6 +202,8 @@ export function getOpsxApplyCommandTemplate() {
|
|
|
198
202
|
- If \`state: "all_done"\`: congratulate, suggest archive
|
|
199
203
|
- Otherwise: proceed to implementation
|
|
200
204
|
|
|
205
|
+
**Workspace guard:** If status JSON reports \`actionContext.mode: "workspace-planning"\` and \`allowedEditRoots\` is empty, explain that full workspace apply is not supported in this slice. Treat linked repos and folders as read-only context, ask the user to select an affected area through an explicit implementation workflow, and STOP before editing files.
|
|
206
|
+
|
|
201
207
|
4. **Read context files**
|
|
202
208
|
|
|
203
209
|
Read every file path listed under \`contextFiles\` from the apply instructions output.
|