@gordon.gan/specflow 1.1.1 → 1.2.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.
Files changed (72) hide show
  1. package/README.md +34 -8
  2. package/dist/cli/commands/change-archive.js +4 -4
  3. package/dist/cli/commands/change-new.js +5 -5
  4. package/dist/cli/commands/change-phase.js +4 -4
  5. package/dist/cli/commands/change-status.js +4 -4
  6. package/dist/cli/commands/context.d.ts +18 -0
  7. package/dist/cli/commands/context.js +125 -0
  8. package/dist/cli/commands/init.d.ts +1 -0
  9. package/dist/cli/commands/init.js +20 -6
  10. package/dist/cli/commands/instructions.d.ts +4 -1
  11. package/dist/cli/commands/instructions.js +62 -9
  12. package/dist/cli/commands/show.d.ts +22 -0
  13. package/dist/cli/commands/show.js +92 -0
  14. package/dist/cli/commands/store.d.ts +16 -0
  15. package/dist/cli/commands/store.js +221 -0
  16. package/dist/cli/commands/validate.d.ts +16 -0
  17. package/dist/cli/commands/validate.js +34 -4
  18. package/dist/cli/commands/workset.d.ts +12 -0
  19. package/dist/cli/commands/workset.js +235 -0
  20. package/dist/cli/index.js +8 -0
  21. package/dist/cli/shared/store-option.d.ts +11 -0
  22. package/dist/cli/shared/store-option.js +40 -0
  23. package/dist/core/artifact-graph/instruction-loader.d.ts +23 -0
  24. package/dist/core/artifact-graph/instruction-loader.js +6 -0
  25. package/dist/core/artifact-graph/types.d.ts +2 -2
  26. package/dist/core/artifact-language.d.ts +7 -0
  27. package/dist/core/artifact-language.js +33 -0
  28. package/dist/core/context-assembly.d.ts +9 -0
  29. package/dist/core/context-assembly.js +68 -0
  30. package/dist/core/diagnostics.d.ts +11 -0
  31. package/dist/core/diagnostics.js +18 -0
  32. package/dist/core/file-state.d.ts +23 -0
  33. package/dist/core/file-state.js +101 -0
  34. package/dist/core/global-config.d.ts +26 -0
  35. package/dist/core/global-config.js +77 -0
  36. package/dist/core/opener-launch.d.ts +3 -0
  37. package/dist/core/opener-launch.js +20 -0
  38. package/dist/core/openers.d.ts +23 -0
  39. package/dist/core/openers.js +20 -0
  40. package/dist/core/project-config.d.ts +16 -0
  41. package/dist/core/project-config.js +104 -0
  42. package/dist/core/reference-index.d.ts +8 -0
  43. package/dist/core/reference-index.js +80 -0
  44. package/dist/core/references.d.ts +17 -0
  45. package/dist/core/references.js +51 -0
  46. package/dist/core/relationship-health.d.ts +22 -0
  47. package/dist/core/relationship-health.js +68 -0
  48. package/dist/core/root-selection.d.ts +26 -0
  49. package/dist/core/root-selection.js +197 -0
  50. package/dist/core/store/errors.d.ts +2 -0
  51. package/dist/core/store/errors.js +1 -0
  52. package/dist/core/store/foundation.d.ts +141 -0
  53. package/dist/core/store/foundation.js +79 -0
  54. package/dist/core/store/health.d.ts +13 -0
  55. package/dist/core/store/health.js +117 -0
  56. package/dist/core/store/operations.d.ts +56 -0
  57. package/dist/core/store/operations.js +268 -0
  58. package/dist/core/store/registry.d.ts +15 -0
  59. package/dist/core/store/registry.js +128 -0
  60. package/dist/core/working-set.d.ts +30 -0
  61. package/dist/core/working-set.js +26 -0
  62. package/dist/core/worksets.d.ts +71 -0
  63. package/dist/core/worksets.js +134 -0
  64. package/dist/integrations/shared/skill-renderer.d.ts +6 -0
  65. package/dist/integrations/shared/skill-renderer.js +22 -0
  66. package/package.json +1 -1
  67. package/prompts/shared/artifact-language.md +30 -0
  68. package/skills/specflow-apply/SKILL.md +12 -27
  69. package/skills/specflow-explore/SKILL.md +6 -0
  70. package/skills/specflow-propose/SKILL.md +6 -0
  71. package/skills/specflow-refine/SKILL.md +6 -0
  72. package/skills/specflow-snap/SKILL.md +6 -0
