gmc-openspec 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +207 -0
- package/bin/openspec.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +494 -0
- package/dist/commands/change.d.ts +35 -0
- package/dist/commands/change.js +277 -0
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +264 -0
- package/dist/commands/config.d.ts +36 -0
- package/dist/commands/config.js +611 -0
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +183 -0
- package/dist/commands/jira.d.ts +3 -0
- package/dist/commands/jira.js +249 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.js +869 -0
- package/dist/commands/show.d.ts +14 -0
- package/dist/commands/show.js +132 -0
- package/dist/commands/spec.d.ts +15 -0
- package/dist/commands/spec.js +225 -0
- package/dist/commands/validate.d.ts +24 -0
- package/dist/commands/validate.js +294 -0
- package/dist/commands/workflow/index.d.ts +17 -0
- package/dist/commands/workflow/index.js +12 -0
- package/dist/commands/workflow/instructions.d.ts +29 -0
- package/dist/commands/workflow/instructions.js +336 -0
- package/dist/commands/workflow/new-change.d.ts +13 -0
- package/dist/commands/workflow/new-change.js +92 -0
- package/dist/commands/workflow/schemas.d.ts +10 -0
- package/dist/commands/workflow/schemas.js +34 -0
- package/dist/commands/workflow/shared.d.ts +57 -0
- package/dist/commands/workflow/shared.js +116 -0
- package/dist/commands/workflow/status.d.ts +14 -0
- package/dist/commands/workflow/status.js +87 -0
- package/dist/commands/workflow/templates.d.ts +16 -0
- package/dist/commands/workflow/templates.js +69 -0
- package/dist/commands/workspace/open.d.ts +29 -0
- package/dist/commands/workspace/open.js +84 -0
- package/dist/commands/workspace/operations.d.ts +23 -0
- package/dist/commands/workspace/operations.js +475 -0
- package/dist/commands/workspace/selection.d.ts +6 -0
- package/dist/commands/workspace/selection.js +113 -0
- package/dist/commands/workspace/types.d.ts +88 -0
- package/dist/commands/workspace/types.js +36 -0
- package/dist/commands/workspace.d.ts +6 -0
- package/dist/commands/workspace.js +868 -0
- package/dist/core/archive.d.ts +11 -0
- package/dist/core/archive.js +318 -0
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +8 -0
- package/dist/core/artifact-graph/index.js +14 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +196 -0
- package/dist/core/artifact-graph/instruction-loader.js +317 -0
- package/dist/core/artifact-graph/outputs.d.ts +14 -0
- package/dist/core/artifact-graph/outputs.js +39 -0
- package/dist/core/artifact-graph/resolver.d.ts +81 -0
- package/dist/core/artifact-graph/resolver.js +257 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +31 -0
- package/dist/core/artifact-graph/types.d.ts +47 -0
- package/dist/core/artifact-graph/types.js +48 -0
- package/dist/core/available-tools.d.ts +17 -0
- package/dist/core/available-tools.js +43 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
- package/dist/core/command-generation/adapters/amazon-q.js +26 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +26 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +27 -0
- package/dist/core/command-generation/adapters/bob.d.ts +14 -0
- package/dist/core/command-generation/adapters/bob.js +45 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +50 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +28 -0
- package/dist/core/command-generation/adapters/codex.d.ts +16 -0
- package/dist/core/command-generation/adapters/codex.js +39 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +28 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +27 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +44 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +27 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +26 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +26 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +29 -0
- package/dist/core/command-generation/adapters/index.d.ts +32 -0
- package/dist/core/command-generation/adapters/index.js +32 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +26 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +26 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
- package/dist/core/command-generation/adapters/opencode.js +29 -0
- package/dist/core/command-generation/adapters/pi.d.ts +18 -0
- package/dist/core/command-generation/adapters/pi.js +55 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
- package/dist/core/command-generation/adapters/qwen.js +26 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
- package/dist/core/command-generation/adapters/windsurf.js +51 -0
- package/dist/core/command-generation/generator.d.ts +21 -0
- package/dist/core/command-generation/generator.js +27 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +98 -0
- package/dist/core/command-generation/types.d.ts +56 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +626 -0
- package/dist/core/completions/completion-provider.d.ts +71 -0
- package/dist/core/completions/completion-provider.js +129 -0
- package/dist/core/completions/factory.d.ts +64 -0
- package/dist/core/completions/factory.js +75 -0
- package/dist/core/completions/generators/bash-generator.d.ts +35 -0
- package/dist/core/completions/generators/bash-generator.js +230 -0
- package/dist/core/completions/generators/fish-generator.d.ts +32 -0
- package/dist/core/completions/generators/fish-generator.js +160 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +36 -0
- package/dist/core/completions/generators/powershell-generator.js +266 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +47 -0
- package/dist/core/completions/generators/zsh-generator.js +274 -0
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +318 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +143 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +102 -0
- package/dist/core/completions/installers/powershell-installer.js +387 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +117 -0
- package/dist/core/completions/installers/zsh-installer.js +421 -0
- package/dist/core/completions/templates/bash-templates.d.ts +6 -0
- package/dist/core/completions/templates/bash-templates.js +30 -0
- package/dist/core/completions/templates/fish-templates.d.ts +7 -0
- package/dist/core/completions/templates/fish-templates.js +45 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
- package/dist/core/completions/templates/powershell-templates.js +34 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
- package/dist/core/completions/templates/zsh-templates.js +45 -0
- package/dist/core/completions/types.d.ts +101 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +34 -0
- package/dist/core/config-schema.d.ts +86 -0
- package/dist/core/config-schema.js +213 -0
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +38 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/global-config.d.ts +49 -0
- package/dist/core/global-config.js +124 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +6 -0
- package/dist/core/init.d.ts +37 -0
- package/dist/core/init.js +593 -0
- package/dist/core/jira/config.d.ts +35 -0
- package/dist/core/jira/config.js +151 -0
- package/dist/core/jira/constants.d.ts +20 -0
- package/dist/core/jira/constants.js +49 -0
- package/dist/core/jira/doctor.d.ts +19 -0
- package/dist/core/jira/doctor.js +173 -0
- package/dist/core/jira/hash.d.ts +3 -0
- package/dist/core/jira/hash.js +9 -0
- package/dist/core/jira/index.d.ts +11 -0
- package/dist/core/jira/index.js +11 -0
- package/dist/core/jira/intake.d.ts +40 -0
- package/dist/core/jira/intake.js +54 -0
- package/dist/core/jira/mcp-config.d.ts +13 -0
- package/dist/core/jira/mcp-config.js +259 -0
- package/dist/core/jira/paths.d.ts +12 -0
- package/dist/core/jira/paths.js +66 -0
- package/dist/core/jira/setup.d.ts +30 -0
- package/dist/core/jira/setup.js +99 -0
- package/dist/core/jira/templates.d.ts +12 -0
- package/dist/core/jira/templates.js +204 -0
- package/dist/core/jira/validation.d.ts +17 -0
- package/dist/core/jira/validation.js +341 -0
- package/dist/core/legacy-cleanup.d.ts +162 -0
- package/dist/core/legacy-cleanup.js +514 -0
- package/dist/core/list.d.ts +9 -0
- package/dist/core/list.js +171 -0
- package/dist/core/migration.d.ts +23 -0
- package/dist/core/migration.js +108 -0
- package/dist/core/parsers/change-parser.d.ts +13 -0
- package/dist/core/parsers/change-parser.js +197 -0
- package/dist/core/parsers/markdown-parser.d.ts +26 -0
- package/dist/core/parsers/markdown-parser.js +227 -0
- package/dist/core/parsers/requirement-blocks.d.ts +37 -0
- package/dist/core/parsers/requirement-blocks.js +201 -0
- package/dist/core/parsers/spec-structure.d.ts +9 -0
- package/dist/core/parsers/spec-structure.js +88 -0
- package/dist/core/planning-home.d.ts +21 -0
- package/dist/core/planning-home.js +124 -0
- package/dist/core/profile-sync-drift.d.ts +38 -0
- package/dist/core/profile-sync-drift.js +200 -0
- package/dist/core/profiles.d.ts +26 -0
- package/dist/core/profiles.js +40 -0
- package/dist/core/project-config.d.ts +64 -0
- package/dist/core/project-config.js +223 -0
- package/dist/core/schemas/base.schema.d.ts +13 -0
- package/dist/core/schemas/base.schema.js +13 -0
- package/dist/core/schemas/change.schema.d.ts +73 -0
- package/dist/core/schemas/change.schema.js +31 -0
- package/dist/core/schemas/index.d.ts +4 -0
- package/dist/core/schemas/index.js +4 -0
- package/dist/core/schemas/spec.schema.d.ts +18 -0
- package/dist/core/schemas/spec.schema.js +15 -0
- package/dist/core/shared/index.d.ts +8 -0
- package/dist/core/shared/index.js +8 -0
- package/dist/core/shared/skill-generation.d.ts +49 -0
- package/dist/core/shared/skill-generation.js +96 -0
- package/dist/core/shared/tool-detection.d.ts +71 -0
- package/dist/core/shared/tool-detection.js +158 -0
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +392 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/index.d.ts +8 -0
- package/dist/core/templates/index.js +9 -0
- package/dist/core/templates/skill-templates.d.ts +19 -0
- package/dist/core/templates/skill-templates.js +18 -0
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +314 -0
- package/dist/core/templates/workflows/archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/archive-change.js +277 -0
- package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/bulk-archive-change.js +492 -0
- package/dist/core/templates/workflows/continue-change.d.ts +10 -0
- package/dist/core/templates/workflows/continue-change.js +234 -0
- package/dist/core/templates/workflows/explore.d.ts +10 -0
- package/dist/core/templates/workflows/explore.js +459 -0
- package/dist/core/templates/workflows/feedback.d.ts +9 -0
- package/dist/core/templates/workflows/feedback.js +108 -0
- package/dist/core/templates/workflows/ff-change.d.ts +10 -0
- package/dist/core/templates/workflows/ff-change.js +200 -0
- package/dist/core/templates/workflows/new-change.d.ts +10 -0
- package/dist/core/templates/workflows/new-change.js +143 -0
- package/dist/core/templates/workflows/onboard.d.ts +10 -0
- package/dist/core/templates/workflows/onboard.js +563 -0
- package/dist/core/templates/workflows/propose.d.ts +10 -0
- package/dist/core/templates/workflows/propose.js +218 -0
- package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
- package/dist/core/templates/workflows/sync-specs.js +290 -0
- package/dist/core/templates/workflows/verify-change.d.ts +10 -0
- package/dist/core/templates/workflows/verify-change.js +338 -0
- package/dist/core/update.d.ts +83 -0
- package/dist/core/update.js +573 -0
- package/dist/core/validation/constants.d.ts +34 -0
- package/dist/core/validation/constants.js +40 -0
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.js +2 -0
- package/dist/core/validation/validator.d.ts +33 -0
- package/dist/core/validation/validator.js +418 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +168 -0
- package/dist/core/workspace/foundation.d.ts +87 -0
- package/dist/core/workspace/foundation.js +379 -0
- package/dist/core/workspace/index.d.ts +6 -0
- package/dist/core/workspace/index.js +6 -0
- package/dist/core/workspace/link-input.d.ts +9 -0
- package/dist/core/workspace/link-input.js +32 -0
- package/dist/core/workspace/open-surface.d.ts +24 -0
- package/dist/core/workspace/open-surface.js +137 -0
- package/dist/core/workspace/openers.d.ts +21 -0
- package/dist/core/workspace/openers.js +119 -0
- package/dist/core/workspace/skills.d.ts +55 -0
- package/dist/core/workspace/skills.js +334 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +28 -0
- package/dist/prompts/searchable-multi-select.js +159 -0
- package/dist/telemetry/config.d.ts +38 -0
- package/dist/telemetry/config.js +136 -0
- package/dist/telemetry/index.d.ts +31 -0
- package/dist/telemetry/index.js +164 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +10 -0
- package/dist/ui/welcome-screen.js +146 -0
- package/dist/utils/change-metadata.d.ts +51 -0
- package/dist/utils/change-metadata.js +147 -0
- package/dist/utils/change-utils.d.ts +71 -0
- package/dist/utils/change-utils.js +123 -0
- package/dist/utils/command-references.d.ts +18 -0
- package/dist/utils/command-references.js +20 -0
- package/dist/utils/file-system.d.ts +41 -0
- package/dist/utils/file-system.js +301 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/interactive.d.ts +18 -0
- package/dist/utils/interactive.js +21 -0
- package/dist/utils/item-discovery.d.ts +4 -0
- package/dist/utils/item-discovery.js +72 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/dist/utils/task-progress.d.ts +8 -0
- package/dist/utils/task-progress.js +36 -0
- package/package.json +79 -0
- package/schemas/spec-driven/schema.yaml +153 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +23 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +9 -0
- package/schemas/workspace-planning/schema.yaml +72 -0
- package/schemas/workspace-planning/templates/design.md +33 -0
- package/schemas/workspace-planning/templates/proposal.md +28 -0
- package/schemas/workspace-planning/templates/spec.md +9 -0
- package/schemas/workspace-planning/templates/tasks.md +15 -0
- package/scripts/postinstall.js +83 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import * as nodeFs from 'node:fs';
|
|
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';
|
|
4
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
5
|
+
import { WorkspaceCliError, asErrorMessage, makeStatus, } from './types.js';
|
|
6
|
+
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
|
+
export async function directoryExists(dirPath) {
|
|
28
|
+
try {
|
|
29
|
+
return (await fs.stat(dirPath)).isDirectory();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function normalizeExistingPathForStorage(existingPath) {
|
|
36
|
+
return process.platform === 'win32'
|
|
37
|
+
? FileSystemUtils.canonicalizeExistingPath(existingPath)
|
|
38
|
+
: existingPath;
|
|
39
|
+
}
|
|
40
|
+
export async function resolveExistingDirectory(inputPath, cwd = process.cwd()) {
|
|
41
|
+
if (inputPath.length === 0) {
|
|
42
|
+
throw new WorkspaceCliError('Repo or folder path must not be empty.', 'linked_path_empty', {
|
|
43
|
+
target: 'link.path',
|
|
44
|
+
fix: 'Choose an existing repo or folder path.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const resolvedPath = path.isAbsolute(inputPath)
|
|
48
|
+
? path.resolve(inputPath)
|
|
49
|
+
: path.resolve(cwd, inputPath);
|
|
50
|
+
if (!(await directoryExists(resolvedPath))) {
|
|
51
|
+
throw new WorkspaceCliError(`Path '${inputPath}' is not an existing folder.`, 'linked_path_missing', {
|
|
52
|
+
target: 'link.path',
|
|
53
|
+
fix: 'Choose an existing repo or folder path.',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return normalizeExistingPathForStorage(resolvedPath);
|
|
57
|
+
}
|
|
58
|
+
export function inferLinkName(absolutePath) {
|
|
59
|
+
return path.basename(absolutePath);
|
|
60
|
+
}
|
|
61
|
+
function normalizeLinksForOutput(sharedState, localState) {
|
|
62
|
+
return Object.keys(sharedState.links)
|
|
63
|
+
.sort((a, b) => a.localeCompare(b))
|
|
64
|
+
.map((name) => ({
|
|
65
|
+
name,
|
|
66
|
+
path: localState?.paths[name] ?? null,
|
|
67
|
+
status: [],
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
function formatDuplicateLinkMessage(linkName, existingPath, replacementPath) {
|
|
71
|
+
return [
|
|
72
|
+
`Cannot use link name '${linkName}' because another link already uses that name.`,
|
|
73
|
+
'Existing link:',
|
|
74
|
+
` ${linkName} -> ${existingPath ?? '(no local path recorded)'}`,
|
|
75
|
+
'',
|
|
76
|
+
'Choose a different link name:',
|
|
77
|
+
` openspec workspace link archived-${linkName} ${replacementPath}`,
|
|
78
|
+
'',
|
|
79
|
+
'If you meant to change the existing link path:',
|
|
80
|
+
` openspec workspace relink ${linkName} ${replacementPath}`,
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
function duplicateLinkError(linkName, existingPath, replacementPath) {
|
|
84
|
+
return new WorkspaceCliError(formatDuplicateLinkMessage(linkName, existingPath, replacementPath), 'duplicate_link_name', {
|
|
85
|
+
target: `links.${linkName}`,
|
|
86
|
+
fix: `Choose a different link name or run 'openspec workspace relink ${linkName} ${replacementPath}'.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function duplicateSetupLinkError(linkName, existingPath, replacementPath) {
|
|
90
|
+
return new WorkspaceCliError([
|
|
91
|
+
`Cannot use link name '${linkName}' because another setup link already uses that name.`,
|
|
92
|
+
'Existing link:',
|
|
93
|
+
` ${linkName} -> ${existingPath}`,
|
|
94
|
+
'',
|
|
95
|
+
'Use explicit --link <name>=<path> values with different names.',
|
|
96
|
+
].join('\n'), 'duplicate_link_name', {
|
|
97
|
+
target: `links.${linkName}`,
|
|
98
|
+
fix: `Use explicit --link ${linkName}-alt=${replacementPath} with a different link name.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function validateWorkspaceNameForSetup(name) {
|
|
102
|
+
try {
|
|
103
|
+
return validateWorkspaceName(name);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new WorkspaceCliError('Workspace name must be kebab-case with lowercase letters, numbers, and single hyphen separators.', 'invalid_workspace_name', {
|
|
107
|
+
target: 'workspace.name',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function validateLinkNameForCommand(name) {
|
|
112
|
+
try {
|
|
113
|
+
return validateWorkspaceLinkName(name);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new WorkspaceCliError(asErrorMessage(error), 'invalid_link_name', {
|
|
117
|
+
target: 'link.name',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function localStateInvalidStatus(error) {
|
|
122
|
+
return makeStatus('error', 'workspace_local_state_invalid', `Machine-local paths could not be read: ${asErrorMessage(error)}`, {
|
|
123
|
+
target: 'workspace.local_state',
|
|
124
|
+
fix: 'Repair or remove .openspec-workspace/local.yaml, then run openspec workspace relink <name> <path> for affected links.',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function workspaceSkillDriftStatus(workspaceName) {
|
|
128
|
+
return makeStatus('warning', 'workspace_skills_out_of_sync', 'Workspace-local agent skills are out of sync with the active global profile.', {
|
|
129
|
+
target: 'workspace.skills',
|
|
130
|
+
fix: `openspec workspace update --workspace ${workspaceName}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function appendWorkspaceSkillDriftStatus(statuses, workspaceName, localState) {
|
|
134
|
+
if (hasWorkspaceSkillProfileDrift(localState)) {
|
|
135
|
+
statuses.push(workspaceSkillDriftStatus(workspaceName));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
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) {
|
|
151
|
+
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', {
|
|
156
|
+
target: 'workspace.name',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
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
|
+
let createdWorkspaceRoot = false;
|
|
165
|
+
try {
|
|
166
|
+
await FileSystemUtils.createDirectory(path.dirname(workspaceRoot));
|
|
167
|
+
await fs.mkdir(workspaceRoot);
|
|
168
|
+
createdWorkspaceRoot = true;
|
|
169
|
+
await FileSystemUtils.createDirectory(getWorkspaceChangesDir(workspaceRoot));
|
|
170
|
+
const sharedState = {
|
|
171
|
+
version: 1,
|
|
172
|
+
name: workspaceName,
|
|
173
|
+
links: Object.fromEntries(Object.keys(links).map((linkName) => [linkName, {}])),
|
|
174
|
+
};
|
|
175
|
+
const localState = {
|
|
176
|
+
version: 1,
|
|
177
|
+
paths: links,
|
|
178
|
+
...(preferredOpener ? { preferred_opener: preferredOpener } : {}),
|
|
179
|
+
};
|
|
180
|
+
await writeWorkspaceSharedState(workspaceRoot, sharedState);
|
|
181
|
+
await writeWorkspaceLocalState(workspaceRoot, localState);
|
|
182
|
+
await syncWorkspaceOpenSurface(workspaceRoot, sharedState, localState);
|
|
183
|
+
await recordWorkspaceInRegistry(workspaceName, workspaceRoot);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (createdWorkspaceRoot) {
|
|
187
|
+
try {
|
|
188
|
+
await fs.rm(workspaceRoot, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Preserve the original creation failure; callers can retry or inspect the path.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
throw new WorkspaceCliError(`Could not create workspace '${workspaceName}': ${asErrorMessage(error)}`, 'workspace_create_failed', {
|
|
195
|
+
target: 'workspace.root',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
name: workspaceName,
|
|
200
|
+
root: workspaceRoot,
|
|
201
|
+
planning_path: getWorkspaceChangesDir(workspaceRoot),
|
|
202
|
+
links: Object.entries(links)
|
|
203
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
204
|
+
.map(([linkName, linkPath]) => ({
|
|
205
|
+
name: linkName,
|
|
206
|
+
path: linkPath,
|
|
207
|
+
status: [],
|
|
208
|
+
})),
|
|
209
|
+
status: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export async function parseSetupLinks(linkInputs) {
|
|
213
|
+
const links = {};
|
|
214
|
+
for (const rawLink of linkInputs ?? []) {
|
|
215
|
+
const parsed = await parseWorkspaceSetupLinkInput(rawLink);
|
|
216
|
+
const resolvedPath = await resolveExistingDirectory(parsed.pathInput);
|
|
217
|
+
const linkName = validateLinkNameForCommand(parsed.name ?? inferLinkName(resolvedPath));
|
|
218
|
+
if (links[linkName]) {
|
|
219
|
+
throw duplicateSetupLinkError(linkName, links[linkName], resolvedPath);
|
|
220
|
+
}
|
|
221
|
+
links[linkName] = resolvedPath;
|
|
222
|
+
}
|
|
223
|
+
return links;
|
|
224
|
+
}
|
|
225
|
+
export async function loadWorkspaceForList(entry) {
|
|
226
|
+
const workspaceStatus = [];
|
|
227
|
+
if (!(await directoryExists(entry.workspaceRoot)) || !(await isWorkspaceRoot(entry.workspaceRoot))) {
|
|
228
|
+
return {
|
|
229
|
+
name: entry.name,
|
|
230
|
+
root: entry.workspaceRoot,
|
|
231
|
+
links: [],
|
|
232
|
+
status: [
|
|
233
|
+
makeStatus('error', 'workspace_root_missing', 'Workspace location does not exist.', {
|
|
234
|
+
target: 'workspace.root',
|
|
235
|
+
fix: 'Remove or repair the local registry record.',
|
|
236
|
+
}),
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
let sharedState;
|
|
241
|
+
let localState = null;
|
|
242
|
+
try {
|
|
243
|
+
sharedState = await readWorkspaceSharedState(entry.workspaceRoot);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
return {
|
|
247
|
+
name: entry.name,
|
|
248
|
+
root: entry.workspaceRoot,
|
|
249
|
+
links: [],
|
|
250
|
+
status: [
|
|
251
|
+
makeStatus('error', 'workspace_state_invalid', `Workspace state could not be read: ${asErrorMessage(error)}`, {
|
|
252
|
+
target: 'workspace.root',
|
|
253
|
+
fix: 'Repair the workspace state files before using this workspace.',
|
|
254
|
+
}),
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
localState = await readOptionalWorkspaceLocalState(entry.workspaceRoot);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
workspaceStatus.push(localStateInvalidStatus(error));
|
|
263
|
+
}
|
|
264
|
+
appendWorkspaceSkillDriftStatus(workspaceStatus, sharedState.name, localState);
|
|
265
|
+
return {
|
|
266
|
+
name: sharedState.name,
|
|
267
|
+
root: entry.workspaceRoot,
|
|
268
|
+
links: normalizeLinksForOutput(sharedState, localState),
|
|
269
|
+
status: workspaceStatus,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export async function loadWorkspaceForDoctor(selected) {
|
|
273
|
+
const commandStatus = [...selected.status];
|
|
274
|
+
const workspaceStatus = [];
|
|
275
|
+
const planningPath = getWorkspaceChangesDir(selected.root);
|
|
276
|
+
if (!(await directoryExists(selected.root)) || !(await isWorkspaceRoot(selected.root))) {
|
|
277
|
+
return {
|
|
278
|
+
workspace: {
|
|
279
|
+
name: selected.name,
|
|
280
|
+
root: selected.root,
|
|
281
|
+
planning_path: planningPath,
|
|
282
|
+
links: [],
|
|
283
|
+
status: [
|
|
284
|
+
makeStatus('error', 'selected_workspace_root_missing', 'Selected workspace location does not exist or is not a valid workspace.', {
|
|
285
|
+
target: 'workspace.root',
|
|
286
|
+
fix: 'Repair the local workspace registry record or choose another workspace.',
|
|
287
|
+
}),
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
status: commandStatus,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
let sharedState;
|
|
294
|
+
let localState;
|
|
295
|
+
let localStateInvalid = false;
|
|
296
|
+
try {
|
|
297
|
+
sharedState = await readWorkspaceSharedState(selected.root);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
workspace: {
|
|
302
|
+
name: selected.name,
|
|
303
|
+
root: selected.root,
|
|
304
|
+
planning_path: planningPath,
|
|
305
|
+
links: [],
|
|
306
|
+
status: [
|
|
307
|
+
makeStatus('error', 'workspace_state_invalid', `Workspace state could not be read: ${asErrorMessage(error)}`, {
|
|
308
|
+
target: 'workspace.root',
|
|
309
|
+
fix: 'Repair .openspec-workspace/workspace.yaml before using this workspace.',
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
status: commandStatus,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
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));
|
|
343
|
+
const links = [];
|
|
344
|
+
for (const linkName of linkNames) {
|
|
345
|
+
const linkStatus = [];
|
|
346
|
+
const localPath = localState.paths[linkName] ?? null;
|
|
347
|
+
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) {
|
|
355
|
+
linkStatus.push(makeStatus('error', 'linked_path_missing_from_local_state', 'Shared link does not have a local path on this machine.', {
|
|
356
|
+
target: `links.${linkName}.path`,
|
|
357
|
+
fix: `openspec workspace relink ${linkName} /path/to/${linkName}`,
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
if (localPath) {
|
|
361
|
+
if (await directoryExists(localPath)) {
|
|
362
|
+
const candidateSpecsPath = path.join(localPath, 'openspec', 'specs');
|
|
363
|
+
repoSpecsPath = (await directoryExists(candidateSpecsPath)) ? candidateSpecsPath : null;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
linkStatus.push(makeStatus('error', 'linked_path_missing', 'Linked path does not exist.', {
|
|
367
|
+
target: `links.${linkName}.path`,
|
|
368
|
+
fix: `openspec workspace relink ${linkName} /path/to/${linkName}`,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
links.push({
|
|
373
|
+
name: linkName,
|
|
374
|
+
path: localPath,
|
|
375
|
+
repo_specs_path: repoSpecsPath,
|
|
376
|
+
status: linkStatus,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
workspace: {
|
|
381
|
+
name: sharedState.name,
|
|
382
|
+
root: selected.root,
|
|
383
|
+
planning_path: planningPath,
|
|
384
|
+
links,
|
|
385
|
+
status: workspaceStatus,
|
|
386
|
+
},
|
|
387
|
+
status: commandStatus,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
export async function readWorkspaceForMutation(selected) {
|
|
391
|
+
if (!(await directoryExists(selected.root)) || !(await isWorkspaceRoot(selected.root))) {
|
|
392
|
+
throw new WorkspaceCliError(`Workspace location does not exist for '${selected.name}': ${selected.root}`, 'selected_workspace_root_missing', {
|
|
393
|
+
target: 'workspace.root',
|
|
394
|
+
fix: 'Run openspec workspace list to inspect known workspaces.',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
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);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function buildLinkMutationPayload(selected, sharedState, localState, linkName, linkPath) {
|
|
408
|
+
return {
|
|
409
|
+
workspace: {
|
|
410
|
+
name: sharedState.name,
|
|
411
|
+
root: selected.root,
|
|
412
|
+
planning_path: getWorkspaceChangesDir(selected.root),
|
|
413
|
+
links: normalizeLinksForOutput(sharedState, localState),
|
|
414
|
+
status: [],
|
|
415
|
+
},
|
|
416
|
+
link: {
|
|
417
|
+
name: linkName,
|
|
418
|
+
path: linkPath,
|
|
419
|
+
status: [],
|
|
420
|
+
},
|
|
421
|
+
status: selected.status,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
export async function addWorkspaceLink(selected, nameOrPath, linkPath) {
|
|
425
|
+
const explicitName = linkPath ? nameOrPath : undefined;
|
|
426
|
+
const pathInput = linkPath ?? nameOrPath;
|
|
427
|
+
const resolvedPath = await resolveExistingDirectory(pathInput);
|
|
428
|
+
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);
|
|
432
|
+
}
|
|
433
|
+
const updatedSharedState = {
|
|
434
|
+
...sharedState,
|
|
435
|
+
links: {
|
|
436
|
+
...sharedState.links,
|
|
437
|
+
[linkName]: {},
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
const updatedLocalState = {
|
|
441
|
+
...localState,
|
|
442
|
+
paths: {
|
|
443
|
+
...localState.paths,
|
|
444
|
+
[linkName]: resolvedPath,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
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);
|
|
452
|
+
}
|
|
453
|
+
export async function updateWorkspaceLink(selected, linkNameInput, linkPath) {
|
|
454
|
+
const linkName = validateLinkNameForCommand(linkNameInput);
|
|
455
|
+
const resolvedPath = await resolveExistingDirectory(linkPath);
|
|
456
|
+
const { sharedState, localState } = await readWorkspaceForMutation(selected);
|
|
457
|
+
if (!sharedState.links[linkName]) {
|
|
458
|
+
throw new WorkspaceCliError(`Unknown workspace link '${linkName}'.`, 'unknown_link_name', {
|
|
459
|
+
target: `links.${linkName}`,
|
|
460
|
+
fix: 'Run openspec workspace doctor to see linked repos or folders.',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const updatedLocalState = {
|
|
464
|
+
...localState,
|
|
465
|
+
paths: {
|
|
466
|
+
...localState.paths,
|
|
467
|
+
[linkName]: resolvedPath,
|
|
468
|
+
},
|
|
469
|
+
};
|
|
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);
|
|
474
|
+
}
|
|
475
|
+
//# sourceMappingURL=operations.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SelectedWorkspace, WorkspaceSelectionOptions } from './types.js';
|
|
2
|
+
export declare function selectWorkspaceRootForCommand(workspaceRoot: string): Promise<SelectedWorkspace>;
|
|
3
|
+
export declare function selectWorkspaceForCommand(options: WorkspaceSelectionOptions, commandName: string, selectionOptions?: {
|
|
4
|
+
preferPositionalName?: boolean;
|
|
5
|
+
}): Promise<SelectedWorkspace>;
|
|
6
|
+
//# sourceMappingURL=selection.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { findWorkspaceRoot, listWorkspaceRegistryEntries, readWorkspaceSharedState, } from '../../core/workspace/index.js';
|
|
2
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
3
|
+
import { isInteractive, resolveNoInteractive } from '../../utils/interactive.js';
|
|
4
|
+
import { readRegistry, validateWorkspaceNameForSetup } from './operations.js';
|
|
5
|
+
import { WorkspaceCliError, makeStatus, } from './types.js';
|
|
6
|
+
function normalizeRegistryRootForComparison(workspaceRoot) {
|
|
7
|
+
try {
|
|
8
|
+
return FileSystemUtils.canonicalizeExistingPath(workspaceRoot);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return workspaceRoot;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function workspaceNotInRegistryWarning() {
|
|
15
|
+
return makeStatus('warning', 'workspace_not_in_local_registry', 'This workspace is not recorded in the local workspace registry.', {
|
|
16
|
+
target: 'workspace.root',
|
|
17
|
+
fix: 'Run a mutating workspace command from this workspace, such as workspace link or workspace relink, to record it locally.',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function isRegisteredWorkspaceRoot(registryRoot, currentWorkspaceRoot) {
|
|
21
|
+
return (registryRoot !== undefined &&
|
|
22
|
+
normalizeRegistryRootForComparison(registryRoot) ===
|
|
23
|
+
normalizeRegistryRootForComparison(currentWorkspaceRoot));
|
|
24
|
+
}
|
|
25
|
+
async function selectedWorkspaceFromRoot(currentWorkspaceRoot, registry) {
|
|
26
|
+
const sharedState = await readWorkspaceSharedState(currentWorkspaceRoot);
|
|
27
|
+
const registeredRoot = registry.workspaces[sharedState.name];
|
|
28
|
+
const isRegistered = isRegisteredWorkspaceRoot(registeredRoot, currentWorkspaceRoot);
|
|
29
|
+
return {
|
|
30
|
+
name: sharedState.name,
|
|
31
|
+
root: currentWorkspaceRoot,
|
|
32
|
+
status: isRegistered ? [] : [workspaceNotInRegistryWarning()],
|
|
33
|
+
unregisteredCurrentWorkspace: !isRegistered,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export async function selectWorkspaceRootForCommand(workspaceRoot) {
|
|
37
|
+
const registry = await readRegistry();
|
|
38
|
+
const currentWorkspaceRoot = await findWorkspaceRoot(workspaceRoot);
|
|
39
|
+
if (!currentWorkspaceRoot) {
|
|
40
|
+
throw new WorkspaceCliError(`No OpenSpec workspace found at '${workspaceRoot}'.`, 'workspace_not_found', {
|
|
41
|
+
target: 'workspace.root',
|
|
42
|
+
fix: 'Pass a path inside an OpenSpec workspace.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return selectedWorkspaceFromRoot(currentWorkspaceRoot, registry);
|
|
46
|
+
}
|
|
47
|
+
export async function selectWorkspaceForCommand(options, commandName, selectionOptions = {}) {
|
|
48
|
+
const registry = await readRegistry();
|
|
49
|
+
if (options.workspace) {
|
|
50
|
+
const workspaceName = validateWorkspaceNameForSetup(options.workspace);
|
|
51
|
+
const registryRoot = registry.workspaces[workspaceName];
|
|
52
|
+
if (!registryRoot) {
|
|
53
|
+
throw new WorkspaceCliError(`Unknown OpenSpec workspace '${workspaceName}'.`, 'workspace_not_found', {
|
|
54
|
+
target: 'workspace.name',
|
|
55
|
+
fix: 'Run openspec workspace list to see known workspaces.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
name: workspaceName,
|
|
60
|
+
root: registryRoot,
|
|
61
|
+
status: [],
|
|
62
|
+
unregisteredCurrentWorkspace: false,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const currentWorkspaceRoot = await findWorkspaceRoot(process.cwd());
|
|
66
|
+
if (currentWorkspaceRoot) {
|
|
67
|
+
return selectedWorkspaceFromRoot(currentWorkspaceRoot, registry);
|
|
68
|
+
}
|
|
69
|
+
const entries = listWorkspaceRegistryEntries(registry);
|
|
70
|
+
if (entries.length === 0) {
|
|
71
|
+
throw new WorkspaceCliError("No known OpenSpec workspaces. Run 'openspec workspace setup' first.\nAfter at least one workspace is known locally, you can also pass --workspace <name>.", 'no_known_workspaces', {
|
|
72
|
+
target: 'workspace.name',
|
|
73
|
+
fix: 'openspec workspace setup',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (entries.length === 1) {
|
|
77
|
+
const [entry] = entries;
|
|
78
|
+
return {
|
|
79
|
+
name: entry.name,
|
|
80
|
+
root: entry.workspaceRoot,
|
|
81
|
+
status: [],
|
|
82
|
+
unregisteredCurrentWorkspace: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (options.json || resolveNoInteractive(options) || !isInteractive(options)) {
|
|
86
|
+
const knownNames = entries.map((entry) => entry.name).join(', ');
|
|
87
|
+
const usesPositionalName = selectionOptions.preferPositionalName;
|
|
88
|
+
const fix = usesPositionalName
|
|
89
|
+
? `openspec workspace ${commandName} <name>`
|
|
90
|
+
: `openspec workspace ${commandName} --workspace <name>`;
|
|
91
|
+
throw new WorkspaceCliError(usesPositionalName
|
|
92
|
+
? `Multiple OpenSpec workspaces are known. Known workspaces: ${knownNames}. Pass a workspace name.`
|
|
93
|
+
: `Multiple OpenSpec workspaces are known. Known workspaces: ${knownNames}. Pass --workspace <name>.`, 'workspace_selection_ambiguous', {
|
|
94
|
+
target: 'workspace.name',
|
|
95
|
+
fix,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const { select } = await import('@inquirer/prompts');
|
|
99
|
+
const selectedName = await select({
|
|
100
|
+
message: 'Select workspace:',
|
|
101
|
+
choices: entries.map((entry) => ({
|
|
102
|
+
name: `${entry.name} (${entry.workspaceRoot})`,
|
|
103
|
+
value: entry.name,
|
|
104
|
+
})),
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
name: selectedName,
|
|
108
|
+
root: registry.workspaces[selectedName],
|
|
109
|
+
status: [],
|
|
110
|
+
unregisteredCurrentWorkspace: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=selection.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type StatusSeverity = 'error' | 'warning';
|
|
2
|
+
export interface WorkspaceStatus {
|
|
3
|
+
severity: StatusSeverity;
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
target?: string;
|
|
7
|
+
fix?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkspaceLinkOutput {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string | null;
|
|
12
|
+
repo_specs_path?: string | null;
|
|
13
|
+
status: WorkspaceStatus[];
|
|
14
|
+
}
|
|
15
|
+
export interface WorkspaceOutput {
|
|
16
|
+
name: string;
|
|
17
|
+
root: string;
|
|
18
|
+
planning_path: string;
|
|
19
|
+
links: WorkspaceLinkOutput[];
|
|
20
|
+
status: WorkspaceStatus[];
|
|
21
|
+
}
|
|
22
|
+
export interface WorkspaceListOutput {
|
|
23
|
+
name: string;
|
|
24
|
+
root: string;
|
|
25
|
+
links: WorkspaceLinkOutput[];
|
|
26
|
+
status: WorkspaceStatus[];
|
|
27
|
+
}
|
|
28
|
+
export interface WorkspaceSetupOptions {
|
|
29
|
+
name?: string;
|
|
30
|
+
link?: string[];
|
|
31
|
+
opener?: string;
|
|
32
|
+
tools?: string;
|
|
33
|
+
json?: boolean;
|
|
34
|
+
noInteractive?: boolean;
|
|
35
|
+
interactive?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface WorkspaceSelectionOptions {
|
|
38
|
+
workspace?: string;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
noInteractive?: boolean;
|
|
41
|
+
interactive?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export type WorkspaceLinkOptions = WorkspaceSelectionOptions;
|
|
44
|
+
export interface WorkspaceUpdateOptions extends WorkspaceSelectionOptions {
|
|
45
|
+
tools?: string;
|
|
46
|
+
force?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface WorkspaceOpenOptions extends WorkspaceSelectionOptions {
|
|
49
|
+
agent?: string;
|
|
50
|
+
editor?: boolean;
|
|
51
|
+
prepareOnly?: boolean;
|
|
52
|
+
change?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface WorkspaceListOptions {
|
|
55
|
+
json?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface SelectedWorkspace {
|
|
58
|
+
name: string;
|
|
59
|
+
root: string;
|
|
60
|
+
status: WorkspaceStatus[];
|
|
61
|
+
unregisteredCurrentWorkspace: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface WorkspaceLinkMutationPayload {
|
|
64
|
+
workspace: WorkspaceOutput;
|
|
65
|
+
link: {
|
|
66
|
+
name: string;
|
|
67
|
+
path: string;
|
|
68
|
+
status: WorkspaceStatus[];
|
|
69
|
+
};
|
|
70
|
+
status: WorkspaceStatus[];
|
|
71
|
+
}
|
|
72
|
+
export declare class WorkspaceCliError extends Error {
|
|
73
|
+
readonly status: WorkspaceStatus;
|
|
74
|
+
constructor(message: string, code: string, options?: {
|
|
75
|
+
target?: string;
|
|
76
|
+
fix?: string;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export declare function makeStatus(severity: StatusSeverity, code: string, message: string, options?: {
|
|
80
|
+
target?: string;
|
|
81
|
+
fix?: string;
|
|
82
|
+
}): WorkspaceStatus;
|
|
83
|
+
export declare function asErrorMessage(error: unknown): string;
|
|
84
|
+
export declare function asStatus(error: unknown): WorkspaceStatus;
|
|
85
|
+
export declare function appendStatus<T extends {
|
|
86
|
+
status: WorkspaceStatus[];
|
|
87
|
+
}>(payload: T, status: WorkspaceStatus): T;
|
|
88
|
+
//# sourceMappingURL=types.d.ts.map
|