@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
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
|
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 {
|
|
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
|
|
22
|
-
|
|
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
|
|
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
|
|
56
|
-
const
|
|
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;
|