@fission-ai/openspec 1.4.1 → 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.
Files changed (206) hide show
  1. package/README.md +24 -3
  2. package/dist/cli/index.d.ts +5 -0
  3. package/dist/cli/index.js +95 -95
  4. package/dist/commands/change.d.ts +5 -1
  5. package/dist/commands/change.js +17 -15
  6. package/dist/commands/config.js +6 -65
  7. package/dist/commands/context.d.ts +3 -0
  8. package/dist/commands/context.js +155 -0
  9. package/dist/commands/doctor.d.ts +8 -0
  10. package/dist/commands/doctor.js +163 -0
  11. package/dist/commands/shared-gather.d.ts +14 -0
  12. package/dist/commands/shared-gather.js +31 -0
  13. package/dist/commands/shared-output.d.ts +18 -0
  14. package/dist/commands/shared-output.js +61 -0
  15. package/dist/commands/show.d.ts +11 -6
  16. package/dist/commands/show.js +70 -25
  17. package/dist/commands/spec.d.ts +5 -1
  18. package/dist/commands/spec.js +15 -4
  19. package/dist/commands/store.d.ts +3 -0
  20. package/dist/commands/store.js +547 -0
  21. package/dist/commands/validate.d.ts +2 -0
  22. package/dist/commands/validate.js +75 -39
  23. package/dist/commands/workflow/index.d.ts +0 -2
  24. package/dist/commands/workflow/index.js +0 -1
  25. package/dist/commands/workflow/instructions.d.ts +11 -1
  26. package/dist/commands/workflow/instructions.js +71 -22
  27. package/dist/commands/workflow/new-change.d.ts +6 -3
  28. package/dist/commands/workflow/new-change.js +42 -77
  29. package/dist/commands/workflow/shared.d.ts +15 -3
  30. package/dist/commands/workflow/shared.js +20 -3
  31. package/dist/commands/workflow/status.d.ts +2 -0
  32. package/dist/commands/workflow/status.js +19 -17
  33. package/dist/commands/workset-input.d.ts +19 -0
  34. package/dist/commands/workset-input.js +112 -0
  35. package/dist/commands/workset-prompts.d.ts +12 -0
  36. package/dist/commands/workset-prompts.js +143 -0
  37. package/dist/commands/workset.d.ts +25 -0
  38. package/dist/commands/workset.js +446 -0
  39. package/dist/core/archive.d.ts +17 -6
  40. package/dist/core/archive.js +209 -56
  41. package/dist/core/artifact-graph/index.d.ts +1 -1
  42. package/dist/core/artifact-graph/instruction-loader.d.ts +18 -13
  43. package/dist/core/artifact-graph/instruction-loader.js +8 -31
  44. package/dist/core/change-metadata/schema.d.ts +1 -0
  45. package/dist/core/change-metadata/schema.js +4 -2
  46. package/dist/core/change-status-policy.d.ts +5 -18
  47. package/dist/core/change-status-policy.js +2 -37
  48. package/dist/core/command-generation/adapters/bob.js +1 -14
  49. package/dist/core/command-generation/adapters/claude.js +1 -14
  50. package/dist/core/command-generation/adapters/cursor.js +1 -14
  51. package/dist/core/command-generation/adapters/pi.js +1 -14
  52. package/dist/core/command-generation/adapters/windsurf.js +1 -14
  53. package/dist/core/command-generation/yaml.d.ts +22 -0
  54. package/dist/core/command-generation/yaml.js +38 -0
  55. package/dist/core/completions/command-registry.js +87 -289
  56. package/dist/core/completions/generators/zsh-generator.js +3 -1
  57. package/dist/core/completions/installers/bash-installer.js +3 -0
  58. package/dist/core/completions/installers/fish-installer.js +8 -0
  59. package/dist/core/completions/installers/powershell-installer.js +30 -2
  60. package/dist/core/completions/installers/zsh-installer.js +3 -0
  61. package/dist/core/completions/shared-flags.d.ts +1 -0
  62. package/dist/core/completions/shared-flags.js +5 -0
  63. package/dist/core/config-schema.d.ts +2 -1
  64. package/dist/core/config-schema.js +26 -0
  65. package/dist/core/file-state.d.ts +36 -0
  66. package/dist/core/file-state.js +112 -0
  67. package/dist/core/global-config.d.ts +2 -0
  68. package/dist/core/id.d.ts +17 -0
  69. package/dist/core/id.js +30 -0
  70. package/dist/core/index.d.ts +3 -3
  71. package/dist/core/index.js +3 -3
  72. package/dist/core/init.js +24 -4
  73. package/dist/core/list.d.ts +2 -0
  74. package/dist/core/list.js +19 -5
  75. package/dist/core/openers.d.ts +77 -0
  76. package/dist/core/openers.js +251 -0
  77. package/dist/core/openspec-root.d.ts +45 -0
  78. package/dist/core/openspec-root.js +192 -0
  79. package/dist/core/planning-home.d.ts +1 -6
  80. package/dist/core/planning-home.js +2 -43
  81. package/dist/core/project-config.d.ts +44 -1
  82. package/dist/core/project-config.js +160 -10
  83. package/dist/core/references.d.ts +63 -0
  84. package/dist/core/references.js +310 -0
  85. package/dist/core/relationship-health.d.ts +65 -0
  86. package/dist/core/relationship-health.js +64 -0
  87. package/dist/core/root-selection.d.ts +122 -0
  88. package/dist/core/root-selection.js +337 -0
  89. package/dist/core/specs-apply.d.ts +6 -1
  90. package/dist/core/specs-apply.js +6 -4
  91. package/dist/core/store/errors.d.ts +20 -0
  92. package/dist/core/{context-store → store}/errors.js +3 -3
  93. package/dist/core/store/foundation.d.ts +56 -0
  94. package/dist/core/store/foundation.js +251 -0
  95. package/dist/core/store/git.d.ts +23 -0
  96. package/dist/core/store/git.js +137 -0
  97. package/dist/core/{context-store → store}/index.d.ts +0 -1
  98. package/dist/core/{context-store → store}/index.js +0 -1
  99. package/dist/core/store/operations.d.ts +114 -0
  100. package/dist/core/store/operations.js +783 -0
  101. package/dist/core/store/registry.d.ts +58 -0
  102. package/dist/core/store/registry.js +275 -0
  103. package/dist/core/templates/workflows/apply-change.js +5 -4
  104. package/dist/core/templates/workflows/archive-change.js +5 -4
  105. package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
  106. package/dist/core/templates/workflows/continue-change.js +7 -2
  107. package/dist/core/templates/workflows/explore.js +5 -0
  108. package/dist/core/templates/workflows/ff-change.js +5 -0
  109. package/dist/core/templates/workflows/new-change.js +5 -0
  110. package/dist/core/templates/workflows/onboard.js +4 -1
  111. package/dist/core/templates/workflows/propose.js +5 -0
  112. package/dist/core/templates/workflows/store-selection.d.ts +8 -0
  113. package/dist/core/templates/workflows/store-selection.js +8 -0
  114. package/dist/core/templates/workflows/sync-specs.js +5 -4
  115. package/dist/core/templates/workflows/verify-change.js +5 -4
  116. package/dist/core/working-set.d.ts +47 -0
  117. package/dist/core/working-set.js +43 -0
  118. package/dist/core/worksets.d.ts +75 -0
  119. package/dist/core/worksets.js +245 -0
  120. package/dist/core/zod-issues.d.ts +4 -0
  121. package/dist/core/zod-issues.js +10 -0
  122. package/dist/utils/change-metadata.d.ts +1 -0
  123. package/dist/utils/change-metadata.js +1 -1
  124. package/dist/utils/change-utils.js +15 -0
  125. package/dist/utils/file-system.js +36 -17
  126. package/package.json +6 -6
  127. package/dist/commands/context-store.d.ts +0 -3
  128. package/dist/commands/context-store.js +0 -475
  129. package/dist/commands/initiative.d.ts +0 -13
  130. package/dist/commands/initiative.js +0 -318
  131. package/dist/commands/workflow/initiative-link.d.ts +0 -24
  132. package/dist/commands/workflow/initiative-link.js +0 -47
  133. package/dist/commands/workflow/set-change.d.ts +0 -13
  134. package/dist/commands/workflow/set-change.js +0 -87
  135. package/dist/commands/workspace/context-status.d.ts +0 -4
  136. package/dist/commands/workspace/context-status.js +0 -59
  137. package/dist/commands/workspace/open-target-selection.d.ts +0 -13
  138. package/dist/commands/workspace/open-target-selection.js +0 -146
  139. package/dist/commands/workspace/open-view.d.ts +0 -62
  140. package/dist/commands/workspace/open-view.js +0 -249
  141. package/dist/commands/workspace/open.d.ts +0 -37
  142. package/dist/commands/workspace/open.js +0 -110
  143. package/dist/commands/workspace/opener-selection.d.ts +0 -11
  144. package/dist/commands/workspace/opener-selection.js +0 -98
  145. package/dist/commands/workspace/operations.d.ts +0 -29
  146. package/dist/commands/workspace/operations.js +0 -543
  147. package/dist/commands/workspace/prompt-theme.d.ts +0 -29
  148. package/dist/commands/workspace/prompt-theme.js +0 -24
  149. package/dist/commands/workspace/registration.d.ts +0 -13
  150. package/dist/commands/workspace/registration.js +0 -84
  151. package/dist/commands/workspace/selection.d.ts +0 -8
  152. package/dist/commands/workspace/selection.js +0 -104
  153. package/dist/commands/workspace/setup-prompts.d.ts +0 -13
  154. package/dist/commands/workspace/setup-prompts.js +0 -121
  155. package/dist/commands/workspace/types.d.ts +0 -103
  156. package/dist/commands/workspace/types.js +0 -36
  157. package/dist/commands/workspace.d.ts +0 -5
  158. package/dist/commands/workspace.js +0 -574
  159. package/dist/core/collections/index.d.ts +0 -3
  160. package/dist/core/collections/index.js +0 -3
  161. package/dist/core/collections/initiatives/collection.d.ts +0 -4
  162. package/dist/core/collections/initiatives/collection.js +0 -17
  163. package/dist/core/collections/initiatives/index.d.ts +0 -6
  164. package/dist/core/collections/initiatives/index.js +0 -6
  165. package/dist/core/collections/initiatives/operations.d.ts +0 -49
  166. package/dist/core/collections/initiatives/operations.js +0 -175
  167. package/dist/core/collections/initiatives/resolution.d.ts +0 -87
  168. package/dist/core/collections/initiatives/resolution.js +0 -374
  169. package/dist/core/collections/initiatives/schema.d.ts +0 -41
  170. package/dist/core/collections/initiatives/schema.js +0 -134
  171. package/dist/core/collections/initiatives/templates.d.ts +0 -12
  172. package/dist/core/collections/initiatives/templates.js +0 -90
  173. package/dist/core/collections/runtime.d.ts +0 -46
  174. package/dist/core/collections/runtime.js +0 -194
  175. package/dist/core/context-store/binding.d.ts +0 -53
  176. package/dist/core/context-store/binding.js +0 -197
  177. package/dist/core/context-store/errors.d.ts +0 -20
  178. package/dist/core/context-store/foundation.d.ts +0 -55
  179. package/dist/core/context-store/foundation.js +0 -321
  180. package/dist/core/context-store/operations.d.ts +0 -85
  181. package/dist/core/context-store/operations.js +0 -528
  182. package/dist/core/context-store/registry.d.ts +0 -45
  183. package/dist/core/context-store/registry.js +0 -229
  184. package/dist/core/workspace/foundation.d.ts +0 -67
  185. package/dist/core/workspace/foundation.js +0 -295
  186. package/dist/core/workspace/index.d.ts +0 -8
  187. package/dist/core/workspace/index.js +0 -8
  188. package/dist/core/workspace/legacy-state.d.ts +0 -28
  189. package/dist/core/workspace/legacy-state.js +0 -200
  190. package/dist/core/workspace/link-input.d.ts +0 -9
  191. package/dist/core/workspace/link-input.js +0 -32
  192. package/dist/core/workspace/open-surface.d.ts +0 -45
  193. package/dist/core/workspace/open-surface.js +0 -215
  194. package/dist/core/workspace/openers.d.ts +0 -21
  195. package/dist/core/workspace/openers.js +0 -124
  196. package/dist/core/workspace/registry.d.ts +0 -24
  197. package/dist/core/workspace/registry.js +0 -146
  198. package/dist/core/workspace/skills.d.ts +0 -57
  199. package/dist/core/workspace/skills.js +0 -334
  200. package/dist/core/workspace/state-io.d.ts +0 -10
  201. package/dist/core/workspace/state-io.js +0 -121
  202. package/schemas/workspace-planning/schema.yaml +0 -72
  203. package/schemas/workspace-planning/templates/design.md +0 -33
  204. package/schemas/workspace-planning/templates/proposal.md +0 -28
  205. package/schemas/workspace-planning/templates/spec.md +0 -9
  206. package/schemas/workspace-planning/templates/tasks.md +0 -15
