@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.
Files changed (63) hide show
  1. package/README.md +6 -0
  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/instructions.d.ts +4 -1
  9. package/dist/cli/commands/instructions.js +58 -9
  10. package/dist/cli/commands/show.d.ts +22 -0
  11. package/dist/cli/commands/show.js +92 -0
  12. package/dist/cli/commands/store.d.ts +16 -0
  13. package/dist/cli/commands/store.js +221 -0
  14. package/dist/cli/commands/validate.d.ts +16 -0
  15. package/dist/cli/commands/validate.js +34 -4
  16. package/dist/cli/commands/workset.d.ts +12 -0
  17. package/dist/cli/commands/workset.js +235 -0
  18. package/dist/cli/index.js +8 -0
  19. package/dist/cli/shared/store-option.d.ts +11 -0
  20. package/dist/cli/shared/store-option.js +40 -0
  21. package/dist/core/artifact-graph/instruction-loader.d.ts +17 -0
  22. package/dist/core/artifact-graph/instruction-loader.js +2 -0
  23. package/dist/core/artifact-graph/types.d.ts +2 -2
  24. package/dist/core/context-assembly.d.ts +9 -0
  25. package/dist/core/context-assembly.js +68 -0
  26. package/dist/core/diagnostics.d.ts +11 -0
  27. package/dist/core/diagnostics.js +18 -0
  28. package/dist/core/file-state.d.ts +23 -0
  29. package/dist/core/file-state.js +101 -0
  30. package/dist/core/global-config.d.ts +26 -0
  31. package/dist/core/global-config.js +77 -0
  32. package/dist/core/opener-launch.d.ts +3 -0
  33. package/dist/core/opener-launch.js +20 -0
  34. package/dist/core/openers.d.ts +23 -0
  35. package/dist/core/openers.js +20 -0
  36. package/dist/core/project-config.d.ts +14 -0
  37. package/dist/core/project-config.js +73 -0
  38. package/dist/core/reference-index.d.ts +8 -0
  39. package/dist/core/reference-index.js +80 -0
  40. package/dist/core/references.d.ts +17 -0
  41. package/dist/core/references.js +51 -0
  42. package/dist/core/relationship-health.d.ts +22 -0
  43. package/dist/core/relationship-health.js +68 -0
  44. package/dist/core/root-selection.d.ts +26 -0
  45. package/dist/core/root-selection.js +197 -0
  46. package/dist/core/store/errors.d.ts +2 -0
  47. package/dist/core/store/errors.js +1 -0
  48. package/dist/core/store/foundation.d.ts +141 -0
  49. package/dist/core/store/foundation.js +79 -0
  50. package/dist/core/store/health.d.ts +13 -0
  51. package/dist/core/store/health.js +117 -0
  52. package/dist/core/store/operations.d.ts +56 -0
  53. package/dist/core/store/operations.js +268 -0
  54. package/dist/core/store/registry.d.ts +15 -0
  55. package/dist/core/store/registry.js +128 -0
  56. package/dist/core/working-set.d.ts +30 -0
  57. package/dist/core/working-set.js +26 -0
  58. package/dist/core/worksets.d.ts +71 -0
  59. package/dist/core/worksets.js +134 -0
  60. package/dist/integrations/shared/skill-renderer.d.ts +6 -0
  61. package/dist/integrations/shared/skill-renderer.js +22 -0
  62. package/package.json +1 -1
  63. package/skills/specflow-apply/SKILL.md +5 -27
