@fission-ai/openspec 1.4.0 → 1.5.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 +24 -3
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +94 -76
- package/dist/commands/change.d.ts +5 -1
- package/dist/commands/change.js +17 -15
- package/dist/commands/config.js +6 -65
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +155 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +163 -0
- package/dist/commands/shared-gather.d.ts +14 -0
- package/dist/commands/shared-gather.js +31 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/show.d.ts +11 -6
- package/dist/commands/show.js +70 -25
- package/dist/commands/spec.d.ts +5 -1
- package/dist/commands/spec.js +15 -4
- package/dist/commands/store.d.ts +3 -0
- package/dist/commands/store.js +547 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.js +75 -39
- package/dist/commands/workflow/index.d.ts +0 -2
- package/dist/commands/workflow/index.js +0 -1
- package/dist/commands/workflow/instructions.d.ts +11 -1
- package/dist/commands/workflow/instructions.js +71 -22
- package/dist/commands/workflow/new-change.d.ts +6 -3
- package/dist/commands/workflow/new-change.js +42 -77
- package/dist/commands/workflow/shared.d.ts +15 -3
- package/dist/commands/workflow/shared.js +20 -3
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +19 -17
- package/dist/commands/workset-input.d.ts +19 -0
- package/dist/commands/workset-input.js +112 -0
- package/dist/commands/workset-prompts.d.ts +12 -0
- package/dist/commands/workset-prompts.js +143 -0
- package/dist/commands/workset.d.ts +25 -0
- package/dist/commands/workset.js +446 -0
- package/dist/core/archive.d.ts +17 -6
- package/dist/core/archive.js +209 -56
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +18 -13
- package/dist/core/artifact-graph/instruction-loader.js +8 -31
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +4 -2
- package/dist/core/change-status-policy.d.ts +5 -18
- package/dist/core/change-status-policy.js +2 -37
- package/dist/core/command-generation/adapters/bob.js +1 -14
- package/dist/core/command-generation/adapters/claude.js +1 -14
- package/dist/core/command-generation/adapters/cursor.js +1 -14
- package/dist/core/command-generation/adapters/pi.js +1 -14
- package/dist/core/command-generation/adapters/windsurf.js +1 -14
- package/dist/core/command-generation/yaml.d.ts +22 -0
- package/dist/core/command-generation/yaml.js +38 -0
- package/dist/core/completions/command-registry.js +87 -289
- package/dist/core/completions/generators/zsh-generator.js +3 -1
- package/dist/core/completions/installers/bash-installer.js +3 -0
- package/dist/core/completions/installers/fish-installer.js +8 -0
- package/dist/core/completions/installers/powershell-installer.js +30 -2
- package/dist/core/completions/installers/zsh-installer.js +3 -0
- package/dist/core/completions/shared-flags.d.ts +1 -0
- package/dist/core/completions/shared-flags.js +5 -0
- package/dist/core/config-schema.d.ts +2 -1
- package/dist/core/config-schema.js +26 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +112 -0
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +3 -3
- package/dist/core/init.js +24 -4
- package/dist/core/list.d.ts +2 -0
- package/dist/core/list.js +19 -5
- package/dist/core/openers.d.ts +77 -0
- package/dist/core/openers.js +251 -0
- package/dist/core/openspec-root.d.ts +45 -0
- package/dist/core/openspec-root.js +192 -0
- package/dist/core/planning-home.d.ts +1 -6
- package/dist/core/planning-home.js +2 -43
- package/dist/core/project-config.d.ts +44 -1
- package/dist/core/project-config.js +160 -10
- package/dist/core/references.d.ts +63 -0
- package/dist/core/references.js +310 -0
- package/dist/core/relationship-health.d.ts +65 -0
- package/dist/core/relationship-health.js +64 -0
- package/dist/core/root-selection.d.ts +122 -0
- package/dist/core/root-selection.js +337 -0
- package/dist/core/specs-apply.d.ts +6 -1
- package/dist/core/specs-apply.js +6 -4
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/{context-store → store}/errors.js +3 -3
- package/dist/core/store/foundation.d.ts +56 -0
- package/dist/core/store/foundation.js +251 -0
- package/dist/core/store/git.d.ts +23 -0
- package/dist/core/store/git.js +137 -0
- package/dist/core/{context-store → store}/index.d.ts +0 -1
- package/dist/core/{context-store → store}/index.js +0 -1
- package/dist/core/store/operations.d.ts +114 -0
- package/dist/core/store/operations.js +783 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -0
- package/dist/core/templates/workflows/apply-change.js +5 -4
- package/dist/core/templates/workflows/archive-change.js +5 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
- package/dist/core/templates/workflows/continue-change.js +7 -2
- package/dist/core/templates/workflows/explore.js +5 -0
- package/dist/core/templates/workflows/ff-change.js +5 -0
- package/dist/core/templates/workflows/new-change.js +5 -0
- package/dist/core/templates/workflows/onboard.js +4 -1
- package/dist/core/templates/workflows/propose.js +5 -0
- package/dist/core/templates/workflows/store-selection.d.ts +8 -0
- package/dist/core/templates/workflows/store-selection.js +8 -0
- package/dist/core/templates/workflows/sync-specs.js +5 -4
- package/dist/core/templates/workflows/verify-change.js +5 -4
- package/dist/core/working-set.d.ts +47 -0
- package/dist/core/working-set.js +43 -0
- package/dist/core/worksets.d.ts +75 -0
- package/dist/core/worksets.js +245 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/utils/change-metadata.d.ts +1 -0
- package/dist/utils/change-metadata.js +1 -1
- package/dist/utils/change-utils.js +15 -0
- package/dist/utils/file-system.js +36 -17
- package/package.json +6 -6
- package/dist/commands/context-store.d.ts +0 -3
- package/dist/commands/context-store.js +0 -475
- package/dist/commands/initiative.d.ts +0 -13
- package/dist/commands/initiative.js +0 -318
- package/dist/commands/workflow/initiative-link.d.ts +0 -24
- package/dist/commands/workflow/initiative-link.js +0 -47
- package/dist/commands/workflow/set-change.d.ts +0 -13
- package/dist/commands/workflow/set-change.js +0 -87
- package/dist/commands/workspace/context-status.d.ts +0 -4
- package/dist/commands/workspace/context-status.js +0 -59
- package/dist/commands/workspace/open-target-selection.d.ts +0 -13
- package/dist/commands/workspace/open-target-selection.js +0 -146
- package/dist/commands/workspace/open-view.d.ts +0 -62
- package/dist/commands/workspace/open-view.js +0 -249
- package/dist/commands/workspace/open.d.ts +0 -37
- package/dist/commands/workspace/open.js +0 -110
- package/dist/commands/workspace/opener-selection.d.ts +0 -11
- package/dist/commands/workspace/opener-selection.js +0 -98
- package/dist/commands/workspace/operations.d.ts +0 -29
- package/dist/commands/workspace/operations.js +0 -543
- package/dist/commands/workspace/prompt-theme.d.ts +0 -29
- package/dist/commands/workspace/prompt-theme.js +0 -24
- package/dist/commands/workspace/registration.d.ts +0 -13
- package/dist/commands/workspace/registration.js +0 -84
- package/dist/commands/workspace/selection.d.ts +0 -9
- package/dist/commands/workspace/selection.js +0 -115
- package/dist/commands/workspace/setup-prompts.d.ts +0 -13
- package/dist/commands/workspace/setup-prompts.js +0 -121
- package/dist/commands/workspace/types.d.ts +0 -103
- package/dist/commands/workspace/types.js +0 -36
- package/dist/commands/workspace.d.ts +0 -6
- package/dist/commands/workspace.js +0 -587
- package/dist/core/collections/index.d.ts +0 -3
- package/dist/core/collections/index.js +0 -3
- package/dist/core/collections/initiatives/collection.d.ts +0 -4
- package/dist/core/collections/initiatives/collection.js +0 -17
- package/dist/core/collections/initiatives/index.d.ts +0 -6
- package/dist/core/collections/initiatives/index.js +0 -6
- package/dist/core/collections/initiatives/operations.d.ts +0 -49
- package/dist/core/collections/initiatives/operations.js +0 -175
- package/dist/core/collections/initiatives/resolution.d.ts +0 -87
- package/dist/core/collections/initiatives/resolution.js +0 -374
- package/dist/core/collections/initiatives/schema.d.ts +0 -41
- package/dist/core/collections/initiatives/schema.js +0 -134
- package/dist/core/collections/initiatives/templates.d.ts +0 -12
- package/dist/core/collections/initiatives/templates.js +0 -90
- package/dist/core/collections/runtime.d.ts +0 -46
- package/dist/core/collections/runtime.js +0 -194
- package/dist/core/context-store/binding.d.ts +0 -53
- package/dist/core/context-store/binding.js +0 -197
- package/dist/core/context-store/errors.d.ts +0 -20
- package/dist/core/context-store/foundation.d.ts +0 -55
- package/dist/core/context-store/foundation.js +0 -321
- package/dist/core/context-store/operations.d.ts +0 -85
- package/dist/core/context-store/operations.js +0 -528
- package/dist/core/context-store/registry.d.ts +0 -45
- package/dist/core/context-store/registry.js +0 -229
- package/dist/core/workspace/foundation.d.ts +0 -67
- package/dist/core/workspace/foundation.js +0 -295
- package/dist/core/workspace/index.d.ts +0 -8
- package/dist/core/workspace/index.js +0 -8
- package/dist/core/workspace/legacy-state.d.ts +0 -28
- package/dist/core/workspace/legacy-state.js +0 -200
- package/dist/core/workspace/link-input.d.ts +0 -9
- package/dist/core/workspace/link-input.js +0 -32
- package/dist/core/workspace/open-surface.d.ts +0 -45
- package/dist/core/workspace/open-surface.js +0 -215
- package/dist/core/workspace/openers.d.ts +0 -21
- package/dist/core/workspace/openers.js +0 -124
- package/dist/core/workspace/registry.d.ts +0 -24
- package/dist/core/workspace/registry.js +0 -146
- package/dist/core/workspace/skills.d.ts +0 -57
- package/dist/core/workspace/skills.js +0 -334
- package/dist/core/workspace/state-io.d.ts +0 -10
- package/dist/core/workspace/state-io.js +0 -119
- package/schemas/workspace-planning/schema.yaml +0 -72
- package/schemas/workspace-planning/templates/design.md +0 -33
- package/schemas/workspace-planning/templates/proposal.md +0 -28
- package/schemas/workspace-planning/templates/spec.md +0 -9
- package/schemas/workspace-planning/templates/tasks.md +0 -15
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import * as nodeFs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { StoreError } from './store/errors.js';
|
|
6
|
+
import { formatZodIssues } from './zod-issues.js';
|
|
7
|
+
const DEFAULT_ATTACH_FLAG = '--add-dir';
|
|
8
|
+
/**
|
|
9
|
+
* Temporary kill-switch (2026-06): worksets open only in IDE-style
|
|
10
|
+
* ('workspace-file') tools while the CLI-agent ('attach-dirs') open flow
|
|
11
|
+
* is reworked. The agents (Claude Code, codex) launch in a single primary
|
|
12
|
+
* cwd rather than a true combined multi-root view, which makes "where does
|
|
13
|
+
* my change land?" ambiguous. Default off; set
|
|
14
|
+
* OPENSPEC_ENABLE_CLI_AGENT_OPENERS=1 to restore them (internal rollback seam).
|
|
15
|
+
*/
|
|
16
|
+
export function isCliAgentOpenersEnabled() {
|
|
17
|
+
return process.env.OPENSPEC_ENABLE_CLI_AGENT_OPENERS === '1';
|
|
18
|
+
}
|
|
19
|
+
/** Whether a tool can be opened right now (CLI-agent styles are gated). */
|
|
20
|
+
export function isOpenerEnabled(opener) {
|
|
21
|
+
return isCliAgentOpenersEnabled() || opener.style !== 'attach-dirs';
|
|
22
|
+
}
|
|
23
|
+
export const BUILTIN_OPENERS = [
|
|
24
|
+
{
|
|
25
|
+
id: 'code',
|
|
26
|
+
label: 'VS Code',
|
|
27
|
+
style: 'workspace-file',
|
|
28
|
+
command: 'code',
|
|
29
|
+
args: [],
|
|
30
|
+
attachFlag: DEFAULT_ATTACH_FLAG,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'cursor',
|
|
34
|
+
label: 'Cursor',
|
|
35
|
+
style: 'workspace-file',
|
|
36
|
+
command: 'cursor',
|
|
37
|
+
args: [],
|
|
38
|
+
attachFlag: DEFAULT_ATTACH_FLAG,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'claude',
|
|
42
|
+
label: 'Claude Code',
|
|
43
|
+
style: 'attach-dirs',
|
|
44
|
+
command: 'claude',
|
|
45
|
+
args: [],
|
|
46
|
+
attachFlag: DEFAULT_ATTACH_FLAG,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'codex',
|
|
50
|
+
label: 'codex',
|
|
51
|
+
style: 'attach-dirs',
|
|
52
|
+
command: 'codex',
|
|
53
|
+
args: ['--sandbox', 'workspace-write'],
|
|
54
|
+
attachFlag: DEFAULT_ATTACH_FLAG,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const OPENER_STYLES = ['workspace-file', 'attach-dirs'];
|
|
58
|
+
const OpenerConfigRowSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
style: z.enum(OPENER_STYLES).optional(),
|
|
61
|
+
label: z.string().min(1).optional(),
|
|
62
|
+
command: z.string().min(1).optional(),
|
|
63
|
+
args: z.array(z.string()).optional(),
|
|
64
|
+
attach_flag: z.string().min(1).optional(),
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
67
|
+
const OpenersConfigSchema = z.record(z.string(), OpenerConfigRowSchema);
|
|
68
|
+
function invalidOpenerConfigError(message, configPath) {
|
|
69
|
+
return new StoreError(`Invalid openers config: ${message}`, 'invalid_opener_config', {
|
|
70
|
+
target: 'openers.config',
|
|
71
|
+
fix: `Each entry under "openers" in ${configPath} may set style ('workspace-file' or 'attach-dirs'), label, command, args, and attach_flag; new tools must set style.`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Merges the global config file's raw `openers` value over the
|
|
76
|
+
* built-in table. A row keyed by a built-in id overrides only the
|
|
77
|
+
* fields it sets; a new id adds a tool (style required, command and
|
|
78
|
+
* label default to the id). Malformed rows fail typed - never
|
|
79
|
+
* silently ignored.
|
|
80
|
+
*/
|
|
81
|
+
function cloneOpener(opener) {
|
|
82
|
+
return { ...opener, args: [...opener.args] };
|
|
83
|
+
}
|
|
84
|
+
export function mergeOpenerTable(rawOpeners, configPath) {
|
|
85
|
+
if (rawOpeners === undefined || rawOpeners === null) {
|
|
86
|
+
return BUILTIN_OPENERS.map(cloneOpener);
|
|
87
|
+
}
|
|
88
|
+
const result = OpenersConfigSchema.safeParse(rawOpeners);
|
|
89
|
+
if (!result.success) {
|
|
90
|
+
throw invalidOpenerConfigError(formatZodIssues(result.error, 'openers'), configPath);
|
|
91
|
+
}
|
|
92
|
+
const table = BUILTIN_OPENERS.map(cloneOpener);
|
|
93
|
+
for (const [id, row] of Object.entries(result.data)) {
|
|
94
|
+
const builtinIndex = table.findIndex((opener) => opener.id === id);
|
|
95
|
+
if (builtinIndex >= 0) {
|
|
96
|
+
const builtin = table[builtinIndex];
|
|
97
|
+
table[builtinIndex] = {
|
|
98
|
+
...builtin,
|
|
99
|
+
...(row.style !== undefined ? { style: row.style } : {}),
|
|
100
|
+
...(row.label !== undefined ? { label: row.label } : {}),
|
|
101
|
+
...(row.command !== undefined ? { command: row.command } : {}),
|
|
102
|
+
...(row.args !== undefined ? { args: row.args } : {}),
|
|
103
|
+
...(row.attach_flag !== undefined
|
|
104
|
+
? { attachFlag: row.attach_flag }
|
|
105
|
+
: {}),
|
|
106
|
+
};
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (row.style === undefined) {
|
|
110
|
+
throw invalidOpenerConfigError(`'${id}' adds a new tool and must set style ('workspace-file' or 'attach-dirs')`, configPath);
|
|
111
|
+
}
|
|
112
|
+
table.push({
|
|
113
|
+
id,
|
|
114
|
+
label: row.label ?? id,
|
|
115
|
+
style: row.style,
|
|
116
|
+
command: row.command ?? id,
|
|
117
|
+
args: row.args ?? [],
|
|
118
|
+
attachFlag: row.attach_flag ?? DEFAULT_ATTACH_FLAG,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return table;
|
|
122
|
+
}
|
|
123
|
+
function getPathValue(env) {
|
|
124
|
+
return env.PATH ?? env.Path ?? env.path ?? '';
|
|
125
|
+
}
|
|
126
|
+
function getPathExtensions(platform, env) {
|
|
127
|
+
if (platform !== 'win32') {
|
|
128
|
+
return [''];
|
|
129
|
+
}
|
|
130
|
+
return (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
|
|
131
|
+
.split(';')
|
|
132
|
+
.map((extension) => extension.trim())
|
|
133
|
+
.filter((extension) => extension.length > 0);
|
|
134
|
+
}
|
|
135
|
+
function defaultIsExecutableFile(candidatePath, platform) {
|
|
136
|
+
try {
|
|
137
|
+
if (!nodeFs.statSync(candidatePath).isFile()) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (platform === 'win32') {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
nodeFs.accessSync(candidatePath, nodeFs.constants.X_OK);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* PATH availability scan (ported from the deleted workspace openers
|
|
157
|
+
* at f858c19^, sharpened for injectability: the path module is keyed
|
|
158
|
+
* by the injected platform, and a command already carrying a known
|
|
159
|
+
* executable extension matches as-is).
|
|
160
|
+
*/
|
|
161
|
+
export function isOpenerCommandAvailable(command, options = {}) {
|
|
162
|
+
const env = options.env ?? process.env;
|
|
163
|
+
const platform = options.platform ?? os.platform();
|
|
164
|
+
const pathModule = platform === 'win32' ? path.win32 : path.posix;
|
|
165
|
+
const isExecutable = options.isExecutableFile ??
|
|
166
|
+
((candidate) => defaultIsExecutableFile(candidate, platform));
|
|
167
|
+
const extensions = getPathExtensions(platform, env);
|
|
168
|
+
const lowerCommand = command.toLowerCase();
|
|
169
|
+
const carriesKnownExtension = extensions.some((extension) => extension.length > 0 && lowerCommand.endsWith(extension.toLowerCase()));
|
|
170
|
+
// One suffix policy: a command already carrying a known executable
|
|
171
|
+
// extension matches as-is and never gets a second extension appended
|
|
172
|
+
// - agreeing with spawn-time resolution.
|
|
173
|
+
const suffixes = carriesKnownExtension ? [''] : extensions;
|
|
174
|
+
if (/[\\/]/u.test(command)) {
|
|
175
|
+
// Direct paths additionally match bare even on win32 (the spawn
|
|
176
|
+
// call receives the literal path).
|
|
177
|
+
const directSuffixes = Array.from(new Set(['', ...suffixes]));
|
|
178
|
+
return directSuffixes.some((suffix) => isExecutable(command + suffix));
|
|
179
|
+
}
|
|
180
|
+
for (const directory of getPathValue(env).split(pathModule.delimiter)) {
|
|
181
|
+
if (directory.length === 0) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (suffixes.some((suffix) => isExecutable(pathModule.join(directory, command + suffix)))) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
/** Table order preserved, available tools first (stable sort). */
|
|
191
|
+
export function listOpenerChoices(table, options = {}) {
|
|
192
|
+
return table
|
|
193
|
+
.filter((opener) => isOpenerEnabled(opener))
|
|
194
|
+
.map((opener) => {
|
|
195
|
+
const available = isOpenerCommandAvailable(opener.command, options);
|
|
196
|
+
return {
|
|
197
|
+
opener,
|
|
198
|
+
available,
|
|
199
|
+
note: available ? null : `(${opener.command} not found on PATH)`,
|
|
200
|
+
};
|
|
201
|
+
})
|
|
202
|
+
.sort((a, b) => {
|
|
203
|
+
if (a.available === b.available) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
return a.available ? -1 : 1;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
export function findOpener(table, id) {
|
|
210
|
+
return table.find((opener) => opener.id === id) ?? null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Pure argv builder. workspace-file: pre-args + the generated file's
|
|
214
|
+
* absolute path (which also defuses the cursor shim's `agent`
|
|
215
|
+
* first-arg hijack). attach-dirs: pre-args + one attach flag + path
|
|
216
|
+
* pair per surviving member, the primary included (the locked "one
|
|
217
|
+
* attach flag per member"); never a trailing positional - both agent
|
|
218
|
+
* CLIs would read one as a starter prompt, which 7.1 locks out.
|
|
219
|
+
*/
|
|
220
|
+
export function buildLaunchCommand(opener, input) {
|
|
221
|
+
if (input.members.length === 0) {
|
|
222
|
+
throw new Error('buildLaunchCommand requires at least one member.');
|
|
223
|
+
}
|
|
224
|
+
// The no-hijack and no-positional guarantees lean on absolute paths
|
|
225
|
+
// (the child resolves relative argv against its own cwd) - keep the
|
|
226
|
+
// invariant local instead of three modules away.
|
|
227
|
+
if (!path.isAbsolute(input.codeWorkspacePath)) {
|
|
228
|
+
throw new Error(`buildLaunchCommand requires an absolute workspace-file path (got '${input.codeWorkspacePath}').`);
|
|
229
|
+
}
|
|
230
|
+
const cwd = input.members[0].path;
|
|
231
|
+
if (opener.style === 'workspace-file') {
|
|
232
|
+
return {
|
|
233
|
+
executable: opener.command,
|
|
234
|
+
args: [...opener.args, input.codeWorkspacePath],
|
|
235
|
+
cwd,
|
|
236
|
+
label: opener.label,
|
|
237
|
+
style: opener.style,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
executable: opener.command,
|
|
242
|
+
args: [
|
|
243
|
+
...opener.args,
|
|
244
|
+
...input.members.flatMap((member) => [opener.attachFlag, member.path]),
|
|
245
|
+
],
|
|
246
|
+
cwd,
|
|
247
|
+
label: opener.label,
|
|
248
|
+
style: opener.style,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=openers.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type StoreDiagnostic } from './store/errors.js';
|
|
2
|
+
export declare const OPENSPEC_ROOT_DIR = "openspec";
|
|
3
|
+
export declare const OPENSPEC_CONFIG_YAML = "openspec/config.yaml";
|
|
4
|
+
export declare const OPENSPEC_CONFIG_YML = "openspec/config.yml";
|
|
5
|
+
export declare const OPENSPEC_SPECS_DIR = "openspec/specs";
|
|
6
|
+
export declare const OPENSPEC_CHANGES_DIR = "openspec/changes";
|
|
7
|
+
export declare const OPENSPEC_ARCHIVE_DIR = "openspec/changes/archive";
|
|
8
|
+
export declare const DEFAULT_OPENSPEC_SCHEMA = "spec-driven";
|
|
9
|
+
export declare const DIRECTORY_ANCHOR_FILE_NAME = ".gitkeep";
|
|
10
|
+
export declare const ANCHORED_OPENSPEC_DIRS: readonly ["openspec/specs", "openspec/changes/archive"];
|
|
11
|
+
export interface CreatedPathLedgerEntry {
|
|
12
|
+
relativePath: string;
|
|
13
|
+
absolutePath: string;
|
|
14
|
+
kind: 'directory' | 'file';
|
|
15
|
+
}
|
|
16
|
+
export interface OpenSpecRootInspection {
|
|
17
|
+
present: boolean | null;
|
|
18
|
+
config: {
|
|
19
|
+
present: boolean | null;
|
|
20
|
+
path?: string;
|
|
21
|
+
};
|
|
22
|
+
specs: {
|
|
23
|
+
present: boolean | null;
|
|
24
|
+
};
|
|
25
|
+
changes: {
|
|
26
|
+
present: boolean | null;
|
|
27
|
+
};
|
|
28
|
+
archive: {
|
|
29
|
+
present: boolean | null;
|
|
30
|
+
};
|
|
31
|
+
healthy: boolean;
|
|
32
|
+
diagnostics: StoreDiagnostic[];
|
|
33
|
+
}
|
|
34
|
+
export interface EnsureOpenSpecRootResult {
|
|
35
|
+
inspection: OpenSpecRootInspection;
|
|
36
|
+
createdArtifacts: string[];
|
|
37
|
+
createdPaths: CreatedPathLedgerEntry[];
|
|
38
|
+
}
|
|
39
|
+
export declare function inspectOpenSpecRoot(storeRoot: string): Promise<OpenSpecRootInspection>;
|
|
40
|
+
export interface EnsureOpenSpecRootOptions {
|
|
41
|
+
anchorEmptyDirectories?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function ensureOpenSpecRoot(storeRoot: string, options?: EnsureOpenSpecRootOptions): Promise<EnsureOpenSpecRootResult>;
|
|
44
|
+
export declare function rollbackCreatedPaths(entries: CreatedPathLedgerEntry[]): Promise<void>;
|
|
45
|
+
//# sourceMappingURL=openspec-root.d.ts.map
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
4
|
+
import { serializeConfig } from './config-prompts.js';
|
|
5
|
+
import { makeStoreDiagnostic, } from './store/errors.js';
|
|
6
|
+
export const OPENSPEC_ROOT_DIR = 'openspec';
|
|
7
|
+
export const OPENSPEC_CONFIG_YAML = 'openspec/config.yaml';
|
|
8
|
+
export const OPENSPEC_CONFIG_YML = 'openspec/config.yml';
|
|
9
|
+
export const OPENSPEC_SPECS_DIR = 'openspec/specs';
|
|
10
|
+
export const OPENSPEC_CHANGES_DIR = 'openspec/changes';
|
|
11
|
+
export const OPENSPEC_ARCHIVE_DIR = 'openspec/changes/archive';
|
|
12
|
+
export const DEFAULT_OPENSPEC_SCHEMA = 'spec-driven';
|
|
13
|
+
export const DIRECTORY_ANCHOR_FILE_NAME = '.gitkeep';
|
|
14
|
+
// Git cannot track empty directories, so clones of a fresh store would lose
|
|
15
|
+
// these and fail root-health checks. Anchored at setup time.
|
|
16
|
+
export const ANCHORED_OPENSPEC_DIRS = [OPENSPEC_SPECS_DIR, OPENSPEC_ARCHIVE_DIR];
|
|
17
|
+
async function pathKind(targetPath) {
|
|
18
|
+
try {
|
|
19
|
+
const stat = await fs.stat(targetPath);
|
|
20
|
+
if (stat.isDirectory())
|
|
21
|
+
return 'directory';
|
|
22
|
+
if (stat.isFile())
|
|
23
|
+
return 'file';
|
|
24
|
+
return 'other';
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (typeof error === 'object' &&
|
|
28
|
+
error !== null &&
|
|
29
|
+
'code' in error &&
|
|
30
|
+
error.code === 'ENOENT') {
|
|
31
|
+
return 'missing';
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function relativeArtifact(relativePath, kind) {
|
|
37
|
+
const normalized = FileSystemUtils.toPosixPath(relativePath);
|
|
38
|
+
return kind === 'directory' ? `${normalized}/` : normalized;
|
|
39
|
+
}
|
|
40
|
+
function unresolvedInspection() {
|
|
41
|
+
return {
|
|
42
|
+
present: null,
|
|
43
|
+
config: { present: null },
|
|
44
|
+
specs: { present: null },
|
|
45
|
+
changes: { present: null },
|
|
46
|
+
archive: { present: null },
|
|
47
|
+
healthy: false,
|
|
48
|
+
diagnostics: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function missingDirectoryDiagnostic(code, message, target) {
|
|
52
|
+
return makeStoreDiagnostic('error', code, message, { target });
|
|
53
|
+
}
|
|
54
|
+
export async function inspectOpenSpecRoot(storeRoot) {
|
|
55
|
+
const rootKind = await pathKind(storeRoot);
|
|
56
|
+
const inspection = unresolvedInspection();
|
|
57
|
+
if (rootKind === 'missing') {
|
|
58
|
+
inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_store_root_missing', 'Store root does not exist.', 'store.root'));
|
|
59
|
+
return inspection;
|
|
60
|
+
}
|
|
61
|
+
if (rootKind !== 'directory') {
|
|
62
|
+
inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_store_root_not_directory', 'Store root is not a directory.', 'store.root'));
|
|
63
|
+
return inspection;
|
|
64
|
+
}
|
|
65
|
+
const openspecPath = path.join(storeRoot, OPENSPEC_ROOT_DIR);
|
|
66
|
+
const openspecKind = await pathKind(openspecPath);
|
|
67
|
+
inspection.present = openspecKind === 'directory';
|
|
68
|
+
if (openspecKind === 'missing') {
|
|
69
|
+
inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_root_missing', 'Missing openspec/ directory.', 'openspec.root'));
|
|
70
|
+
return inspection;
|
|
71
|
+
}
|
|
72
|
+
if (openspecKind !== 'directory') {
|
|
73
|
+
inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_root_not_directory', 'openspec/ exists but is not a directory.', 'openspec.root'));
|
|
74
|
+
return inspection;
|
|
75
|
+
}
|
|
76
|
+
const configYamlKind = await pathKind(path.join(storeRoot, OPENSPEC_CONFIG_YAML));
|
|
77
|
+
const configYmlKind = await pathKind(path.join(storeRoot, OPENSPEC_CONFIG_YML));
|
|
78
|
+
if (configYamlKind === 'file') {
|
|
79
|
+
inspection.config = { present: true, path: OPENSPEC_CONFIG_YAML };
|
|
80
|
+
}
|
|
81
|
+
else if (configYmlKind === 'file') {
|
|
82
|
+
inspection.config = { present: true, path: OPENSPEC_CONFIG_YML };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
inspection.config = { present: false };
|
|
86
|
+
if (configYamlKind !== 'missing' || configYmlKind !== 'missing') {
|
|
87
|
+
inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_config_not_file', 'OpenSpec config path exists but is not a file.', 'openspec.config'));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_config_missing', 'Missing openspec/config.yaml or openspec/config.yml.', 'openspec.config'));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const [key, relativePath, code, message, target] of [
|
|
94
|
+
['specs', OPENSPEC_SPECS_DIR, 'openspec_specs_missing', 'Missing openspec/specs/.', 'openspec.specs'],
|
|
95
|
+
['changes', OPENSPEC_CHANGES_DIR, 'openspec_changes_missing', 'Missing openspec/changes/.', 'openspec.changes'],
|
|
96
|
+
['archive', OPENSPEC_ARCHIVE_DIR, 'openspec_archive_missing', 'Missing openspec/changes/archive/.', 'openspec.archive'],
|
|
97
|
+
]) {
|
|
98
|
+
const kind = await pathKind(path.join(storeRoot, relativePath));
|
|
99
|
+
inspection[key] = { present: kind === 'directory' };
|
|
100
|
+
if (kind === 'directory')
|
|
101
|
+
continue;
|
|
102
|
+
inspection.diagnostics.push(missingDirectoryDiagnostic(kind === 'missing' ? code : code.replace('_missing', '_not_directory'), kind === 'missing' ? message : `${relativePath}/ exists but is not a directory.`, target));
|
|
103
|
+
}
|
|
104
|
+
inspection.healthy =
|
|
105
|
+
inspection.present === true &&
|
|
106
|
+
inspection.config.present === true &&
|
|
107
|
+
inspection.specs.present === true &&
|
|
108
|
+
inspection.changes.present === true &&
|
|
109
|
+
inspection.archive.present === true;
|
|
110
|
+
return inspection;
|
|
111
|
+
}
|
|
112
|
+
async function ensureDirectory(storeRoot, relativePath, ledger) {
|
|
113
|
+
const absolutePath = path.join(storeRoot, relativePath);
|
|
114
|
+
const kind = await pathKind(absolutePath);
|
|
115
|
+
if (kind === 'directory')
|
|
116
|
+
return;
|
|
117
|
+
if (kind !== 'missing') {
|
|
118
|
+
throw new Error(`${relativePath}/ exists but is not a directory.`);
|
|
119
|
+
}
|
|
120
|
+
await fs.mkdir(absolutePath, { recursive: true });
|
|
121
|
+
ledger.push({
|
|
122
|
+
relativePath: relativeArtifact(relativePath, 'directory'),
|
|
123
|
+
absolutePath,
|
|
124
|
+
kind: 'directory',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async function ensureDefaultConfig(storeRoot, ledger) {
|
|
128
|
+
const configYamlPath = path.join(storeRoot, OPENSPEC_CONFIG_YAML);
|
|
129
|
+
const configYmlPath = path.join(storeRoot, OPENSPEC_CONFIG_YML);
|
|
130
|
+
const yamlKind = await pathKind(configYamlPath);
|
|
131
|
+
const ymlKind = await pathKind(configYmlPath);
|
|
132
|
+
if (yamlKind === 'file' || ymlKind === 'file')
|
|
133
|
+
return;
|
|
134
|
+
if (yamlKind !== 'missing' || ymlKind !== 'missing') {
|
|
135
|
+
throw new Error('OpenSpec config path exists but is not a file.');
|
|
136
|
+
}
|
|
137
|
+
await FileSystemUtils.writeFile(configYamlPath, serializeConfig({ schema: DEFAULT_OPENSPEC_SCHEMA }));
|
|
138
|
+
ledger.push({
|
|
139
|
+
relativePath: relativeArtifact(OPENSPEC_CONFIG_YAML, 'file'),
|
|
140
|
+
absolutePath: configYamlPath,
|
|
141
|
+
kind: 'file',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function ensureDirectoryAnchor(storeRoot, relativeDir, ledger) {
|
|
145
|
+
const directory = path.join(storeRoot, relativeDir);
|
|
146
|
+
if ((await fs.readdir(directory)).length > 0)
|
|
147
|
+
return;
|
|
148
|
+
const relativePath = `${relativeDir}/${DIRECTORY_ANCHOR_FILE_NAME}`;
|
|
149
|
+
const absolutePath = path.join(directory, DIRECTORY_ANCHOR_FILE_NAME);
|
|
150
|
+
await fs.writeFile(absolutePath, '', 'utf-8');
|
|
151
|
+
ledger.push({
|
|
152
|
+
relativePath: relativeArtifact(relativePath, 'file'),
|
|
153
|
+
absolutePath,
|
|
154
|
+
kind: 'file',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
export async function ensureOpenSpecRoot(storeRoot, options = {}) {
|
|
158
|
+
const ledger = [];
|
|
159
|
+
const rootKind = await pathKind(storeRoot);
|
|
160
|
+
if (rootKind === 'missing') {
|
|
161
|
+
await fs.mkdir(storeRoot, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
else if (rootKind !== 'directory') {
|
|
164
|
+
throw new Error('Store root is not a directory.');
|
|
165
|
+
}
|
|
166
|
+
await ensureDirectory(storeRoot, OPENSPEC_ROOT_DIR, ledger);
|
|
167
|
+
await ensureDirectory(storeRoot, OPENSPEC_SPECS_DIR, ledger);
|
|
168
|
+
await ensureDirectory(storeRoot, OPENSPEC_CHANGES_DIR, ledger);
|
|
169
|
+
await ensureDirectory(storeRoot, OPENSPEC_ARCHIVE_DIR, ledger);
|
|
170
|
+
await ensureDefaultConfig(storeRoot, ledger);
|
|
171
|
+
if (options.anchorEmptyDirectories) {
|
|
172
|
+
for (const relativeDir of ANCHORED_OPENSPEC_DIRS) {
|
|
173
|
+
await ensureDirectoryAnchor(storeRoot, relativeDir, ledger);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
inspection: await inspectOpenSpecRoot(storeRoot),
|
|
178
|
+
createdArtifacts: ledger.map((entry) => entry.relativePath),
|
|
179
|
+
createdPaths: ledger,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export async function rollbackCreatedPaths(entries) {
|
|
183
|
+
for (const entry of [...entries].reverse()) {
|
|
184
|
+
if (entry.kind === 'file') {
|
|
185
|
+
await fs.rm(entry.absolutePath, { force: true }).catch(() => undefined);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
await fs.rmdir(entry.absolutePath).catch(() => undefined);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=openspec-root.js.map
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
export type PlanningHomeKind = 'repo'
|
|
1
|
+
export type PlanningHomeKind = 'repo';
|
|
2
2
|
export interface PlanningHome {
|
|
3
3
|
kind: PlanningHomeKind;
|
|
4
4
|
root: string;
|
|
5
5
|
changesDir: string;
|
|
6
6
|
defaultSchema: string;
|
|
7
|
-
workspace?: {
|
|
8
|
-
name: string;
|
|
9
|
-
links: string[];
|
|
10
|
-
};
|
|
11
7
|
}
|
|
12
8
|
export interface ResolvePlanningHomeOptions {
|
|
13
9
|
startPath?: string;
|
|
14
10
|
allowImplicitRepoRoot?: boolean;
|
|
15
11
|
}
|
|
16
|
-
export declare function findWorkspacePlanningRootSync(startPath?: string): string | null;
|
|
17
12
|
export declare function findRepoPlanningRootSync(startPath?: string): string | null;
|
|
18
13
|
export declare function resolveCurrentPlanningHomeSync(options?: ResolvePlanningHomeOptions): PlanningHome;
|
|
19
14
|
export declare function getChangeDir(planningHome: PlanningHome, changeName: string): string;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { getWorkspaceChangesDir, readWorkspaceViewStateSync, workspaceStateFileExistsSync, } from './workspace/index.js';
|
|
4
3
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
5
4
|
const REPO_DEFAULT_SCHEMA = 'spec-driven';
|
|
6
|
-
const WORKSPACE_DEFAULT_SCHEMA = 'workspace-planning';
|
|
7
5
|
function pathExistsAsDirectory(candidatePath) {
|
|
8
6
|
try {
|
|
9
7
|
return fs.statSync(candidatePath).isDirectory();
|
|
@@ -36,41 +34,9 @@ function findNearestAncestor(startPath, predicate) {
|
|
|
36
34
|
currentDir = parentDir;
|
|
37
35
|
}
|
|
38
36
|
}
|
|
39
|
-
export function findWorkspacePlanningRootSync(startPath = process.cwd()) {
|
|
40
|
-
return findNearestAncestor(startPath, workspaceStateFileExistsSync);
|
|
41
|
-
}
|
|
42
37
|
export function findRepoPlanningRootSync(startPath = process.cwd()) {
|
|
43
38
|
return findNearestAncestor(startPath, (dirPath) => pathExistsAsDirectory(path.join(dirPath, 'openspec')));
|
|
44
39
|
}
|
|
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
40
|
function repoPlanningHome(repoRoot) {
|
|
75
41
|
return {
|
|
76
42
|
kind: 'repo',
|
|
@@ -82,13 +48,7 @@ function repoPlanningHome(repoRoot) {
|
|
|
82
48
|
export function resolveCurrentPlanningHomeSync(options = {}) {
|
|
83
49
|
const startPath = options.startPath ?? process.cwd();
|
|
84
50
|
const searchStart = getSearchStartDirectory(startPath);
|
|
85
|
-
const workspaceRoot = findWorkspacePlanningRootSync(searchStart);
|
|
86
51
|
const repoRoot = findRepoPlanningRootSync(searchStart);
|
|
87
|
-
if (workspaceRoot && isSameOrDescendant(workspaceRoot, searchStart)) {
|
|
88
|
-
if (!repoRoot || countPathSegments(workspaceRoot) >= countPathSegments(repoRoot)) {
|
|
89
|
-
return workspacePlanningHome(workspaceRoot);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
52
|
if (repoRoot) {
|
|
93
53
|
return repoPlanningHome(repoRoot);
|
|
94
54
|
}
|
|
@@ -101,8 +61,7 @@ export function getChangeDir(planningHome, changeName) {
|
|
|
101
61
|
return FileSystemUtils.joinPath(planningHome.changesDir, changeName);
|
|
102
62
|
}
|
|
103
63
|
export function formatChangeLocation(planningHome, changeName) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return relative.length > 0 ? relative : changeDir;
|
|
64
|
+
// Repo homes always nest changesDir under the root.
|
|
65
|
+
return path.relative(planningHome.root, getChangeDir(planningHome, changeName));
|
|
107
66
|
}
|
|
108
67
|
//# sourceMappingURL=planning-home.js.map
|
|
@@ -16,8 +16,18 @@ export declare const ProjectConfigSchema: z.ZodObject<{
|
|
|
16
16
|
schema: z.ZodString;
|
|
17
17
|
context: z.ZodOptional<z.ZodString>;
|
|
18
18
|
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
19
|
+
store: z.ZodOptional<z.ZodString>;
|
|
19
20
|
}, z.core.$strip>;
|
|
20
|
-
|
|
21
|
+
/** Normalized in-memory shape of a referenced store declaration. */
|
|
22
|
+
export interface DeclarationEntry {
|
|
23
|
+
id: string;
|
|
24
|
+
/** Clone source rendered into onboarding fixes. */
|
|
25
|
+
remote?: string;
|
|
26
|
+
}
|
|
27
|
+
export type ProjectConfig = z.infer<typeof ProjectConfigSchema> & {
|
|
28
|
+
references?: DeclarationEntry[];
|
|
29
|
+
};
|
|
30
|
+
export declare const MAX_CONTEXT_SIZE: number;
|
|
21
31
|
/**
|
|
22
32
|
* Read and parse openspec/config.yaml from project root.
|
|
23
33
|
* Uses resilient parsing - validates each field independently using Zod safeParse.
|
|
@@ -61,4 +71,37 @@ export declare function suggestSchemas(invalidSchemaName: string, availableSchem
|
|
|
61
71
|
name: string;
|
|
62
72
|
isBuiltIn: boolean;
|
|
63
73
|
}[]): string;
|
|
74
|
+
export interface StorePointerRead {
|
|
75
|
+
/** The declared store id, when present and a string. */
|
|
76
|
+
value?: string;
|
|
77
|
+
/** Set when the pointer cannot be trusted: the config file could not be
|
|
78
|
+
* read as YAML, or the store key is present but not a string. An empty
|
|
79
|
+
* or comments-only config is NOT malformed - it simply has no pointer. */
|
|
80
|
+
malformed?: 'unparseable' | 'non_string';
|
|
81
|
+
/** Absolute path of the config file actually read, or null when none exists. */
|
|
82
|
+
filePath: string | null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Warning-silent targeted read of the `store:` pointer. Used by root
|
|
86
|
+
* resolution (which must not re-emit the resilient parser's field
|
|
87
|
+
* warnings) and by `openspec init`'s pointer guard. Unlike
|
|
88
|
+
* `readProjectConfig`, a malformed value is REPORTED, not dropped —
|
|
89
|
+
* a dropped pointer would silently flip where work lands.
|
|
90
|
+
*/
|
|
91
|
+
export declare function readStorePointer(projectRoot: string): StorePointerRead;
|
|
92
|
+
/** Shared .yaml/.yml probe used by readProjectConfig and readStorePointer. */
|
|
93
|
+
export declare function resolveConfigFilePath(projectRoot: string): string | null;
|
|
94
|
+
/** Human rendering of a malformed pointer reason, shared by every surface. */
|
|
95
|
+
export declare function storePointerProblem(reason: 'unparseable' | 'non_string'): string;
|
|
96
|
+
export interface OpenSpecDirClassification {
|
|
97
|
+
/** True when openspec/specs or openspec/changes exists as a directory. */
|
|
98
|
+
hasPlanningShape: boolean;
|
|
99
|
+
pointer: StorePointerRead;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* One classification for "real root vs config-only pointer dir", shared
|
|
103
|
+
* by root resolution and the init pointer guard so they can never
|
|
104
|
+
* disagree (slice 3.2).
|
|
105
|
+
*/
|
|
106
|
+
export declare function classifyOpenSpecDir(projectRoot: string): OpenSpecDirClassification;
|
|
64
107
|
//# sourceMappingURL=project-config.d.ts.map
|