@fission-ai/openspec 1.4.1 → 1.6.0-beta.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/README.md +37 -4
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +95 -95
- package/dist/commands/change.d.ts +5 -2
- package/dist/commands/change.js +23 -55
- package/dist/commands/config.js +6 -65
- 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/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 +11 -6
- package/dist/commands/show.js +70 -25
- package/dist/commands/spec.d.ts +5 -1
- package/dist/commands/spec.js +15 -4
- package/dist/commands/store.d.ts +3 -0
- package/dist/commands/store.js +547 -0
- package/dist/commands/validate.d.ts +10 -0
- package/dist/commands/validate.js +88 -40
- package/dist/commands/workflow/index.d.ts +0 -2
- package/dist/commands/workflow/index.js +0 -1
- package/dist/commands/workflow/instructions.d.ts +11 -1
- package/dist/commands/workflow/instructions.js +71 -22
- package/dist/commands/workflow/new-change.d.ts +6 -3
- package/dist/commands/workflow/new-change.js +42 -77
- package/dist/commands/workflow/shared.d.ts +15 -3
- package/dist/commands/workflow/shared.js +20 -3
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +19 -17
- 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 +17 -6
- package/dist/core/archive.js +220 -68
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +18 -13
- package/dist/core/artifact-graph/instruction-loader.js +8 -31
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +4 -2
- package/dist/core/change-status-policy.d.ts +5 -18
- package/dist/core/change-status-policy.js +2 -37
- package/dist/core/command-generation/adapters/bob.js +1 -14
- package/dist/core/command-generation/adapters/claude.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +4 -15
- package/dist/core/command-generation/adapters/cursor.js +1 -14
- package/dist/core/command-generation/adapters/index.d.ts +2 -0
- package/dist/core/command-generation/adapters/index.js +2 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +44 -0
- package/dist/core/command-generation/adapters/pi.js +1 -14
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +48 -0
- package/dist/core/command-generation/adapters/windsurf.js +1 -14
- package/dist/core/command-generation/registry.js +4 -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.js +87 -289
- package/dist/core/completions/generators/zsh-generator.js +3 -1
- package/dist/core/completions/installers/bash-installer.js +3 -0
- package/dist/core/completions/installers/fish-installer.js +8 -0
- package/dist/core/completions/installers/powershell-installer.js +30 -2
- package/dist/core/completions/installers/zsh-installer.js +3 -0
- package/dist/core/completions/shared-flags.d.ts +1 -0
- package/dist/core/completions/shared-flags.js +5 -0
- package/dist/core/config-schema.d.ts +2 -1
- package/dist/core/config-schema.js +26 -0
- package/dist/core/config.js +1 -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 +2 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +3 -3
- package/dist/core/init.js +27 -6
- package/dist/core/list.d.ts +2 -0
- package/dist/core/list.js +37 -14
- 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 +195 -0
- package/dist/core/parsers/change-parser.js +2 -1
- package/dist/core/parsers/markdown-parser.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +5 -59
- package/dist/core/parsers/requirement-blocks.d.ts +11 -0
- package/dist/core/parsers/requirement-blocks.js +42 -8
- package/dist/core/parsers/requirement-text.d.ts +55 -0
- package/dist/core/parsers/requirement-text.js +136 -0
- package/dist/core/planning-home.d.ts +1 -6
- package/dist/core/planning-home.js +2 -43
- package/dist/core/profile-sync-drift.js +1 -0
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -1
- package/dist/core/project-config.d.ts +44 -1
- package/dist/core/project-config.js +160 -10
- 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.js +7 -2
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/skill-generation.js +5 -1
- package/dist/core/shared/tool-detection.d.ts +2 -2
- package/dist/core/shared/tool-detection.js +2 -0
- package/dist/core/specs-apply.d.ts +6 -1
- package/dist/core/specs-apply.js +41 -5
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/{context-store → store}/errors.js +3 -3
- 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/{context-store → store}/index.d.ts +0 -1
- package/dist/core/{context-store → store}/index.js +0 -1
- package/dist/core/store/operations.d.ts +114 -0
- package/dist/core/store/operations.js +803 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -0
- package/dist/core/templates/skill-templates.d.ts +1 -0
- package/dist/core/templates/skill-templates.js +1 -0
- package/dist/core/templates/workflows/apply-change.js +5 -4
- package/dist/core/templates/workflows/archive-change.js +5 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
- package/dist/core/templates/workflows/continue-change.js +7 -2
- package/dist/core/templates/workflows/explore.js +5 -0
- package/dist/core/templates/workflows/ff-change.js +5 -0
- package/dist/core/templates/workflows/new-change.js +5 -0
- package/dist/core/templates/workflows/onboard.js +4 -1
- package/dist/core/templates/workflows/propose.js +5 -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.js +5 -4
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +167 -0
- package/dist/core/templates/workflows/verify-change.js +5 -4
- package/dist/core/update.js +4 -4
- package/dist/core/validation/validator.d.ts +7 -0
- package/dist/core/validation/validator.js +101 -38
- package/dist/core/view.js +1 -1
- 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/utils/change-metadata.d.ts +1 -0
- package/dist/utils/change-metadata.js +1 -1
- package/dist/utils/change-utils.js +15 -0
- package/dist/utils/file-system.js +36 -17
- package/dist/utils/task-progress.d.ts +10 -1
- package/dist/utils/task-progress.js +66 -2
- package/package.json +26 -23
- package/dist/commands/context-store.d.ts +0 -3
- package/dist/commands/context-store.js +0 -475
- package/dist/commands/initiative.d.ts +0 -13
- package/dist/commands/initiative.js +0 -318
- package/dist/commands/workflow/initiative-link.d.ts +0 -24
- package/dist/commands/workflow/initiative-link.js +0 -47
- package/dist/commands/workflow/set-change.d.ts +0 -13
- package/dist/commands/workflow/set-change.js +0 -87
- package/dist/commands/workspace/context-status.d.ts +0 -4
- package/dist/commands/workspace/context-status.js +0 -59
- package/dist/commands/workspace/open-target-selection.d.ts +0 -13
- package/dist/commands/workspace/open-target-selection.js +0 -146
- package/dist/commands/workspace/open-view.d.ts +0 -62
- package/dist/commands/workspace/open-view.js +0 -249
- package/dist/commands/workspace/open.d.ts +0 -37
- package/dist/commands/workspace/open.js +0 -110
- package/dist/commands/workspace/opener-selection.d.ts +0 -11
- package/dist/commands/workspace/opener-selection.js +0 -98
- package/dist/commands/workspace/operations.d.ts +0 -29
- package/dist/commands/workspace/operations.js +0 -543
- package/dist/commands/workspace/prompt-theme.d.ts +0 -29
- package/dist/commands/workspace/prompt-theme.js +0 -24
- package/dist/commands/workspace/registration.d.ts +0 -13
- package/dist/commands/workspace/registration.js +0 -84
- package/dist/commands/workspace/selection.d.ts +0 -8
- package/dist/commands/workspace/selection.js +0 -104
- package/dist/commands/workspace/setup-prompts.d.ts +0 -13
- package/dist/commands/workspace/setup-prompts.js +0 -121
- package/dist/commands/workspace/types.d.ts +0 -103
- package/dist/commands/workspace/types.js +0 -36
- package/dist/commands/workspace.d.ts +0 -5
- package/dist/commands/workspace.js +0 -574
- package/dist/core/collections/index.d.ts +0 -3
- package/dist/core/collections/index.js +0 -3
- package/dist/core/collections/initiatives/collection.d.ts +0 -4
- package/dist/core/collections/initiatives/collection.js +0 -17
- package/dist/core/collections/initiatives/index.d.ts +0 -6
- package/dist/core/collections/initiatives/index.js +0 -6
- package/dist/core/collections/initiatives/operations.d.ts +0 -49
- package/dist/core/collections/initiatives/operations.js +0 -175
- package/dist/core/collections/initiatives/resolution.d.ts +0 -87
- package/dist/core/collections/initiatives/resolution.js +0 -374
- package/dist/core/collections/initiatives/schema.d.ts +0 -41
- package/dist/core/collections/initiatives/schema.js +0 -134
- package/dist/core/collections/initiatives/templates.d.ts +0 -12
- package/dist/core/collections/initiatives/templates.js +0 -90
- package/dist/core/collections/runtime.d.ts +0 -46
- package/dist/core/collections/runtime.js +0 -194
- package/dist/core/context-store/binding.d.ts +0 -53
- package/dist/core/context-store/binding.js +0 -197
- package/dist/core/context-store/errors.d.ts +0 -20
- package/dist/core/context-store/foundation.d.ts +0 -55
- package/dist/core/context-store/foundation.js +0 -321
- package/dist/core/context-store/operations.d.ts +0 -85
- package/dist/core/context-store/operations.js +0 -528
- package/dist/core/context-store/registry.d.ts +0 -45
- package/dist/core/context-store/registry.js +0 -229
- package/dist/core/workspace/foundation.d.ts +0 -67
- package/dist/core/workspace/foundation.js +0 -295
- package/dist/core/workspace/index.d.ts +0 -8
- package/dist/core/workspace/index.js +0 -8
- package/dist/core/workspace/legacy-state.d.ts +0 -28
- package/dist/core/workspace/legacy-state.js +0 -200
- package/dist/core/workspace/link-input.d.ts +0 -9
- package/dist/core/workspace/link-input.js +0 -32
- package/dist/core/workspace/open-surface.d.ts +0 -45
- package/dist/core/workspace/open-surface.js +0 -215
- package/dist/core/workspace/openers.d.ts +0 -21
- package/dist/core/workspace/openers.js +0 -124
- package/dist/core/workspace/registry.d.ts +0 -24
- package/dist/core/workspace/registry.js +0 -146
- package/dist/core/workspace/skills.d.ts +0 -57
- package/dist/core/workspace/skills.js +0 -334
- package/dist/core/workspace/state-io.d.ts +0 -10
- package/dist/core/workspace/state-io.js +0 -121
- package/schemas/workspace-planning/schema.yaml +0 -72
- package/schemas/workspace-planning/templates/design.md +0 -33
- package/schemas/workspace-planning/templates/proposal.md +0 -28
- package/schemas/workspace-planning/templates/spec.md +0 -9
- package/schemas/workspace-planning/templates/tasks.md +0 -15
package/dist/core/specs-apply.js
CHANGED
|
@@ -57,7 +57,7 @@ export async function findSpecUpdates(changeDir, mainSpecsDir) {
|
|
|
57
57
|
* Build an updated spec by applying delta operations.
|
|
58
58
|
* Returns the rebuilt content and counts of operations.
|
|
59
59
|
*/
|
|
60
|
-
export async function buildUpdatedSpec(update, changeName) {
|
|
60
|
+
export async function buildUpdatedSpec(update, changeName, options = {}) {
|
|
61
61
|
// Read change spec content (delta-format expected)
|
|
62
62
|
const changeContent = await fs.readFile(update.source, 'utf-8');
|
|
63
63
|
// Parse deltas from the change spec file
|
|
@@ -148,7 +148,7 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
148
148
|
throw new Error(`${specName}: target spec does not exist; only ADDED requirements are allowed for new specs. MODIFIED and RENAMED operations require an existing spec.`);
|
|
149
149
|
}
|
|
150
150
|
// Warn about REMOVED requirements being ignored for new specs
|
|
151
|
-
if (plan.removed.length > 0) {
|
|
151
|
+
if (plan.removed.length > 0 && !options.silent) {
|
|
152
152
|
console.log(chalk.yellow(`⚠️ Warning: ${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`));
|
|
153
153
|
}
|
|
154
154
|
isNewSpec = true;
|
|
@@ -207,7 +207,8 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
207
207
|
// MODIFIED
|
|
208
208
|
for (const mod of plan.modified) {
|
|
209
209
|
const key = normalizeRequirementName(mod.name);
|
|
210
|
-
|
|
210
|
+
const currentBlock = nameToBlock.get(key);
|
|
211
|
+
if (!currentBlock) {
|
|
211
212
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
|
|
212
213
|
}
|
|
213
214
|
// Replace block with provided raw (ensure header line matches key)
|
|
@@ -215,6 +216,10 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
215
216
|
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
|
216
217
|
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`);
|
|
217
218
|
}
|
|
219
|
+
const missingScenarios = findMissingCurrentScenarios(currentBlock, mod);
|
|
220
|
+
if (missingScenarios.length > 0) {
|
|
221
|
+
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - current spec contains scenario(s) not present in the modified block: ${missingScenarios.map(name => `"${name}"`).join(', ')}. Refresh the change spec before archiving to avoid dropping scenarios.`);
|
|
222
|
+
}
|
|
218
223
|
nameToBlock.set(key, mod);
|
|
219
224
|
}
|
|
220
225
|
// ADDED
|
|
@@ -265,13 +270,15 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
265
270
|
/**
|
|
266
271
|
* Write an updated spec to disk.
|
|
267
272
|
*/
|
|
268
|
-
export async function writeUpdatedSpec(update, rebuilt, counts) {
|
|
273
|
+
export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
|
|
269
274
|
// Create target directory if needed
|
|
270
275
|
const targetDir = path.dirname(update.target);
|
|
271
276
|
await fs.mkdir(targetDir, { recursive: true });
|
|
272
277
|
await fs.writeFile(update.target, rebuilt);
|
|
278
|
+
if (options.silent)
|
|
279
|
+
return;
|
|
273
280
|
const specName = path.basename(path.dirname(update.target));
|
|
274
|
-
console.log(`Applying changes to openspec/specs/${specName}/spec.md:`);
|
|
281
|
+
console.log(`Applying changes to ${options.displayPath ?? `openspec/specs/${specName}/spec.md`}:`);
|
|
275
282
|
if (counts.added)
|
|
276
283
|
console.log(` + ${counts.added} added`);
|
|
277
284
|
if (counts.modified)
|
|
@@ -288,6 +295,35 @@ export function buildSpecSkeleton(specFolderName, changeName) {
|
|
|
288
295
|
const titleBase = specFolderName;
|
|
289
296
|
return `# ${titleBase} Specification\n\n## Purpose\nTBD - created by archiving change ${changeName}. Update Purpose after archive.\n\n## Requirements\n`;
|
|
290
297
|
}
|
|
298
|
+
function findMissingCurrentScenarios(current, incoming) {
|
|
299
|
+
const incomingScenarioNames = new Set(parseScenarioBlocks(incoming.raw).map((scenario) => scenario.name));
|
|
300
|
+
return parseScenarioBlocks(current.raw)
|
|
301
|
+
.filter((scenario) => !incomingScenarioNames.has(scenario.name))
|
|
302
|
+
.map((scenario) => scenario.name);
|
|
303
|
+
}
|
|
304
|
+
function parseScenarioBlocks(requirementRaw) {
|
|
305
|
+
const lines = requirementRaw.replace(/\r\n?/g, '\n').split('\n');
|
|
306
|
+
const scenarios = [];
|
|
307
|
+
let index = 0;
|
|
308
|
+
while (index < lines.length) {
|
|
309
|
+
const headerMatch = lines[index].match(/^####\s*Scenario:\s*(.+)\s*$/);
|
|
310
|
+
if (!headerMatch) {
|
|
311
|
+
index++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const start = index;
|
|
315
|
+
const name = headerMatch[1].trim();
|
|
316
|
+
index++;
|
|
317
|
+
while (index < lines.length && !/^####\s*Scenario:\s*(.+)\s*$/.test(lines[index])) {
|
|
318
|
+
index++;
|
|
319
|
+
}
|
|
320
|
+
scenarios.push({
|
|
321
|
+
name,
|
|
322
|
+
raw: lines.slice(start, index).join('\n').trimEnd(),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return scenarios;
|
|
326
|
+
}
|
|
291
327
|
/**
|
|
292
328
|
* Apply all delta specs from a change to main specs.
|
|
293
329
|
*
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type StoreDiagnosticSeverity = 'error' | 'warning' | 'info';
|
|
2
|
+
export interface StoreDiagnostic {
|
|
3
|
+
severity: StoreDiagnosticSeverity;
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
target?: string;
|
|
7
|
+
fix?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class StoreError extends Error {
|
|
10
|
+
readonly diagnostic: StoreDiagnostic;
|
|
11
|
+
constructor(message: string, code: string, options?: {
|
|
12
|
+
target?: string;
|
|
13
|
+
fix?: string;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export declare function makeStoreDiagnostic(severity: StoreDiagnosticSeverity, code: string, message: string, options?: {
|
|
17
|
+
target?: string;
|
|
18
|
+
fix?: string;
|
|
19
|
+
}): StoreDiagnostic;
|
|
20
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export class
|
|
1
|
+
export class StoreError extends Error {
|
|
2
2
|
diagnostic;
|
|
3
3
|
constructor(message, code, options = {}) {
|
|
4
4
|
super(message);
|
|
5
|
-
this.name = '
|
|
5
|
+
this.name = 'StoreError';
|
|
6
6
|
this.diagnostic = {
|
|
7
7
|
severity: 'error',
|
|
8
8
|
code,
|
|
@@ -11,7 +11,7 @@ export class ContextStoreError extends Error {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
export function
|
|
14
|
+
export function makeStoreDiagnostic(severity, code, message, options = {}) {
|
|
15
15
|
return {
|
|
16
16
|
severity,
|
|
17
17
|
code,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const STORE_METADATA_DIR_NAME = ".openspec-store";
|
|
2
|
+
export declare const STORE_METADATA_FILE_NAME = "store.yaml";
|
|
3
|
+
export declare const STORES_DIR_NAME = "stores";
|
|
4
|
+
export declare const STORE_REGISTRY_FILE_NAME = "registry.yaml";
|
|
5
|
+
export interface StorePathOptions {
|
|
6
|
+
globalDataDir?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface StoreGitBackendConfig {
|
|
9
|
+
type: 'git';
|
|
10
|
+
local_path: string;
|
|
11
|
+
remote?: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
}
|
|
14
|
+
export type StoreBackendConfig = StoreGitBackendConfig;
|
|
15
|
+
export interface StoreRegistryEntryState {
|
|
16
|
+
backend: StoreBackendConfig;
|
|
17
|
+
}
|
|
18
|
+
export interface StoreRegistryState {
|
|
19
|
+
version: 1;
|
|
20
|
+
stores: Record<string, StoreRegistryEntryState>;
|
|
21
|
+
}
|
|
22
|
+
export interface StoreRegistryEntry {
|
|
23
|
+
id: string;
|
|
24
|
+
backend: StoreBackendConfig;
|
|
25
|
+
}
|
|
26
|
+
export interface StoreMetadataState {
|
|
27
|
+
version: 1;
|
|
28
|
+
id: string;
|
|
29
|
+
/** Canonical clone source, team-authored. Optional (slice 3.3). */
|
|
30
|
+
remote?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ResolveGitStoreBackendInput {
|
|
33
|
+
localPath: string;
|
|
34
|
+
remote?: string;
|
|
35
|
+
branch?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function getStoresDir(options?: StorePathOptions): string;
|
|
38
|
+
export declare function getStoreRegistryPath(options?: StorePathOptions): string;
|
|
39
|
+
export declare function getStoreMetadataDir(storeRoot: string): string;
|
|
40
|
+
export declare function getStoreMetadataPath(storeRoot: string): string;
|
|
41
|
+
export declare function validateStoreId(id: string): string;
|
|
42
|
+
export declare function isValidStoreId(id: string): boolean;
|
|
43
|
+
export declare function parseStoreRegistryState(content: string): StoreRegistryState;
|
|
44
|
+
export declare function parseStoreMetadataState(content: string): StoreMetadataState;
|
|
45
|
+
export declare function serializeStoreRegistryState(state: StoreRegistryState): string;
|
|
46
|
+
export declare function serializeStoreMetadataState(state: StoreMetadataState): string;
|
|
47
|
+
export declare function listStoreRegistryEntries(registry: StoreRegistryState): StoreRegistryEntry[];
|
|
48
|
+
export declare function isStoreRoot(candidateRoot: string): Promise<boolean>;
|
|
49
|
+
export declare function readStoreRegistryState(options?: StorePathOptions): Promise<StoreRegistryState | null>;
|
|
50
|
+
export declare function writeStoreRegistryState(state: StoreRegistryState, options?: StorePathOptions): Promise<void>;
|
|
51
|
+
export declare function updateStoreRegistryState(updater: (state: StoreRegistryState | null) => StoreRegistryState | Promise<StoreRegistryState>, options?: StorePathOptions): Promise<StoreRegistryState>;
|
|
52
|
+
export declare function readStoreMetadataState(storeRoot: string): Promise<StoreMetadataState>;
|
|
53
|
+
export declare function readOptionalStoreMetadataState(storeRoot: string): Promise<StoreMetadataState | null>;
|
|
54
|
+
export declare function writeStoreMetadataState(storeRoot: string, state: StoreMetadataState): Promise<void>;
|
|
55
|
+
export declare function resolveGitStoreBackendConfig(input: ResolveGitStoreBackendInput, cwd?: string): Promise<StoreGitBackendConfig>;
|
|
56
|
+
//# sourceMappingURL=foundation.d.ts.map
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import * as nodeFs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { folderStyleNameProblem, isKebabId, KEBAB_ID_DESCRIPTION, KEBAB_ID_FIX, } from '../id.js';
|
|
6
|
+
import { getGlobalDataDir } from '../global-config.js';
|
|
7
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
8
|
+
import { acquireFileLock, isNodeErrorCode, makeLockErrorFactory, pathIsDirectory, pathIsFile, releaseFileLock, writeFileAtomically, } from '../file-state.js';
|
|
9
|
+
import { formatZodIssues } from '../zod-issues.js';
|
|
10
|
+
import { StoreError } from './errors.js';
|
|
11
|
+
const fs = nodeFs.promises;
|
|
12
|
+
export const STORE_METADATA_DIR_NAME = '.openspec-store';
|
|
13
|
+
export const STORE_METADATA_FILE_NAME = 'store.yaml';
|
|
14
|
+
export const STORES_DIR_NAME = 'stores';
|
|
15
|
+
export const STORE_REGISTRY_FILE_NAME = 'registry.yaml';
|
|
16
|
+
function joinStorePath(basePath, ...segments) {
|
|
17
|
+
return FileSystemUtils.joinPath(basePath, ...segments);
|
|
18
|
+
}
|
|
19
|
+
export function getStoresDir(options = {}) {
|
|
20
|
+
return joinStorePath(options.globalDataDir ?? getGlobalDataDir(), STORES_DIR_NAME);
|
|
21
|
+
}
|
|
22
|
+
export function getStoreRegistryPath(options = {}) {
|
|
23
|
+
return joinStorePath(getStoresDir(options), STORE_REGISTRY_FILE_NAME);
|
|
24
|
+
}
|
|
25
|
+
export function getStoreMetadataDir(storeRoot) {
|
|
26
|
+
return joinStorePath(storeRoot, STORE_METADATA_DIR_NAME);
|
|
27
|
+
}
|
|
28
|
+
export function getStoreMetadataPath(storeRoot) {
|
|
29
|
+
return joinStorePath(getStoreMetadataDir(storeRoot), STORE_METADATA_FILE_NAME);
|
|
30
|
+
}
|
|
31
|
+
export function validateStoreId(id) {
|
|
32
|
+
const folderProblem = folderStyleNameProblem(id, 'Store id');
|
|
33
|
+
if (folderProblem !== null) {
|
|
34
|
+
throw new StoreError(folderProblem, 'invalid_store_id', {
|
|
35
|
+
target: 'store.id',
|
|
36
|
+
fix: KEBAB_ID_FIX,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (!isKebabId(id)) {
|
|
40
|
+
throw new StoreError(`Store id ${KEBAB_ID_DESCRIPTION}`, 'invalid_store_id', {
|
|
41
|
+
target: 'store.id',
|
|
42
|
+
fix: KEBAB_ID_FIX,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return id;
|
|
46
|
+
}
|
|
47
|
+
export function isValidStoreId(id) {
|
|
48
|
+
try {
|
|
49
|
+
validateStoreId(id);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isFileNotFoundError(error) {
|
|
57
|
+
return isNodeErrorCode(error, 'ENOENT');
|
|
58
|
+
}
|
|
59
|
+
function normalizeExistingPathForStorage(existingPath) {
|
|
60
|
+
return FileSystemUtils.canonicalizeExistingPath(existingPath);
|
|
61
|
+
}
|
|
62
|
+
function nonEmptyOptionalString() {
|
|
63
|
+
return z.string().min(1).optional();
|
|
64
|
+
}
|
|
65
|
+
const GitBackendConfigSchema = z.object({
|
|
66
|
+
type: z.literal('git'),
|
|
67
|
+
local_path: z.string().min(1),
|
|
68
|
+
remote: nonEmptyOptionalString(),
|
|
69
|
+
branch: nonEmptyOptionalString(),
|
|
70
|
+
}).strict();
|
|
71
|
+
const RegistryEntrySchema = z.object({
|
|
72
|
+
backend: GitBackendConfigSchema,
|
|
73
|
+
}).strict();
|
|
74
|
+
const RegistryStateSchema = z.object({
|
|
75
|
+
version: z.literal(1),
|
|
76
|
+
stores: z.record(z.string(), RegistryEntrySchema),
|
|
77
|
+
// Legacy code-checkout map data is tolerated on read and dropped on
|
|
78
|
+
// the next write.
|
|
79
|
+
repos: z.unknown().optional(),
|
|
80
|
+
}).strict();
|
|
81
|
+
const MetadataStateSchema = z.object({
|
|
82
|
+
version: z.literal(1),
|
|
83
|
+
id: z.string(),
|
|
84
|
+
remote: nonEmptyOptionalString(),
|
|
85
|
+
}).strict();
|
|
86
|
+
function storeStateDiagnostic(label) {
|
|
87
|
+
if (label.includes('metadata')) {
|
|
88
|
+
return {
|
|
89
|
+
code: 'invalid_store_metadata',
|
|
90
|
+
target: 'store.metadata',
|
|
91
|
+
fix: 'Repair .openspec-store/store.yaml.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
code: 'invalid_store_registry',
|
|
96
|
+
target: 'store.registry',
|
|
97
|
+
fix: `Repair or remove ${getStoreRegistryPath({})}.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function invalidStoreStateError(label, message) {
|
|
101
|
+
const diagnostic = storeStateDiagnostic(label);
|
|
102
|
+
return new StoreError(`Invalid ${label}: ${message}`, diagnostic.code, {
|
|
103
|
+
target: diagnostic.target,
|
|
104
|
+
fix: diagnostic.fix,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function parseYamlObject(content, label) {
|
|
108
|
+
try {
|
|
109
|
+
return parseYaml(content);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
throw invalidStoreStateError(label, message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function assertValidStoreIds(ids, label) {
|
|
117
|
+
for (const id of ids) {
|
|
118
|
+
if (!isKebabId(id)) {
|
|
119
|
+
throw invalidStoreStateError(label, `'${id}': ${KEBAB_ID_DESCRIPTION}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function parseStoreRegistryState(content) {
|
|
124
|
+
const raw = parseYamlObject(content, 'store registry state');
|
|
125
|
+
const result = RegistryStateSchema.safeParse(raw);
|
|
126
|
+
if (!result.success) {
|
|
127
|
+
throw invalidStoreStateError('store registry state', formatZodIssues(result.error));
|
|
128
|
+
}
|
|
129
|
+
assertValidStoreIds(Object.keys(result.data.stores), 'store id');
|
|
130
|
+
return {
|
|
131
|
+
version: 1,
|
|
132
|
+
stores: result.data.stores,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function parseStoreMetadataState(content) {
|
|
136
|
+
const raw = parseYamlObject(content, 'store metadata state');
|
|
137
|
+
const result = MetadataStateSchema.safeParse(raw);
|
|
138
|
+
if (!result.success) {
|
|
139
|
+
throw invalidStoreStateError('store metadata state', formatZodIssues(result.error));
|
|
140
|
+
}
|
|
141
|
+
validateStoreId(result.data.id);
|
|
142
|
+
return {
|
|
143
|
+
version: 1,
|
|
144
|
+
id: result.data.id,
|
|
145
|
+
...(result.data.remote !== undefined ? { remote: result.data.remote } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function serializeStoreRegistryState(state) {
|
|
149
|
+
const result = RegistryStateSchema.safeParse(state);
|
|
150
|
+
if (!result.success) {
|
|
151
|
+
throw invalidStoreStateError('store registry state', formatZodIssues(result.error));
|
|
152
|
+
}
|
|
153
|
+
assertValidStoreIds(Object.keys(result.data.stores), 'store id');
|
|
154
|
+
return stringifyYaml({
|
|
155
|
+
version: 1,
|
|
156
|
+
stores: result.data.stores,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export function serializeStoreMetadataState(state) {
|
|
160
|
+
const result = MetadataStateSchema.safeParse(state);
|
|
161
|
+
if (!result.success) {
|
|
162
|
+
throw invalidStoreStateError('store metadata state', formatZodIssues(result.error));
|
|
163
|
+
}
|
|
164
|
+
validateStoreId(result.data.id);
|
|
165
|
+
return stringifyYaml({
|
|
166
|
+
version: 1,
|
|
167
|
+
id: result.data.id,
|
|
168
|
+
...(result.data.remote !== undefined ? { remote: result.data.remote } : {}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
export function listStoreRegistryEntries(registry) {
|
|
172
|
+
return Object.entries(registry.stores)
|
|
173
|
+
.map(([id, store]) => ({ id, backend: store.backend }))
|
|
174
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
175
|
+
}
|
|
176
|
+
export async function isStoreRoot(candidateRoot) {
|
|
177
|
+
return pathIsFile(getStoreMetadataPath(candidateRoot));
|
|
178
|
+
}
|
|
179
|
+
export async function readStoreRegistryState(options = {}) {
|
|
180
|
+
const registryPath = getStoreRegistryPath(options);
|
|
181
|
+
if (!(await pathIsFile(registryPath))) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return parseStoreRegistryState(await fs.readFile(registryPath, 'utf-8'));
|
|
185
|
+
}
|
|
186
|
+
export async function writeStoreRegistryState(state, options = {}) {
|
|
187
|
+
await writeFileAtomically(getStoreRegistryPath(options), serializeStoreRegistryState(state));
|
|
188
|
+
}
|
|
189
|
+
const storeRegistryLockError = makeLockErrorFactory({
|
|
190
|
+
createSubject: 'the registry lock file',
|
|
191
|
+
busyMessage: 'Store registry is busy.',
|
|
192
|
+
code: 'store_registry_busy',
|
|
193
|
+
target: 'store.registry',
|
|
194
|
+
});
|
|
195
|
+
export async function updateStoreRegistryState(updater, options = {}) {
|
|
196
|
+
const registryPath = getStoreRegistryPath(options);
|
|
197
|
+
const lockPath = `${registryPath}.lock`;
|
|
198
|
+
const lock = await acquireFileLock({
|
|
199
|
+
lockPath,
|
|
200
|
+
errorFor: storeRegistryLockError,
|
|
201
|
+
});
|
|
202
|
+
try {
|
|
203
|
+
const next = await updater(await readStoreRegistryState(options));
|
|
204
|
+
await writeStoreRegistryState(next, options);
|
|
205
|
+
return next;
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await releaseFileLock(lock, lockPath);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export async function readStoreMetadataState(storeRoot) {
|
|
212
|
+
return parseStoreMetadataState(await fs.readFile(getStoreMetadataPath(storeRoot), 'utf-8'));
|
|
213
|
+
}
|
|
214
|
+
export async function readOptionalStoreMetadataState(storeRoot) {
|
|
215
|
+
try {
|
|
216
|
+
return await readStoreMetadataState(storeRoot);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (isFileNotFoundError(error)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
export async function writeStoreMetadataState(storeRoot, state) {
|
|
226
|
+
await FileSystemUtils.writeFile(getStoreMetadataPath(storeRoot), serializeStoreMetadataState(state));
|
|
227
|
+
}
|
|
228
|
+
export async function resolveGitStoreBackendConfig(input, cwd = process.cwd()) {
|
|
229
|
+
if (input.localPath.length === 0) {
|
|
230
|
+
throw new Error('Store local path must not be empty.');
|
|
231
|
+
}
|
|
232
|
+
const resolvedPath = path.isAbsolute(input.localPath)
|
|
233
|
+
? path.resolve(input.localPath)
|
|
234
|
+
: path.resolve(cwd, input.localPath);
|
|
235
|
+
if (!(await pathIsDirectory(resolvedPath))) {
|
|
236
|
+
throw new Error(`Store local path does not exist: ${input.localPath}`);
|
|
237
|
+
}
|
|
238
|
+
if (input.remote !== undefined && input.remote.length === 0) {
|
|
239
|
+
throw new Error('Store backend remote must not be empty when provided.');
|
|
240
|
+
}
|
|
241
|
+
if (input.branch !== undefined && input.branch.length === 0) {
|
|
242
|
+
throw new Error('Store branch must not be empty when provided.');
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
type: 'git',
|
|
246
|
+
local_path: normalizeExistingPathForStorage(resolvedPath),
|
|
247
|
+
...(input.remote ? { remote: input.remote } : {}),
|
|
248
|
+
...(input.branch ? { branch: input.branch } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=foundation.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare function isGitRepositoryAtRoot(storeRoot: string): Promise<boolean>;
|
|
2
|
+
export declare function initGitRepository(storeRoot: string): Promise<boolean>;
|
|
3
|
+
/**
|
|
4
|
+
* `git var` resolves identity exactly as `git commit` would (config, env vars,
|
|
5
|
+
* auto-detection), so this fails precisely when the initial commit would.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertGitCommitIdentity(probeCwd: string): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Index-preserving initial commit: the pathspec on `git commit` keeps files
|
|
10
|
+
* the user had already staged out of setup's commit and leaves them staged.
|
|
11
|
+
* Pathspecs may be files or directories.
|
|
12
|
+
*/
|
|
13
|
+
export declare function commitStoreFiles(storeRoot: string, id: string, pathspecs: string[]): Promise<boolean>;
|
|
14
|
+
export declare function gitHasCommits(storeRoot: string): Promise<boolean | null>;
|
|
15
|
+
export declare function gitHasUncommittedChanges(storeRoot: string): Promise<boolean | null>;
|
|
16
|
+
export declare function gitHasRemote(storeRoot: string): Promise<boolean | null>;
|
|
17
|
+
/**
|
|
18
|
+
* The configured origin URL, read from local Git config only — never a
|
|
19
|
+
* network touch. Null when there is no repository or no origin.
|
|
20
|
+
*/
|
|
21
|
+
export declare function gitOriginUrl(storeRoot: string): Promise<string | null>;
|
|
22
|
+
export declare function gitDirectoryHasTrackedFiles(storeRoot: string, relativeDir: string): Promise<boolean | null>;
|
|
23
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import * as nodeFs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { StoreError } from './errors.js';
|
|
6
|
+
const fs = nodeFs.promises;
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
/**
|
|
9
|
+
* Git mechanics for stores: repository detection, setup-time init and
|
|
10
|
+
* commit, and the read-only facts doctor reports. Nothing here clones, pulls,
|
|
11
|
+
* pushes, or syncs — setup-time `git init` plus one initial commit is the
|
|
12
|
+
* entire write surface.
|
|
13
|
+
*/
|
|
14
|
+
function isSpawnNotFoundError(error) {
|
|
15
|
+
return (typeof error === 'object' &&
|
|
16
|
+
error !== null &&
|
|
17
|
+
'code' in error &&
|
|
18
|
+
error.code === 'ENOENT');
|
|
19
|
+
}
|
|
20
|
+
export async function isGitRepositoryAtRoot(storeRoot) {
|
|
21
|
+
try {
|
|
22
|
+
const stat = await fs.stat(path.join(storeRoot, '.git'));
|
|
23
|
+
return stat.isDirectory() || stat.isFile();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function initGitRepository(storeRoot) {
|
|
30
|
+
if (await isGitRepositoryAtRoot(storeRoot)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await execFileAsync('git', ['init'], { cwd: storeRoot });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new StoreError(`Failed to initialize Git repository: ${error instanceof Error ? error.message : String(error)}`, 'store_git_init_failed', {
|
|
38
|
+
target: 'store.git',
|
|
39
|
+
fix: 'Install Git or rerun setup with --no-init-git.',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `git var` resolves identity exactly as `git commit` would (config, env vars,
|
|
46
|
+
* auto-detection), so this fails precisely when the initial commit would.
|
|
47
|
+
*/
|
|
48
|
+
export async function assertGitCommitIdentity(probeCwd) {
|
|
49
|
+
for (const identVar of ['GIT_COMMITTER_IDENT', 'GIT_AUTHOR_IDENT']) {
|
|
50
|
+
try {
|
|
51
|
+
await execFileAsync('git', ['var', identVar], { cwd: probeCwd });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (isSpawnNotFoundError(error)) {
|
|
55
|
+
throw new StoreError('Git is not available, so setup cannot create the initial store commit.', 'store_git_init_failed', {
|
|
56
|
+
target: 'store.git',
|
|
57
|
+
fix: 'Install Git or rerun setup with --no-init-git.',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
throw new StoreError('No usable Git commit identity is configured, so setup cannot create the initial store commit.', 'store_git_identity_missing', {
|
|
61
|
+
target: 'store.git',
|
|
62
|
+
fix: 'Run git config --global user.name "Your Name" and git config --global user.email "you@example.com", or rerun setup with --no-init-git.',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Index-preserving initial commit: the pathspec on `git commit` keeps files
|
|
69
|
+
* the user had already staged out of setup's commit and leaves them staged.
|
|
70
|
+
* Pathspecs may be files or directories.
|
|
71
|
+
*/
|
|
72
|
+
export async function commitStoreFiles(storeRoot, id, pathspecs) {
|
|
73
|
+
if (pathspecs.length === 0) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await execFileAsync('git', ['add', '--', ...pathspecs], { cwd: storeRoot });
|
|
78
|
+
await execFileAsync('git', ['commit', '-m', `Initialize OpenSpec store ${id}`, '--', ...pathspecs], { cwd: storeRoot });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// Best-effort unstage so a failed commit (gpg signing, hooks) does not
|
|
82
|
+
// leave setup's files in the user's index after rollback deletes them.
|
|
83
|
+
await execFileAsync('git', ['rm', '--cached', '-r', '-f', '-q', '--', ...pathspecs], {
|
|
84
|
+
cwd: storeRoot,
|
|
85
|
+
}).catch(() => undefined);
|
|
86
|
+
throw new StoreError(`Failed to create the initial store commit: ${error instanceof Error ? error.message : String(error)}`, 'store_git_commit_failed', {
|
|
87
|
+
target: 'store.git',
|
|
88
|
+
fix: 'Commit the created files manually, or rerun setup with --no-init-git.',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
async function gitProbe(storeRoot, args) {
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await execFileAsync('git', ['-C', storeRoot, ...args]);
|
|
96
|
+
return stdout;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function gitHasCommits(storeRoot) {
|
|
103
|
+
try {
|
|
104
|
+
await execFileAsync('git', ['-C', storeRoot, 'rev-parse', '--verify', '--quiet', 'HEAD']);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (isSpawnNotFoundError(error))
|
|
109
|
+
return null;
|
|
110
|
+
// Exit 1 = repo exists but HEAD has no commits. Anything else (exit 128:
|
|
111
|
+
// corrupt or fake .git) is unknown, not "commitless".
|
|
112
|
+
const exitCode = error.code;
|
|
113
|
+
return exitCode === 1 ? false : null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function gitHasUncommittedChanges(storeRoot) {
|
|
117
|
+
const stdout = await gitProbe(storeRoot, ['status', '--porcelain']);
|
|
118
|
+
return stdout === null ? null : stdout.trim().length > 0;
|
|
119
|
+
}
|
|
120
|
+
export async function gitHasRemote(storeRoot) {
|
|
121
|
+
const stdout = await gitProbe(storeRoot, ['remote']);
|
|
122
|
+
return stdout === null ? null : stdout.trim().length > 0;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The configured origin URL, read from local Git config only — never a
|
|
126
|
+
* network touch. Null when there is no repository or no origin.
|
|
127
|
+
*/
|
|
128
|
+
export async function gitOriginUrl(storeRoot) {
|
|
129
|
+
const stdout = await gitProbe(storeRoot, ['remote', 'get-url', 'origin']);
|
|
130
|
+
const url = stdout?.trim();
|
|
131
|
+
return url ? url : null;
|
|
132
|
+
}
|
|
133
|
+
export async function gitDirectoryHasTrackedFiles(storeRoot, relativeDir) {
|
|
134
|
+
const stdout = await gitProbe(storeRoot, ['ls-files', '--', relativeDir]);
|
|
135
|
+
return stdout === null ? null : stdout.trim().length > 0;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=git.js.map
|