package/README.md CHANGED
@@ -13,6 +13,10 @@ SpecFlow 把 **OpenSpec**(结构化需求规划)和 **Superpowers**(TDD、
13
13
  | Claude Code | `/specflow:propose` 等斜杠命令 | 本文 |
14
14
  | Cursor | `specflow:propose` 等命令 | [Cursor 指南](./CURSOR_PACKAGING_AND_USAGE_GUIDE.md) |
15
15
  | OpenAI Codex | `$specflow-propose` 等技能 | [Codex 指南](./CODEX_PACKAGING_AND_USAGE_GUIDE.md) |
16
+ | 多语言 / Go Profile | SpecFlow + 语言专项 skills | [语言 Profile 指南](./LANGUAGE_PROFILE_USAGE_GUIDE.md) |
17
+ | 多仓 / 前后端一体需求 | Contract Hub + Spokes + 产物说明 | [多仓完整指南](./MULTI_REPO_GUIDE.md) |
18
+ | 多仓原生能力(设计) | Store / References / Workset | [多仓技术方案](./MULTI_REPO_TECHNICAL_DESIGN.md) |
19
+ | 产物与作用 | explore / proposal / delta / design / tasks / 主 specs | [产物说明](./SPECFLOW_ARTIFACTS.md) |
16
20
 
17
21
  ---
18
22
 
@@ -269,6 +273,8 @@ CLI 从当前目录**向上查找**项目根(识别 `specflow/config.yaml`)
269
273
 
270
274
  ## 关键概念
271
275
 
276
+ 各产物职责、门禁与命令对照的完整说明见 [SPECFLOW_ARTIFACTS.md](./SPECFLOW_ARTIFACTS.md)。
277
+
272
278
  ### Specs — 唯一事实来源
273
279
 
274
280
  存放在 `specflow/specs/`,描述各 capability 的行为规格:
@@ -6,7 +6,7 @@
6
6
  import { join } from 'node:path';
7
7
  import * as fs from 'node:fs';
8
8
  import { archiveChange } from '../../core/archive.js';
9
- import { requireProjectRoot } from '../../utils/project-root.js';
9
+ import { addStoreOption, resolveRootFromCommandOptions } from '../shared/store-option.js';
10
10
  /**
11
11
  * Archives a change, throwing on failure.
12
12
  *
@@ -36,15 +36,15 @@ export async function archiveChangeCommand(changeName, projectRoot, options = {}
36
36
  * Registers the `change archive` subcommand with Commander.
37
37
  */
38
38
  export function registerChangeArchiveCommand(changeCmd) {
39
- changeCmd
39
+ addStoreOption(changeCmd
40
40
  .command('archive <name>')
41
41
  .description('Archive a completed change')
42
42
  .option('--force', 'Archive even if the change is not in phase=apply')
43
43
  .action(async (name, opts) => {
44
- const projectRoot = requireProjectRoot();
44
+ const projectRoot = await resolveRootFromCommandOptions({ store: opts.store });
45
45
  const result = await archiveChangeCommand(name, projectRoot, {
46
46
  force: opts.force ?? false,
47
47
  });
48
48
  console.info(`Archived change "${name}" to ${result.archivePath}`);
49
- });
49
+ }));
50
50
  }
@@ -6,7 +6,7 @@
6
6
  import { join } from 'node:path';
7
7
  import { validateChangeName, writeChangeMetadata } from '../../utils/change-utils.js';
8
8
  import { directoryExists } from '../../utils/file-system.js';
9
- import { requireProjectRoot } from '../../utils/project-root.js';
9
+ import { addStoreOption, resolveRootFromCommandOptions } from '../shared/store-option.js';
10
10
  const CHANGES_REL_PATH = 'specflow/changes';
11
11
  /**
12
12
  * Returns today's date as YYYY-MM-DD string.
@@ -42,12 +42,12 @@ export async function createChange(name, projectRoot) {
42
42
  * Registers the `change new` subcommand with Commander.
43
43
  */
44
44
  export function registerChangeNewCommand(changeCmd) {
45
- changeCmd
45
+ addStoreOption(changeCmd
46
46
  .command('new <name>')
47
47
  .description('Create a new change directory')
48
- .action(async (name) => {
49
- const projectRoot = requireProjectRoot();
48
+ .action(async (name, opts) => {
49
+ const projectRoot = await resolveRootFromCommandOptions({ store: opts.store });
50
50
  await createChange(name, projectRoot);
51
51
  console.info(`Created change: ${name}`);
52
- });
52
+ }));
53
53
  }
@@ -4,7 +4,7 @@
4
4
  * Read or update the lifecycle phase of a change.
5
5
  */
6
6
  import { CHANGE_PHASES, getChangeMetadata, normalizeChangePhase, updatePhase, } from '../../utils/change-utils.js';
7
- import { requireProjectRoot } from '../../utils/project-root.js';
7
+ import { addStoreOption, resolveRootFromCommandOptions } from '../shared/store-option.js';
8
8
  /**
9
9
  * Returns true if the candidate value is a valid ChangePhase.
10
10
  */