@@ -1,98 +0,0 @@
1
- import { getDefaultWorkspaceOpenerChoiceValue, getWorkspaceSkillToolIds, listWorkspaceOpenerChoices, parseWorkspacePreferredOpenerValue, } from '../../core/workspace/index.js';
2
- import { isInteractive, resolveNoInteractive } from '../../utils/interactive.js';
3
- import { WorkspaceCliError, asErrorMessage } from './types.js';
4
- import { workspaceSelectTheme } from './prompt-theme.js';
5
- function formatOpenerChoiceName(choice) {
6
- return choice.unavailableNote ? `${choice.label} (${choice.unavailableNote})` : choice.label;
7
- }
8
- export async function promptPreferredOpener(message, openerChoices = listWorkspaceOpenerChoices()) {
9
- const { select } = await import('@inquirer/prompts');
10
- const selectedValue = await select({
11
- message,
12
- default: getDefaultWorkspaceOpenerChoiceValue(openerChoices),
13
- choices: openerChoices.map((choice) => ({
14
- name: formatOpenerChoiceName(choice),
15
- short: choice.label,
16
- value: choice.value,
17
- description: choice.unavailableNote ?? `Use ${choice.label}`,
18
- })),
19
- theme: workspaceSelectTheme,
20
- });
21
- return parseWorkspacePreferredOpenerValue(selectedValue);
22
- }
23
- export function parseSetupOpenerOption(opener) {
24
- if (!opener) {
25
- return undefined;
26
- }
27
- try {
28
- return parseWorkspacePreferredOpenerValue(opener);
29
- }
30
- catch (error) {
31
- throw new WorkspaceCliError(asErrorMessage(error), 'unsupported_workspace_opener', {
32
- target: 'workspace.opener',
33
- fix: 'Use --opener codex-cli, --opener claude, --opener github-copilot, or --opener editor.',
34
- });
35
- }
36
- }
37
- export function parseWorkspaceAgentOverride(agent) {
38
- let opener = null;
39
- try {
40
- opener = parseWorkspacePreferredOpenerValue(agent);
41
- }
42
- catch {
43
- opener = null;
44
- }
45
- if (!opener || opener.kind !== 'agent') {
46
- throw new WorkspaceCliError(`Unsupported workspace agent '${agent}'. Supported agents: codex-cli, claude, github-copilot.`, 'unsupported_workspace_agent', {
47
- target: 'workspace.opener',
48
- fix: 'Use --agent codex-cli, --agent claude, or --agent github-copilot.',
49
- });
50
- }
51
- return opener;
52
- }
53
- export function getPreferredWorkspaceSkillAgentId(preferredOpener) {
54
- if (!preferredOpener || preferredOpener.kind !== 'agent') {
55
- return null;
56
- }
57
- const toolId = preferredOpener.id === 'codex-cli' ? 'codex' : preferredOpener.id;
58
- return getWorkspaceSkillToolIds().includes(toolId) ? toolId : null;
59
- }
60
- export function resolveWorkspaceOpenOpenerOverride(options) {
61
- if (options.agent && options.editor) {
62
- throw new WorkspaceCliError('workspace open accepts either --agent <tool> or --editor, not both.', 'workspace_opener_conflict', {
63
- target: 'workspace.opener',
64
- fix: 'Choose one opener override.',
65
- });
66
- }
67
- if (options.agent) {
68
- return parseWorkspaceAgentOverride(options.agent);
69
- }
70
- if (options.editor) {
71
- return parseWorkspacePreferredOpenerValue('editor');
72
- }
73
- return undefined;
74
- }
75
- export async function resolveWorkspaceOpenOpener(localState, options) {
76
- const override = resolveWorkspaceOpenOpenerOverride(options);
77
- if (override) {
78
- return override;
79
- }
80
- if (localState.preferred_opener) {
81
- return localState.preferred_opener;
82
- }
83
- if (!resolveNoInteractive(options) && isInteractive(options)) {
84
- const openerChoices = listWorkspaceOpenerChoices().filter((choice) => choice.available);
85
- if (openerChoices.length === 0) {
86
- throw new WorkspaceCliError('No supported workspace opener is available on PATH.', 'workspace_no_available_openers', {
87
- target: 'workspace.opener',
88
- fix: "Install VS Code ('code'), codex-cli ('codex'), or Claude ('claude'), then retry.",
89
- });
90
- }
91
- return promptPreferredOpener('Open with:', openerChoices);
92
- }
93
- throw new WorkspaceCliError('This workspace does not have a preferred opener yet.', 'workspace_opener_unset', {
94
- target: 'workspace.opener',
95
- fix: 'Pass --agent <tool> or --editor, or run workspace setup interactively to choose a default opener.',
96
- });
97
- }
98
- //# sourceMappingURL=opener-selection.js.map
@@ -1,29 +0,0 @@
1
- import { WorkspacePreferredOpener, WorkspaceRegistryEntry, WorkspaceContextState, WorkspaceViewState } from '../../core/workspace/index.js';
2
- import { SelectedWorkspace, WorkspaceLinkMutationPayload, WorkspaceListOutput, WorkspaceOutput, WorkspaceStatus } from './types.js';
3
- export declare function directoryExists(dirPath: string): Promise<boolean>;
4
- export declare function resolveExistingDirectory(inputPath: string, cwd?: string): Promise<string>;
5
- export declare function inferLinkName(absolutePath: string): string;
6
- export declare function validateWorkspaceNameForSetup(name: string): string;
7
- export declare function validateLinkNameForCommand(name: string): string;
8
- export declare function createManagedWorkspace(name: string, links: Record<string, string>, preferredOpener?: WorkspacePreferredOpener, context?: WorkspaceContextState | null, tools?: string[]): Promise<WorkspaceOutput>;
9
- export declare function parseSetupLinks(linkInputs: string[] | undefined): Promise<Record<string, string>>;
10
- export declare function loadWorkspaceForList(entry: WorkspaceRegistryEntry): Promise<WorkspaceListOutput>;
11
- export declare function loadWorkspaceForDoctor(selected: SelectedWorkspace): Promise<{
12
- workspace: WorkspaceOutput;
13
- status: WorkspaceStatus[];
14
- }>;
15
- export declare function readWorkspaceForMutation(selected: SelectedWorkspace): Promise<WorkspaceViewState>;
16
- export declare function addWorkspaceLink(selected: SelectedWorkspace, nameOrPath: string, linkPath?: string): Promise<WorkspaceLinkMutationPayload>;
17
- export declare function updateWorkspaceLink(selected: SelectedWorkspace, linkNameInput: string, linkPath: string): Promise<WorkspaceLinkMutationPayload>;
18
- export declare function deriveWorkspaceNameForInitiative(initiativeId: string): string;
19
- export declare function selectOrCreateWorkspaceForInitiativeOpen(input: {
20
- workspaceName?: string;
21
- context: WorkspaceContextState;
22
- preferredOpener?: WorkspacePreferredOpener;
23
- linksForNewWorkspace?: () => Promise<Record<string, string>>;
24
- }): Promise<{
25
- selected: SelectedWorkspace;
26
- created: boolean;
27
- state: WorkspaceViewState;
28
- }>;
29
- //# sourceMappingURL=operations.d.ts.map
@@ -1,543 +0,0 @@
1
- import * as nodeFs from 'node:fs';
2
- import * as path from 'node:path';
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';
5
- import { FileSystemUtils } from '../../utils/file-system.js';
6
- import { WorkspaceCliError, asErrorMessage, makeStatus, } from './types.js';
7
- import { collectWorkspaceContextStatuses } from './context-status.js';
8
- const fs = nodeFs.promises;
9
- export async function directoryExists(dirPath) {
10
- try {
11
- return (await fs.stat(dirPath)).isDirectory();
12
- }
13
- catch {
14
- return false;
15
- }
16
- }
17
- function normalizeExistingPathForStorage(existingPath) {
18
- return FileSystemUtils.canonicalizeExistingPath(existingPath);
19
- }
20
- export async function resolveExistingDirectory(inputPath, cwd = process.cwd()) {
21
- if (inputPath.length === 0) {
22
- throw new WorkspaceCliError('Repo or folder path must not be empty.', 'linked_path_empty', {
23
- target: 'link.path',
24
- fix: 'Choose an existing repo or folder path.',
25
- });
26
- }
27
- const resolvedPath = path.isAbsolute(inputPath)
28
- ? path.resolve(inputPath)
29
- : path.resolve(cwd, inputPath);
30
- if (!(await directoryExists(resolvedPath))) {
31
- throw new WorkspaceCliError(`Path '${inputPath}' is not an existing folder.`, 'linked_path_missing', {
32
- target: 'link.path',
33
- fix: 'Choose an existing repo or folder path.',
34
- });
35
- }
36
- return normalizeExistingPathForStorage(resolvedPath);
37
- }
38
- export function inferLinkName(absolutePath) {
39
- return path.basename(absolutePath);
40
- }
41
- function normalizeLinksForOutput(viewState) {
42
- return Object.keys(viewState.links)
43
- .sort((a, b) => a.localeCompare(b))
44
- .map((name) => ({
45
- name,
46
- path: viewState.links[name] ?? null,
47
- status: [],
48
- }));
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
- }
60
- function formatDuplicateLinkMessage(linkName, existingPath, replacementPath) {
61
- return [
62
- `Cannot use link name '${linkName}' because another link already uses that name.`,
63
- 'Existing link:',
64
- ` ${linkName} -> ${existingPath ?? '(no local path recorded)'}`,
65
- '',
66
- 'Choose a different link name:',
67
- ` openspec workspace link archived-${linkName} ${replacementPath}`,
68
- '',
69
- 'If you meant to change the existing link path:',
70
- ` openspec workspace relink ${linkName} ${replacementPath}`,
71
- ].join('\n');
72
- }
73
- function duplicateLinkError(linkName, existingPath, replacementPath) {
74
- return new WorkspaceCliError(formatDuplicateLinkMessage(linkName, existingPath, replacementPath), 'duplicate_link_name', {
75
- target: `links.${linkName}`,
76
- fix: `Choose a different link name or run 'openspec workspace relink ${linkName} ${replacementPath}'.`,
77
- });
78
- }
79
- function hasWorkspaceLink(links, linkName) {
80
- return Object.prototype.hasOwnProperty.call(links, linkName);
81
- }
82
- function duplicateSetupLinkError(linkName, existingPath, replacementPath) {
83
- return new WorkspaceCliError([
84
- `Cannot use link name '${linkName}' because another setup link already uses that name.`,
85
- 'Existing link:',
86
- ` ${linkName} -> ${existingPath}`,
87
- '',
88
- 'Use explicit --link <name>=<path> values with different names.',
89
- ].join('\n'), 'duplicate_link_name', {
90
- target: `links.${linkName}`,
91
- fix: `Use explicit --link ${linkName}-alt=${replacementPath} with a different link name.`,
92
- });
93
- }
94
- export function validateWorkspaceNameForSetup(name) {
95
- try {
96
- return validateWorkspaceName(name);
97
- }
98
- catch {
99
- throw new WorkspaceCliError('Workspace name must be kebab-case with lowercase letters, numbers, and single hyphen separators.', 'invalid_workspace_name', {
100
- target: 'workspace.name',
101
- });
102
- }
103
- }
104
- export function validateLinkNameForCommand(name) {
105
- try {
106
- return validateWorkspaceLinkName(name);
107
- }
108
- catch (error) {
109
- throw new WorkspaceCliError(asErrorMessage(error), 'invalid_link_name', {
110
- target: 'link.name',
111
- });
112
- }
113
- }
114
- function localStateInvalidStatus(error) {
115
- return makeStatus('error', 'workspace_local_state_invalid', `Machine-local paths could not be read: ${asErrorMessage(error)}`, {
116
- target: 'workspace.local_state',
117
- fix: 'Repair .openspec-workspace/view.yaml, then run openspec workspace relink <name> <path> for affected links.',
118
- });
119
- }
120
- function workspaceSkillDriftStatus(workspaceName) {
121
- return makeStatus('warning', 'workspace_skills_out_of_sync', 'Workspace-local agent skills are out of sync with the active global profile.', {
122
- target: 'workspace.skills',
123
- fix: `openspec workspace update --workspace ${workspaceName}`,
124
- });
125
- }
126
- function appendWorkspaceSkillDriftStatus(statuses, workspaceName, viewState) {
127
- if (hasWorkspaceSkillProfileDrift(viewState)) {
128
- statuses.push(workspaceSkillDriftStatus(workspaceName));
129
- }
130
- }
131
- export async function createManagedWorkspace(name, links, preferredOpener, context = null, tools) {
132
- const workspaceName = validateWorkspaceNameForSetup(name);
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', {
137
- target: 'workspace.name',
138
- });
139
- }
140
- let createdWorkspaceRoot = false;
141
- try {
142
- await FileSystemUtils.createDirectory(path.dirname(targetWorkspaceRoot));
143
- await fs.mkdir(targetWorkspaceRoot);
144
- createdWorkspaceRoot = true;
145
- workspaceRoot = FileSystemUtils.canonicalizeExistingPath(targetWorkspaceRoot);
146
- const viewState = {
147
- version: 1,
148
- name: workspaceName,
149
- context,
150
- links,
151
- ...(preferredOpener ? { preferred_opener: preferredOpener } : {}),
152
- ...(tools ? { tools } : {}),
153
- };
154
- await writeWorkspaceViewState(workspaceRoot, viewState);
155
- await syncWorkspaceOpenSurface(workspaceRoot, viewState);
156
- }
157
- catch (error) {
158
- if (createdWorkspaceRoot) {
159
- try {
160
- await fs.rm(targetWorkspaceRoot, { recursive: true, force: true });
161
- }
162
- catch {
163
- // Preserve the original creation failure; callers can retry or inspect the path.
164
- }
165
- }
166
- throw new WorkspaceCliError(`Could not create workspace '${workspaceName}': ${asErrorMessage(error)}`, 'workspace_create_failed', {
167
- target: 'workspace.root',
168
- });
169
- }
170
- return {
171
- name: workspaceName,
172
- root: workspaceRoot,
173
- planning_path: getWorkspaceChangesDir(workspaceRoot),
174
- state_path: getWorkspaceViewStatePath(workspaceRoot),
175
- context: workspaceContextToOutput(context),
176
- links: Object.entries(links)
177
- .sort(([a], [b]) => a.localeCompare(b))
178
- .map(([linkName, linkPath]) => ({
179
- name: linkName,
180
- path: linkPath,
181
- status: [],
182
- })),
183
- status: [],
184
- };
185
- }
186
- export async function parseSetupLinks(linkInputs) {
187
- const links = {};
188
- for (const rawLink of linkInputs ?? []) {
189
- const parsed = await parseWorkspaceSetupLinkInput(rawLink);
190
- const resolvedPath = await resolveExistingDirectory(parsed.pathInput);
191
- const linkName = validateLinkNameForCommand(parsed.name ?? inferLinkName(resolvedPath));
192
- if (links[linkName]) {
193
- throw duplicateSetupLinkError(linkName, links[linkName], resolvedPath);
194
- }
195
- links[linkName] = resolvedPath;
196
- }
197
- return links;
198
- }
199
- export async function loadWorkspaceForList(entry) {
200
- const workspaceStatus = [];
201
- if (!(await directoryExists(entry.workspaceRoot)) || !(await isWorkspaceRoot(entry.workspaceRoot))) {
202
- return {
203
- name: entry.name,
204
- root: entry.workspaceRoot,
205
- context: null,
206
- links: [],
207
- status: [
208
- makeStatus('error', 'workspace_root_missing', 'Workspace location does not exist.', {
209
- target: 'workspace.root',
210
- fix: 'Remove or repair the local workspace view.',
211
- }),
212
- ],
213
- };
214
- }
215
- let viewState;
216
- try {
217
- viewState = await readWorkspaceViewState(entry.workspaceRoot);
218
- }
219
- catch (error) {
220
- return {
221
- name: entry.name,
222
- root: entry.workspaceRoot,
223
- context: null,
224
- links: [],
225
- status: [
226
- makeStatus('error', 'workspace_state_invalid', `Workspace state could not be read: ${asErrorMessage(error)}`, {
227
- target: 'workspace.root',
228
- fix: 'Repair the workspace state files before using this workspace.',
229
- }),
230
- ],
231
- };
232
- }
233
- appendWorkspaceSkillDriftStatus(workspaceStatus, viewState.name, viewState);
234
- workspaceStatus.push(...(await collectWorkspaceContextStatuses(viewState.context)));
235
- return {
236
- name: viewState.name,
237
- root: entry.workspaceRoot,
238
- context: workspaceContextToOutput(viewState.context),
239
- links: normalizeLinksForOutput(viewState),
240
- status: workspaceStatus,
241
- };
242
- }
243
- export async function loadWorkspaceForDoctor(selected) {
244
- const commandStatus = [...selected.status];
245
- const workspaceStatus = [];
246
- const planningPath = getWorkspaceChangesDir(selected.root);
247
- if (!(await directoryExists(selected.root)) || !(await isWorkspaceRoot(selected.root))) {
248
- return {
249
- workspace: {
250
- name: selected.name,
251
- root: selected.root,
252
- planning_path: planningPath,
253
- state_path: getWorkspaceViewStatePath(selected.root),
254
- context: null,
255
- links: [],
256
- status: [
257
- makeStatus('error', 'selected_workspace_root_missing', 'Selected workspace location does not exist or is not a valid workspace.', {
258
- target: 'workspace.root',
259
- fix: 'Repair the local workspace view or choose another workspace.',
260
- }),
261
- ],
262
- },
263
- status: commandStatus,
264
- };
265
- }
266
- let viewState;
267
- try {
268
- viewState = await readWorkspaceViewState(selected.root);
269
- }
270
- catch (error) {
271
- return {
272
- workspace: {
273
- name: selected.name,
274
- root: selected.root,
275
- planning_path: planningPath,
276
- state_path: getWorkspaceViewStatePath(selected.root),
277
- context: null,
278
- links: [],
279
- status: [
280
- makeStatus('error', 'workspace_state_invalid', `Workspace state could not be read: ${asErrorMessage(error)}`, {
281
- target: 'workspace.root',
282
- fix: 'Repair .openspec-workspace/view.yaml before using this workspace.',
283
- }),
284
- ],
285
- },
286
- status: commandStatus,
287
- };
288
- }
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));
292
- const links = [];
293
- for (const linkName of linkNames) {
294
- const linkStatus = [];
295
- const localPath = viewState.links[linkName] ?? null;
296
- let repoSpecsPath = null;
297
- if (!localPath) {
298
- linkStatus.push(makeStatus('error', 'linked_path_missing_from_local_state', 'Shared link does not have a local path on this machine.', {
299
- target: `links.${linkName}.path`,
300
- fix: `openspec workspace relink ${linkName} /path/to/${linkName}`,
301
- }));
302
- }
303
- if (localPath) {
304
- if (await directoryExists(localPath)) {
305
- const candidateSpecsPath = path.join(localPath, 'openspec', 'specs');
306
- repoSpecsPath = (await directoryExists(candidateSpecsPath)) ? candidateSpecsPath : null;
307
- }
308
- else {
309
- linkStatus.push(makeStatus('error', 'linked_path_missing', 'Linked path does not exist.', {
310
- target: `links.${linkName}.path`,
311
- fix: `openspec workspace relink ${linkName} /path/to/${linkName}`,
312
- }));
313
- }
314
- }
315
- links.push({
316
- name: linkName,
317
- path: localPath,
318
- repo_specs_path: repoSpecsPath,
319
- status: linkStatus,
320
- });
321
- }
322
- return {
323
- workspace: {
324
- name: viewState.name,
325
- root: selected.root,
326
- planning_path: planningPath,
327
- state_path: getWorkspaceViewStatePath(selected.root),
328
- context: workspaceContextToOutput(viewState.context),
329
- links,
330
- status: workspaceStatus,
331
- },
332
- status: commandStatus,
333
- };
334
- }
335
- async function readWorkspaceViewForMutation(selected) {
336
- if (!(await directoryExists(selected.root)) || !(await isWorkspaceRoot(selected.root))) {
337
- throw new WorkspaceCliError(`Workspace location does not exist for '${selected.name}': ${selected.root}`, 'selected_workspace_root_missing', {
338
- target: 'workspace.root',
339
- fix: 'Run openspec workspace list to inspect known workspaces.',
340
- });
341
- }
342
- try {
343
- return await readWorkspaceViewState(selected.root);
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 .openspec-workspace/view.yaml before using this workspace.',
349
- });
350
- }
351
- }
352
- export async function readWorkspaceForMutation(selected) {
353
- return readWorkspaceViewForMutation(selected);
354
- }
355
- function buildLinkMutationPayload(selected, viewState, linkName, linkPath) {
356
- return {
357
- workspace: {
358
- name: viewState.name,
359
- root: selected.root,
360
- planning_path: getWorkspaceChangesDir(selected.root),
361
- state_path: getWorkspaceViewStatePath(selected.root),
362
- context: workspaceContextToOutput(viewState.context),
363
- links: normalizeLinksForOutput(viewState),
364
- status: [],
365
- },
366
- link: {
367
- name: linkName,
368
- path: linkPath,
369
- status: [],
370
- },
371
- status: selected.status,
372
- };
373
- }
374
- export async function addWorkspaceLink(selected, nameOrPath, linkPath) {
375
- const explicitName = linkPath ? nameOrPath : undefined;
376
- const pathInput = linkPath ?? nameOrPath;
377
- const resolvedPath = await resolveExistingDirectory(pathInput);
378
- const linkName = validateLinkNameForCommand(explicitName ?? inferLinkName(resolvedPath));
379
- const viewState = await readWorkspaceViewForMutation(selected);
380
- if (hasWorkspaceLink(viewState.links, linkName)) {
381
- throw duplicateLinkError(linkName, viewState.links[linkName] ?? null, resolvedPath);
382
- }
383
- const updatedViewState = {
384
- ...viewState,
385
- links: {
386
- ...viewState.links,
387
- [linkName]: resolvedPath,
388
- },
389
- };
390
- await writeWorkspaceViewState(selected.root, updatedViewState);
391
- await syncWorkspaceOpenSurface(selected.root, updatedViewState);
392
- return buildLinkMutationPayload(selected, updatedViewState, linkName, resolvedPath);
393
- }
394
- export async function updateWorkspaceLink(selected, linkNameInput, linkPath) {
395
- const linkName = validateLinkNameForCommand(linkNameInput);
396
- const resolvedPath = await resolveExistingDirectory(linkPath);
397
- const viewState = await readWorkspaceViewForMutation(selected);
398
- if (!hasWorkspaceLink(viewState.links, linkName)) {
399
- throw new WorkspaceCliError(`Unknown workspace link '${linkName}'.`, 'unknown_link_name', {
400
- target: `links.${linkName}`,
401
- fix: 'Run openspec workspace doctor to see linked repos or folders.',
402
- });
403
- }
404
- const updatedViewState = {
405
- ...viewState,
406
- links: {
407
- ...viewState.links,
408
- [linkName]: resolvedPath,
409
- },
410
- };
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
- };
542
- }
543
- //# sourceMappingURL=operations.js.map