gmc-openspec 1.1.0 → 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.
Files changed (110) hide show
  1. package/README.md +2 -2
  2. package/bin/openspec.js +3 -1
  3. package/dist/cli/index.d.ts +4 -1
  4. package/dist/cli/index.js +36 -2
  5. package/dist/commands/config.js +4 -4
  6. package/dist/commands/context-store.d.ts +3 -0
  7. package/dist/commands/context-store.js +475 -0
  8. package/dist/commands/initiative.d.ts +13 -0
  9. package/dist/commands/initiative.js +318 -0
  10. package/dist/commands/workflow/index.d.ts +2 -0
  11. package/dist/commands/workflow/index.js +1 -0
  12. package/dist/commands/workflow/initiative-link.d.ts +24 -0
  13. package/dist/commands/workflow/initiative-link.js +47 -0
  14. package/dist/commands/workflow/instructions.js +10 -2
  15. package/dist/commands/workflow/new-change.d.ts +4 -0
  16. package/dist/commands/workflow/new-change.js +72 -23
  17. package/dist/commands/workflow/set-change.d.ts +13 -0
  18. package/dist/commands/workflow/set-change.js +87 -0
  19. package/dist/commands/workflow/shared.d.ts +2 -0
  20. package/dist/commands/workflow/status.js +3 -0
  21. package/dist/commands/workspace/context-status.d.ts +4 -0
  22. package/dist/commands/workspace/context-status.js +59 -0
  23. package/dist/commands/workspace/open-target-selection.d.ts +13 -0
  24. package/dist/commands/workspace/open-target-selection.js +146 -0
  25. package/dist/commands/workspace/open-view.d.ts +62 -0
  26. package/dist/commands/workspace/open-view.js +249 -0
  27. package/dist/commands/workspace/open.d.ts +16 -8
  28. package/dist/commands/workspace/open.js +40 -14
  29. package/dist/commands/workspace/opener-selection.d.ts +11 -0
  30. package/dist/commands/workspace/opener-selection.js +98 -0
  31. package/dist/commands/workspace/operations.d.ts +14 -8
  32. package/dist/commands/workspace/operations.js +228 -160
  33. package/dist/commands/workspace/prompt-theme.d.ts +29 -0
  34. package/dist/commands/workspace/prompt-theme.js +24 -0
  35. package/dist/commands/workspace/registration.d.ts +13 -0
  36. package/dist/commands/workspace/registration.js +84 -0
  37. package/dist/commands/workspace/selection.d.ts +3 -0
  38. package/dist/commands/workspace/selection.js +42 -40
  39. package/dist/commands/workspace/setup-prompts.d.ts +13 -0
  40. package/dist/commands/workspace/setup-prompts.js +121 -0
  41. package/dist/commands/workspace/types.d.ts +15 -0
  42. package/dist/commands/workspace.js +59 -340
  43. package/dist/core/artifact-graph/index.d.ts +2 -1
  44. package/dist/core/artifact-graph/instruction-loader.d.ts +10 -23
  45. package/dist/core/artifact-graph/instruction-loader.js +28 -89
  46. package/dist/core/artifact-graph/types.d.ts +0 -7
  47. package/dist/core/artifact-graph/types.js +0 -19
  48. package/dist/core/change-metadata/index.d.ts +2 -0
  49. package/dist/core/change-metadata/index.js +2 -0
  50. package/dist/core/change-metadata/schema.d.ts +18 -0
  51. package/dist/core/change-metadata/schema.js +28 -0
  52. package/dist/core/change-status-policy.d.ts +50 -0
  53. package/dist/core/change-status-policy.js +70 -0
  54. package/dist/core/collections/index.d.ts +3 -0
  55. package/dist/core/collections/index.js +3 -0
  56. package/dist/core/collections/initiatives/collection.d.ts +4 -0
  57. package/dist/core/collections/initiatives/collection.js +17 -0
  58. package/dist/core/collections/initiatives/index.d.ts +6 -0
  59. package/dist/core/collections/initiatives/index.js +6 -0
  60. package/dist/core/collections/initiatives/operations.d.ts +49 -0
  61. package/dist/core/collections/initiatives/operations.js +175 -0
  62. package/dist/core/collections/initiatives/resolution.d.ts +87 -0
  63. package/dist/core/collections/initiatives/resolution.js +374 -0
  64. package/dist/core/collections/initiatives/schema.d.ts +41 -0
  65. package/dist/core/collections/initiatives/schema.js +134 -0
  66. package/dist/core/collections/initiatives/templates.d.ts +12 -0
  67. package/dist/core/collections/initiatives/templates.js +90 -0
  68. package/dist/core/collections/runtime.d.ts +46 -0
  69. package/dist/core/collections/runtime.js +194 -0
  70. package/dist/core/completions/command-registry.d.ts +1 -5
  71. package/dist/core/completions/command-registry.js +475 -70
  72. package/dist/core/completions/shared-flags.d.ts +12 -0
  73. package/dist/core/completions/shared-flags.js +28 -0
  74. package/dist/core/config.js +2 -1
  75. package/dist/core/context-store/binding.d.ts +53 -0
  76. package/dist/core/context-store/binding.js +197 -0
  77. package/dist/core/context-store/errors.d.ts +20 -0
  78. package/dist/core/context-store/errors.js +22 -0
  79. package/dist/core/context-store/foundation.d.ts +55 -0
  80. package/dist/core/context-store/foundation.js +321 -0
  81. package/dist/core/context-store/index.d.ts +6 -0
  82. package/dist/core/context-store/index.js +6 -0
  83. package/dist/core/context-store/operations.d.ts +85 -0
  84. package/dist/core/context-store/operations.js +528 -0
  85. package/dist/core/context-store/registry.d.ts +45 -0
  86. package/dist/core/context-store/registry.js +229 -0
  87. package/dist/core/index.d.ts +2 -0
  88. package/dist/core/index.js +2 -0
  89. package/dist/core/planning-home.js +5 -21
  90. package/dist/core/validation/validator.d.ts +11 -0
  91. package/dist/core/validation/validator.js +19 -2
  92. package/dist/core/workspace/foundation.d.ts +28 -48
  93. package/dist/core/workspace/foundation.js +130 -214
  94. package/dist/core/workspace/index.d.ts +2 -0
  95. package/dist/core/workspace/index.js +2 -0
  96. package/dist/core/workspace/legacy-state.d.ts +28 -0
  97. package/dist/core/workspace/legacy-state.js +200 -0
  98. package/dist/core/workspace/open-surface.d.ts +29 -8
  99. package/dist/core/workspace/open-surface.js +122 -44
  100. package/dist/core/workspace/openers.js +11 -6
  101. package/dist/core/workspace/registry.d.ts +24 -0
  102. package/dist/core/workspace/registry.js +146 -0
  103. package/dist/core/workspace/skills.d.ts +4 -2
  104. package/dist/core/workspace/skills.js +2 -2
  105. package/dist/core/workspace/state-io.d.ts +10 -0
  106. package/dist/core/workspace/state-io.js +119 -0
  107. package/dist/utils/change-metadata.d.ts +5 -2
  108. package/dist/utils/change-metadata.js +6 -12
  109. package/dist/utils/change-utils.d.ts +2 -2
  110. 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, isWorkspaceRoot, parseWorkspaceSetupLinkInput, readOptionalWorkspaceLocalState, readWorkspaceRegistryState, readWorkspaceSharedState, syncWorkspaceOpenSurface, validateWorkspaceLinkName, validateWorkspaceName, writeWorkspaceLocalState, writeWorkspaceRegistryState, writeWorkspaceSharedState, } from '../../core/workspace/index.js';
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 process.platform === 'win32'
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(sharedState, localState) {
62
- return Object.keys(sharedState.links)
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: localState?.paths[name] ?? null,
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 or remove .openspec-workspace/local.yaml, then run openspec workspace relink <name> <path> for affected links.',
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, localState) {
134
- if (hasWorkspaceSkillProfileDrift(localState)) {
126
+ function appendWorkspaceSkillDriftStatus(statuses, workspaceName, viewState) {
127
+ if (hasWorkspaceSkillProfileDrift(viewState)) {
135
128
  statuses.push(workspaceSkillDriftStatus(workspaceName));
136
129
  }
137
130
  }
138
- async function readLocalStateForMutation(workspaceRoot) {
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 workspaceRoot = getManagedWorkspaceRoot(workspaceName);
153
- const registry = await readRegistry();
154
- if (registry.workspaces[workspaceName]) {
155
- throw new WorkspaceCliError(`Workspace '${workspaceName}' is already recorded in the local workspace registry at ${registry.workspaces[workspaceName]}.`, 'workspace_already_exists', {
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(workspaceRoot));
167
- await fs.mkdir(workspaceRoot);
142
+ await FileSystemUtils.createDirectory(path.dirname(targetWorkspaceRoot));
143
+ await fs.mkdir(targetWorkspaceRoot);
168
144
  createdWorkspaceRoot = true;
169
- await FileSystemUtils.createDirectory(getWorkspaceChangesDir(workspaceRoot));
170
- const sharedState = {
145
+ workspaceRoot = FileSystemUtils.canonicalizeExistingPath(targetWorkspaceRoot);
146
+ const viewState = {
171
147
  version: 1,
172
148
  name: workspaceName,
173
- links: Object.fromEntries(Object.keys(links).map((linkName) => [linkName, {}])),
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 writeWorkspaceSharedState(workspaceRoot, sharedState);
181
- await writeWorkspaceLocalState(workspaceRoot, localState);
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(workspaceRoot, { recursive: true, force: true });
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 registry record.',
210
+ fix: 'Remove or repair the local workspace view.',
236
211
  }),
237
212
  ],
238
213
  };
239
214
  }
240
- let sharedState;
241
- let localState = null;
215
+ let viewState;
242
216
  try {
243
- sharedState = await readWorkspaceSharedState(entry.workspaceRoot);
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
- try {
259
- localState = await readOptionalWorkspaceLocalState(entry.workspaceRoot);
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: sharedState.name,
236
+ name: viewState.name,
267
237
  root: entry.workspaceRoot,
268
- links: normalizeLinksForOutput(sharedState, localState),
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 registry record or choose another 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 sharedState;
294
- let localState;
295
- let localStateInvalid = false;
266
+ let viewState;
296
267
  try {
297
- sharedState = await readWorkspaceSharedState(selected.root);
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
- try {
317
- const optionalLocalState = await readOptionalWorkspaceLocalState(selected.root);
318
- localState = optionalLocalState ?? emptyLocalState();
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 = localState.paths[linkName] ?? null;
295
+ const localPath = viewState.links[linkName] ?? null;
347
296
  let repoSpecsPath = null;
348
- if (!sharedNames.has(linkName)) {
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: sharedState.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
- export async function readWorkspaceForMutation(selected) {
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
- return {
398
- sharedState: await readWorkspaceSharedState(selected.root),
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, sharedState, localState, linkName, linkPath) {
355
+ function buildLinkMutationPayload(selected, viewState, linkName, linkPath) {
408
356
  return {
409
357
  workspace: {
410
- name: sharedState.name,
358
+ name: viewState.name,
411
359
  root: selected.root,
412
360
  planning_path: getWorkspaceChangesDir(selected.root),
413
- links: normalizeLinksForOutput(sharedState, localState),
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 { sharedState, localState } = await readWorkspaceForMutation(selected);
430
- if (sharedState.links[linkName]) {
431
- throw duplicateLinkError(linkName, localState.paths[linkName] ?? null, resolvedPath);
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 updatedSharedState = {
434
- ...sharedState,
383
+ const updatedViewState = {
384
+ ...viewState,
435
385
  links: {
436
- ...sharedState.links,
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 writeWorkspaceSharedState(selected.root, updatedSharedState);
448
- await writeWorkspaceLocalState(selected.root, updatedLocalState);
449
- await syncWorkspaceOpenSurface(selected.root, updatedSharedState, updatedLocalState);
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 { sharedState, localState } = await readWorkspaceForMutation(selected);
457
- if (!sharedState.links[linkName]) {
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 updatedLocalState = {
464
- ...localState,
465
- paths: {
466
- ...localState.paths,
404
+ const updatedViewState = {
405
+ ...viewState,
406
+ links: {
407
+ ...viewState.links,
467
408
  [linkName]: resolvedPath,
468
409
  },
469
410
  };
470
- await writeWorkspaceLocalState(selected.root, updatedLocalState);
471
- await syncWorkspaceOpenSurface(selected.root, sharedState, updatedLocalState);
472
- await recordSelectedWorkspaceAfterMutation(selected);
473
- return buildLinkMutationPayload(selected, sharedState, updatedLocalState, linkName, resolvedPath);
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