@@ -0,0 +1,92 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { sortDiagnostics } from '../../core/diagnostics.js';
4
+ import { resolveCommandPlanningRoot } from '../shared/store-option.js';
5
+ export async function runShowSpec(options) {
6
+ let root = options.fixtures?.root;
7
+ let storeId = options.fixtures?.storeId ?? options.storeId;
8
+ let status = [];
9
+ if (root === undefined) {
10
+ const resolved = await resolveCommandPlanningRoot({
11
+ cwd: options.cwd,
12
+ store: options.storeId,
13
+ });
14
+ root = resolved.root;
15
+ storeId = resolved.storeId ?? options.storeId;
16
+ status = [...resolved.diagnostics];
17
+ }
18
+ const specPath = path.join(root, 'specflow', 'specs', options.specId, 'spec.md');
19
+ let content = options.fixtures?.content;
20
+ if (content === undefined) {
21
+ try {
22
+ content = await fs.readFile(specPath, 'utf-8');
23
+ }
24
+ catch {
25
+ return {
26
+ stdout: '',
27
+ stderr: `Spec '${options.specId}' not found.`,
28
+ status: sortDiagnostics([
29
+ ...status,
30
+ {
31
+ severity: 'error',
32
+ code: 'spec_not_found',
33
+ message: `Spec '${options.specId}' not found.`,
34
+ target: `specs.${options.specId}`,
35
+ },
36
+ ]),
37
+ exitCode: 1,
38
+ };
39
+ }
40
+ }
41
+ if (options.json) {
42
+ return {
43
+ stdout: JSON.stringify({
44
+ id: options.specId,
45
+ type: options.type,
46
+ root,
47
+ store_id: storeId ?? null,
48
+ content,
49
+ status,
50
+ }, null, 2),
51
+ stderr: '',
52
+ status,
53
+ exitCode: 0,
54
+ };
55
+ }
56
+ const banner = storeId ? `Resolved store: ${storeId} (${root})\n` : `Resolved root: ${root}\n`;
57
+ return {
58
+ stdout: content,
59
+ stderr: banner,
60
+ status,
61
+ exitCode: 0,
62
+ };
63
+ }
64
+ import { addStoreOption } from '../shared/store-option.js';
65
+ export function registerShowCommand(program) {
66
+ addStoreOption(program
67
+ .command('show <spec-id>')
68
+ .description('Show a baseline spec from the resolved planning root')
69
+ .option('--type <type>', 'Artifact type', 'spec')
70
+ .option('--json', 'Output as JSON')
71
+ .action(async (specId, opts) => {
72
+ const result = await runShowSpec({
73
+ specId,
74
+ type: opts.type === 'spec' ? 'spec' : 'spec',
75
+ storeId: opts.store,
76
+ json: opts.json,
77
+ });
78
+ if (result.stdout) {
79
+ process.stdout.write(result.stdout);
80
+ if (!result.stdout.endsWith('\n')) {
81
+ process.stdout.write('\n');
82
+ }
83
+ }
84
+ if (result.stderr) {
85
+ process.stderr.write(result.stderr);
86
+ }
87
+ if (result.exitCode !== 0 && opts.json) {
88
+ console.log(JSON.stringify({ status: result.status }, null, 2));
89
+ }
90
+ process.exit(result.exitCode);
91
+ }));
92
+ }
@@ -0,0 +1,16 @@
1
+ import type { Command } from 'commander';
2
+ import { type Diagnostic } from '../../core/diagnostics.js';
3
+ export interface StoreListOptions {
4
+ json?: boolean;
5
+ dataDir?: string;
6
+ }
7
+ export interface StoreListOutput {
8
+ stores: Array<{
9
+ id: string;
10
+ root: string;
11
+ provenance: string;
12
+ }>;
13
+ status: Diagnostic[];
14
+ }
15
+ export declare function runStoreList(options?: StoreListOptions): Promise<StoreListOutput>;
16
+ export declare function registerStoreCommand(program: Command): void;
@@ -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 result = await validateSpecFile(file);
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(result, null, 2));
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();