@@ -50,12 +50,12 @@ export async function setPhase(name, phase, projectRoot) {
50
50
  * specflow change phase <name> --set refined # update phase
51
51
  */
52
52
  export function registerChangePhaseCommand(changeCmd) {
53
- changeCmd
53
+ addStoreOption(changeCmd
54
54
  .command('phase <name>')
55
55
  .description('Show or set the lifecycle phase of a change')
56
56
  .option('--set <value>', 'Set the phase to the given value')
57
57
  .action(async (name, opts) => {
58
- const projectRoot = requireProjectRoot();
58
+ const projectRoot = await resolveRootFromCommandOptions({ store: opts.store });
59
59
  if (opts.set !== undefined) {
60
60
  await setPhase(name, opts.set, projectRoot);
61
61
  console.info(`Change "${name}" phase set to ${opts.set}`);
@@ -63,5 +63,5 @@ export function registerChangePhaseCommand(changeCmd) {
63
63
  }
64
64
  const phase = await getPhase(name, projectRoot);
65
65
  console.info(phase ?? '(unset)');
66
- });
66
+ }));
67
67
  }
@@ -9,8 +9,8 @@ import yaml from 'js-yaml';
9
9
  import { resolveSchema } from '../../core/artifact-graph/resolver.js';
10
10
  import { getCompletedArtifacts, getInProgressArtifacts } from '../../core/artifact-graph/state.js';
11
11
  import { formatStatus } from '../../core/artifact-graph/outputs.js';
12
- import { requireProjectRoot } from '../../utils/project-root.js';
13
12
  import { readChangeMetadata } from '../../utils/change-metadata.js';
13
+ import { addStoreOption, resolveRootFromCommandOptions } from '../shared/store-option.js';
14
14
  /**
15
15
  * Reads the project config to determine the schema name.
16
16
  */
