@gordon.gan/specflow 1.1.1 → 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
- package/skills/specflow-apply/SKILL.md +5 -27
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { getGlobalDataDir } from '../../core/global-config.js';
|
|
2
|
+
import { listRegisteredStores, prepareStoreSetup, prepareStoreRegistration, removeManagedStore, unregisterStore, } from '../../core/store/operations.js';
|
|
3
|
+
import { readRegistry } from '../../core/store/registry.js';
|
|
4
|
+
import { getStoreRegistryPath } from '../../core/global-config.js';
|
|
5
|
+
import { sortDiagnostics, hasErrorDiagnostic } from '../../core/diagnostics.js';
|
|
6
|
+
import { StoreError } from '../../core/store/foundation.js';
|
|
7
|
+
import { flattenStoreDoctorDiagnostics, inspectAllStoresHealth } from '../../core/store/health.js';
|
|
8
|
+
export async function runStoreList(options = {}) {
|
|
9
|
+
const dataDir = options.dataDir ?? getGlobalDataDir();
|
|
10
|
+
const stores = await listRegisteredStores(dataDir);
|
|
11
|
+
return {
|
|
12
|
+
stores: stores.map((s) => ({ id: s.id, root: s.root, provenance: s.provenance })),
|
|
13
|
+
status: [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function printJson(payload) {
|
|
17
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
18
|
+
}
|
|
19
|
+
async function confirmIdentityCreation(storeId) {
|
|
20
|
+
if (!process.stdin.isTTY) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const readline = await import('node:readline/promises');
|
|
24
|
+
const { stdin, stdout } = await import('node:process');
|
|
25
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
26
|
+
const answer = await rl.question(`Create store identity for '${storeId}'? [y/N] `);
|
|
27
|
+
rl.close();
|
|
28
|
+
return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
|
|
29
|
+
}
|
|
30
|
+
function emitFailure(json, payload, error) {
|
|
31
|
+
const diagnostics = error instanceof StoreError
|
|
32
|
+
? [...error.diagnostics]
|
|
33
|
+
: [
|
|
34
|
+
{
|
|
35
|
+
severity: 'error',
|
|
36
|
+
code: 'store_error',
|
|
37
|
+
message: error instanceof Error ? error.message : String(error),
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
const output = { ...payload, status: sortDiagnostics(diagnostics) };
|
|
41
|
+
if (json) {
|
|
42
|
+
printJson(output);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.error(diagnostics.map((d) => d.message).join('\n'));
|
|
46
|
+
}
|
|
47
|
+
process.exit(hasErrorDiagnostic(diagnostics) ? 1 : 0);
|
|
48
|
+
}
|
|
49
|
+
export function registerStoreCommand(program) {
|
|
50
|
+
const store = program.command('store').description('Manage planning stores');
|
|
51
|
+
store
|
|
52
|
+
.command('list')
|
|
53
|
+
.description('List registered planning stores')
|
|
54
|
+
.option('--json', 'Output as JSON')
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
try {
|
|
57
|
+
const output = await runStoreList(opts);
|
|
58
|
+
if (opts.json) {
|
|
59
|
+
printJson(output);
|
|
60
|
+
}
|
|
61
|
+
else if (output.stores.length === 0) {
|
|
62
|
+
console.log('No stores registered.');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
for (const entry of output.stores) {
|
|
66
|
+
console.log(`${entry.id}\t${entry.root}\t(${entry.provenance})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
emitFailure(opts.json, { stores: [], status: [] }, error);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
store
|
|
75
|
+
.command('setup <id>')
|
|
76
|
+
.description('Create and register a new managed planning store')
|
|
77
|
+
.requiredOption('--path <path>', 'Target path that must not exist')
|
|
78
|
+
.option('--remote <url>', 'Optional canonical remote URL')
|
|
79
|
+
.option('--json', 'Output as JSON')
|
|
80
|
+
.action(async (id, opts) => {
|
|
81
|
+
try {
|
|
82
|
+
const dataDir = opts.dataDir ?? getGlobalDataDir();
|
|
83
|
+
const result = await prepareStoreSetup({
|
|
84
|
+
id,
|
|
85
|
+
targetPath: opts.path,
|
|
86
|
+
dataDir,
|
|
87
|
+
remote: opts.remote,
|
|
88
|
+
});
|
|
89
|
+
const output = {
|
|
90
|
+
store: { id: result.identity.id, root: result.root },
|
|
91
|
+
created_files: result.createdArtifacts,
|
|
92
|
+
status: sortDiagnostics(result.diagnostics),
|
|
93
|
+
};
|
|
94
|
+
if (opts.json) {
|
|
95
|
+
printJson(output);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log(`Created managed store '${id}' at ${result.root}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
emitFailure(opts.json, { store: null, created_files: [], status: [] }, error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
store
|
|
106
|
+
.command('register <path>')
|
|
107
|
+
.description('Register an existing checkout as an external store')
|
|
108
|
+
.requiredOption('--id <id>', 'Store ID to adopt')
|
|
109
|
+
.option('--yes', 'Non-interactive adoption confirmation')
|
|
110
|
+
.option('--json', 'Output as JSON')
|
|
111
|
+
.action(async (storePath, opts) => {
|
|
112
|
+
try {
|
|
113
|
+
const dataDir = opts.dataDir ?? getGlobalDataDir();
|
|
114
|
+
const confirmIdentity = opts.yes === true || (await confirmIdentityCreation(opts.id));
|
|
115
|
+
const result = await prepareStoreRegistration({
|
|
116
|
+
id: opts.id,
|
|
117
|
+
localPath: storePath,
|
|
118
|
+
dataDir,
|
|
119
|
+
confirmIdentity,
|
|
120
|
+
});
|
|
121
|
+
const output = {
|
|
122
|
+
store: result.store,
|
|
123
|
+
already_registered: result.alreadyRegistered,
|
|
124
|
+
created_files: result.createdArtifacts,
|
|
125
|
+
status: sortDiagnostics(result.diagnostics),
|
|
126
|
+
};
|
|
127
|
+
if (opts.json) {
|
|
128
|
+
printJson(output);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(result.alreadyRegistered
|
|
132
|
+
? `Store '${opts.id}' was already registered.`
|
|
133
|
+
: `Registered external store '${opts.id}'.`);
|
|
134
|
+
for (const file of result.createdArtifacts) {
|
|
135
|
+
console.log(`Created ${file}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
emitFailure(opts.json, { store: null, status: [] }, error);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
store
|
|
144
|
+
.command('unregister <id>')
|
|
145
|
+
.description('Remove a store from the local registry without deleting its checkout')
|
|
146
|
+
.option('--json', 'Output as JSON')
|
|
147
|
+
.action(async (id, opts) => {
|
|
148
|
+
try {
|
|
149
|
+
const dataDir = opts.dataDir ?? getGlobalDataDir();
|
|
150
|
+
const result = await unregisterStore({ id, dataDir });
|
|
151
|
+
const output = { store: { id }, removed: result.removed, status: sortDiagnostics(result.diagnostics) };
|
|
152
|
+
if (opts.json) {
|
|
153
|
+
printJson(output);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.log(`Unregistered store '${id}'.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
emitFailure(opts.json, { store: { id }, status: [] }, error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
store
|
|
164
|
+
.command('doctor')
|
|
165
|
+
.description('Inspect registered store health without mutating state')
|
|
166
|
+
.option('--json', 'Output as JSON')
|
|
167
|
+
.action(async (opts) => {
|
|
168
|
+
const dataDir = opts.dataDir ?? getGlobalDataDir();
|
|
169
|
+
const registry = await readRegistry(getStoreRegistryPath(dataDir));
|
|
170
|
+
const inspected = await inspectAllStoresHealth(registry);
|
|
171
|
+
const stores = inspected.map(({ id, root, provenance }) => ({ id, root, provenance }));
|
|
172
|
+
const status = flattenStoreDoctorDiagnostics(inspected);
|
|
173
|
+
const output = { stores, status };
|
|
174
|
+
if (opts.json) {
|
|
175
|
+
printJson(output);
|
|
176
|
+
}
|
|
177
|
+
else if (stores.length === 0) {
|
|
178
|
+
console.log('No stores registered.');
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
for (const store of stores) {
|
|
182
|
+
console.log(`${store.id}\t${store.root}\t(${store.provenance})`);
|
|
183
|
+
}
|
|
184
|
+
for (const diagnostic of status) {
|
|
185
|
+
console.error(`${diagnostic.severity}: ${diagnostic.message}`);
|
|
186
|
+
}
|
|
187
|
+
if (hasErrorDiagnostic(status)) {
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
store
|
|
193
|
+
.command('remove <id>')
|
|
194
|
+
.description('Unregister and delete a managed store checkout')
|
|
195
|
+
.option('--json', 'Output as JSON')
|
|
196
|
+
.action(async (id, opts) => {
|
|
197
|
+
try {
|
|
198
|
+
const dataDir = opts.dataDir ?? getGlobalDataDir();
|
|
199
|
+
const result = await removeManagedStore({ id, dataDir });
|
|
200
|
+
const output = {
|
|
201
|
+
store: { id },
|
|
202
|
+
registry_removed: result.registryRemoved,
|
|
203
|
+
checkout_deleted: result.checkoutDeleted,
|
|
204
|
+
status: sortDiagnostics(result.diagnostics),
|
|
205
|
+
};
|
|
206
|
+
if (opts.json) {
|
|
207
|
+
printJson(output);
|
|
208
|
+
}
|
|
209
|
+
else if (result.checkoutDeleted) {
|
|
210
|
+
console.log(`Removed managed store '${id}'.`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.error(result.diagnostics.map((d) => d.message).join('\n'));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
emitFailure(opts.json, { store: { id }, status: [] }, error);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -5,6 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Command } from 'commander';
|
|
7
7
|
import type { ValidationResult } from '../../core/validation/types.js';
|
|
8
|
+
import type { StoreRegistry } from '../../core/store/foundation.js';
|
|
9
|
+
export interface ResolveValidatePathOptions {
|
|
10
|
+
store?: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
registry?: StoreRegistry;
|
|
13
|
+
}
|
|
14
|
+
export interface ResolvedValidatePath {
|
|
15
|
+
readonly filePath: string;
|
|
16
|
+
readonly root: string;
|
|
17
|
+
readonly storeId?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a validate target path against the selected planning root.
|
|
21
|
+
* Absolute paths are kept; relative paths are joined to the planning root.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveValidateFilePath(filePath: string, options?: ResolveValidatePathOptions): Promise<ResolvedValidatePath>;
|
|
8
24
|
/**
|
|
9
25
|
* Validates a spec file at the given path.
|
|
10
26
|
*
|
|
@@ -4,7 +4,28 @@
|
|
|
4
4
|
* Validates a spec file for structural correctness.
|
|
5
5
|
*/
|
|
6
6
|
import { promises as fs } from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
7
8
|
import { validateSpec } from '../../core/validation/validator.js';
|
|
9
|
+
import { addStoreOption, resolveCommandPlanningRoot } from '../shared/store-option.js';
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a validate target path against the selected planning root.
|
|
12
|
+
* Absolute paths are kept; relative paths are joined to the planning root.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveValidateFilePath(filePath, options = {}) {
|
|
15
|
+
const resolved = await resolveCommandPlanningRoot({
|
|
16
|
+
cwd: options.cwd,
|
|
17
|
+
store: options.store,
|
|
18
|
+
registry: options.registry,
|
|
19
|
+
});
|
|
20
|
+
const absolute = path.isAbsolute(filePath)
|
|
21
|
+
? path.resolve(filePath)
|
|
22
|
+
: path.resolve(resolved.root, filePath);
|
|
23
|
+
return {
|
|
24
|
+
filePath: absolute,
|
|
25
|
+
root: resolved.root,
|
|
26
|
+
storeId: resolved.storeId,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
8
29
|
/**
|
|
9
30
|
* Validates a spec file at the given path.
|
|
10
31
|
*
|
|
@@ -22,16 +43,25 @@ export async function validateSpecFile(filePath) {
|
|
|
22
43
|
* Registers the `validate` command with Commander.
|
|
23
44
|
*/
|
|
24
45
|
export function registerValidateCommand(program) {
|
|
25
|
-
program
|
|
46
|
+
addStoreOption(program
|
|
26
47
|
.command('validate <file>')
|
|
27
48
|
.description('Validate a spec file for structural correctness')
|
|
28
49
|
.option('--json', 'Output as JSON')
|
|
29
50
|
.action(async (file, opts) => {
|
|
30
|
-
const
|
|
51
|
+
const resolved = await resolveValidateFilePath(file, { store: opts.store });
|
|
52
|
+
const result = await validateSpecFile(resolved.filePath);
|
|
31
53
|
if (opts.json) {
|
|
32
|
-
console.info(JSON.stringify(
|
|
54
|
+
console.info(JSON.stringify({
|
|
55
|
+
...result,
|
|
56
|
+
root: resolved.root,
|
|
57
|
+
store_id: resolved.storeId ?? null,
|
|
58
|
+
file: resolved.filePath,
|
|
59
|
+
}, null, 2));
|
|
33
60
|
}
|
|
34
61
|
else if (result.valid) {
|
|
62
|
+
if (resolved.storeId) {
|
|
63
|
+
console.info(`Resolved store: ${resolved.storeId} (${resolved.root})`);
|
|
64
|
+
}
|
|
35
65
|
console.info('Valid: no errors found.');
|
|
36
66
|
}
|
|
37
67
|
else {
|
|
@@ -42,5 +72,5 @@ export function registerValidateCommand(program) {
|
|
|
42
72
|
}
|
|
43
73
|
process.exitCode = 1;
|
|
44
74
|
}
|
|
45
|
-
});
|
|
75
|
+
}));
|
|
46
76
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Workset } from '../../core/worksets.js';
|
|
2
|
+
export interface PrepareWorksetOpenInput {
|
|
3
|
+
workset: Workset;
|
|
4
|
+
existingPaths?: Set<string>;
|
|
5
|
+
}
|
|
6
|
+
export interface PreparedWorksetOpen {
|
|
7
|
+
surviving: Workset['members'];
|
|
8
|
+
skipped: Workset['members'];
|
|
9
|
+
}
|
|
10
|
+
export declare function prepareWorksetOpen(input: PrepareWorksetOpenInput): Promise<PreparedWorksetOpen>;
|
|
11
|
+
import type { Command } from 'commander';
|
|
12
|
+
export declare function registerWorksetCommand(program: Command): void;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { StoreError } from '../../core/store/foundation.js';
|
|
3
|
+
async function memberPathExists(memberPath) {
|
|
4
|
+
try {
|
|
5
|
+
await fs.stat(memberPath);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function prepareWorksetOpen(input) {
|
|
13
|
+
const existing = new Set();
|
|
14
|
+
if (input.existingPaths) {
|
|
15
|
+
for (const memberPath of input.existingPaths) {
|
|
16
|
+
existing.add(memberPath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
for (const member of input.workset.members) {
|
|
21
|
+
if (await memberPathExists(member.path)) {
|
|
22
|
+
existing.add(member.path);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const primary = input.workset.members[0];
|
|
27
|
+
if (!existing.has(primary.path)) {
|
|
28
|
+
throw new StoreError('Primary workset member is unavailable.', {
|
|
29
|
+
severity: 'error',
|
|
30
|
+
code: 'workset_primary_missing',
|
|
31
|
+
message: 'The first saved member path is unavailable.',
|
|
32
|
+
target: `workset.members.${primary.name}`,
|
|
33
|
+
fix: 'Recreate the workset with a valid primary path.',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const surviving = [primary];
|
|
37
|
+
const skipped = input.workset.members.slice(1).filter((member) => {
|
|
38
|
+
if (existing.has(member.path)) {
|
|
39
|
+
surviving.push(member);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
return { surviving, skipped };
|
|
45
|
+
}
|
|
46
|
+
import { getGlobalDataDir } from '../../core/global-config.js';
|
|
47
|
+
import { defaultWorksetsStatePath, readWorksetsState, updateWorksetsState, withWorkset, validateWorksetMembers, validateWorksetName, getWorksetCodeWorkspacePath, removeWorksetByName, } from '../../core/worksets.js';
|
|
48
|
+
import { BUILTIN_OPENERS, buildLaunchCommand, findOpener, isCliAgentOpener } from '../../core/openers.js';
|
|
49
|
+
import { isOpenerAvailable, launchOpener } from '../../core/opener-launch.js';
|
|
50
|
+
import { writeFileAtomically } from '../../core/file-state.js';
|
|
51
|
+
export function registerWorksetCommand(program) {
|
|
52
|
+
const workset = program.command('workset').description('Manage machine-local worksets');
|
|
53
|
+
workset
|
|
54
|
+
.command('create <name>')
|
|
55
|
+
.description('Create a saved workset')
|
|
56
|
+
.requiredOption('--member <member>', 'Member label:absolute-path', collect, [])
|
|
57
|
+
.option('--tool <tool>', 'Preferred opener (cursor|code)')
|
|
58
|
+
.option('--json', 'Output as JSON')
|
|
59
|
+
.action(async (name, opts) => {
|
|
60
|
+
validateWorksetName(name);
|
|
61
|
+
const members = opts.member.map((entry) => {
|
|
62
|
+
const separator = entry.indexOf(':');
|
|
63
|
+
if (separator <= 0) {
|
|
64
|
+
throw new StoreError(`Invalid member '${entry}'.`, {
|
|
65
|
+
severity: 'error',
|
|
66
|
+
code: 'invalid_workset_member',
|
|
67
|
+
message: 'Use label:absolute-path format.',
|
|
68
|
+
target: 'workset.member',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { name: entry.slice(0, separator), path: entry.slice(separator + 1) };
|
|
72
|
+
});
|
|
73
|
+
validateWorksetMembers(members);
|
|
74
|
+
if (opts.tool && isCliAgentOpener(opts.tool)) {
|
|
75
|
+
throw new StoreError('CLI-agent openers are disabled in V1.', {
|
|
76
|
+
severity: 'error',
|
|
77
|
+
code: 'workset_cli_opener_disabled',
|
|
78
|
+
message: 'V1 supports only Cursor and VS Code openers.',
|
|
79
|
+
target: 'workset.tool',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const dataDir = getGlobalDataDir();
|
|
83
|
+
const statePath = defaultWorksetsStatePath(dataDir);
|
|
84
|
+
const saved = await updateWorksetsState(statePath, (state) => withWorkset(state, { name, members, ...(opts.tool ? { tool: opts.tool } : {}) }));
|
|
85
|
+
const payload = { workset: saved.worksets.find((entry) => entry.name === name), status: [] };
|
|
86
|
+
if (opts.json) {
|
|
87
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(`Saved workset '${name}'.`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
workset
|
|
94
|
+
.command('list')
|
|
95
|
+
.description('List saved worksets')
|
|
96
|
+
.option('--json', 'Output as JSON')
|
|
97
|
+
.action(async (opts) => {
|
|
98
|
+
const state = await readWorksetsState(defaultWorksetsStatePath(getGlobalDataDir()));
|
|
99
|
+
const payload = { worksets: state.worksets, status: [] };
|
|
100
|
+
if (opts.json) {
|
|
101
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
for (const entry of state.worksets) {
|
|
105
|
+
console.log(`${entry.name}\t${entry.members.length} members`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
workset
|
|
110
|
+
.command('show <name>')
|
|
111
|
+
.description('Inspect a saved workset')
|
|
112
|
+
.option('--json', 'Output as JSON')
|
|
113
|
+
.action(async (name, opts) => {
|
|
114
|
+
const state = await readWorksetsState(defaultWorksetsStatePath(getGlobalDataDir()));
|
|
115
|
+
const entry = state.worksets.find((item) => item.name === name);
|
|
116
|
+
if (!entry) {
|
|
117
|
+
throw new StoreError(`Workset '${name}' not found.`, {
|
|
118
|
+
severity: 'error',
|
|
119
|
+
code: 'workset_not_found',
|
|
120
|
+
message: `Workset '${name}' not found.`,
|
|
121
|
+
target: 'workset.name',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const payload = { workset: entry, status: [] };
|
|
125
|
+
if (opts.json) {
|
|
126
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
workset
|
|
133
|
+
.command('remove <name>')
|
|
134
|
+
.description('Remove a saved workset and its derived workspace file')
|
|
135
|
+
.option('--json', 'Output as JSON')
|
|
136
|
+
.action(async (name, opts) => {
|
|
137
|
+
const dataDir = getGlobalDataDir();
|
|
138
|
+
const statePath = defaultWorksetsStatePath(dataDir);
|
|
139
|
+
const removed = await removeWorksetByName(statePath, name, dataDir);
|
|
140
|
+
if (!removed) {
|
|
141
|
+
throw new StoreError(`Workset '${name}' not found.`, {
|
|
142
|
+
severity: 'error',
|
|
143
|
+
code: 'workset_not_found',
|
|
144
|
+
message: `Workset '${name}' not found.`,
|
|
145
|
+
target: 'workset.name',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const payload = { workset: { name }, removed: true, status: [] };
|
|
149
|
+
if (opts.json) {
|
|
150
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log(`Removed workset '${name}'.`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
workset
|
|
157
|
+
.command('open <name>')
|
|
158
|
+
.description('Regenerate and optionally launch a workset editor view')
|
|
159
|
+
.option('--tool <tool>', 'Override preferred opener')
|
|
160
|
+
.option('--no-launch', 'Generate workspace file without launching the editor')
|
|
161
|
+
.option('--json', 'Output as JSON')
|
|
162
|
+
.action(async (name, opts) => {
|
|
163
|
+
const dataDir = getGlobalDataDir();
|
|
164
|
+
const state = await readWorksetsState(defaultWorksetsStatePath(dataDir));
|
|
165
|
+
const entry = state.worksets.find((item) => item.name === name);
|
|
166
|
+
if (!entry) {
|
|
167
|
+
throw new StoreError(`Workset '${name}' not found.`, {
|
|
168
|
+
severity: 'error',
|
|
169
|
+
code: 'workset_not_found',
|
|
170
|
+
message: `Workset '${name}' not found.`,
|
|
171
|
+
target: 'workset.name',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const prepared = await prepareWorksetOpen({ workset: entry });
|
|
175
|
+
const openerId = opts.tool ?? entry.tool ?? 'code';
|
|
176
|
+
if (isCliAgentOpener(openerId)) {
|
|
177
|
+
throw new StoreError('CLI-agent openers are disabled in V1.', {
|
|
178
|
+
severity: 'error',
|
|
179
|
+
code: 'workset_cli_opener_disabled',
|
|
180
|
+
message: 'V1 supports only Cursor and VS Code openers.',
|
|
181
|
+
target: 'workset.tool',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const opener = findOpener(BUILTIN_OPENERS, openerId);
|
|
185
|
+
if (!opener) {
|
|
186
|
+
throw new StoreError(`Unknown opener '${openerId}'.`, {
|
|
187
|
+
severity: 'error',
|
|
188
|
+
code: 'workset_unknown_opener',
|
|
189
|
+
message: `Unknown opener '${openerId}'.`,
|
|
190
|
+
target: 'workset.tool',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (!isOpenerAvailable(opener)) {
|
|
194
|
+
throw new StoreError(`Opener '${openerId}' is not available on PATH.`, {
|
|
195
|
+
severity: 'error',
|
|
196
|
+
code: 'workset_opener_unavailable',
|
|
197
|
+
message: `Could not find '${opener.command}' on PATH.`,
|
|
198
|
+
target: 'workset.tool',
|
|
199
|
+
fix: 'Install the editor or choose another opener with --tool.',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const workspacePath = getWorksetCodeWorkspacePath(dataDir, name);
|
|
203
|
+
const folders = prepared.surviving.map((member) => ({ name: member.name, path: member.path }));
|
|
204
|
+
await writeFileAtomically(workspacePath, JSON.stringify({ folders }, null, 2) + '\n');
|
|
205
|
+
const launch = buildLaunchCommand(opener, {
|
|
206
|
+
members: [...prepared.surviving],
|
|
207
|
+
codeWorkspacePath: workspacePath,
|
|
208
|
+
});
|
|
209
|
+
if (opts.launch !== false) {
|
|
210
|
+
launchOpener(opener, workspacePath, launch.cwd);
|
|
211
|
+
}
|
|
212
|
+
const payload = {
|
|
213
|
+
workset: entry,
|
|
214
|
+
workspace_path: workspacePath,
|
|
215
|
+
launch,
|
|
216
|
+
skipped_members: prepared.skipped,
|
|
217
|
+
status: [],
|
|
218
|
+
};
|
|
219
|
+
if (opts.json) {
|
|
220
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
console.log(`Generated ${workspacePath}`);
|
|
224
|
+
if (opts.launch !== false) {
|
|
225
|
+
console.log(`Launched ${launch.executable} ${launch.args.join(' ')}`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.log(`Launch: ${launch.executable} ${launch.args.join(' ')}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function collect(value, previous) {
|
|
234
|
+
return [...previous, value];
|
|
235
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -12,6 +12,10 @@ import { registerInitCommand } from './commands/init.js';
|
|
|
12
12
|
import { registerSyncCommand } from './commands/sync.js';
|
|
13
13
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
14
14
|
import { registerParityReportCommand } from './commands/parity-report.js';
|
|
15
|
+
import { registerStoreCommand } from './commands/store.js';
|
|
16
|
+
import { registerShowCommand } from './commands/show.js';
|
|
17
|
+
import { registerContextCommand } from './commands/context.js';
|
|
18
|
+
import { registerWorksetCommand } from './commands/workset.js';
|
|
15
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
20
|
const __dirname = dirname(__filename);
|
|
17
21
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
|
|
@@ -35,6 +39,10 @@ registerInitCommand(program);
|
|
|
35
39
|
registerSyncCommand(program);
|
|
36
40
|
registerDoctorCommand(program);
|
|
37
41
|
registerParityReportCommand(program);
|
|
42
|
+
registerStoreCommand(program);
|
|
43
|
+
registerShowCommand(program);
|
|
44
|
+
registerContextCommand(program);
|
|
45
|
+
registerWorksetCommand(program);
|
|
38
46
|
program.exitOverride();
|
|
39
47
|
try {
|
|
40
48
|
await program.parseAsync();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { type ResolvePlanningRootOptions, type ResolvedPlanningRoot } from '../../core/root-selection.js';
|
|
3
|
+
export interface CommandPlanningRootOptions extends ResolvePlanningRootOptions {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveCommandPlanningRoot(options?: CommandPlanningRootOptions): Promise<ResolvedPlanningRoot>;
|
|
7
|
+
export declare function addStoreOption(command: Command): Command;
|
|
8
|
+
export declare function resolveRootFromCommandOptions(options: {
|
|
9
|
+
store?: string;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getGlobalConfig, getStoreRegistryPath } from '../../core/global-config.js';
|
|
2
|
+
import { readRegistry } from '../../core/store/registry.js';
|
|
3
|
+
import { hasErrorDiagnostic } from '../../core/diagnostics.js';
|
|
4
|
+
import { resolvePlanningRoot, } from '../../core/root-selection.js';
|
|
5
|
+
export async function resolveCommandPlanningRoot(options = {}) {
|
|
6
|
+
const registryPath = getStoreRegistryPath();
|
|
7
|
+
let registry = options.registry;
|
|
8
|
+
if (!registry) {
|
|
9
|
+
try {
|
|
10
|
+
registry = await readRegistry(registryPath);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
registry = { version: 1, stores: {} };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const globalConfig = options.globalConfig ?? getGlobalConfig();
|
|
17
|
+
const resolved = await resolvePlanningRoot({
|
|
18
|
+
startPath: options.cwd ?? options.startPath ?? process.cwd(),
|
|
19
|
+
store: options.store,
|
|
20
|
+
registry,
|
|
21
|
+
globalConfig,
|
|
22
|
+
readConfig: options.readConfig,
|
|
23
|
+
allowImplicitRoot: options.allowImplicitRoot,
|
|
24
|
+
});
|
|
25
|
+
if (hasErrorDiagnostic(resolved.diagnostics) && resolved.source === 'implicit') {
|
|
26
|
+
const message = resolved.diagnostics.map((d) => d.message).join('\n');
|
|
27
|
+
throw new Error(message);
|
|
28
|
+
}
|
|
29
|
+
return resolved;
|
|
30
|
+
}
|
|
31
|
+
export function addStoreOption(command) {
|
|
32
|
+
return command.option('--store <id>', 'Select planning store for this command');
|
|
33
|
+
}
|
|
34
|
+
export async function resolveRootFromCommandOptions(options) {
|
|
35
|
+
const resolved = await resolveCommandPlanningRoot({
|
|
36
|
+
cwd: options.cwd,
|
|
37
|
+
store: options.store,
|
|
38
|
+
});
|
|
39
|
+
return resolved.root;
|
|
40
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { SchemaYaml } from './types.js';
|
|
2
|
+
import type { Diagnostic } from '../diagnostics.js';
|
|
3
|
+
import type { RootSource } from '../root-selection.js';
|
|
2
4
|
/**
|
|
3
5
|
* Dependency information included in artifact instructions.
|
|
4
6
|
*/
|
|
@@ -10,6 +12,15 @@ export interface DependencyInfo {
|
|
|
10
12
|
/** Description of the dependency artifact */
|
|
11
13
|
readonly description: string;
|
|
12
14
|
}
|
|
15
|
+
export interface ReferencedStoresSection {
|
|
16
|
+
readonly rendered: string;
|
|
17
|
+
readonly truncated: boolean;
|
|
18
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
19
|
+
}
|
|
20
|
+
export interface RootProvenanceSection {
|
|
21
|
+
readonly source: RootSource;
|
|
22
|
+
readonly storeId?: string;
|
|
23
|
+
}
|
|
13
24
|
/**
|
|
14
25
|
* Loaded instructions for creating an artifact.
|
|
15
26
|
*/
|
|
@@ -18,6 +29,10 @@ export interface ArtifactInstructions {
|
|
|
18
29
|
readonly instruction: string | undefined;
|
|
19
30
|
/** Project context from the config */
|
|
20
31
|
readonly context: string | undefined;
|
|
32
|
+
/** Separately budgeted referenced-store index */
|
|
33
|
+
readonly referencedStores?: ReferencedStoresSection;
|
|
34
|
+
/** Selected planning root provenance */
|
|
35
|
+
readonly rootProvenance?: RootProvenanceSection;
|
|
21
36
|
/** Dependencies with their metadata */
|
|
22
37
|
readonly dependencies: readonly DependencyInfo[];
|
|
23
38
|
/** The artifact's generates path */
|
|
@@ -30,6 +45,8 @@ export interface ArtifactInstructions {
|
|
|
30
45
|
*/
|
|
31
46
|
export interface ProjectConfig {
|
|
32
47
|
readonly context?: string;
|
|
48
|
+
readonly referencedStores?: ReferencedStoresSection;
|
|
49
|
+
readonly rootProvenance?: RootProvenanceSection;
|
|
33
50
|
readonly [key: string]: unknown;
|
|
34
51
|
}
|
|
35
52
|
/**
|
|
@@ -17,6 +17,8 @@ export function loadInstructions(artifactId, changeDir, schema, config) {
|
|
|
17
17
|
return {
|
|
18
18
|
instruction: artifact.instruction,
|
|
19
19
|
context: config?.context ?? undefined,
|
|
20
|
+
referencedStores: config?.referencedStores,
|
|
21
|
+
rootProvenance: config?.rootProvenance,
|
|
20
22
|
dependencies,
|
|
21
23
|
generates: artifact.generates,
|
|
22
24
|
description: artifact.description,
|