@fission-ai/openspec 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +94 -76
- package/dist/commands/change.d.ts +5 -1
- package/dist/commands/change.js +17 -15
- 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 +2 -0
- package/dist/commands/validate.js +75 -39
- 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 +209 -56
- 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.js +1 -14
- package/dist/core/command-generation/adapters/cursor.js +1 -14
- package/dist/core/command-generation/adapters/pi.js +1 -14
- package/dist/core/command-generation/adapters/windsurf.js +1 -14
- 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/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 +24 -4
- package/dist/core/list.d.ts +2 -0
- package/dist/core/list.js +19 -5
- 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/planning-home.d.ts +1 -6
- package/dist/core/planning-home.js +2 -43
- 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/specs-apply.d.ts +6 -1
- package/dist/core/specs-apply.js +6 -4
- 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 +783 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -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/verify-change.js +5 -4
- 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/package.json +6 -6
- 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 -9
- package/dist/commands/workspace/selection.js +0 -115
- 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 -6
- package/dist/commands/workspace.js +0 -587
- 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 -119
- 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
|
@@ -24,5 +24,10 @@ export const COMMON_FLAGS = {
|
|
|
24
24
|
takesValue: true,
|
|
25
25
|
values: ['change', 'spec'],
|
|
26
26
|
},
|
|
27
|
+
store: {
|
|
28
|
+
name: 'store',
|
|
29
|
+
description: "Store id to use as the OpenSpec root (a store is a standalone OpenSpec repo you've registered)",
|
|
30
|
+
takesValue: true,
|
|
31
|
+
},
|
|
27
32
|
};
|
|
28
33
|
//# sourceMappingURL=shared-flags.js.map
|
|
@@ -58,13 +58,14 @@ export declare function deleteNestedValue(obj: Record<string, unknown>, path: st
|
|
|
58
58
|
* Coerce a string value to its appropriate type.
|
|
59
59
|
* - "true" / "false" -> boolean
|
|
60
60
|
* - Numeric strings -> number
|
|
61
|
+
* - JSON arrays/objects -> parsed containers
|
|
61
62
|
* - Everything else -> string
|
|
62
63
|
*
|
|
63
64
|
* @param value - The string value to coerce
|
|
64
65
|
* @param forceString - If true, always return the value as a string
|
|
65
66
|
* @returns The coerced value
|
|
66
67
|
*/
|
|
67
|
-
export declare function coerceValue(value: string, forceString?: boolean): string | number | boolean
|
|
68
|
+
export declare function coerceValue(value: string, forceString?: boolean): string | number | boolean | unknown[] | Record<string, unknown>;
|
|
68
69
|
/**
|
|
69
70
|
* Format a value for YAML-like display.
|
|
70
71
|
*
|
|
@@ -125,6 +125,7 @@ export function deleteNestedValue(obj, path) {
|
|
|
125
125
|
* Coerce a string value to its appropriate type.
|
|
126
126
|
* - "true" / "false" -> boolean
|
|
127
127
|
* - Numeric strings -> number
|
|
128
|
+
* - JSON arrays/objects -> parsed containers
|
|
128
129
|
* - Everything else -> string
|
|
129
130
|
*
|
|
130
131
|
* @param value - The string value to coerce
|
|
@@ -147,8 +148,33 @@ export function coerceValue(value, forceString = false) {
|
|
|
147
148
|
if (!isNaN(num) && isFinite(num) && value.trim() !== '') {
|
|
148
149
|
return num;
|
|
149
150
|
}
|
|
151
|
+
const jsonContainer = parseJsonContainer(value);
|
|
152
|
+
if (jsonContainer !== undefined) {
|
|
153
|
+
return jsonContainer;
|
|
154
|
+
}
|
|
150
155
|
return value;
|
|
151
156
|
}
|
|
157
|
+
function parseJsonContainer(value) {
|
|
158
|
+
const trimmed = value.trim();
|
|
159
|
+
const looksLikeContainer = (trimmed.startsWith('[') && trimmed.endsWith(']')) ||
|
|
160
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}'));
|
|
161
|
+
if (!looksLikeContainer) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(trimmed);
|
|
166
|
+
if (Array.isArray(parsed)) {
|
|
167
|
+
return parsed;
|
|
168
|
+
}
|
|
169
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
170
|
+
return parsed;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
152
178
|
/**
|
|
153
179
|
* Format a value for YAML-like display.
|
|
154
180
|
*
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as nodeFs from 'node:fs';
|
|
2
|
+
import { StoreError } from './store/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared machine-local state-file mechanics (extracted from the store
|
|
5
|
+
* registry in slice 7.1, its second consumer). Callers own the
|
|
6
|
+
* diagnostic data (code, target, wording); the factory owns the
|
|
7
|
+
* shared mechanics - the fix strings describe the lock's own
|
|
8
|
+
* behavior (stale-steal, creation), so their templates live here.
|
|
9
|
+
*/
|
|
10
|
+
export type FileLockErrorKind = 'create-failed' | 'timeout';
|
|
11
|
+
export interface FileLockErrorInfo {
|
|
12
|
+
lockPath: string;
|
|
13
|
+
/** The original errno error for 'create-failed'. */
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface FileLockOptions {
|
|
17
|
+
lockPath: string;
|
|
18
|
+
errorFor: (kind: FileLockErrorKind, info: FileLockErrorInfo) => Error;
|
|
19
|
+
}
|
|
20
|
+
export interface LockErrorData {
|
|
21
|
+
/** Noun phrase for the create-failed message, e.g. "the registry lock file". */
|
|
22
|
+
createSubject: string;
|
|
23
|
+
/** The full timeout message, e.g. "Store registry is busy." */
|
|
24
|
+
busyMessage: string;
|
|
25
|
+
code: string;
|
|
26
|
+
target: string;
|
|
27
|
+
}
|
|
28
|
+
/** One template for lock diagnostics; callers supply the data. */
|
|
29
|
+
export declare function makeLockErrorFactory(data: LockErrorData): (kind: FileLockErrorKind, info: FileLockErrorInfo) => StoreError;
|
|
30
|
+
export declare function isNodeErrorCode(error: unknown, code: string): boolean;
|
|
31
|
+
export declare function pathIsFile(filePath: string): Promise<boolean>;
|
|
32
|
+
export declare function pathIsDirectory(dirPath: string): Promise<boolean>;
|
|
33
|
+
export declare function writeFileAtomically(filePath: string, content: string): Promise<void>;
|
|
34
|
+
export declare function acquireFileLock(options: FileLockOptions): Promise<nodeFs.promises.FileHandle>;
|
|
35
|
+
export declare function releaseFileLock(lock: nodeFs.promises.FileHandle, lockPath: string): Promise<void>;
|
|
36
|
+
//# sourceMappingURL=file-state.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as nodeFs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
4
|
+
import { StoreError } from './store/errors.js';
|
|
5
|
+
const fs = nodeFs.promises;
|
|
6
|
+
/** One template for lock diagnostics; callers supply the data. */
|
|
7
|
+
export function makeLockErrorFactory(data) {
|
|
8
|
+
return (kind, info) => {
|
|
9
|
+
if (kind === 'create-failed') {
|
|
10
|
+
// A permission or filesystem problem, not contention - say so.
|
|
11
|
+
return new StoreError(`Cannot create ${data.createSubject} ${info.lockPath} (${info.cause?.code ?? info.cause}).`, data.code, {
|
|
12
|
+
target: data.target,
|
|
13
|
+
fix: `Check permissions on ${path.dirname(info.lockPath)}.`,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return new StoreError(data.busyMessage, data.code, {
|
|
17
|
+
target: data.target,
|
|
18
|
+
fix: `Retry shortly; if this persists, delete the stale lock file ${info.lockPath}.`,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const STALE_LOCK_THRESHOLD_MS = 30_000;
|
|
23
|
+
const LOCK_DEADLINE_MS = 5000;
|
|
24
|
+
const LOCK_POLL_MS = 25;
|
|
25
|
+
export function isNodeErrorCode(error, code) {
|
|
26
|
+
return (typeof error === 'object' &&
|
|
27
|
+
error !== null &&
|
|
28
|
+
'code' in error &&
|
|
29
|
+
error.code === code);
|
|
30
|
+
}
|
|
31
|
+
export async function pathIsFile(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
return (await fs.stat(filePath)).isFile();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Deliberately not FileSystemUtils.directoryExists: that variant
|
|
40
|
+
// debug-logs non-ENOENT failures, which is noise inside prompt
|
|
41
|
+
// validators, and pathIsFile has no FileSystemUtils equivalent - the
|
|
42
|
+
// silent symmetric pair lives here.
|
|
43
|
+
export async function pathIsDirectory(dirPath) {
|
|
44
|
+
try {
|
|
45
|
+
return (await fs.stat(dirPath)).isDirectory();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function sleep(milliseconds) {
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
53
|
+
}
|
|
54
|
+
export async function writeFileAtomically(filePath, content) {
|
|
55
|
+
const dirPath = path.dirname(filePath);
|
|
56
|
+
await FileSystemUtils.createDirectory(dirPath);
|
|
57
|
+
const tempPath = path.join(dirPath, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
58
|
+
try {
|
|
59
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
60
|
+
await fs.rename(tempPath, filePath);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function acquireFileLock(options) {
|
|
68
|
+
const { lockPath, errorFor } = options;
|
|
69
|
+
const lockDir = path.dirname(lockPath);
|
|
70
|
+
await FileSystemUtils.createDirectory(lockDir);
|
|
71
|
+
if (!(await FileSystemUtils.canWriteFile(lockDir))) {
|
|
72
|
+
throw errorFor('create-failed', { lockPath, cause: 'EACCES' });
|
|
73
|
+
}
|
|
74
|
+
const deadline = Date.now() + LOCK_DEADLINE_MS;
|
|
75
|
+
while (true) {
|
|
76
|
+
try {
|
|
77
|
+
return await fs.open(lockPath, 'wx');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!isNodeErrorCode(error, 'EEXIST')) {
|
|
81
|
+
// A permission or filesystem problem, not contention - say so.
|
|
82
|
+
throw errorFor('create-failed', { lockPath, cause: error });
|
|
83
|
+
}
|
|
84
|
+
// A crashed process leaves the lock behind forever; state-file
|
|
85
|
+
// writes are sub-second, so an old lock is an orphan - steal it.
|
|
86
|
+
let staleStolen = false;
|
|
87
|
+
try {
|
|
88
|
+
const lockStat = await fs.stat(lockPath);
|
|
89
|
+
if (Date.now() - lockStat.mtimeMs > STALE_LOCK_THRESHOLD_MS) {
|
|
90
|
+
await fs.rm(lockPath, { force: true });
|
|
91
|
+
staleStolen = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// The holder released between open and stat - retry, but stay
|
|
96
|
+
// bounded: a persistently failing stat (EPERM, delete-pending)
|
|
97
|
+
// must hit the deadline instead of spinning forever.
|
|
98
|
+
}
|
|
99
|
+
if (!staleStolen) {
|
|
100
|
+
if (Date.now() >= deadline) {
|
|
101
|
+
throw errorFor('timeout', { lockPath });
|
|
102
|
+
}
|
|
103
|
+
await sleep(LOCK_POLL_MS);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function releaseFileLock(lock, lockPath) {
|
|
109
|
+
await lock.close().catch(() => undefined);
|
|
110
|
+
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=file-state.js.map
|
|
@@ -8,6 +8,8 @@ export interface GlobalConfig {
|
|
|
8
8
|
profile?: Profile;
|
|
9
9
|
delivery?: Delivery;
|
|
10
10
|
workflows?: string[];
|
|
11
|
+
/** Workset opener rows (slice 7.1); hand-edited, validated on use. */
|
|
12
|
+
openers?: unknown;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Gets the global configuration directory path following XDG Base Directory Specification.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one kebab id grammar. Store ids, change ids, and legacy initiative ids
|
|
3
|
+
* all share it.
|
|
4
|
+
*/
|
|
5
|
+
export declare const KEBAB_ID_REGEX: RegExp;
|
|
6
|
+
export declare function isKebabId(value: string): boolean;
|
|
7
|
+
/** Human rendering of the grammar, shared so the wording never forks. */
|
|
8
|
+
export declare const KEBAB_ID_DESCRIPTION = "must be kebab-case with lowercase letters, numbers, and single hyphen separators";
|
|
9
|
+
/** The fix-line twin of KEBAB_ID_DESCRIPTION, shared for the same reason. */
|
|
10
|
+
export declare const KEBAB_ID_FIX = "Use kebab-case with lowercase letters, numbers, and single hyphen separators.";
|
|
11
|
+
/**
|
|
12
|
+
* The folder-safe-name grammar (store ids layer the kebab grammar on
|
|
13
|
+
* top of it; workset member labels use it alone). Returns a problem
|
|
14
|
+
* description, or null when valid.
|
|
15
|
+
*/
|
|
16
|
+
export declare function folderStyleNameProblem(value: string, label: string): string | null;
|
|
17
|
+
//# sourceMappingURL=id.d.ts.map
|
package/dist/core/id.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one kebab id grammar. Store ids, change ids, and legacy initiative ids
|
|
3
|
+
* all share it.
|
|
4
|
+
*/
|
|
5
|
+
export const KEBAB_ID_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
6
|
+
export function isKebabId(value) {
|
|
7
|
+
return KEBAB_ID_REGEX.test(value);
|
|
8
|
+
}
|
|
9
|
+
/** Human rendering of the grammar, shared so the wording never forks. */
|
|
10
|
+
export const KEBAB_ID_DESCRIPTION = 'must be kebab-case with lowercase letters, numbers, and single hyphen separators';
|
|
11
|
+
/** The fix-line twin of KEBAB_ID_DESCRIPTION, shared for the same reason. */
|
|
12
|
+
export const KEBAB_ID_FIX = 'Use kebab-case with lowercase letters, numbers, and single hyphen separators.';
|
|
13
|
+
/**
|
|
14
|
+
* The folder-safe-name grammar (store ids layer the kebab grammar on
|
|
15
|
+
* top of it; workset member labels use it alone). Returns a problem
|
|
16
|
+
* description, or null when valid.
|
|
17
|
+
*/
|
|
18
|
+
export function folderStyleNameProblem(value, label) {
|
|
19
|
+
if (value.length === 0) {
|
|
20
|
+
return `${label} must not be empty`;
|
|
21
|
+
}
|
|
22
|
+
if (value === '.' || value === '..') {
|
|
23
|
+
return `${label} must not be '${value}'`;
|
|
24
|
+
}
|
|
25
|
+
if (/[\\/]/u.test(value)) {
|
|
26
|
+
return `${label} must not contain path separators`;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=id.js.map
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, type GlobalDataDirOptions, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
|
|
2
|
-
export * from './
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './collections/index.js';
|
|
2
|
+
export * from './references.js';
|
|
3
|
+
export * from './store/index.js';
|
|
5
4
|
export * from './planning-home.js';
|
|
5
|
+
export * from './openspec-root.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Core OpenSpec logic will be implemented here
|
|
2
2
|
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
5
|
-
export * from './collections/index.js';
|
|
3
|
+
export * from './references.js';
|
|
4
|
+
export * from './store/index.js';
|
|
6
5
|
export * from './planning-home.js';
|
|
6
|
+
export * from './openspec-root.js';
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
package/dist/core/init.js
CHANGED
|
@@ -10,6 +10,8 @@ import ora from 'ora';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
|
+
import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
|
|
14
|
+
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
13
15
|
import { transformToHyphenCommands } from '../utils/command-references.js';
|
|
14
16
|
import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
|
|
15
17
|
import { PALETTE } from './styles/palette.js';
|
|
@@ -65,6 +67,28 @@ export class InitCommand {
|
|
|
65
67
|
const openspecPath = path.join(projectPath, openspecDir);
|
|
66
68
|
// Validation happens silently in the background
|
|
67
69
|
const extendMode = await this.validate(projectPath, openspecPath);
|
|
70
|
+
// Pointer guard (slice 3.2): a config-only openspec/ with a store:
|
|
71
|
+
// declaration is externalized planning, not a root to extend — and a
|
|
72
|
+
// subdirectory of such a repo must not silently grow a nested root.
|
|
73
|
+
// Refuse before legacy cleanup, migration, or prompts touch anything.
|
|
74
|
+
// In extend mode the walk finds projectPath itself; otherwise it
|
|
75
|
+
// finds the nearest ancestor root (so pointer-repo subdirectories
|
|
76
|
+
// refuse exactly where a normal command would resolve the pointer).
|
|
77
|
+
const guardRoot = findRepoPlanningRootSync(projectPath);
|
|
78
|
+
if (guardRoot) {
|
|
79
|
+
const { hasPlanningShape, pointer } = classifyOpenSpecDir(guardRoot);
|
|
80
|
+
if (!hasPlanningShape) {
|
|
81
|
+
if (pointer.malformed) {
|
|
82
|
+
throw new Error(`The store declaration in ${pointer.filePath} is invalid (` +
|
|
83
|
+
storePointerProblem(pointer.malformed) +
|
|
84
|
+
`). Fix or remove the store: line before running openspec init.`);
|
|
85
|
+
}
|
|
86
|
+
if (pointer.value !== undefined) {
|
|
87
|
+
throw new Error(`This repo's planning is externalized to store '${pointer.value}' (${pointer.filePath}). ` +
|
|
88
|
+
`Remove the store: line first to convert this repo to a local OpenSpec root.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
68
92
|
// Check for legacy artifacts and handle cleanup
|
|
69
93
|
await this.handleLegacyCleanup(projectPath, extendMode);
|
|
70
94
|
// Detect available tools in the project (task 7.1)
|
|
@@ -440,10 +464,6 @@ export class InitCommand {
|
|
|
440
464
|
if (configYamlExists || configYmlExists) {
|
|
441
465
|
return 'exists';
|
|
442
466
|
}
|
|
443
|
-
// In non-interactive mode without --force, skip config creation
|
|
444
|
-
if (!this.canPromptInteractively() && !this.force) {
|
|
445
|
-
return 'skipped';
|
|
446
|
-
}
|
|
447
467
|
try {
|
|
448
468
|
const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
|
|
449
469
|
await FileSystemUtils.writeFile(configPath, yamlContent);
|
package/dist/core/list.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { RootOutput } from './root-selection.js';
|
|
1
2
|
interface ListOptions {
|
|
2
3
|
sort?: 'recent' | 'name';
|
|
3
4
|
json?: boolean;
|
|
5
|
+
root?: RootOutput;
|
|
4
6
|
}
|
|
5
7
|
export declare class ListCommand {
|
|
6
8
|
execute(targetPath?: string, mode?: 'changes' | 'specs', options?: ListOptions): Promise<void>;
|
package/dist/core/list.js
CHANGED
|
@@ -61,7 +61,7 @@ function formatRelativeTime(date) {
|
|
|
61
61
|
}
|
|
62
62
|
export class ListCommand {
|
|
63
63
|
async execute(targetPath = '.', mode = 'changes', options = {}) {
|
|
64
|
-
const { sort = 'recent', json = false } = options;
|
|
64
|
+
const { sort = 'recent', json = false, root } = options;
|
|
65
65
|
if (mode === 'changes') {
|
|
66
66
|
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
|
67
67
|
// Check if changes directory exists
|
|
@@ -78,7 +78,7 @@ export class ListCommand {
|
|
|
78
78
|
.map(entry => entry.name);
|
|
79
79
|
if (changeDirs.length === 0) {
|
|
80
80
|
if (json) {
|
|
81
|
-
console.log(JSON.stringify({ changes: [] }));
|
|
81
|
+
console.log(JSON.stringify({ changes: [], ...(root ? { root } : {}) }, null, 2));
|
|
82
82
|
}
|
|
83
83
|
else {
|
|
84
84
|
console.log('No active changes found.');
|
|
@@ -114,7 +114,7 @@ export class ListCommand {
|
|
|
114
114
|
lastModified: c.lastModified.toISOString(),
|
|
115
115
|
status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
|
|
116
116
|
}));
|
|
117
|
-
console.log(JSON.stringify({ changes: jsonOutput }, null, 2));
|
|
117
|
+
console.log(JSON.stringify({ changes: jsonOutput, ...(root ? { root } : {}) }, null, 2));
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
// Display results
|
|
@@ -135,13 +135,23 @@ export class ListCommand {
|
|
|
135
135
|
await fs.access(specsDir);
|
|
136
136
|
}
|
|
137
137
|
catch {
|
|
138
|
-
|
|
138
|
+
if (json) {
|
|
139
|
+
console.log(JSON.stringify({ specs: [], ...(root ? { root } : {}) }, null, 2));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log('No specs found.');
|
|
143
|
+
}
|
|
139
144
|
return;
|
|
140
145
|
}
|
|
141
146
|
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
|
142
147
|
const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
143
148
|
if (specDirs.length === 0) {
|
|
144
|
-
|
|
149
|
+
if (json) {
|
|
150
|
+
console.log(JSON.stringify({ specs: [], ...(root ? { root } : {}) }, null, 2));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log('No specs found.');
|
|
154
|
+
}
|
|
145
155
|
return;
|
|
146
156
|
}
|
|
147
157
|
const specs = [];
|
|
@@ -159,6 +169,10 @@ export class ListCommand {
|
|
|
159
169
|
}
|
|
160
170
|
}
|
|
161
171
|
specs.sort((a, b) => a.id.localeCompare(b.id));
|
|
172
|
+
if (json) {
|
|
173
|
+
console.log(JSON.stringify({ specs, ...(root ? { root } : {}) }, null, 2));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
162
176
|
console.log('Specs:');
|
|
163
177
|
const padding = ' ';
|
|
164
178
|
const nameWidth = Math.max(...specs.map(s => s.id.length));
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { WorksetMember } from './worksets.js';
|
|
2
|
+
/**
|
|
3
|
+
* The workset opener table (slice 7.1). Supporting a new tool is
|
|
4
|
+
* configuration, not code: every tool is an instance of one of exactly
|
|
5
|
+
* two launch styles - 'workspace-file' (invoke with the generated
|
|
6
|
+
* .code-workspace) or 'attach-dirs' (pre-args plus one attach flag per
|
|
7
|
+
* member; no positional, ever - agent sessions open clean). Users add
|
|
8
|
+
* tools or adjust parameters under the global config file's `openers`
|
|
9
|
+
* key (the git difftool/mergetool pattern).
|
|
10
|
+
*/
|
|
11
|
+
export type OpenerStyle = 'workspace-file' | 'attach-dirs';
|
|
12
|
+
export interface OpenerDefinition {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
style: OpenerStyle;
|
|
16
|
+
command: string;
|
|
17
|
+
/** Pre-args before any attach flags or the workspace-file path. */
|
|
18
|
+
args: string[];
|
|
19
|
+
/** attach-dirs only; one flag + path pair per member. */
|
|
20
|
+
attachFlag: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Temporary kill-switch (2026-06): worksets open only in IDE-style
|
|
24
|
+
* ('workspace-file') tools while the CLI-agent ('attach-dirs') open flow
|
|
25
|
+
* is reworked. The agents (Claude Code, codex) launch in a single primary
|
|
26
|
+
* cwd rather than a true combined multi-root view, which makes "where does
|
|
27
|
+
* my change land?" ambiguous. Default off; set
|
|
28
|
+
* OPENSPEC_ENABLE_CLI_AGENT_OPENERS=1 to restore them (internal rollback seam).
|
|
29
|
+
*/
|
|
30
|
+
export declare function isCliAgentOpenersEnabled(): boolean;
|
|
31
|
+
/** Whether a tool can be opened right now (CLI-agent styles are gated). */
|
|
32
|
+
export declare function isOpenerEnabled(opener: OpenerDefinition): boolean;
|
|
33
|
+
export declare const BUILTIN_OPENERS: readonly OpenerDefinition[];
|
|
34
|
+
export declare function mergeOpenerTable(rawOpeners: unknown, configPath: string): OpenerDefinition[];
|
|
35
|
+
export interface OpenerScanOptions {
|
|
36
|
+
env?: NodeJS.ProcessEnv;
|
|
37
|
+
platform?: NodeJS.Platform;
|
|
38
|
+
/** Stat seam for tests (win32 candidate paths on posix hosts). */
|
|
39
|
+
isExecutableFile?: (candidatePath: string) => boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* PATH availability scan (ported from the deleted workspace openers
|
|
43
|
+
* at f858c19^, sharpened for injectability: the path module is keyed
|
|
44
|
+
* by the injected platform, and a command already carrying a known
|
|
45
|
+
* executable extension matches as-is).
|
|
46
|
+
*/
|
|
47
|
+
export declare function isOpenerCommandAvailable(command: string, options?: OpenerScanOptions): boolean;
|
|
48
|
+
export interface OpenerChoice {
|
|
49
|
+
opener: OpenerDefinition;
|
|
50
|
+
available: boolean;
|
|
51
|
+
/** `(<command> not found on PATH)` when unavailable. */
|
|
52
|
+
note: string | null;
|
|
53
|
+
}
|
|
54
|
+
/** Table order preserved, available tools first (stable sort). */
|
|
55
|
+
export declare function listOpenerChoices(table: OpenerDefinition[], options?: OpenerScanOptions): OpenerChoice[];
|
|
56
|
+
export declare function findOpener(table: OpenerDefinition[], id: string): OpenerDefinition | null;
|
|
57
|
+
export interface LaunchCommand {
|
|
58
|
+
executable: string;
|
|
59
|
+
args: string[];
|
|
60
|
+
/** The surviving primary member's path. */
|
|
61
|
+
cwd: string;
|
|
62
|
+
label: string;
|
|
63
|
+
style: OpenerStyle;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Pure argv builder. workspace-file: pre-args + the generated file's
|
|
67
|
+
* absolute path (which also defuses the cursor shim's `agent`
|
|
68
|
+
* first-arg hijack). attach-dirs: pre-args + one attach flag + path
|
|
69
|
+
* pair per surviving member, the primary included (the locked "one
|
|
70
|
+
* attach flag per member"); never a trailing positional - both agent
|
|
71
|
+
* CLIs would read one as a starter prompt, which 7.1 locks out.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildLaunchCommand(opener: OpenerDefinition, input: {
|
|
74
|
+
members: WorksetMember[];
|
|
75
|
+
codeWorkspacePath: string;
|
|
76
|
+
}): LaunchCommand;
|
|
77
|
+
//# sourceMappingURL=openers.d.ts.map
|