@@ -87,12 +87,12 @@ export function formatChangeStatus(status) {
87
87
  * Registers the `change status` subcommand with Commander.
88
88
  */
89
89
  export function registerChangeStatusCommand(changeCmd) {
90
- changeCmd
90
+ addStoreOption(changeCmd
91
91
  .command('status <name>')
92
92
  .description('Show artifact status for a change')
93
93
  .option('--json', 'Output as JSON')
94
94
  .action(async (name, opts) => {
95
- const projectRoot = requireProjectRoot();
95
+ const projectRoot = await resolveRootFromCommandOptions({ store: opts.store });
96
96
  const status = await getChangeStatus(name, projectRoot);
97
97
  if (opts.json) {
98
98
  console.info(JSON.stringify(status, null, 2));
@@ -100,5 +100,5 @@ export function registerChangeStatusCommand(changeCmd) {
100
100
  else {
101
101
  console.info(formatChangeStatus(status));
102
102
  }
103
- });
103
+ }));
104
104
  }
@@ -0,0 +1,18 @@
1
+ import type { Command } from 'commander';
2
+ import { assembleWorkingSet } from '../../core/working-set.js';
3
+ import type { Diagnostic } from '../../core/diagnostics.js';
4
+ import { type RelationshipHealthResult } from '../../core/relationship-health.js';
5
+ export interface ContextCommandOptions {
6
+ store?: string;
7
+ cwd?: string;
8
+ codeWorkspacePath?: string;
9
+ force?: boolean;
10
+ registryPath?: string;
11
+ }
12
+ export declare function runContextCommand(options?: ContextCommandOptions): Promise<{
13
+ workingSet: ReturnType<typeof assembleWorkingSet>;
14
+ workspacePath?: string;
15
+ status: readonly Diagnostic[];
16
+ relationships: RelationshipHealthResult;
17
+ }>;
18
+ export declare function registerContextCommand(program: Command): void;
@@ -0,0 +1,125 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { readFileSync } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import yaml from 'js-yaml';
5
+ import { StoreError } from '../../core/store/foundation.js';
6
+ import { buildWorkingSetCodeWorkspaceJson } from '../../core/working-set.js';
7
+ import { assembleContextFromRoot } from '../../core/context-assembly.js';
8
+ import { resolveCommandPlanningRoot, addStoreOption } from '../shared/store-option.js';
9
+ import { readRegistry } from '../../core/store/registry.js';
10
+ import { getStoreRegistryPath } from '../../core/global-config.js';
11
+ import { parseProjectConfig } from '../../core/project-config.js';
12
+ import { normalizeReferences } from '../../core/references.js';
13
+ import { inspectRelationships, } from '../../core/relationship-health.js';
14
+ export async function runContextCommand(options = {}) {
15
+ const registry = await readRegistry(options.registryPath ?? getStoreRegistryPath());
16
+ const resolved = await resolveCommandPlanningRoot({
17
+ cwd: options.cwd,
18
+ store: options.store,
19
+ registry,
20
+ });
21
+ const assembled = await assembleContextFromRoot(resolved, registry);
22
+ let projectConfig = parseProjectConfig({});
23
+ try {
24
+ const configPath = path.join(resolved.root, 'specflow', 'config.yaml');
25
+ projectConfig = parseProjectConfig(yaml.load(readFileSync(configPath, 'utf-8')));
26
+ }
27
+ catch {
28
+ // empty config when unavailable
29
+ }
30
+ const rootStoreId = resolved.storeId;
31
+ const references = normalizeReferences(projectConfig.references, rootStoreId);
32
+ const relationships = await inspectRelationships({
33
+ resolved,
34
+ references,
35
+ registry,
36
+ projectConfig,
37
+ });
38
+ if (!options.codeWorkspacePath) {
39
+ return {
40
+ workingSet: assembled.workingSet,
41
+ status: assembled.diagnostics,
42
+ relationships,
43
+ };
44
+ }
45
+ if (!options.force) {
46
+ try {
47
+ await fs.stat(options.codeWorkspacePath);
48
+ throw new StoreError('Workspace file already exists.', {
49
+ severity: 'error',
50
+ code: 'workspace_exists',
51
+ message: 'Refusing to overwrite an existing workspace file without --force.',
52
+ target: 'context.codeWorkspace',
53
+ fix: 'Pass --force to overwrite the file.',
54
+ });
55
+ }
56
+ catch (error) {
57
+ if (error instanceof StoreError) {
58
+ throw error;
59
+ }
60
+ }
61
+ }
62
+ const json = buildWorkingSetCodeWorkspaceJson(assembled.workingSet);
63
+ await fs.writeFile(options.codeWorkspacePath, JSON.stringify(json, null, 2) + '\n', 'utf-8');
64
+ return {
65
+ workingSet: assembled.workingSet,
66
+ workspacePath: options.codeWorkspacePath,
67
+ status: assembled.diagnostics,
68
+ relationships,
69
+ };
70
+ }
71
+ export function registerContextCommand(program) {
72
+ addStoreOption(program
73
+ .command('context')
74
+ .description('Show the derived working set for the resolved planning root')
75
+ .option('--code-workspace <path>', 'Write a VS Code workspace file for healthy members')
76
+ .option('--force', 'Overwrite an existing workspace file')
77
+ .option('--relationships', 'Include relationship health categories in human output')
78
+ .option('--json', 'Output as JSON')
79
+ .action(async (opts) => {
80
+ try {
81
+ const result = await runContextCommand({
82
+ store: opts.store,
83
+ codeWorkspacePath: opts.codeWorkspace,
84
+ force: opts.force,
85
+ });
86
+ const payload = {
87
+ working_set: result.workingSet,
88
+ workspace_path: result.workspacePath ?? null,
89
+ relationships: {
90
+ root_health: result.relationships.rootHealth,
91
+ selected_store_health: result.relationships.selectedStoreHealth,
92
+ reference_health: result.relationships.referenceHealth,
93
+ registry_health: result.relationships.registryHealth,
94
+ pointer_health: result.relationships.pointerHealth,
95
+ mutations_performed: result.relationships.mutationsPerformed,
96
+ },
97
+ status: result.status,
98
+ };
99
+ if (opts.json) {
100
+ console.log(JSON.stringify(payload, null, 2));
101
+ }
102
+ else {
103
+ console.log(JSON.stringify(payload.working_set, null, 2));
104
+ if (opts.relationships) {
105
+ console.log(JSON.stringify(payload.relationships, null, 2));
106
+ }
107
+ for (const diagnostic of result.status) {
108
+ console.error(`${diagnostic.severity}: ${diagnostic.message}`);
109
+ }
110
+ }
111
+ }
112
+ catch (error) {
113
+ const diagnostics = error instanceof StoreError
114
+ ? error.diagnostics
115
+ : [{ severity: 'error', code: 'context_error', message: String(error) }];
116
+ if (opts.json) {
117
+ console.log(JSON.stringify({ working_set: null, status: diagnostics }, null, 2));
118
+ }
119
+ else {
120
+ console.error(diagnostics.map((d) => d.message).join('\n'));
121
+ }
122
+ process.exit(1);
123
+ }
124
+ }));
125
+ }
@@ -14,7 +14,10 @@ import type { ArtifactInstructions } from '../../core/artifact-graph/instruction
14
14
  * @returns The artifact instructions with dependencies and output path
15
15
  * @throws When the artifact or change is not found
16
16
  */
17
- export declare function getInstructions(artifactId: string, changeName: string, projectRoot: string): Promise<ArtifactInstructions>;
17
+ export declare function getInstructions(artifactId: string, changeName: string, projectRoot: string, options?: {
18
+ store?: string;
19
+ cwd?: string;
20
+ }): Promise<ArtifactInstructions>;
18
21
  /**
19
22
  * Registers the `instructions` command with Commander.
20
23
  */
@@ -8,7 +8,12 @@ import * as fs from 'node:fs';
8
8
  import yaml from 'js-yaml';
9
9
  import { resolveSchema } from '../../core/artifact-graph/resolver.js';
10
10
  import { loadInstructions } from '../../core/artifact-graph/instruction-loader.js';
11
- import { requireProjectRoot } from '../../utils/project-root.js';
11
+ import { addStoreOption, resolveCommandPlanningRoot } from '../shared/store-option.js';
12
+ import { parseProjectConfig } from '../../core/project-config.js';
13
+ import { buildReferencedStoreIndex } from '../../core/reference-index.js';
14
+ import { readRegistry } from '../../core/store/registry.js';
15
+ import { getStoreRegistryPath } from '../../core/global-config.js';
16
+ import { collectReferenceGuidance } from '../../integrations/shared/skill-renderer.js';
12
17
  /**
13
18
  * Reads the project config to determine the schema name and optional context.
14
19
  */
@@ -18,13 +23,46 @@ function readProjectConfig(projectRoot) {
18
23
  const content = fs.readFileSync(configPath, 'utf-8');
19
24
  const parsed = yaml.load(content);
20
25
  const schemaName = typeof parsed?.schema === 'string' ? parsed.schema : 'specflow';
21
- const config = { context: typeof parsed?.context === 'string' ? parsed.context : undefined };
22
- return { schemaName, config };
26
+ const projectConfig = parseProjectConfig(parsed);
27
+ const config = {
28
+ context: projectConfig.context,
29
+ references: projectConfig.references,
30
+ };
31
+ return { schemaName, config: config };
23
32
  }
24
33
  catch {
25
34
  return { schemaName: 'specflow' };
26
35
  }
27
36
  }
