gmc-openspec 1.1.0 → 1.4.2
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 +2 -2
- package/bin/openspec.js +3 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +36 -2
- package/dist/commands/config.js +4 -4
- 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.js +10 -2
- package/dist/commands/workflow/new-change.d.ts +4 -0
- package/dist/commands/workflow/new-change.js +72 -23
- 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 +2 -0
- package/dist/commands/workflow/status.js +3 -0
- 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 +16 -8
- package/dist/commands/workspace/open.js +40 -14
- 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 +14 -8
- package/dist/commands/workspace/operations.js +228 -160
- 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 +3 -0
- package/dist/commands/workspace/selection.js +42 -40
- 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 +15 -0
- package/dist/commands/workspace.js +59 -340
- package/dist/core/artifact-graph/index.d.ts +2 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +10 -23
- package/dist/core/artifact-graph/instruction-loader.js +28 -89
- package/dist/core/artifact-graph/types.d.ts +0 -7
- package/dist/core/artifact-graph/types.js +0 -19
- 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 +475 -70
- package/dist/core/completions/shared-flags.d.ts +12 -0
- package/dist/core/completions/shared-flags.js +28 -0
- package/dist/core/config.js +2 -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/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/jira/templates.js +14 -5
- package/dist/core/planning-home.js +5 -21
- package/dist/core/validation/validator.d.ts +11 -0
- package/dist/core/validation/validator.js +19 -2
- package/dist/core/workspace/foundation.d.ts +28 -48
- package/dist/core/workspace/foundation.js +130 -214
- package/dist/core/workspace/index.d.ts +2 -0
- package/dist/core/workspace/index.js +2 -0
- package/dist/core/workspace/legacy-state.d.ts +28 -0
- package/dist/core/workspace/legacy-state.js +200 -0
- package/dist/core/workspace/open-surface.d.ts +29 -8
- package/dist/core/workspace/open-surface.js +122 -44
- package/dist/core/workspace/openers.js +11 -6
- package/dist/core/workspace/registry.d.ts +24 -0
- package/dist/core/workspace/registry.js +146 -0
- package/dist/core/workspace/skills.d.ts +4 -2
- package/dist/core/workspace/skills.js +2 -2
- package/dist/core/workspace/state-io.d.ts +10 -0
- package/dist/core/workspace/state-io.js +119 -0
- package/dist/utils/change-metadata.d.ts +5 -2
- package/dist/utils/change-metadata.js +6 -12
- package/dist/utils/change-utils.d.ts +2 -2
- package/package.json +1 -1
|
@@ -1,29 +1,11 @@
|
|
|
1
1
|
import * as nodeFs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { getManagedWorkspaceRoot, hasWorkspaceSkillProfileDrift, getWorkspaceChangesDir,
|
|
3
|
+
import { getWorkspaceContextInitiativeId, getWorkspaceContextStoreId, getManagedWorkspaceRoot, hasWorkspaceSkillProfileDrift, getWorkspaceChangesDir, getWorkspaceViewStatePath, isWorkspaceRoot, listKnownWorkspaceEntries, parseWorkspaceSetupLinkInput, readWorkspaceViewState, syncWorkspaceOpenSurface, validateWorkspaceLinkName, validateWorkspaceName, writeWorkspaceViewState, } from '../../core/workspace/index.js';
|
|
4
|
+
import { formatContextStoreBinding, sameContextStoreBinding, } from '../../core/context-store/index.js';
|
|
4
5
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
5
6
|
import { WorkspaceCliError, asErrorMessage, makeStatus, } from './types.js';
|
|
7
|
+
import { collectWorkspaceContextStatuses } from './context-status.js';
|
|
6
8
|
const fs = nodeFs.promises;
|
|
7
|
-
function emptyRegistry() {
|
|
8
|
-
return { version: 1, workspaces: {} };
|
|
9
|
-
}
|
|
10
|
-
function emptyLocalState() {
|
|
11
|
-
return { version: 1, paths: {} };
|
|
12
|
-
}
|
|
13
|
-
export async function readRegistry() {
|
|
14
|
-
return (await readWorkspaceRegistryState()) ?? emptyRegistry();
|
|
15
|
-
}
|
|
16
|
-
async function recordWorkspaceInRegistry(name, workspaceRoot) {
|
|
17
|
-
const registry = await readRegistry();
|
|
18
|
-
const recordedWorkspaceRoot = normalizeExistingPathForStorage(workspaceRoot);
|
|
19
|
-
await writeWorkspaceRegistryState({
|
|
20
|
-
version: 1,
|
|
21
|
-
workspaces: {
|
|
22
|
-
...registry.workspaces,
|
|
23
|
-
[name]: recordedWorkspaceRoot,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
9
|
export async function directoryExists(dirPath) {
|
|
28
10
|
try {
|
|
29
11
|
return (await fs.stat(dirPath)).isDirectory();
|
|
@@ -33,9 +15,7 @@ export async function directoryExists(dirPath) {
|
|
|
33
15
|
}
|
|
34
16
|
}
|
|
35
17
|
function normalizeExistingPathForStorage(existingPath) {
|
|
36
|
-
return
|
|
37
|
-
? FileSystemUtils.canonicalizeExistingPath(existingPath)
|
|
38
|
-
: existingPath;
|
|
18
|
+
return FileSystemUtils.canonicalizeExistingPath(existingPath);
|
|
39
19
|
}
|
|
40
20
|
export async function resolveExistingDirectory(inputPath, cwd = process.cwd()) {
|
|
41
21
|
if (inputPath.length === 0) {
|
|
@@ -58,15 +38,25 @@ export async function resolveExistingDirectory(inputPath, cwd = process.cwd()) {
|
|
|
58
38
|
export function inferLinkName(absolutePath) {
|
|
59
39
|
return path.basename(absolutePath);
|
|
60
40
|
}
|
|
61
|
-
function normalizeLinksForOutput(
|
|
62
|
-
return Object.keys(
|
|
41
|
+
function normalizeLinksForOutput(viewState) {
|
|
42
|
+
return Object.keys(viewState.links)
|
|
63
43
|
.sort((a, b) => a.localeCompare(b))
|
|
64
44
|
.map((name) => ({
|
|
65
45
|
name,
|
|
66
|
-
path:
|
|
46
|
+
path: viewState.links[name] ?? null,
|
|
67
47
|
status: [],
|
|
68
48
|
}));
|
|
69
49
|
}
|
|
50
|
+
function workspaceContextToOutput(context) {
|
|
51
|
+
if (!context) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
store: getWorkspaceContextStoreId(context),
|
|
56
|
+
initiative: getWorkspaceContextInitiativeId(context),
|
|
57
|
+
store_selector: context.store.selector,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
70
60
|
function formatDuplicateLinkMessage(linkName, existingPath, replacementPath) {
|
|
71
61
|
return [
|
|
72
62
|
`Cannot use link name '${linkName}' because another link already uses that name.`,
|
|
@@ -86,6 +76,9 @@ function duplicateLinkError(linkName, existingPath, replacementPath) {
|
|
|
86
76
|
fix: `Choose a different link name or run 'openspec workspace relink ${linkName} ${replacementPath}'.`,
|
|
87
77
|
});
|
|
88
78
|
}
|
|
79
|
+
function hasWorkspaceLink(links, linkName) {
|
|
80
|
+
return Object.prototype.hasOwnProperty.call(links, linkName);
|
|
81
|
+
}
|
|
89
82
|
function duplicateSetupLinkError(linkName, existingPath, replacementPath) {
|
|
90
83
|
return new WorkspaceCliError([
|
|
91
84
|
`Cannot use link name '${linkName}' because another setup link already uses that name.`,
|
|
@@ -121,7 +114,7 @@ export function validateLinkNameForCommand(name) {
|
|
|
121
114
|
function localStateInvalidStatus(error) {
|
|
122
115
|
return makeStatus('error', 'workspace_local_state_invalid', `Machine-local paths could not be read: ${asErrorMessage(error)}`, {
|
|
123
116
|
target: 'workspace.local_state',
|
|
124
|
-
fix: 'Repair
|
|
117
|
+
fix: 'Repair workspace.yaml, then run openspec workspace relink <name> <path> for affected links.',
|
|
125
118
|
});
|
|
126
119
|
}
|
|
127
120
|
function workspaceSkillDriftStatus(workspaceName) {
|
|
@@ -130,62 +123,41 @@ function workspaceSkillDriftStatus(workspaceName) {
|
|
|
130
123
|
fix: `openspec workspace update --workspace ${workspaceName}`,
|
|
131
124
|
});
|
|
132
125
|
}
|
|
133
|
-
function appendWorkspaceSkillDriftStatus(statuses, workspaceName,
|
|
134
|
-
if (hasWorkspaceSkillProfileDrift(
|
|
126
|
+
function appendWorkspaceSkillDriftStatus(statuses, workspaceName, viewState) {
|
|
127
|
+
if (hasWorkspaceSkillProfileDrift(viewState)) {
|
|
135
128
|
statuses.push(workspaceSkillDriftStatus(workspaceName));
|
|
136
129
|
}
|
|
137
130
|
}
|
|
138
|
-
async function
|
|
139
|
-
try {
|
|
140
|
-
return (await readOptionalWorkspaceLocalState(workspaceRoot)) ?? emptyLocalState();
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
const status = localStateInvalidStatus(error);
|
|
144
|
-
throw new WorkspaceCliError(status.message, status.code, {
|
|
145
|
-
target: status.target,
|
|
146
|
-
fix: status.fix,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
export async function createManagedWorkspace(name, links, preferredOpener) {
|
|
131
|
+
export async function createManagedWorkspace(name, links, preferredOpener, context = null, tools) {
|
|
151
132
|
const workspaceName = validateWorkspaceNameForSetup(name);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
throw new WorkspaceCliError(`Workspace '${workspaceName}'
|
|
133
|
+
const targetWorkspaceRoot = getManagedWorkspaceRoot(workspaceName);
|
|
134
|
+
let workspaceRoot = targetWorkspaceRoot;
|
|
135
|
+
if (await directoryExists(targetWorkspaceRoot)) {
|
|
136
|
+
throw new WorkspaceCliError(`Workspace '${workspaceName}' already exists at ${targetWorkspaceRoot}.`, 'workspace_already_exists', {
|
|
156
137
|
target: 'workspace.name',
|
|
157
138
|
});
|
|
158
139
|
}
|
|
159
|
-
if (await directoryExists(workspaceRoot)) {
|
|
160
|
-
throw new WorkspaceCliError(`Workspace '${workspaceName}' already exists at ${workspaceRoot}.`, 'workspace_already_exists', {
|
|
161
|
-
target: 'workspace.root',
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
140
|
let createdWorkspaceRoot = false;
|
|
165
141
|
try {
|
|
166
|
-
await FileSystemUtils.createDirectory(path.dirname(
|
|
167
|
-
await fs.mkdir(
|
|
142
|
+
await FileSystemUtils.createDirectory(path.dirname(targetWorkspaceRoot));
|
|
143
|
+
await fs.mkdir(targetWorkspaceRoot);
|
|
168
144
|
createdWorkspaceRoot = true;
|
|
169
|
-
|
|
170
|
-
const
|
|
145
|
+
workspaceRoot = FileSystemUtils.canonicalizeExistingPath(targetWorkspaceRoot);
|
|
146
|
+
const viewState = {
|
|
171
147
|
version: 1,
|
|
172
148
|
name: workspaceName,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const localState = {
|
|
176
|
-
version: 1,
|
|
177
|
-
paths: links,
|
|
149
|
+
context,
|
|
150
|
+
links,
|
|
178
151
|
...(preferredOpener ? { preferred_opener: preferredOpener } : {}),
|
|
152
|
+
...(tools ? { tools } : {}),
|
|
179
153
|
};
|
|
180
|
-
await
|
|
181
|
-
await
|
|
182
|
-
await syncWorkspaceOpenSurface(workspaceRoot, sharedState, localState);
|
|
183
|
-
await recordWorkspaceInRegistry(workspaceName, workspaceRoot);
|
|
154
|
+
await writeWorkspaceViewState(workspaceRoot, viewState);
|
|
155
|
+
await syncWorkspaceOpenSurface(workspaceRoot, viewState);
|
|
184
156
|
}
|
|
185
157
|
catch (error) {
|
|
186
158
|
if (createdWorkspaceRoot) {
|
|
187
159
|
try {
|
|
188
|
-
await fs.rm(
|
|
160
|
+
await fs.rm(targetWorkspaceRoot, { recursive: true, force: true });
|
|
189
161
|
}
|
|
190
162
|
catch {
|
|
191
163
|
// Preserve the original creation failure; callers can retry or inspect the path.
|
|
@@ -199,6 +171,8 @@ export async function createManagedWorkspace(name, links, preferredOpener) {
|
|
|
199
171
|
name: workspaceName,
|
|
200
172
|
root: workspaceRoot,
|
|
201
173
|
planning_path: getWorkspaceChangesDir(workspaceRoot),
|
|
174
|
+
state_path: getWorkspaceViewStatePath(workspaceRoot),
|
|
175
|
+
context: workspaceContextToOutput(context),
|
|
202
176
|
links: Object.entries(links)
|
|
203
177
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
204
178
|
.map(([linkName, linkPath]) => ({
|
|
@@ -228,24 +202,25 @@ export async function loadWorkspaceForList(entry) {
|
|
|
228
202
|
return {
|
|
229
203
|
name: entry.name,
|
|
230
204
|
root: entry.workspaceRoot,
|
|
205
|
+
context: null,
|
|
231
206
|
links: [],
|
|
232
207
|
status: [
|
|
233
208
|
makeStatus('error', 'workspace_root_missing', 'Workspace location does not exist.', {
|
|
234
209
|
target: 'workspace.root',
|
|
235
|
-
fix: 'Remove or repair the local
|
|
210
|
+
fix: 'Remove or repair the local workspace view.',
|
|
236
211
|
}),
|
|
237
212
|
],
|
|
238
213
|
};
|
|
239
214
|
}
|
|
240
|
-
let
|
|
241
|
-
let localState = null;
|
|
215
|
+
let viewState;
|
|
242
216
|
try {
|
|
243
|
-
|
|
217
|
+
viewState = await readWorkspaceViewState(entry.workspaceRoot);
|
|
244
218
|
}
|
|
245
219
|
catch (error) {
|
|
246
220
|
return {
|
|
247
221
|
name: entry.name,
|
|
248
222
|
root: entry.workspaceRoot,
|
|
223
|
+
context: null,
|
|
249
224
|
links: [],
|
|
250
225
|
status: [
|
|
251
226
|
makeStatus('error', 'workspace_state_invalid', `Workspace state could not be read: ${asErrorMessage(error)}`, {
|
|
@@ -255,17 +230,13 @@ export async function loadWorkspaceForList(entry) {
|
|
|
255
230
|
],
|
|
256
231
|
};
|
|
257
232
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
workspaceStatus.push(localStateInvalidStatus(error));
|
|
263
|
-
}
|
|
264
|
-
appendWorkspaceSkillDriftStatus(workspaceStatus, sharedState.name, localState);
|
|
233
|
+
appendWorkspaceSkillDriftStatus(workspaceStatus, viewState.name, viewState);
|
|
234
|
+
workspaceStatus.push(...(await collectWorkspaceContextStatuses(viewState.context)));
|
|
265
235
|
return {
|
|
266
|
-
name:
|
|
236
|
+
name: viewState.name,
|
|
267
237
|
root: entry.workspaceRoot,
|
|
268
|
-
|
|
238
|
+
context: workspaceContextToOutput(viewState.context),
|
|
239
|
+
links: normalizeLinksForOutput(viewState),
|
|
269
240
|
status: workspaceStatus,
|
|
270
241
|
};
|
|
271
242
|
}
|
|
@@ -279,22 +250,22 @@ export async function loadWorkspaceForDoctor(selected) {
|
|
|
279
250
|
name: selected.name,
|
|
280
251
|
root: selected.root,
|
|
281
252
|
planning_path: planningPath,
|
|
253
|
+
state_path: getWorkspaceViewStatePath(selected.root),
|
|
254
|
+
context: null,
|
|
282
255
|
links: [],
|
|
283
256
|
status: [
|
|
284
257
|
makeStatus('error', 'selected_workspace_root_missing', 'Selected workspace location does not exist or is not a valid workspace.', {
|
|
285
258
|
target: 'workspace.root',
|
|
286
|
-
fix: 'Repair the local workspace
|
|
259
|
+
fix: 'Repair the local workspace view or choose another workspace.',
|
|
287
260
|
}),
|
|
288
261
|
],
|
|
289
262
|
},
|
|
290
263
|
status: commandStatus,
|
|
291
264
|
};
|
|
292
265
|
}
|
|
293
|
-
let
|
|
294
|
-
let localState;
|
|
295
|
-
let localStateInvalid = false;
|
|
266
|
+
let viewState;
|
|
296
267
|
try {
|
|
297
|
-
|
|
268
|
+
viewState = await readWorkspaceViewState(selected.root);
|
|
298
269
|
}
|
|
299
270
|
catch (error) {
|
|
300
271
|
return {
|
|
@@ -302,6 +273,8 @@ export async function loadWorkspaceForDoctor(selected) {
|
|
|
302
273
|
name: selected.name,
|
|
303
274
|
root: selected.root,
|
|
304
275
|
planning_path: planningPath,
|
|
276
|
+
state_path: getWorkspaceViewStatePath(selected.root),
|
|
277
|
+
context: null,
|
|
305
278
|
links: [],
|
|
306
279
|
status: [
|
|
307
280
|
makeStatus('error', 'workspace_state_invalid', `Workspace state could not be read: ${asErrorMessage(error)}`, {
|
|
@@ -313,45 +286,15 @@ export async function loadWorkspaceForDoctor(selected) {
|
|
|
313
286
|
status: commandStatus,
|
|
314
287
|
};
|
|
315
288
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (!optionalLocalState) {
|
|
320
|
-
workspaceStatus.push(makeStatus('warning', 'workspace_local_state_missing', 'Machine-local paths are not recorded yet.', {
|
|
321
|
-
target: 'workspace.local_state',
|
|
322
|
-
fix: 'Run openspec workspace relink <name> <path> for each linked repo or folder on this machine.',
|
|
323
|
-
}));
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
localState = emptyLocalState();
|
|
328
|
-
localStateInvalid = true;
|
|
329
|
-
workspaceStatus.push(localStateInvalidStatus(error));
|
|
330
|
-
}
|
|
331
|
-
if (!localStateInvalid) {
|
|
332
|
-
appendWorkspaceSkillDriftStatus(workspaceStatus, sharedState.name, localState);
|
|
333
|
-
}
|
|
334
|
-
if (!(await directoryExists(planningPath))) {
|
|
335
|
-
workspaceStatus.push(makeStatus('error', 'workspace_planning_path_missing', 'Workspace planning path does not exist.', {
|
|
336
|
-
target: 'workspace.planning_path',
|
|
337
|
-
fix: `Create ${planningPath} or recreate the workspace with openspec workspace setup.`,
|
|
338
|
-
}));
|
|
339
|
-
}
|
|
340
|
-
const sharedNames = new Set(Object.keys(sharedState.links));
|
|
341
|
-
const localNames = new Set(Object.keys(localState.paths));
|
|
342
|
-
const linkNames = [...new Set([...sharedNames, ...localNames])].sort((a, b) => a.localeCompare(b));
|
|
289
|
+
appendWorkspaceSkillDriftStatus(workspaceStatus, viewState.name, viewState);
|
|
290
|
+
workspaceStatus.push(...(await collectWorkspaceContextStatuses(viewState.context)));
|
|
291
|
+
const linkNames = Object.keys(viewState.links).sort((a, b) => a.localeCompare(b));
|
|
343
292
|
const links = [];
|
|
344
293
|
for (const linkName of linkNames) {
|
|
345
294
|
const linkStatus = [];
|
|
346
|
-
const localPath =
|
|
295
|
+
const localPath = viewState.links[linkName] ?? null;
|
|
347
296
|
let repoSpecsPath = null;
|
|
348
|
-
if (!
|
|
349
|
-
linkStatus.push(makeStatus('warning', 'local_path_without_shared_link', 'Local path is recorded without a shared workspace link.', {
|
|
350
|
-
target: `links.${linkName}`,
|
|
351
|
-
fix: `Add a shared link with openspec workspace link ${linkName} ${localPath ?? '/path/to/folder'} or remove the local-only path from .openspec-workspace/local.yaml.`,
|
|
352
|
-
}));
|
|
353
|
-
}
|
|
354
|
-
if (sharedNames.has(linkName) && !localPath && !localStateInvalid) {
|
|
297
|
+
if (!localPath) {
|
|
355
298
|
linkStatus.push(makeStatus('error', 'linked_path_missing_from_local_state', 'Shared link does not have a local path on this machine.', {
|
|
356
299
|
target: `links.${linkName}.path`,
|
|
357
300
|
fix: `openspec workspace relink ${linkName} /path/to/${linkName}`,
|
|
@@ -378,39 +321,46 @@ export async function loadWorkspaceForDoctor(selected) {
|
|
|
378
321
|
}
|
|
379
322
|
return {
|
|
380
323
|
workspace: {
|
|
381
|
-
name:
|
|
324
|
+
name: viewState.name,
|
|
382
325
|
root: selected.root,
|
|
383
326
|
planning_path: planningPath,
|
|
327
|
+
state_path: getWorkspaceViewStatePath(selected.root),
|
|
328
|
+
context: workspaceContextToOutput(viewState.context),
|
|
384
329
|
links,
|
|
385
330
|
status: workspaceStatus,
|
|
386
331
|
},
|
|
387
332
|
status: commandStatus,
|
|
388
333
|
};
|
|
389
334
|
}
|
|
390
|
-
|
|
335
|
+
async function readWorkspaceViewForMutation(selected) {
|
|
391
336
|
if (!(await directoryExists(selected.root)) || !(await isWorkspaceRoot(selected.root))) {
|
|
392
337
|
throw new WorkspaceCliError(`Workspace location does not exist for '${selected.name}': ${selected.root}`, 'selected_workspace_root_missing', {
|
|
393
338
|
target: 'workspace.root',
|
|
394
339
|
fix: 'Run openspec workspace list to inspect known workspaces.',
|
|
395
340
|
});
|
|
396
341
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
localState: await readLocalStateForMutation(selected.root),
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
export async function recordSelectedWorkspaceAfterMutation(selected) {
|
|
403
|
-
if (selected.unregisteredCurrentWorkspace) {
|
|
404
|
-
await recordWorkspaceInRegistry(selected.name, selected.root);
|
|
342
|
+
try {
|
|
343
|
+
return await readWorkspaceViewState(selected.root);
|
|
405
344
|
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
throw new WorkspaceCliError(`Workspace state could not be read: ${asErrorMessage(error)}`, 'workspace_state_invalid', {
|
|
347
|
+
target: 'workspace.state',
|
|
348
|
+
fix: 'Repair workspace.yaml before using this workspace.',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export async function readWorkspaceForMutation(selected) {
|
|
353
|
+
return readWorkspaceViewForMutation(selected);
|
|
406
354
|
}
|
|
407
|
-
function buildLinkMutationPayload(selected,
|
|
355
|
+
function buildLinkMutationPayload(selected, viewState, linkName, linkPath) {
|
|
408
356
|
return {
|
|
409
357
|
workspace: {
|
|
410
|
-
name:
|
|
358
|
+
name: viewState.name,
|
|
411
359
|
root: selected.root,
|
|
412
360
|
planning_path: getWorkspaceChangesDir(selected.root),
|
|
413
|
-
|
|
361
|
+
state_path: getWorkspaceViewStatePath(selected.root),
|
|
362
|
+
context: workspaceContextToOutput(viewState.context),
|
|
363
|
+
links: normalizeLinksForOutput(viewState),
|
|
414
364
|
status: [],
|
|
415
365
|
},
|
|
416
366
|
link: {
|
|
@@ -426,50 +376,168 @@ export async function addWorkspaceLink(selected, nameOrPath, linkPath) {
|
|
|
426
376
|
const pathInput = linkPath ?? nameOrPath;
|
|
427
377
|
const resolvedPath = await resolveExistingDirectory(pathInput);
|
|
428
378
|
const linkName = validateLinkNameForCommand(explicitName ?? inferLinkName(resolvedPath));
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
431
|
-
throw duplicateLinkError(linkName,
|
|
379
|
+
const viewState = await readWorkspaceViewForMutation(selected);
|
|
380
|
+
if (hasWorkspaceLink(viewState.links, linkName)) {
|
|
381
|
+
throw duplicateLinkError(linkName, viewState.links[linkName] ?? null, resolvedPath);
|
|
432
382
|
}
|
|
433
|
-
const
|
|
434
|
-
...
|
|
383
|
+
const updatedViewState = {
|
|
384
|
+
...viewState,
|
|
435
385
|
links: {
|
|
436
|
-
...
|
|
437
|
-
[linkName]: {},
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
const updatedLocalState = {
|
|
441
|
-
...localState,
|
|
442
|
-
paths: {
|
|
443
|
-
...localState.paths,
|
|
386
|
+
...viewState.links,
|
|
444
387
|
[linkName]: resolvedPath,
|
|
445
388
|
},
|
|
446
389
|
};
|
|
447
|
-
await
|
|
448
|
-
await
|
|
449
|
-
|
|
450
|
-
await recordSelectedWorkspaceAfterMutation(selected);
|
|
451
|
-
return buildLinkMutationPayload(selected, updatedSharedState, updatedLocalState, linkName, resolvedPath);
|
|
390
|
+
await writeWorkspaceViewState(selected.root, updatedViewState);
|
|
391
|
+
await syncWorkspaceOpenSurface(selected.root, updatedViewState);
|
|
392
|
+
return buildLinkMutationPayload(selected, updatedViewState, linkName, resolvedPath);
|
|
452
393
|
}
|
|
453
394
|
export async function updateWorkspaceLink(selected, linkNameInput, linkPath) {
|
|
454
395
|
const linkName = validateLinkNameForCommand(linkNameInput);
|
|
455
396
|
const resolvedPath = await resolveExistingDirectory(linkPath);
|
|
456
|
-
const
|
|
457
|
-
if (!
|
|
397
|
+
const viewState = await readWorkspaceViewForMutation(selected);
|
|
398
|
+
if (!hasWorkspaceLink(viewState.links, linkName)) {
|
|
458
399
|
throw new WorkspaceCliError(`Unknown workspace link '${linkName}'.`, 'unknown_link_name', {
|
|
459
400
|
target: `links.${linkName}`,
|
|
460
401
|
fix: 'Run openspec workspace doctor to see linked repos or folders.',
|
|
461
402
|
});
|
|
462
403
|
}
|
|
463
|
-
const
|
|
464
|
-
...
|
|
465
|
-
|
|
466
|
-
...
|
|
404
|
+
const updatedViewState = {
|
|
405
|
+
...viewState,
|
|
406
|
+
links: {
|
|
407
|
+
...viewState.links,
|
|
467
408
|
[linkName]: resolvedPath,
|
|
468
409
|
},
|
|
469
410
|
};
|
|
470
|
-
await
|
|
471
|
-
await syncWorkspaceOpenSurface(selected.root,
|
|
472
|
-
|
|
473
|
-
|
|
411
|
+
await writeWorkspaceViewState(selected.root, updatedViewState);
|
|
412
|
+
await syncWorkspaceOpenSurface(selected.root, updatedViewState);
|
|
413
|
+
return buildLinkMutationPayload(selected, updatedViewState, linkName, resolvedPath);
|
|
414
|
+
}
|
|
415
|
+
function sameWorkspaceContext(left, right) {
|
|
416
|
+
return (left !== null &&
|
|
417
|
+
sameContextStoreBinding(left.store, right.store) &&
|
|
418
|
+
getWorkspaceContextInitiativeId(left) === getWorkspaceContextInitiativeId(right));
|
|
419
|
+
}
|
|
420
|
+
function formatWorkspaceContext(context) {
|
|
421
|
+
return context
|
|
422
|
+
? `${formatContextStoreBinding(context.store)}/${getWorkspaceContextInitiativeId(context)}`
|
|
423
|
+
: 'no initiative context';
|
|
424
|
+
}
|
|
425
|
+
export function deriveWorkspaceNameForInitiative(initiativeId) {
|
|
426
|
+
return validateWorkspaceNameForSetup(initiativeId);
|
|
427
|
+
}
|
|
428
|
+
async function readExistingManagedWorkspaceView(workspaceName) {
|
|
429
|
+
const workspaceRoot = getManagedWorkspaceRoot(workspaceName);
|
|
430
|
+
if (!(await directoryExists(workspaceRoot))) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
if (!(await isWorkspaceRoot(workspaceRoot))) {
|
|
434
|
+
throw new WorkspaceCliError(`Workspace name '${workspaceName}' collides with a non-workspace directory at ${workspaceRoot}.`, 'workspace_name_collision', {
|
|
435
|
+
target: 'workspace.name',
|
|
436
|
+
fix: 'Choose an explicit unused workspace name.',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
root: workspaceRoot,
|
|
441
|
+
state: await readWorkspaceViewState(workspaceRoot),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function selectedWorkspaceFromManagedView(root, state) {
|
|
445
|
+
return {
|
|
446
|
+
name: state.name,
|
|
447
|
+
root,
|
|
448
|
+
status: [],
|
|
449
|
+
unregisteredCurrentWorkspace: false,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
export async function selectOrCreateWorkspaceForInitiativeOpen(input) {
|
|
453
|
+
if (input.workspaceName) {
|
|
454
|
+
const workspaceName = validateWorkspaceNameForSetup(input.workspaceName);
|
|
455
|
+
const existing = await readExistingManagedWorkspaceView(workspaceName);
|
|
456
|
+
if (!existing) {
|
|
457
|
+
const links = input.linksForNewWorkspace ? await input.linksForNewWorkspace() : {};
|
|
458
|
+
const workspace = await createManagedWorkspace(workspaceName, links, input.preferredOpener, input.context);
|
|
459
|
+
return {
|
|
460
|
+
selected: {
|
|
461
|
+
name: workspace.name,
|
|
462
|
+
root: workspace.root,
|
|
463
|
+
status: [],
|
|
464
|
+
unregisteredCurrentWorkspace: false,
|
|
465
|
+
},
|
|
466
|
+
created: true,
|
|
467
|
+
state: await readWorkspaceViewState(workspace.root),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (sameWorkspaceContext(existing.state.context, input.context)) {
|
|
471
|
+
return {
|
|
472
|
+
selected: selectedWorkspaceFromManagedView(existing.root, existing.state),
|
|
473
|
+
created: false,
|
|
474
|
+
state: existing.state,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (!existing.state.context) {
|
|
478
|
+
throw new WorkspaceCliError(`Workspace '${workspaceName}' is not bound to an initiative.`, 'workspace_context_bind_required', {
|
|
479
|
+
target: 'workspace.context',
|
|
480
|
+
fix: 'Choose a new workspace name for this initiative or use a future workspace rebind/update surface.',
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
throw new WorkspaceCliError(`Workspace '${workspaceName}' is already bound to ${formatWorkspaceContext(existing.state.context)}.`, 'workspace_context_conflict', {
|
|
484
|
+
target: 'workspace.context',
|
|
485
|
+
fix: 'Choose a different workspace name or open the initiative already bound to this workspace.',
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const matches = [];
|
|
489
|
+
for (const entry of await listKnownWorkspaceEntries()) {
|
|
490
|
+
try {
|
|
491
|
+
const state = await readWorkspaceViewState(entry.workspaceRoot);
|
|
492
|
+
if (sameWorkspaceContext(state.context, input.context)) {
|
|
493
|
+
matches.push({ root: entry.workspaceRoot, state });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// Broken workspaces are surfaced by list/doctor; initiative open should not
|
|
498
|
+
// guess through unreadable local view records.
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (matches.length === 1) {
|
|
502
|
+
const [match] = matches;
|
|
503
|
+
return {
|
|
504
|
+
selected: selectedWorkspaceFromManagedView(match.root, match.state),
|
|
505
|
+
created: false,
|
|
506
|
+
state: match.state,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
if (matches.length > 1) {
|
|
510
|
+
const names = matches.map((match) => match.state.name).sort((a, b) => a.localeCompare(b));
|
|
511
|
+
throw new WorkspaceCliError(`Multiple workspaces are already bound to ${formatWorkspaceContext(input.context)}: ${names.join(', ')}.`, 'workspace_initiative_selection_ambiguous', {
|
|
512
|
+
target: 'workspace.name',
|
|
513
|
+
fix: 'Retry with an explicit workspace name.',
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const derivedName = deriveWorkspaceNameForInitiative(getWorkspaceContextInitiativeId(input.context));
|
|
517
|
+
const existingDerived = await readExistingManagedWorkspaceView(derivedName);
|
|
518
|
+
if (existingDerived) {
|
|
519
|
+
if (sameWorkspaceContext(existingDerived.state.context, input.context)) {
|
|
520
|
+
return {
|
|
521
|
+
selected: selectedWorkspaceFromManagedView(existingDerived.root, existingDerived.state),
|
|
522
|
+
created: false,
|
|
523
|
+
state: existingDerived.state,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
throw new WorkspaceCliError(`Default workspace name '${derivedName}' is already used by a workspace with ${formatWorkspaceContext(existingDerived.state.context)}.`, 'workspace_name_collision', {
|
|
527
|
+
target: 'workspace.name',
|
|
528
|
+
fix: `Retry with an explicit workspace name: openspec workspace open <name> --initiative ${getWorkspaceContextStoreId(input.context)}/${getWorkspaceContextInitiativeId(input.context)}`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const workspace = await createManagedWorkspace(derivedName, input.linksForNewWorkspace ? await input.linksForNewWorkspace() : {}, input.preferredOpener, input.context);
|
|
532
|
+
return {
|
|
533
|
+
selected: {
|
|
534
|
+
name: workspace.name,
|
|
535
|
+
root: workspace.root,
|
|
536
|
+
status: [],
|
|
537
|
+
unregisteredCurrentWorkspace: false,
|
|
538
|
+
},
|
|
539
|
+
created: true,
|
|
540
|
+
state: await readWorkspaceViewState(workspace.root),
|
|
541
|
+
};
|
|
474
542
|
}
|
|
475
543
|
//# sourceMappingURL=operations.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const workspacePromptTheme: {
|
|
2
|
+
prefix: string;
|
|
3
|
+
style: {
|
|
4
|
+
answer: (text: string) => string;
|
|
5
|
+
defaultAnswer: (text: string) => string;
|
|
6
|
+
error: (text: string) => string;
|
|
7
|
+
help: (text: string) => string;
|
|
8
|
+
highlight: (text: string) => string;
|
|
9
|
+
key: (text: string) => string;
|
|
10
|
+
message: (text: string) => string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare const workspaceSelectTheme: {
|
|
14
|
+
icon: {
|
|
15
|
+
cursor: string;
|
|
16
|
+
};
|
|
17
|
+
style: {
|
|
18
|
+
keysHelpTip: (keys: [key: string, action: string][]) => string;
|
|
19
|
+
answer: (text: string) => string;
|
|
20
|
+
defaultAnswer: (text: string) => string;
|
|
21
|
+
error: (text: string) => string;
|
|
22
|
+
help: (text: string) => string;
|
|
23
|
+
highlight: (text: string) => string;
|
|
24
|
+
key: (text: string) => string;
|
|
25
|
+
message: (text: string) => string;
|
|
26
|
+
};
|
|
27
|
+
prefix: string;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=prompt-theme.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export const workspacePromptTheme = {
|
|
3
|
+
prefix: '',
|
|
4
|
+
style: {
|
|
5
|
+
answer: (text) => chalk.cyan(text),
|
|
6
|
+
defaultAnswer: (text) => chalk.dim(text),
|
|
7
|
+
error: (text) => chalk.red(text),
|
|
8
|
+
help: (text) => chalk.dim(text),
|
|
9
|
+
highlight: (text) => chalk.cyan(text),
|
|
10
|
+
key: (text) => chalk.cyan(text),
|
|
11
|
+
message: (text) => chalk.bold(text),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export const workspaceSelectTheme = {
|
|
15
|
+
...workspacePromptTheme,
|
|
16
|
+
icon: {
|
|
17
|
+
cursor: chalk.cyan('>'),
|
|
18
|
+
},
|
|
19
|
+
style: {
|
|
20
|
+
...workspacePromptTheme.style,
|
|
21
|
+
keysHelpTip: (keys) => chalk.dim(keys.map(([key, action]) => `${key}: ${action}`).join(' | ')),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=prompt-theme.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { WorkspaceLinkOptions, WorkspaceListOptions, WorkspaceOpenOptions, WorkspaceSetupOptions, WorkspaceUpdateOptions } from './types.js';
|
|
3
|
+
export interface WorkspaceCommandActions {
|
|
4
|
+
setup(options: WorkspaceSetupOptions): Promise<void>;
|
|
5
|
+
list(options: WorkspaceListOptions): Promise<void>;
|
|
6
|
+
link(nameOrPath: string | undefined, linkPath: string | undefined, options: WorkspaceLinkOptions): Promise<void>;
|
|
7
|
+
relink(linkNameInput: string | undefined, linkPath: string | undefined, options: WorkspaceLinkOptions): Promise<void>;
|
|
8
|
+
doctor(options: WorkspaceLinkOptions): Promise<void>;
|
|
9
|
+
update(positionalName: string | undefined, options: WorkspaceUpdateOptions): Promise<void>;
|
|
10
|
+
open(positionalName: string | undefined, options: WorkspaceOpenOptions): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerWorkspaceCommandWith(program: Command, workspaceCommand: WorkspaceCommandActions): void;
|
|
13
|
+
//# sourceMappingURL=registration.d.ts.map
|