@gordon.gan/specflow 1.1.0 → 1.2.0-beta
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 +6 -0
- package/dist/cli/commands/change-archive.js +4 -4
- package/dist/cli/commands/change-new.js +5 -5
- package/dist/cli/commands/change-phase.js +4 -4
- package/dist/cli/commands/change-status.js +4 -4
- package/dist/cli/commands/context.d.ts +18 -0
- package/dist/cli/commands/context.js +125 -0
- package/dist/cli/commands/instructions.d.ts +4 -1
- package/dist/cli/commands/instructions.js +58 -9
- package/dist/cli/commands/show.d.ts +22 -0
- package/dist/cli/commands/show.js +92 -0
- package/dist/cli/commands/store.d.ts +16 -0
- package/dist/cli/commands/store.js +221 -0
- package/dist/cli/commands/validate.d.ts +16 -0
- package/dist/cli/commands/validate.js +34 -4
- package/dist/cli/commands/workset.d.ts +12 -0
- package/dist/cli/commands/workset.js +235 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/shared/store-option.d.ts +11 -0
- package/dist/cli/shared/store-option.js +40 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +17 -0
- package/dist/core/artifact-graph/instruction-loader.js +2 -0
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/context-assembly.d.ts +9 -0
- package/dist/core/context-assembly.js +68 -0
- package/dist/core/diagnostics.d.ts +11 -0
- package/dist/core/diagnostics.js +18 -0
- package/dist/core/file-state.d.ts +23 -0
- package/dist/core/file-state.js +101 -0
- package/dist/core/global-config.d.ts +26 -0
- package/dist/core/global-config.js +77 -0
- package/dist/core/opener-launch.d.ts +3 -0
- package/dist/core/opener-launch.js +20 -0
- package/dist/core/openers.d.ts +23 -0
- package/dist/core/openers.js +20 -0
- package/dist/core/project-config.d.ts +14 -0
- package/dist/core/project-config.js +73 -0
- package/dist/core/reference-index.d.ts +8 -0
- package/dist/core/reference-index.js +80 -0
- package/dist/core/references.d.ts +17 -0
- package/dist/core/references.js +51 -0
- package/dist/core/relationship-health.d.ts +22 -0
- package/dist/core/relationship-health.js +68 -0
- package/dist/core/root-selection.d.ts +26 -0
- package/dist/core/root-selection.js +197 -0
- package/dist/core/store/errors.d.ts +2 -0
- package/dist/core/store/errors.js +1 -0
- package/dist/core/store/foundation.d.ts +141 -0
- package/dist/core/store/foundation.js +79 -0
- package/dist/core/store/health.d.ts +13 -0
- package/dist/core/store/health.js +117 -0
- package/dist/core/store/operations.d.ts +56 -0
- package/dist/core/store/operations.js +268 -0
- package/dist/core/store/registry.d.ts +15 -0
- package/dist/core/store/registry.js +128 -0
- package/dist/core/working-set.d.ts +30 -0
- package/dist/core/working-set.js +26 -0
- package/dist/core/worksets.d.ts +71 -0
- package/dist/core/worksets.js +134 -0
- package/dist/integrations/shared/skill-renderer.d.ts +6 -0
- package/dist/integrations/shared/skill-renderer.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export interface WorksetMember {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Workset {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly members: readonly WorksetMember[];
|
|
9
|
+
readonly tool?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const WorksetsStateSchema: z.ZodObject<{
|
|
12
|
+
version: z.ZodLiteral<1>;
|
|
13
|
+
worksets: z.ZodArray<z.ZodObject<{
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
members: z.ZodArray<z.ZodObject<{
|
|
16
|
+
name: z.ZodString;
|
|
17
|
+
path: z.ZodString;
|
|
18
|
+
}, "strict", z.ZodTypeAny, {
|
|
19
|
+
path: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}, {
|
|
22
|
+
path: string;
|
|
23
|
+
name: string;
|
|
24
|
+
}>, "many">;
|
|
25
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, "strict", z.ZodTypeAny, {
|
|
27
|
+
name: string;
|
|
28
|
+
members: {
|
|
29
|
+
path: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}[];
|
|
32
|
+
tool?: string | undefined;
|
|
33
|
+
}, {
|
|
34
|
+
name: string;
|
|
35
|
+
members: {
|
|
36
|
+
path: string;
|
|
37
|
+
name: string;
|
|
38
|
+
}[];
|
|
39
|
+
tool?: string | undefined;
|
|
40
|
+
}>, "many">;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
worksets: {
|
|
43
|
+
name: string;
|
|
44
|
+
members: {
|
|
45
|
+
path: string;
|
|
46
|
+
name: string;
|
|
47
|
+
}[];
|
|
48
|
+
tool?: string | undefined;
|
|
49
|
+
}[];
|
|
50
|
+
version: 1;
|
|
51
|
+
}, {
|
|
52
|
+
worksets: {
|
|
53
|
+
name: string;
|
|
54
|
+
members: {
|
|
55
|
+
path: string;
|
|
56
|
+
name: string;
|
|
57
|
+
}[];
|
|
58
|
+
tool?: string | undefined;
|
|
59
|
+
}[];
|
|
60
|
+
version: 1;
|
|
61
|
+
}>;
|
|
62
|
+
export type WorksetsState = z.infer<typeof WorksetsStateSchema>;
|
|
63
|
+
export declare function validateWorksetName(name: string): void;
|
|
64
|
+
export declare function validateWorksetMembers(members: readonly WorksetMember[]): void;
|
|
65
|
+
export declare function readWorksetsState(statePath: string): Promise<WorksetsState>;
|
|
66
|
+
export declare function updateWorksetsState(statePath: string, updater: (state: WorksetsState) => WorksetsState): Promise<WorksetsState>;
|
|
67
|
+
export declare function withWorkset(state: WorksetsState, workset: Workset): WorksetsState;
|
|
68
|
+
export declare function getWorksetCodeWorkspacePath(dataDir: string, name: string): string;
|
|
69
|
+
export declare function defaultWorksetsStatePath(dataDir?: string): string;
|
|
70
|
+
export declare function removeWorksetByName(statePath: string, name: string, dataDir: string): Promise<boolean>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { acquireFileLock, releaseFileLock, writeFileAtomically, makeLockErrorFactory } from './file-state.js';
|
|
6
|
+
import { getWorksetsStatePath } from './global-config.js';
|
|
7
|
+
import { StoreError } from './store/foundation.js';
|
|
8
|
+
const KEBAB_CASE_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
9
|
+
const WorksetMemberSchema = z.object({ name: z.string(), path: z.string() }).strict();
|
|
10
|
+
const WorksetSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
members: z.array(WorksetMemberSchema).min(1),
|
|
14
|
+
tool: z.string().optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
const WorksetsStateSchema = z.object({
|
|
18
|
+
version: z.literal(1),
|
|
19
|
+
worksets: z.array(WorksetSchema),
|
|
20
|
+
});
|
|
21
|
+
function lockPathFor(statePath) {
|
|
22
|
+
return `${statePath}.lock`;
|
|
23
|
+
}
|
|
24
|
+
function makeWorksetLockError() {
|
|
25
|
+
return makeLockErrorFactory({
|
|
26
|
+
createSubject: 'the workset lock file',
|
|
27
|
+
busyMessage: 'Workset state is busy.',
|
|
28
|
+
code: 'workset_busy',
|
|
29
|
+
target: 'worksets.state',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function validateWorksetName(name) {
|
|
33
|
+
if (!KEBAB_CASE_NAME.test(name)) {
|
|
34
|
+
throw new StoreError(`Invalid workset name '${name}'.`, {
|
|
35
|
+
severity: 'error',
|
|
36
|
+
code: 'invalid_workset_name',
|
|
37
|
+
message: 'Workset names must be kebab-case.',
|
|
38
|
+
target: 'workset.name',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function validateWorksetMembers(members) {
|
|
43
|
+
if (members.length === 0) {
|
|
44
|
+
throw new StoreError('Workset must contain at least one member.', {
|
|
45
|
+
severity: 'error',
|
|
46
|
+
code: 'invalid_workset_members',
|
|
47
|
+
message: 'Worksets require at least one member.',
|
|
48
|
+
target: 'workset.members',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const labels = new Set();
|
|
52
|
+
for (const member of members) {
|
|
53
|
+
if (!path.isAbsolute(member.path)) {
|
|
54
|
+
throw new StoreError(`Member path must be absolute: ${member.path}`, {
|
|
55
|
+
severity: 'error',
|
|
56
|
+
code: 'invalid_workset_path',
|
|
57
|
+
message: 'Workset member paths must be absolute.',
|
|
58
|
+
target: `workset.members.${member.name}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (labels.has(member.name)) {
|
|
62
|
+
throw new StoreError(`Duplicate workset member label '${member.name}'.`, {
|
|
63
|
+
severity: 'error',
|
|
64
|
+
code: 'duplicate_workset_member',
|
|
65
|
+
message: 'Workset member labels must be unique.',
|
|
66
|
+
target: `workset.members.${member.name}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
labels.add(member.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function readWorksetsState(statePath) {
|
|
73
|
+
try {
|
|
74
|
+
const content = await fs.readFile(statePath, 'utf-8');
|
|
75
|
+
const parsed = yaml.load(content);
|
|
76
|
+
return WorksetsStateSchema.parse(parsed);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code === 'ENOENT') {
|
|
80
|
+
return { version: 1, worksets: [] };
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function writeWorksetsState(statePath, state) {
|
|
86
|
+
const content = yaml.dump(state, { lineWidth: -1, noRefs: true });
|
|
87
|
+
await writeFileAtomically(statePath, content);
|
|
88
|
+
}
|
|
89
|
+
export async function updateWorksetsState(statePath, updater) {
|
|
90
|
+
const lock = await acquireFileLock({
|
|
91
|
+
lockPath: lockPathFor(statePath),
|
|
92
|
+
errorFor: makeWorksetLockError(),
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
const current = await readWorksetsState(statePath);
|
|
96
|
+
const next = WorksetsStateSchema.parse(updater(current));
|
|
97
|
+
await writeWorksetsState(statePath, next);
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await releaseFileLock(lock, lockPathFor(statePath));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function withWorkset(state, workset) {
|
|
105
|
+
validateWorksetName(workset.name);
|
|
106
|
+
validateWorksetMembers(workset.members);
|
|
107
|
+
const without = state.worksets.filter((entry) => entry.name !== workset.name);
|
|
108
|
+
return { version: 1, worksets: [...without, workset].sort((a, b) => a.name.localeCompare(b.name)) };
|
|
109
|
+
}
|
|
110
|
+
export function getWorksetCodeWorkspacePath(dataDir, name) {
|
|
111
|
+
return path.join(dataDir, 'worksets', `${name}.code-workspace`);
|
|
112
|
+
}
|
|
113
|
+
export function defaultWorksetsStatePath(dataDir) {
|
|
114
|
+
return getWorksetsStatePath(dataDir);
|
|
115
|
+
}
|
|
116
|
+
export async function removeWorksetByName(statePath, name, dataDir) {
|
|
117
|
+
let removed = false;
|
|
118
|
+
await updateWorksetsState(statePath, (state) => {
|
|
119
|
+
const exists = state.worksets.some((entry) => entry.name === name);
|
|
120
|
+
if (!exists) {
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
removed = true;
|
|
124
|
+
return {
|
|
125
|
+
version: 1,
|
|
126
|
+
worksets: state.worksets.filter((entry) => entry.name !== name),
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
if (removed) {
|
|
130
|
+
const workspacePath = getWorksetCodeWorkspacePath(dataDir, name);
|
|
131
|
+
await fs.rm(workspacePath, { force: true }).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
return removed;
|
|
134
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { IdeTarget } from './types.js';
|
|
2
|
+
import type { ReferencedStoresSection } from '../../core/artifact-graph/instruction-loader.js';
|
|
3
|
+
export interface ReferenceGuidanceInput {
|
|
4
|
+
readonly referencedStores?: ReferencedStoresSection;
|
|
5
|
+
}
|
|
6
|
+
/** Renders identical reference guidance for every IDE target. */
|
|
7
|
+
export declare function collectReferenceGuidance(input: ReferenceGuidanceInput): Record<IdeTarget, string>;
|
|
2
8
|
export declare function renderIdeContent(content: string, ide: IdeTarget): string;
|
|
3
9
|
/** @deprecated Use renderIdeContent */
|
|
4
10
|
export declare const renderSkillContent: typeof renderIdeContent;
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { COMMAND_CATALOG } from './command-catalog.js';
|
|
2
|
+
function formatReferenceBlock(section) {
|
|
3
|
+
if (!section || section.rendered.trim().length === 0) {
|
|
4
|
+
return '';
|
|
5
|
+
}
|
|
6
|
+
const lines = [
|
|
7
|
+
'## Referenced store index (read-only)',
|
|
8
|
+
section.rendered,
|
|
9
|
+
];
|
|
10
|
+
if (section.truncated) {
|
|
11
|
+
lines.push('Index truncated. Use `specflow show <spec-id> --type spec --store <store-id>` to fetch full specs.');
|
|
12
|
+
}
|
|
13
|
+
return lines.join('\n');
|
|
14
|
+
}
|
|
15
|
+
/** Renders identical reference guidance for every IDE target. */
|
|
16
|
+
export function collectReferenceGuidance(input) {
|
|
17
|
+
const block = formatReferenceBlock(input.referencedStores);
|
|
18
|
+
return {
|
|
19
|
+
claude: block,
|
|
20
|
+
cursor: block,
|
|
21
|
+
codex: block,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
2
24
|
function runtimePrefix(ide) {
|
|
3
25
|
if (ide === 'claude') {
|
|
4
26
|
return '.claude/specflow/';
|
package/package.json
CHANGED