37
+ async function enrichInstructionConfig(projectRoot, options) {
38
+ const registry = await readRegistry(getStoreRegistryPath());
39
+ const resolved = await resolveCommandPlanningRoot({
40
+ cwd: options.cwd ?? projectRoot,
41
+ store: options.store,
42
+ registry,
43
+ });
44
+ const configPath = join(resolved.root, 'specflow', 'config.yaml');
45
+ let parsed = parseProjectConfig({});
46
+ try {
47
+ parsed = parseProjectConfig(yaml.load(fs.readFileSync(configPath, 'utf-8')));
48
+ }
49
+ catch {
50
+ // use empty config when project config is unavailable
51
+ }
52
+ const referencedStores = await buildReferencedStoreIndex({
53
+ references: parsed.references,
54
+ registry,
55
+ rootStoreId: resolved.storeId,
56
+ });
57
+ return {
58
+ context: parsed.context,
59
+ referencedStores,
60
+ rootProvenance: {
61
+ source: resolved.source,
62
+ storeId: resolved.storeId,
63
+ },
64
+ };
65
+ }
28
66
  /**
29
67
  * Gets instructions for creating an artifact within a change.
30
68
  *
@@ -34,36 +72,47 @@ function readProjectConfig(projectRoot) {
34
72
  * @returns The artifact instructions with dependencies and output path
35
73
  * @throws When the artifact or change is not found
36
74
  */
