@codewalla_india/openspec 1.0.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.
- package/LICENSE +22 -0
- package/README.md +225 -0
- package/bin/openspec.js +5 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.js +548 -0
- package/dist/commands/change.d.ts +39 -0
- package/dist/commands/change.js +279 -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 +552 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +155 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +163 -0
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +183 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.js +869 -0
- package/dist/commands/shared-gather.d.ts +14 -0
- package/dist/commands/shared-gather.js +31 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/show.d.ts +19 -0
- package/dist/commands/show.js +177 -0
- package/dist/commands/spec.d.ts +19 -0
- package/dist/commands/spec.js +236 -0
- package/dist/commands/store.d.ts +3 -0
- package/dist/commands/store.js +547 -0
- package/dist/commands/validate.d.ts +26 -0
- package/dist/commands/validate.js +330 -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 +45 -0
- package/dist/commands/workflow/instructions.js +500 -0
- package/dist/commands/workflow/new-change.d.ts +20 -0
- package/dist/commands/workflow/new-change.js +106 -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 +84 -0
- package/dist/commands/workflow/shared.js +133 -0
- package/dist/commands/workflow/status.d.ts +16 -0
- package/dist/commands/workflow/status.js +92 -0
- package/dist/commands/workflow/templates.d.ts +16 -0
- package/dist/commands/workflow/templates.js +69 -0
- package/dist/commands/workset-input.d.ts +19 -0
- package/dist/commands/workset-input.js +112 -0
- package/dist/commands/workset-prompts.d.ts +12 -0
- package/dist/commands/workset-prompts.js +143 -0
- package/dist/commands/workset.d.ts +25 -0
- package/dist/commands/workset.js +446 -0
- package/dist/core/archive.d.ts +22 -0
- package/dist/core/archive.js +471 -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 +9 -0
- package/dist/core/artifact-graph/index.js +14 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +188 -0
- package/dist/core/artifact-graph/instruction-loader.js +233 -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 +40 -0
- package/dist/core/artifact-graph/types.js +29 -0
- package/dist/core/available-tools.d.ts +17 -0
- package/dist/core/available-tools.js +43 -0
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +19 -0
- package/dist/core/change-metadata/schema.js +30 -0
- package/dist/core/change-status-policy.d.ts +37 -0
- package/dist/core/change-status-policy.js +35 -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 +32 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +37 -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 +31 -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 +42 -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 +38 -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/command-generation/yaml.d.ts +22 -0
- package/dist/core/command-generation/yaml.js +38 -0
- package/dist/core/completions/command-registry.d.ts +3 -0
- package/dist/core/completions/command-registry.js +778 -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 +276 -0
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +321 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +151 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +102 -0
- package/dist/core/completions/installers/powershell-installer.js +415 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +117 -0
- package/dist/core/completions/installers/zsh-installer.js +424 -0
- package/dist/core/completions/shared-flags.d.ts +13 -0
- package/dist/core/completions/shared-flags.js +33 -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/comprehension/config.d.ts +20 -0
- package/dist/core/comprehension/config.js +23 -0
- package/dist/core/comprehension/fingerprint.d.ts +5 -0
- package/dist/core/comprehension/fingerprint.js +25 -0
- package/dist/core/comprehension/index.d.ts +49 -0
- package/dist/core/comprehension/index.js +78 -0
- package/dist/core/comprehension/pass-record.d.ts +29 -0
- package/dist/core/comprehension/pass-record.js +64 -0
- package/dist/core/comprehension/stats.d.ts +18 -0
- package/dist/core/comprehension/stats.js +41 -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 +87 -0
- package/dist/core/config-schema.js +239 -0
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +39 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +112 -0
- package/dist/core/global-config.d.ts +51 -0
- package/dist/core/global-config.js +124 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +7 -0
- package/dist/core/init.d.ts +37 -0
- package/dist/core/init.js +613 -0
- package/dist/core/legacy-cleanup.d.ts +162 -0
- package/dist/core/legacy-cleanup.js +514 -0
- package/dist/core/list.d.ts +11 -0
- package/dist/core/list.js +185 -0
- package/dist/core/migration.d.ts +23 -0
- package/dist/core/migration.js +108 -0
- package/dist/core/openers.d.ts +77 -0
- package/dist/core/openers.js +251 -0
- package/dist/core/openspec-root.d.ts +45 -0
- package/dist/core/openspec-root.js +192 -0
- package/dist/core/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 +16 -0
- package/dist/core/planning-home.js +67 -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 +120 -0
- package/dist/core/project-config.js +406 -0
- package/dist/core/references.d.ts +63 -0
- package/dist/core/references.js +310 -0
- package/dist/core/relationship-health.d.ts +65 -0
- package/dist/core/relationship-health.js +64 -0
- package/dist/core/root-selection.d.ts +122 -0
- package/dist/core/root-selection.js +337 -0
- package/dist/core/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 +78 -0
- package/dist/core/specs-apply.js +394 -0
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/store/errors.js +22 -0
- package/dist/core/store/foundation.d.ts +56 -0
- package/dist/core/store/foundation.js +251 -0
- package/dist/core/store/git.d.ts +23 -0
- package/dist/core/store/git.js +137 -0
- package/dist/core/store/index.d.ts +5 -0
- package/dist/core/store/index.js +5 -0
- package/dist/core/store/operations.d.ts +114 -0
- package/dist/core/store/operations.js +783 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -0
- package/dist/core/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 +337 -0
- package/dist/core/templates/workflows/archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/archive-change.js +278 -0
- package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/bulk-archive-change.js +493 -0
- package/dist/core/templates/workflows/comprehension-guidance.d.ts +9 -0
- package/dist/core/templates/workflows/comprehension-guidance.js +58 -0
- package/dist/core/templates/workflows/continue-change.d.ts +10 -0
- package/dist/core/templates/workflows/continue-change.js +239 -0
- package/dist/core/templates/workflows/explore.d.ts +10 -0
- package/dist/core/templates/workflows/explore.js +464 -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 +205 -0
- package/dist/core/templates/workflows/mcp-guidance.d.ts +13 -0
- package/dist/core/templates/workflows/mcp-guidance.js +116 -0
- package/dist/core/templates/workflows/new-change.d.ts +10 -0
- package/dist/core/templates/workflows/new-change.js +148 -0
- package/dist/core/templates/workflows/onboard.d.ts +10 -0
- package/dist/core/templates/workflows/onboard.js +566 -0
- package/dist/core/templates/workflows/propose.d.ts +10 -0
- package/dist/core/templates/workflows/propose.js +228 -0
- package/dist/core/templates/workflows/store-selection.d.ts +8 -0
- package/dist/core/templates/workflows/store-selection.js +8 -0
- package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
- package/dist/core/templates/workflows/sync-specs.js +291 -0
- package/dist/core/templates/workflows/verify-change.d.ts +10 -0
- package/dist/core/templates/workflows/verify-change.js +346 -0
- package/dist/core/update.d.ts +82 -0
- package/dist/core/update.js +557 -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 +44 -0
- package/dist/core/validation/validator.js +435 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +168 -0
- package/dist/core/working-set.d.ts +47 -0
- package/dist/core/working-set.js +43 -0
- package/dist/core/worksets.d.ts +75 -0
- package/dist/core/worksets.js +245 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/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 +55 -0
- package/dist/utils/change-metadata.js +141 -0
- package/dist/utils/change-utils.d.ts +71 -0
- package/dist/utils/change-utils.js +138 -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 +320 -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 +84 -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/scripts/postinstall.js +83 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import * as nodeFs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
7
|
+
import { ANCHORED_OPENSPEC_DIRS, DIRECTORY_ANCHOR_FILE_NAME, OPENSPEC_ROOT_DIR, ensureOpenSpecRoot, inspectOpenSpecRoot, rollbackCreatedPaths, } from '../openspec-root.js';
|
|
8
|
+
import { STORE_METADATA_DIR_NAME, getStoreMetadataDir, getStoreMetadataPath, getStoreRegistryPath, listStoreRegistryEntries, readStoreRegistryState, readOptionalStoreMetadataState, resolveGitStoreBackendConfig, validateStoreId, writeStoreMetadataState, } from './foundation.js';
|
|
9
|
+
import { StoreError, makeStoreDiagnostic } from './errors.js';
|
|
10
|
+
import { assertGitCommitIdentity, commitStoreFiles, gitDirectoryHasTrackedFiles, gitHasCommits, gitHasRemote, gitHasUncommittedChanges, gitOriginUrl, initGitRepository, isGitRepositoryAtRoot, } from './git.js';
|
|
11
|
+
import { getStoreRootForBackend, assertNoRegisteredStoreConflict, commitStoreRegistration, getRegisteredStore, listRegisteredStores, unregisterStoreRegistration, } from './registry.js';
|
|
12
|
+
const fs = nodeFs.promises;
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
async function pathKind(targetPath) {
|
|
15
|
+
try {
|
|
16
|
+
const stat = await fs.stat(targetPath);
|
|
17
|
+
if (stat.isDirectory())
|
|
18
|
+
return 'directory';
|
|
19
|
+
if (stat.isFile())
|
|
20
|
+
return 'file';
|
|
21
|
+
return 'other';
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (typeof error === 'object' &&
|
|
25
|
+
error !== null &&
|
|
26
|
+
'code' in error &&
|
|
27
|
+
error.code === 'ENOENT') {
|
|
28
|
+
return 'missing';
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function isDirectoryEmpty(directory) {
|
|
34
|
+
return (await fs.readdir(directory)).length === 0;
|
|
35
|
+
}
|
|
36
|
+
async function readStoreMetadataForOperation(storeRoot) {
|
|
37
|
+
try {
|
|
38
|
+
return await readOptionalStoreMetadataState(storeRoot);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new StoreError(error instanceof Error ? error.message : String(error), 'invalid_store_metadata', {
|
|
42
|
+
target: 'store.metadata',
|
|
43
|
+
fix: `Repair ${getStoreMetadataPath(storeRoot)}.`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function isGitOnlyDirectory(storeRoot) {
|
|
48
|
+
const entries = await fs.readdir(storeRoot);
|
|
49
|
+
return entries.length === 1 && entries[0] === '.git' && await isGitRepositoryAtRoot(storeRoot);
|
|
50
|
+
}
|
|
51
|
+
function alreadyRegisteredDiagnostic(id) {
|
|
52
|
+
return makeStoreDiagnostic('info', 'store_already_registered', `Store '${id}' is already registered at this path.`, {
|
|
53
|
+
target: 'store.registry',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function createdPath(relativePath, absolutePath, kind) {
|
|
57
|
+
return {
|
|
58
|
+
relativePath,
|
|
59
|
+
absolutePath,
|
|
60
|
+
kind,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function nearestExistingDirectory(targetPath) {
|
|
64
|
+
let current = path.resolve(targetPath);
|
|
65
|
+
while (true) {
|
|
66
|
+
const kind = await pathKind(current);
|
|
67
|
+
if (kind === 'directory')
|
|
68
|
+
return current;
|
|
69
|
+
if (kind !== 'missing')
|
|
70
|
+
return null;
|
|
71
|
+
const parent = path.dirname(current);
|
|
72
|
+
if (parent === current)
|
|
73
|
+
return null;
|
|
74
|
+
current = parent;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function findContainingGitRepositoryRoot(storeRoot) {
|
|
78
|
+
const resolvedStoreRoot = path.resolve(storeRoot);
|
|
79
|
+
const nearestParent = await nearestExistingDirectory(path.dirname(resolvedStoreRoot));
|
|
80
|
+
if (!nearestParent)
|
|
81
|
+
return null;
|
|
82
|
+
const comparableStoreRoot = path.resolve(FileSystemUtils.canonicalizeExistingPath(nearestParent), path.relative(nearestParent, resolvedStoreRoot));
|
|
83
|
+
const gitRootContainsStore = (gitRoot) => {
|
|
84
|
+
const normalizedGitRoot = FileSystemUtils.canonicalizeExistingPath(gitRoot);
|
|
85
|
+
const relative = path.relative(normalizedGitRoot, comparableStoreRoot);
|
|
86
|
+
return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative)
|
|
87
|
+
? normalizedGitRoot
|
|
88
|
+
: null;
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
const { stdout } = await execFileAsync('git', [
|
|
92
|
+
'-C',
|
|
93
|
+
nearestParent,
|
|
94
|
+
'rev-parse',
|
|
95
|
+
'--show-toplevel',
|
|
96
|
+
]);
|
|
97
|
+
return gitRootContainsStore(stdout.trim());
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
let current = nearestParent;
|
|
101
|
+
while (true) {
|
|
102
|
+
if (await isGitRepositoryAtRoot(current)) {
|
|
103
|
+
return gitRootContainsStore(current);
|
|
104
|
+
}
|
|
105
|
+
const parent = path.dirname(current);
|
|
106
|
+
if (parent === current)
|
|
107
|
+
return null;
|
|
108
|
+
current = parent;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function assertSetupPathIsNotNestedInGitRepo(storeRoot, options) {
|
|
113
|
+
if (options.allowInsideGitRepository)
|
|
114
|
+
return;
|
|
115
|
+
const containingGitRoot = await findContainingGitRepositoryRoot(storeRoot);
|
|
116
|
+
if (!containingGitRoot)
|
|
117
|
+
return;
|
|
118
|
+
throw new StoreError(`Store setup path is inside another Git repository: ${containingGitRoot}`, 'store_setup_inside_git_repo', {
|
|
119
|
+
target: 'store.root',
|
|
120
|
+
fix: 'Choose a path outside that Git repository.',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export function expandUserPath(inputPath) {
|
|
124
|
+
const trimmed = inputPath.trim();
|
|
125
|
+
if (trimmed === '~')
|
|
126
|
+
return os.homedir();
|
|
127
|
+
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
128
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
129
|
+
}
|
|
130
|
+
return trimmed;
|
|
131
|
+
}
|
|
132
|
+
function resolveSetupRoot(id, inputPath) {
|
|
133
|
+
// A store is a repo the user places; setup never silently picks app data.
|
|
134
|
+
if (inputPath === undefined || inputPath.trim().length === 0) {
|
|
135
|
+
throw new StoreError('Pass --path with the folder where this store should live.', 'store_setup_path_required', {
|
|
136
|
+
target: 'store.root',
|
|
137
|
+
fix: `openspec store setup ${id} --path ~/openspec/${id}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return path.resolve(expandUserPath(inputPath));
|
|
141
|
+
}
|
|
142
|
+
function resolveRegisterRoot(inputPath) {
|
|
143
|
+
if (inputPath === undefined || inputPath.trim().length === 0) {
|
|
144
|
+
throw new StoreError('Pass a store path.', 'store_path_required', {
|
|
145
|
+
target: 'store.root',
|
|
146
|
+
fix: 'openspec store register /path/to/store',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return path.resolve(expandUserPath(inputPath));
|
|
150
|
+
}
|
|
151
|
+
function inferStoreIdFromPath(storeRoot) {
|
|
152
|
+
return validateStoreId(path.basename(storeRoot));
|
|
153
|
+
}
|
|
154
|
+
function normalizeRegistryPathForComparison(targetPath) {
|
|
155
|
+
try {
|
|
156
|
+
return FileSystemUtils.canonicalizeExistingPath(targetPath);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return path.resolve(targetPath);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function isRegisteredAtPath(registry, id, storeRoot) {
|
|
163
|
+
const entry = registry?.stores?.[id];
|
|
164
|
+
if (!entry)
|
|
165
|
+
return false;
|
|
166
|
+
return (normalizeRegistryPathForComparison(getStoreRootForBackend(entry.backend)) ===
|
|
167
|
+
normalizeRegistryPathForComparison(storeRoot));
|
|
168
|
+
}
|
|
169
|
+
function mutationPayload(id, storeRoot, git, createdFiles, registry, diagnostics = [], remotes) {
|
|
170
|
+
return {
|
|
171
|
+
store: {
|
|
172
|
+
id,
|
|
173
|
+
root: storeRoot,
|
|
174
|
+
metadataPath: getStoreMetadataPath(storeRoot),
|
|
175
|
+
},
|
|
176
|
+
...(remotes && (remotes.canonical || remotes.observed) ? { remotes } : {}),
|
|
177
|
+
registryCommit: {
|
|
178
|
+
path: getStoreRegistryPath(),
|
|
179
|
+
registered: registry.registered,
|
|
180
|
+
alreadyRegistered: registry.alreadyRegistered,
|
|
181
|
+
},
|
|
182
|
+
git: {
|
|
183
|
+
isRepository: git.isRepository,
|
|
184
|
+
initialized: git.initialized,
|
|
185
|
+
committed: git.committed,
|
|
186
|
+
},
|
|
187
|
+
createdArtifacts: createdFiles,
|
|
188
|
+
diagnostics,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function remoteRequiresHandEditError(id, storeRoot) {
|
|
192
|
+
return new StoreError(`Store '${id}' already has an identity file; --remote cannot change it.`, 'store_remote_requires_hand_edit', {
|
|
193
|
+
target: 'store.metadata',
|
|
194
|
+
fix: `Edit ${getStoreMetadataPath(storeRoot)} and commit it.`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Backend config carrying the observed origin. Guarded by an at-root
|
|
199
|
+
* repository check: `git -C` discovers repositories by walking UP the
|
|
200
|
+
* tree, so probing a non-repo store folder nested inside another repo
|
|
201
|
+
* would record the ENCLOSING repo's origin.
|
|
202
|
+
*/
|
|
203
|
+
async function resolveBackendWithObservedOrigin(storeRoot) {
|
|
204
|
+
const origin = (await isGitRepositoryAtRoot(storeRoot))
|
|
205
|
+
? await gitOriginUrl(storeRoot)
|
|
206
|
+
: null;
|
|
207
|
+
return resolveGitStoreBackendConfig({
|
|
208
|
+
localPath: storeRoot,
|
|
209
|
+
...(origin ? { remote: origin } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function prepareSetupPlan(input) {
|
|
213
|
+
const id = validateStoreId(input.id ?? '');
|
|
214
|
+
if (input.remote !== undefined && input.remote.length === 0) {
|
|
215
|
+
throw new StoreError('Store remote must not be empty when provided.', 'store_remote_empty', {
|
|
216
|
+
target: 'store.metadata',
|
|
217
|
+
fix: 'Pass a clone URL: --remote <url>.',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const storeRoot = resolveSetupRoot(id, input.path);
|
|
221
|
+
const kind = await pathKind(storeRoot);
|
|
222
|
+
if (kind === 'file' || kind === 'other') {
|
|
223
|
+
throw new StoreError(`Store setup path is not a directory: ${storeRoot}`, 'store_setup_path_not_directory', {
|
|
224
|
+
target: 'store.root',
|
|
225
|
+
fix: 'Choose an empty directory or an existing healthy OpenSpec root.',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Stores may be Git-backed, but creating one inside an implementation
|
|
229
|
+
// repo is almost always an accidental nested-repo setup.
|
|
230
|
+
await assertSetupPathIsNotNestedInGitRepo(storeRoot, {
|
|
231
|
+
allowInsideGitRepository: input.allowInsideGitRepository,
|
|
232
|
+
});
|
|
233
|
+
let metadata = null;
|
|
234
|
+
let backend;
|
|
235
|
+
if (kind === 'directory') {
|
|
236
|
+
metadata = await readStoreMetadataForOperation(storeRoot);
|
|
237
|
+
if (metadata) {
|
|
238
|
+
if (metadata.id !== id) {
|
|
239
|
+
throw new StoreError(`Store metadata id '${metadata.id}' does not match requested id '${id}'.`, 'store_metadata_id_mismatch', {
|
|
240
|
+
target: 'store.metadata',
|
|
241
|
+
fix: `Use id '${metadata.id}' or choose a different setup path.`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (input.remote !== undefined) {
|
|
245
|
+
// Silent acceptance is the forbidden outcome: the identity file
|
|
246
|
+
// already exists, so --remote cannot reach the committed shape.
|
|
247
|
+
throw remoteRequiresHandEditError(id, storeRoot);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const openspecRoot = await inspectOpenSpecRoot(storeRoot);
|
|
252
|
+
const safeFreshDirectory = await isDirectoryEmpty(storeRoot) || await isGitOnlyDirectory(storeRoot);
|
|
253
|
+
if (!openspecRoot.healthy && !safeFreshDirectory) {
|
|
254
|
+
throw new StoreError('Store setup does not support initializing a non-empty folder that is not a healthy OpenSpec root.', 'store_setup_non_empty_directory', {
|
|
255
|
+
target: 'store.root',
|
|
256
|
+
fix: 'Choose an empty folder, a Git-only folder, or an existing healthy OpenSpec root.',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
backend = await resolveBackendWithObservedOrigin(storeRoot);
|
|
261
|
+
}
|
|
262
|
+
const registry = await readStoreRegistryState();
|
|
263
|
+
const conflictBackend = backend ?? {
|
|
264
|
+
type: 'git',
|
|
265
|
+
local_path: FileSystemUtils.canonicalizeExistingPath(storeRoot),
|
|
266
|
+
};
|
|
267
|
+
assertNoRegisteredStoreConflict(registry, id, conflictBackend);
|
|
268
|
+
return {
|
|
269
|
+
id,
|
|
270
|
+
storeRoot,
|
|
271
|
+
kind,
|
|
272
|
+
registry,
|
|
273
|
+
...(backend ? { backend } : {}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Resolves the effective Git mode for a prepared setup: on by default for new
|
|
278
|
+
* stores, off for reruns of an already-registered store (which must stay
|
|
279
|
+
* no-ops), and always honoring an explicit --init-git/--no-init-git.
|
|
280
|
+
*/
|
|
281
|
+
export function resolveSetupGitEnabled(prepared, initGit) {
|
|
282
|
+
return initGit ?? !isRegisteredAtPath(prepared.registry, prepared.id, prepared.root);
|
|
283
|
+
}
|
|
284
|
+
export async function prepareStoreSetup(input) {
|
|
285
|
+
const plan = await prepareSetupPlan(input);
|
|
286
|
+
return {
|
|
287
|
+
id: plan.id,
|
|
288
|
+
root: plan.storeRoot,
|
|
289
|
+
rootKind: plan.kind,
|
|
290
|
+
registry: plan.registry,
|
|
291
|
+
...(plan.backend ? { backend: plan.backend } : {}),
|
|
292
|
+
...(input.remote !== undefined ? { remote: input.remote } : {}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
export async function setupPreparedStore(prepared, input = {}) {
|
|
296
|
+
const plan = {
|
|
297
|
+
id: prepared.id,
|
|
298
|
+
storeRoot: prepared.root,
|
|
299
|
+
kind: prepared.rootKind,
|
|
300
|
+
registry: prepared.registry,
|
|
301
|
+
...(prepared.backend ? { backend: prepared.backend } : {}),
|
|
302
|
+
};
|
|
303
|
+
const { id, storeRoot, kind, registry } = plan;
|
|
304
|
+
let { backend } = plan;
|
|
305
|
+
// The prepare/execute split can span an unbounded interactive
|
|
306
|
+
// confirmation. Re-assert the prepare-time directory facts: if the
|
|
307
|
+
// path appeared in the gap, the plan (and its rollback policy) no
|
|
308
|
+
// longer describes reality - refuse and let a rerun re-prepare.
|
|
309
|
+
if (kind === 'missing' && (await fs.access(storeRoot).then(() => true, () => false))) {
|
|
310
|
+
throw new StoreError(`The path ${storeRoot} was created while setup was waiting for confirmation.`, 'store_setup_path_changed', {
|
|
311
|
+
target: 'store.root',
|
|
312
|
+
fix: 'Rerun openspec store setup to re-evaluate the directory.',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const createdFiles = [];
|
|
316
|
+
let createdPaths = [];
|
|
317
|
+
let gitInitialized = false;
|
|
318
|
+
let committed = false;
|
|
319
|
+
// Reruns for an already-registered store stay strict no-ops: no anchor
|
|
320
|
+
// retrofit, no git init, no new commit, no identity requirement. Only an
|
|
321
|
+
// explicit --init-git overrides that for the git side.
|
|
322
|
+
const alreadyRegisteredHere = isRegisteredAtPath(registry, id, storeRoot);
|
|
323
|
+
// --no-init-git opts out of every Git action: no preflight, no init, no
|
|
324
|
+
// commit, even when the target is already a repository.
|
|
325
|
+
const gitEnabled = input.initGit ?? !alreadyRegisteredHere;
|
|
326
|
+
const repoExisted = await isGitRepositoryAtRoot(storeRoot);
|
|
327
|
+
// Identity preflight runs before anything is created so a missing identity
|
|
328
|
+
// never leaves half-made state behind.
|
|
329
|
+
if (gitEnabled) {
|
|
330
|
+
await assertGitCommitIdentity((await nearestExistingDirectory(storeRoot)) ?? process.cwd());
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const root = await ensureOpenSpecRoot(storeRoot, {
|
|
334
|
+
anchorEmptyDirectories: !alreadyRegisteredHere,
|
|
335
|
+
});
|
|
336
|
+
createdFiles.push(...root.createdArtifacts);
|
|
337
|
+
createdPaths = root.createdPaths;
|
|
338
|
+
backend ??= await resolveBackendWithObservedOrigin(storeRoot);
|
|
339
|
+
assertNoRegisteredStoreConflict(registry, id, backend);
|
|
340
|
+
// The identity file is written before the initial commit so clones carry
|
|
341
|
+
// it; without it, register falls back to the conversion prompt.
|
|
342
|
+
const existingMetadata = await readStoreMetadataForOperation(storeRoot);
|
|
343
|
+
if (existingMetadata && prepared.remote !== undefined) {
|
|
344
|
+
// Re-assert the prepare-phase refusal: metadata that materialized
|
|
345
|
+
// between prepare and execute must not silently swallow --remote.
|
|
346
|
+
throw remoteRequiresHandEditError(id, storeRoot);
|
|
347
|
+
}
|
|
348
|
+
if (!existingMetadata) {
|
|
349
|
+
const metadataDir = getStoreMetadataDir(storeRoot);
|
|
350
|
+
const metadataDirMissing = (await pathKind(metadataDir)) === 'missing';
|
|
351
|
+
await writeStoreMetadataState(storeRoot, {
|
|
352
|
+
version: 1,
|
|
353
|
+
id,
|
|
354
|
+
...(prepared.remote !== undefined ? { remote: prepared.remote } : {}),
|
|
355
|
+
});
|
|
356
|
+
if (metadataDirMissing) {
|
|
357
|
+
createdPaths.push(createdPath('.openspec-store/', metadataDir, 'directory'));
|
|
358
|
+
}
|
|
359
|
+
createdPaths.push(createdPath('.openspec-store/store.yaml', getStoreMetadataPath(storeRoot), 'file'));
|
|
360
|
+
createdFiles.push('.openspec-store/store.yaml');
|
|
361
|
+
}
|
|
362
|
+
gitInitialized = gitEnabled ? await initGitRepository(storeRoot) : false;
|
|
363
|
+
const isRepository = gitInitialized || repoExisted;
|
|
364
|
+
// "Files created for rollback" and "files a clone needs" are different
|
|
365
|
+
// sets: when setup initialized the repository itself, the initial commit
|
|
366
|
+
// must contain the full store shape or clones of a converted root would
|
|
367
|
+
// be unhealthy. In a pre-existing repo the user owns the history, so
|
|
368
|
+
// setup commits only what it created.
|
|
369
|
+
const commitPathspecs = gitInitialized
|
|
370
|
+
? [OPENSPEC_ROOT_DIR, STORE_METADATA_DIR_NAME]
|
|
371
|
+
: createdPaths
|
|
372
|
+
.filter((entry) => entry.kind === 'file')
|
|
373
|
+
.map((entry) => entry.relativePath);
|
|
374
|
+
committed = gitEnabled && isRepository
|
|
375
|
+
? await commitStoreFiles(storeRoot, id, commitPathspecs)
|
|
376
|
+
: false;
|
|
377
|
+
// Identity creation is setup's job (done above, before the commit);
|
|
378
|
+
// registration only verifies it and records the machine-local entry.
|
|
379
|
+
const registered = await commitStoreRegistration({
|
|
380
|
+
id,
|
|
381
|
+
backend,
|
|
382
|
+
writeMetadataIfMissing: false,
|
|
383
|
+
});
|
|
384
|
+
const diagnostics = registered.alreadyRegistered && createdFiles.length === 0
|
|
385
|
+
? [alreadyRegisteredDiagnostic(id)]
|
|
386
|
+
: [];
|
|
387
|
+
const canonical = prepared.remote ?? existingMetadata?.remote;
|
|
388
|
+
return mutationPayload(id, registered.storeRoot, {
|
|
389
|
+
isRepository,
|
|
390
|
+
initialized: gitInitialized,
|
|
391
|
+
committed,
|
|
392
|
+
}, createdFiles, {
|
|
393
|
+
registered: registered.registryUpdated,
|
|
394
|
+
alreadyRegistered: registered.alreadyRegistered,
|
|
395
|
+
}, diagnostics, {
|
|
396
|
+
...(canonical ? { canonical } : {}),
|
|
397
|
+
...(backend.remote ? { observed: backend.remote } : {}),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
// Once the initial commit landed in a (possibly user-owned) repository,
|
|
402
|
+
// the files are durable state; deleting them would orphan the commit.
|
|
403
|
+
// The only remaining failure is the registry write, which is retryable.
|
|
404
|
+
if (committed) {
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
if (createdPaths.length > 0) {
|
|
408
|
+
await rollbackCreatedPaths(createdPaths);
|
|
409
|
+
}
|
|
410
|
+
// G14: a half-made .git is never durable state pre-commit - clean it
|
|
411
|
+
// up regardless of whether the ledger recorded other creations, or a
|
|
412
|
+
// rerun registers a commitless store.
|
|
413
|
+
if (gitInitialized) {
|
|
414
|
+
await fs.rm(path.join(storeRoot, '.git'), { recursive: true, force: true }).catch(() => undefined);
|
|
415
|
+
}
|
|
416
|
+
if (kind === 'missing') {
|
|
417
|
+
// Non-recursive both ways: never delete content this operation did
|
|
418
|
+
// not create (the execute-time re-check guarantees kind is accurate,
|
|
419
|
+
// but rmdir is the belt to that suspender).
|
|
420
|
+
await fs.rmdir(storeRoot).catch(() => undefined);
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
export async function setupStore(input) {
|
|
426
|
+
return setupPreparedStore(await prepareStoreSetup(input), {
|
|
427
|
+
initGit: input.initGit,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
export async function registerExistingStore(input) {
|
|
431
|
+
const storeRoot = resolveRegisterRoot(input.path);
|
|
432
|
+
const kind = await pathKind(storeRoot);
|
|
433
|
+
if (kind === 'missing') {
|
|
434
|
+
throw new StoreError(`Store path does not exist: ${storeRoot}`, 'store_path_missing', {
|
|
435
|
+
target: 'store.root',
|
|
436
|
+
fix: 'Clone or create the store folder before registering it.',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (kind !== 'directory') {
|
|
440
|
+
throw new StoreError(`Store path is not a directory: ${storeRoot}`, 'store_path_not_directory', {
|
|
441
|
+
target: 'store.root',
|
|
442
|
+
fix: 'Pass an existing store directory.',
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const openspecRoot = await inspectOpenSpecRoot(storeRoot);
|
|
446
|
+
if (!openspecRoot.healthy) {
|
|
447
|
+
const problems = openspecRoot.diagnostics.map((diagnostic) => diagnostic.message).join(' ') ||
|
|
448
|
+
'The OpenSpec root is missing or incomplete.';
|
|
449
|
+
const isEmptyCloneSuspect = (await isGitRepositoryAtRoot(storeRoot)) &&
|
|
450
|
+
(await gitHasCommits(storeRoot)) === false;
|
|
451
|
+
const emptyCloneHint = isEmptyCloneSuspect
|
|
452
|
+
? ' This folder is a Git repository with no commits — if it is a clone, the origin store needs an initial commit before the clone has any files.'
|
|
453
|
+
: '';
|
|
454
|
+
throw new StoreError(`Store register requires an existing healthy OpenSpec root. ${problems}${emptyCloneHint}`, 'store_register_root_unhealthy', {
|
|
455
|
+
target: 'openspec.root',
|
|
456
|
+
fix: isEmptyCloneSuspect
|
|
457
|
+
? 'If this is a store clone: commit and push the origin store, pull it into this clone, then rerun register.'
|
|
458
|
+
: 'Run openspec store setup for a new store, or point register at a checkout whose openspec/ files are present.',
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const metadata = await readStoreMetadataForOperation(storeRoot);
|
|
462
|
+
const explicitId = input.id !== undefined ? validateStoreId(input.id) : undefined;
|
|
463
|
+
if (metadata && explicitId !== undefined && metadata.id !== explicitId) {
|
|
464
|
+
// The fix must account for whether the metadata id is already registered,
|
|
465
|
+
// so following it never lands on the already-registered error.
|
|
466
|
+
const currentRegistry = await readStoreRegistryState();
|
|
467
|
+
const registeredElsewhere = currentRegistry?.stores?.[metadata.id] !== undefined &&
|
|
468
|
+
!isRegisteredAtPath(currentRegistry, metadata.id, storeRoot);
|
|
469
|
+
throw new StoreError(`Store metadata id '${metadata.id}' does not match --id '${explicitId}'. The id comes from the store's committed .openspec-store/store.yaml.`, 'store_metadata_id_mismatch', {
|
|
470
|
+
target: 'store.id',
|
|
471
|
+
fix: registeredElsewhere
|
|
472
|
+
? `One checkout per store id is supported, and '${metadata.id}' is already registered. Run openspec store unregister ${metadata.id} first to register this checkout instead.`
|
|
473
|
+
: `Use --id ${metadata.id} or register a different folder.`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const id = metadata?.id ?? explicitId ?? inferStoreIdFromPath(storeRoot);
|
|
477
|
+
if (!metadata && !input.allowCreateIdentity) {
|
|
478
|
+
throw new StoreError(`Turn this OpenSpec root into store '${id}'?`, 'store_register_identity_confirmation_required', {
|
|
479
|
+
target: 'store.metadata',
|
|
480
|
+
fix: `Run interactively or pass --yes to create ${getStoreMetadataPath(storeRoot)}.`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
const backend = await resolveBackendWithObservedOrigin(storeRoot);
|
|
484
|
+
const registry = await readStoreRegistryState();
|
|
485
|
+
assertNoRegisteredStoreConflict(registry, id, backend);
|
|
486
|
+
const createdFiles = [];
|
|
487
|
+
const isRepository = await isGitRepositoryAtRoot(storeRoot);
|
|
488
|
+
const registered = await commitStoreRegistration({
|
|
489
|
+
id,
|
|
490
|
+
backend,
|
|
491
|
+
writeMetadataIfMissing: true,
|
|
492
|
+
});
|
|
493
|
+
if (registered.metadataCreated) {
|
|
494
|
+
createdFiles.push('.openspec-store/store.yaml');
|
|
495
|
+
}
|
|
496
|
+
const diagnostics = registered.alreadyRegistered && createdFiles.length === 0
|
|
497
|
+
? [alreadyRegisteredDiagnostic(id)]
|
|
498
|
+
: [];
|
|
499
|
+
// Register never commits; converted roots are the user's repo to commit.
|
|
500
|
+
return mutationPayload(id, registered.storeRoot, {
|
|
501
|
+
isRepository,
|
|
502
|
+
initialized: false,
|
|
503
|
+
committed: false,
|
|
504
|
+
}, createdFiles, {
|
|
505
|
+
registered: registered.registryUpdated,
|
|
506
|
+
alreadyRegistered: registered.alreadyRegistered,
|
|
507
|
+
}, diagnostics, {
|
|
508
|
+
...(metadata?.remote ? { canonical: metadata.remote } : {}),
|
|
509
|
+
...(backend.remote ? { observed: backend.remote } : {}),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function cleanupStoreOutput(id, storeRoot) {
|
|
513
|
+
return {
|
|
514
|
+
id,
|
|
515
|
+
root: storeRoot,
|
|
516
|
+
metadataPath: getStoreMetadataPath(storeRoot),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
export async function prepareStoreCleanup(input) {
|
|
520
|
+
const id = validateStoreId(input.id);
|
|
521
|
+
const entry = await getRegisteredStore({
|
|
522
|
+
id,
|
|
523
|
+
globalDataDir: input.globalDataDir,
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
...cleanupStoreOutput(entry.id, entry.storeRoot),
|
|
527
|
+
backend: entry.backend,
|
|
528
|
+
...(input.globalDataDir ? { globalDataDir: input.globalDataDir } : {}),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
export async function unregisterStore(input) {
|
|
532
|
+
const target = await prepareStoreCleanup(input);
|
|
533
|
+
const removed = await unregisterStoreRegistration({
|
|
534
|
+
id: target.id,
|
|
535
|
+
expectedBackend: target.backend,
|
|
536
|
+
globalDataDir: target.globalDataDir,
|
|
537
|
+
});
|
|
538
|
+
return {
|
|
539
|
+
store: cleanupStoreOutput(removed.id, removed.storeRoot),
|
|
540
|
+
registryCommit: {
|
|
541
|
+
path: getStoreRegistryPath({ globalDataDir: target.globalDataDir }),
|
|
542
|
+
removed: true,
|
|
543
|
+
},
|
|
544
|
+
files: {
|
|
545
|
+
deleted: false,
|
|
546
|
+
leftOnDisk: removed.storeRoot,
|
|
547
|
+
},
|
|
548
|
+
diagnostics: [],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async function assertSafeToDeleteStoreRoot(storeRoot, id) {
|
|
552
|
+
const kind = await pathKind(storeRoot);
|
|
553
|
+
if (kind === 'missing') {
|
|
554
|
+
return { exists: false };
|
|
555
|
+
}
|
|
556
|
+
if (kind !== 'directory') {
|
|
557
|
+
throw new StoreError(`Store path is not a directory: ${storeRoot}`, 'store_remove_path_not_directory', {
|
|
558
|
+
target: 'store.root',
|
|
559
|
+
fix: 'Run "openspec store unregister <id>" if you only want to forget this local registry entry.',
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const metadata = await readStoreMetadataForOperation(storeRoot);
|
|
563
|
+
if (!metadata) {
|
|
564
|
+
throw new StoreError('Store remove refuses to delete a folder without store metadata.', 'store_remove_metadata_missing', {
|
|
565
|
+
target: 'store.metadata',
|
|
566
|
+
fix: 'Run "openspec store unregister <id>" if you only want to forget this local registry entry.',
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (metadata.id !== id) {
|
|
570
|
+
throw new StoreError(`Store metadata id '${metadata.id}' does not match requested id '${id}'.`, 'store_metadata_id_mismatch', {
|
|
571
|
+
target: 'store.metadata',
|
|
572
|
+
fix: 'Repair the registry or run store unregister instead of deleting this folder.',
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return { exists: true };
|
|
576
|
+
}
|
|
577
|
+
export async function removeStore(target) {
|
|
578
|
+
const id = validateStoreId(target.id);
|
|
579
|
+
const diagnostics = [];
|
|
580
|
+
let deleted = false;
|
|
581
|
+
// Order matters: the registry entry goes first, the files second. A
|
|
582
|
+
// failed file deletion leaves recoverable orphan files; the reverse
|
|
583
|
+
// order would leave a phantom registration pointing at nothing.
|
|
584
|
+
let rootMissing = false;
|
|
585
|
+
const removed = await unregisterStoreRegistration({
|
|
586
|
+
id,
|
|
587
|
+
expectedBackend: target.backend,
|
|
588
|
+
globalDataDir: target.globalDataDir,
|
|
589
|
+
beforeCommit: async (entry) => {
|
|
590
|
+
const safeTarget = await assertSafeToDeleteStoreRoot(entry.storeRoot, id);
|
|
591
|
+
rootMissing = !safeTarget.exists;
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
if (rootMissing) {
|
|
595
|
+
diagnostics.push(makeStoreDiagnostic('warning', 'store_root_missing', 'Store files were already missing.', {
|
|
596
|
+
target: 'store.root',
|
|
597
|
+
}));
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
try {
|
|
601
|
+
await fs.rm(removed.storeRoot, { recursive: true, force: true });
|
|
602
|
+
deleted = true;
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
diagnostics.push(makeStoreDiagnostic('warning', 'store_files_left_on_disk', `The registration was removed, but deleting ${removed.storeRoot} failed (${error.message}).`, {
|
|
606
|
+
target: 'store.root',
|
|
607
|
+
fix: `Delete the folder manually: ${removed.storeRoot}`,
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
store: cleanupStoreOutput(removed.id, removed.storeRoot),
|
|
613
|
+
registryCommit: {
|
|
614
|
+
path: getStoreRegistryPath({ globalDataDir: target.globalDataDir }),
|
|
615
|
+
removed: true,
|
|
616
|
+
},
|
|
617
|
+
files: {
|
|
618
|
+
deleted,
|
|
619
|
+
...(deleted ? { deletedPath: removed.storeRoot } : {}),
|
|
620
|
+
},
|
|
621
|
+
diagnostics,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
export async function listStores() {
|
|
625
|
+
const entries = await listRegisteredStores();
|
|
626
|
+
return {
|
|
627
|
+
stores: entries.map((entry) => ({
|
|
628
|
+
id: entry.id,
|
|
629
|
+
root: entry.storeRoot,
|
|
630
|
+
})),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function doctorStatusForError(error, code, target, fix) {
|
|
634
|
+
if (error instanceof StoreError) {
|
|
635
|
+
return error.diagnostic;
|
|
636
|
+
}
|
|
637
|
+
return makeStoreDiagnostic('error', code, error instanceof Error ? error.message : String(error), {
|
|
638
|
+
target,
|
|
639
|
+
...(fix ? { fix } : {}),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
async function inspectStore(entry) {
|
|
643
|
+
const root = getStoreRootForBackend(entry.backend);
|
|
644
|
+
const metadataPath = getStoreMetadataPath(root);
|
|
645
|
+
const diagnostics = [];
|
|
646
|
+
const kind = await pathKind(root);
|
|
647
|
+
let metadata = {
|
|
648
|
+
present: null,
|
|
649
|
+
valid: null,
|
|
650
|
+
remote: null,
|
|
651
|
+
};
|
|
652
|
+
let git = {
|
|
653
|
+
isRepository: null,
|
|
654
|
+
hasCommits: null,
|
|
655
|
+
hasUncommittedChanges: null,
|
|
656
|
+
hasRemote: null,
|
|
657
|
+
originUrl: null,
|
|
658
|
+
};
|
|
659
|
+
let openspecRoot = await inspectOpenSpecRoot(root);
|
|
660
|
+
if (kind === 'missing') {
|
|
661
|
+
diagnostics.push(makeStoreDiagnostic('error', 'store_root_missing', 'Store location does not exist.', {
|
|
662
|
+
target: 'store.root',
|
|
663
|
+
fix: `Run openspec store register /path/to/${entry.id} --id ${entry.id}.`,
|
|
664
|
+
}));
|
|
665
|
+
}
|
|
666
|
+
else if (kind !== 'directory') {
|
|
667
|
+
diagnostics.push(makeStoreDiagnostic('error', 'store_root_not_directory', 'Store location is not a directory.', {
|
|
668
|
+
target: 'store.root',
|
|
669
|
+
fix: 'Register a directory path for this store.',
|
|
670
|
+
}));
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
openspecRoot = await inspectOpenSpecRoot(root);
|
|
674
|
+
diagnostics.push(...openspecRoot.diagnostics);
|
|
675
|
+
try {
|
|
676
|
+
const parsed = await readOptionalStoreMetadataState(root);
|
|
677
|
+
if (!parsed) {
|
|
678
|
+
metadata = { present: false, valid: false, remote: null };
|
|
679
|
+
diagnostics.push(makeStoreDiagnostic('error', 'store_metadata_missing', 'Store metadata is missing.', {
|
|
680
|
+
target: 'store.metadata',
|
|
681
|
+
fix: `Create ${metadataPath} or rerun store register.`,
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
else if (parsed.id !== entry.id) {
|
|
685
|
+
metadata = { present: true, valid: false, id: parsed.id, remote: null };
|
|
686
|
+
diagnostics.push(makeStoreDiagnostic('error', 'store_metadata_id_mismatch', `Store metadata id '${parsed.id}' does not match registry id '${entry.id}'.`, {
|
|
687
|
+
target: 'store.metadata',
|
|
688
|
+
fix: 'Repair the local registry or store metadata so the ids match.',
|
|
689
|
+
}));
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
metadata = {
|
|
693
|
+
present: true,
|
|
694
|
+
valid: true,
|
|
695
|
+
id: parsed.id,
|
|
696
|
+
remote: parsed.remote ?? null,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
metadata = { present: true, valid: false, remote: null };
|
|
702
|
+
diagnostics.push(doctorStatusForError(error, 'store_metadata_invalid', 'store.metadata', `Repair ${metadataPath}.`));
|
|
703
|
+
}
|
|
704
|
+
const isRepository = await isGitRepositoryAtRoot(root);
|
|
705
|
+
git = {
|
|
706
|
+
isRepository,
|
|
707
|
+
hasCommits: null,
|
|
708
|
+
hasUncommittedChanges: null,
|
|
709
|
+
hasRemote: null,
|
|
710
|
+
originUrl: null,
|
|
711
|
+
};
|
|
712
|
+
// Read-only Git facts; doctor reports and never repairs.
|
|
713
|
+
if (isRepository) {
|
|
714
|
+
git.hasCommits = await gitHasCommits(root);
|
|
715
|
+
git.hasUncommittedChanges = await gitHasUncommittedChanges(root);
|
|
716
|
+
git.hasRemote = await gitHasRemote(root);
|
|
717
|
+
git.originUrl = await gitOriginUrl(root);
|
|
718
|
+
if (git.hasCommits === false) {
|
|
719
|
+
diagnostics.push(makeStoreDiagnostic('warning', 'store_git_no_commits', 'Git repository has no commits yet; clones of this store will be empty until an initial commit exists.', {
|
|
720
|
+
target: 'store.git',
|
|
721
|
+
fix: 'Commit the store files, then push to share them.',
|
|
722
|
+
}));
|
|
723
|
+
}
|
|
724
|
+
else if (git.hasCommits === true) {
|
|
725
|
+
const fragileDirs = [];
|
|
726
|
+
for (const relativeDir of ANCHORED_OPENSPEC_DIRS) {
|
|
727
|
+
const dirKind = await pathKind(path.join(root, relativeDir));
|
|
728
|
+
if (dirKind !== 'directory')
|
|
729
|
+
continue;
|
|
730
|
+
if ((await gitDirectoryHasTrackedFiles(root, relativeDir)) === false) {
|
|
731
|
+
fragileDirs.push(`${relativeDir}/`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (fragileDirs.length > 0) {
|
|
735
|
+
diagnostics.push(makeStoreDiagnostic('warning', 'store_clone_fragile_directories', `These directories contain no tracked files and will be lost in clones: ${fragileDirs.join(', ')}.`, {
|
|
736
|
+
target: 'store.git',
|
|
737
|
+
fix: `Track a file in each directory (for example ${DIRECTORY_ANCHOR_FILE_NAME}) and commit it.`,
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
id: entry.id,
|
|
745
|
+
root,
|
|
746
|
+
metadataPath,
|
|
747
|
+
openspecRoot,
|
|
748
|
+
metadata,
|
|
749
|
+
git,
|
|
750
|
+
diagnostics,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
export async function doctorStores(id) {
|
|
754
|
+
const selectedId = id !== undefined ? validateStoreId(id) : undefined;
|
|
755
|
+
const registry = await readStoreRegistryState();
|
|
756
|
+
if (!registry) {
|
|
757
|
+
if (selectedId !== undefined) {
|
|
758
|
+
throw new StoreError(`Unknown store '${selectedId}'.`, 'store_not_found', {
|
|
759
|
+
target: 'store.id',
|
|
760
|
+
fix: 'Run openspec store list to see registered stores.',
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
return { stores: [], diagnostics: [] };
|
|
764
|
+
}
|
|
765
|
+
const entries = listStoreRegistryEntries(registry);
|
|
766
|
+
const selected = selectedId
|
|
767
|
+
? entries.filter((entry) => entry.id === selectedId)
|
|
768
|
+
: entries;
|
|
769
|
+
if (selectedId && selected.length === 0) {
|
|
770
|
+
throw new StoreError(`Unknown store '${selectedId}'.`, 'store_not_found', {
|
|
771
|
+
target: 'store.id',
|
|
772
|
+
fix: 'Run openspec store list to see registered stores.',
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
stores: await Promise.all(selected.map(inspectStore)),
|
|
777
|
+
diagnostics: [],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
export function normalizeStorePathForComparison(targetPath) {
|
|
781
|
+
return FileSystemUtils.canonicalizeExistingPath(targetPath);
|
|
782
|
+
}
|
|
783
|
+
//# sourceMappingURL=operations.js.map
|