37
- export async function getInstructions(artifactId, changeName, projectRoot) {
75
+ export async function getInstructions(artifactId, changeName, projectRoot, options = {}) {
38
76
  const changeDir = join(projectRoot, 'specflow', 'changes', changeName);
39
77
  if (!fs.existsSync(changeDir)) {
40
78
  throw new Error(`Change "${changeName}" not found at ${changeDir}`);
41
79
  }
42
- const { schemaName, config } = readProjectConfig(projectRoot);
80
+ const { schemaName } = readProjectConfig(projectRoot);
43
81
  const schema = resolveSchema(schemaName, projectRoot);
82
+ const config = await enrichInstructionConfig(projectRoot, options);
44
83
  return loadInstructions(artifactId, changeDir, schema, config);
45
84
  }
46
85
  /**
47
86
  * Registers the `instructions` command with Commander.
48
87
  */
49
88
  export function registerInstructionsCommand(program) {
50
- program
89
+ addStoreOption(program
51
90
  .command('instructions <artifact-id> <change-name>')
52
91
  .description('Show instructions for creating an artifact')
53
92
  .option('--json', 'Output as JSON')
54
93
  .action(async (artifactId, changeName, opts) => {
55
- const projectRoot = requireProjectRoot();
56
- const instructions = await getInstructions(artifactId, changeName, projectRoot);
94
+ const registry = await readRegistry(getStoreRegistryPath());
95
+ const resolved = await resolveCommandPlanningRoot({ store: opts.store, registry });
96
+ const projectRoot = resolved.root;
97
+ const instructions = await getInstructions(artifactId, changeName, projectRoot, { store: opts.store });
57
98
  if (opts.json) {
58
99
  console.info(JSON.stringify(instructions, null, 2));
59
100
  }
60
101
  else {
61
102
  console.info(`Artifact: ${artifactId}`);
103
+ if (instructions.rootProvenance) {
104
+ console.info(`Planning root: ${instructions.rootProvenance.source}${instructions.rootProvenance.storeId ? ` (${instructions.rootProvenance.storeId})` : ''}`);
105
+ }
62
106
  console.info(`Generates: ${instructions.generates}`);
63
107
  console.info(`Description: ${instructions.description}`);
64
108
  if (instructions.instruction) {
65
109
  console.info(`\nInstruction:\n${instructions.instruction}`);
66
110
  }
111
+ const referenceGuidance = collectReferenceGuidance({ referencedStores: instructions.referencedStores });
112
+ const guidance = referenceGuidance.claude;
113
+ if (guidance) {
114
+ console.info(`\n${guidance}`);
115
+ }
67
116
  if (instructions.dependencies.length > 0) {
68
117
  console.info('\nDependencies:');
69
118
  for (const dep of instructions.dependencies) {
@@ -71,5 +120,5 @@ export function registerInstructionsCommand(program) {
71
120
  }
72
121
  }
73
122
  }
74
- });
123
+ }));
75
124
  }
@@ -0,0 +1,22 @@
1
+ import type { Diagnostic } from '../../core/diagnostics.js';
2
+ export interface ShowSpecOptions {
3
+ specId: string;
4
+ type: 'spec';
5
+ storeId?: string;
6
+ cwd?: string;
7
+ json?: boolean;
8
+ fixtures?: {
9
+ content?: string;
10
+ root?: string;
11
+ storeId?: string;
12
+ };
13
+ }
14
+ export interface ShowSpecResult {
15
+ stdout: string;
16
+ stderr: string;
17
+ status: Diagnostic[];
18
+ exitCode: number;
19
+ }
20
+ export declare function runShowSpec(options: ShowSpecOptions): Promise<ShowSpecResult>;
21
+ import type { Command } from 'commander';
22
+ export declare function registerShowCommand(program: Command): void;
@@ -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;