@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.
- package/README.md +34 -8
- 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/init.d.ts +1 -0
- package/dist/cli/commands/init.js +20 -6
- package/dist/cli/commands/instructions.d.ts +4 -1
- package/dist/cli/commands/instructions.js +62 -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 +23 -0
- package/dist/core/artifact-graph/instruction-loader.js +6 -0
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/artifact-language.d.ts +7 -0
- package/dist/core/artifact-language.js +33 -0
- 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 +16 -0
- package/dist/core/project-config.js +104 -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/prompts/shared/artifact-language.md +30 -0
- package/skills/specflow-apply/SKILL.md +12 -27
- package/skills/specflow-explore/SKILL.md +6 -0
- package/skills/specflow-propose/SKILL.md +6 -0
- package/skills/specflow-refine/SKILL.md +6 -0
- package/skills/specflow-snap/SKILL.md +6 -0
package/README.md
CHANGED
|
@@ -13,6 +13,12 @@ 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 + 产物与 CLI | [多仓完整指南](./MULTI_REPO_GUIDE.md) |
|
|
18
|
+
| 多仓原生能力(设计) | Store / References / Workset | [多仓技术方案](./MULTI_REPO_TECHNICAL_DESIGN.md) |
|
|
19
|
+
| 多仓场景示例(TALOS) | contracts + api + web 实操路径 | [TALOS 多仓教程](./TALOS_MULTI_REPO_TUTORIAL.md) |
|
|
20
|
+
| TALOS 公共需求详例 | 工作区邀请成员(Hub→BE→FE→联调) | [公共需求 Walkthrough](./docs/TALOS_SHARED_REQUIREMENT_WALKTHROUGH.md) |
|
|
21
|
+
| 产物与作用 | explore / proposal / delta / design / tasks / 主 specs | [产物说明](./SPECFLOW_ARTIFACTS.md) |
|
|
16
22
|
|
|
17
23
|
---
|
|
18
24
|
|
|
@@ -111,7 +117,7 @@ npm install -g @gordon.gan/specflow
|
|
|
111
117
|
npm install -g github:Gordon-Gan-Jiang/specflow
|
|
112
118
|
|
|
113
119
|
# 验证
|
|
114
|
-
specflow --version #
|
|
120
|
+
specflow --version # 以 npm / package.json 为准(开发中可见 1.2.0-beta)
|
|
115
121
|
specflow --help
|
|
116
122
|
```
|
|
117
123
|
|
|
@@ -126,13 +132,32 @@ cd specflow && npm install && npm run build && npm link
|
|
|
126
132
|
|
|
127
133
|
```bash
|
|
128
134
|
cd your-project
|
|
129
|
-
specflow init
|
|
130
|
-
specflow init --
|
|
131
|
-
specflow init --
|
|
135
|
+
specflow init # 默认:Claude + Cursor,英文产物
|
|
136
|
+
specflow init --artifact-language zh-CN # 简体中文产物
|
|
137
|
+
specflow init --artifact-language zh # zh 是 zh-CN 的 CLI 别名
|
|
138
|
+
specflow init --ide codex # 仅 Codex
|
|
139
|
+
specflow init --ide all --artifact-language en # Claude + Cursor + Codex
|
|
132
140
|
```
|
|
133
141
|
|
|
134
142
|
init 会创建 `specflow/` 目录、写入 `config.yaml`,并按 `--ide` 生成对应托管资产(skills、commands、prompts 等),同时幂等地追加 `.gitignore` 忽略可再生的 prompts/schemas/templates。
|
|
135
143
|
|
|
144
|
+
产物语言保存在项目配置中:
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
schema: specflow
|
|
148
|
+
|
|
149
|
+
artifacts:
|
|
150
|
+
language: zh-CN # en | zh-CN;未配置时默认为 en
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
语言配置控制 proposal、spec、design、tasks 等产物的自然语言叙述。SpecFlow
|
|
154
|
+
解析依赖的 `ADDED Requirements`、`Requirement:`、`Scenario:`、`WHEN`、`THEN`
|
|
155
|
+
等协议标记始终保持英文;capability ID、change name、路径、命令和代码也不会翻译。
|
|
156
|
+
|
|
157
|
+
`init` 对已有 `specflow/config.yaml` 永不覆盖,包括使用
|
|
158
|
+
`--force-assets` 或传入不同的 `--artifact-language`。已有项目需要直接编辑
|
|
159
|
+
`artifacts.language`;修改只影响后续新建或主动重写的内容,不会自动翻译历史产物。
|
|
160
|
+
|
|
136
161
|
### 跑通第一个变更
|
|
137
162
|
|
|
138
163
|
**需求已清晰:**
|
|
@@ -243,7 +268,7 @@ CLI 从当前目录**向上查找**项目根(识别 `specflow/config.yaml`)
|
|
|
243
268
|
|
|
244
269
|
| 命令 | 说明 |
|
|
245
270
|
|------|------|
|
|
246
|
-
| `specflow init [--ide claude\|cursor\|codex\|both\|all]` |
|
|
271
|
+
| `specflow init [--ide claude\|cursor\|codex\|both\|all] [--artifact-language en\|zh-CN]` | 初始化项目目录、产物语言与 IDE 托管资产;`zh` 是 `zh-CN` 的别名 |
|
|
247
272
|
| `specflow sync [--ide ...] [--no-parity-strict]` | 从已安装的 specflow 包同步/更新 IDE 资产(升级后用) |
|
|
248
273
|
| `specflow doctor [--parity] [--json]` | 诊断 IDE 资产是否完整、迁移状态是否正常 |
|
|
249
274
|
| `specflow parity-report [--json]` | 对比 Claude / Cursor / Codex 能力是否一致 |
|
|
@@ -269,6 +294,8 @@ CLI 从当前目录**向上查找**项目根(识别 `specflow/config.yaml`)
|
|
|
269
294
|
|
|
270
295
|
## 关键概念
|
|
271
296
|
|
|
297
|
+
各产物职责、门禁与命令对照的完整说明见 [SPECFLOW_ARTIFACTS.md](./SPECFLOW_ARTIFACTS.md)。
|
|
298
|
+
|
|
272
299
|
### Specs — 唯一事实来源
|
|
273
300
|
|
|
274
301
|
存放在 `specflow/specs/`,描述各 capability 的行为规格:
|
|
@@ -387,11 +414,10 @@ specflow doctor --parity
|
|
|
387
414
|
specflow parity-report
|
|
388
415
|
```
|
|
389
416
|
|
|
390
|
-
|
|
417
|
+
刷新已初始化项目的托管资产(不会覆盖 `specflow/config.yaml`):
|
|
391
418
|
|
|
392
419
|
```bash
|
|
393
|
-
|
|
394
|
-
specflow init
|
|
420
|
+
specflow init --force-assets --ide all
|
|
395
421
|
```
|
|
396
422
|
|
|
397
423
|
---
|
|
@@ -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
|
+
}
|
|
@@ -7,13 +7,19 @@ import { validateReportAgainstManifest } from '../../integrations/shared/parity-
|
|
|
7
7
|
import { appendManagedBlock } from '../../integrations/shared/marker-write.js';
|
|
8
8
|
import { getRegeneratableIgnoreLines } from '../../integrations/shared/managed-assets.js';
|
|
9
9
|
import { detectMigrationState } from '../../integrations/shared/migration-state.js';
|
|
10
|
-
|
|
10
|
+
import { DEFAULT_ARTIFACT_LANGUAGE, requireArtifactLanguage, } from '../../core/artifact-language.js';
|
|
11
|
+
function renderConfigYaml(artifactLanguage) {
|
|
12
|
+
return `schema: specflow
|
|
13
|
+
|
|
14
|
+
artifacts:
|
|
15
|
+
language: ${artifactLanguage}
|
|
11
16
|
|
|
12
17
|
# Project context (customize for your project)
|
|
13
18
|
# context: |
|
|
14
19
|
# Tech stack: ...
|
|
15
20
|
# Architecture: ...
|
|
16
21
|
`;
|
|
22
|
+
}
|
|
17
23
|
function findPackageRoot() {
|
|
18
24
|
const currentFile = fileURLToPath(import.meta.url);
|
|
19
25
|
let dir = join(currentFile, '..');
|
|
@@ -32,8 +38,8 @@ async function createDirectoryStructure(projectRoot) {
|
|
|
32
38
|
fs.mkdir(join(projectRoot, 'specflow', 'specs'), { recursive: true }),
|
|
33
39
|
]);
|
|
34
40
|
}
|
|
35
|
-
async function writeConfig(projectRoot) {
|
|
36
|
-
await fs.writeFile(join(projectRoot, 'specflow', 'config.yaml'),
|
|
41
|
+
async function writeConfig(projectRoot, artifactLanguage) {
|
|
42
|
+
await fs.writeFile(join(projectRoot, 'specflow', 'config.yaml'), renderConfigYaml(artifactLanguage), 'utf-8');
|
|
37
43
|
}
|
|
38
44
|
async function pathExists(path) {
|
|
39
45
|
try {
|
|
@@ -52,6 +58,9 @@ async function hasIdeAssets(projectRoot, ide) {
|
|
|
52
58
|
return validateReportAgainstManifest(report).ok;
|
|
53
59
|
}
|
|
54
60
|
export async function initProject(projectRoot, packageRoot, options = {}) {
|
|
61
|
+
const artifactLanguage = options.artifactLanguage === undefined
|
|
62
|
+
? DEFAULT_ARTIFACT_LANGUAGE
|
|
63
|
+
: requireArtifactLanguage(options.artifactLanguage);
|
|
55
64
|
const ide = options.ide ?? 'both';
|
|
56
65
|
const forceAssets = options.forceAssets ?? false;
|
|
57
66
|
const parityStrict = options.parityStrict ?? true;
|
|
@@ -60,14 +69,17 @@ export async function initProject(projectRoot, packageRoot, options = {}) {
|
|
|
60
69
|
const migrationState = await detectMigrationState(projectRoot);
|
|
61
70
|
if (!initialized) {
|
|
62
71
|
await createDirectoryStructure(projectRoot);
|
|
63
|
-
await writeConfig(projectRoot);
|
|
72
|
+
await writeConfig(projectRoot, artifactLanguage);
|
|
64
73
|
}
|
|
74
|
+
const languageEditMessage = options.artifactLanguage
|
|
75
|
+
? ' Edit specflow/config.yaml explicitly to change artifact language in an initialized project.'
|
|
76
|
+
: '';
|
|
65
77
|
const selectedIdeTargets = adapters.map((a) => a.ide);
|
|
66
78
|
const allTargetsHealthy = (await Promise.all(selectedIdeTargets.map((target) => hasIdeAssets(projectRoot, target)))).every(Boolean);
|
|
67
79
|
if (initialized && !forceAssets && allTargetsHealthy) {
|
|
68
80
|
return {
|
|
69
81
|
status: 'already_initialized',
|
|
70
|
-
message: `Project at ${projectRoot} is already initialized for ide=${ide} (state=${migrationState}). Use --force-assets to refresh managed assets
|
|
82
|
+
message: `Project at ${projectRoot} is already initialized for ide=${ide} (state=${migrationState}). Use --force-assets to refresh managed assets.${languageEditMessage}`,
|
|
71
83
|
};
|
|
72
84
|
}
|
|
73
85
|
for (const adapter of adapters) {
|
|
@@ -86,7 +98,7 @@ export async function initProject(projectRoot, packageRoot, options = {}) {
|
|
|
86
98
|
return {
|
|
87
99
|
status: initialized ? 'updated_assets' : 'initialized',
|
|
88
100
|
message: initialized
|
|
89
|
-
? `Updated ${ide} assets at ${projectRoot}`
|
|
101
|
+
? `Updated ${ide} assets at ${projectRoot}.${languageEditMessage}`
|
|
90
102
|
: `Initialized specflow project at ${projectRoot}`,
|
|
91
103
|
};
|
|
92
104
|
}
|
|
@@ -102,6 +114,7 @@ export function registerInitCommand(program) {
|
|
|
102
114
|
.command('init')
|
|
103
115
|
.description('Initialize a project with specflow directory structure and assets')
|
|
104
116
|
.option('--ide <target>', 'Target IDE assets: claude | cursor | codex | both | all', 'both')
|
|
117
|
+
.option('--artifact-language <language>', 'Artifact content language: en | zh-CN (alias: zh)')
|
|
105
118
|
.option('--force-assets', 'Refresh managed IDE assets even when project is initialized')
|
|
106
119
|
.option('--no-parity-strict', 'Disable strict parity validation after asset generation')
|
|
107
120
|
.action(async (opts) => {
|
|
@@ -109,6 +122,7 @@ export function registerInitCommand(program) {
|
|
|
109
122
|
const packageRoot = findPackageRoot();
|
|
110
123
|
const result = await initProject(projectRoot, packageRoot, {
|
|
111
124
|
ide: opts.ide ?? 'both',
|
|
125
|
+
artifactLanguage: opts.artifactLanguage,
|
|
112
126
|
forceAssets: opts.forceAssets ?? false,
|
|
113
127
|
parityStrict: opts.parityStrict ?? true,
|
|
114
128
|
});
|
|
@@ -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,48 @@ 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
|
+
artifactLanguage: projectConfig.artifactLanguage,
|
|
30
|
+
references: projectConfig.references,
|
|
31
|
+
};
|
|
32
|
+
return { schemaName, config: config };
|
|
23
33
|
}
|
|
24
34
|
catch {
|
|
25
35
|
return { schemaName: 'specflow' };
|
|
26
36
|
}
|
|
27
37
|
}
|
|
38
|
+
async function enrichInstructionConfig(projectRoot, options) {
|
|
39
|
+
const registry = await readRegistry(getStoreRegistryPath());
|
|
40
|
+
const resolved = await resolveCommandPlanningRoot({
|
|
41
|
+
cwd: options.cwd ?? projectRoot,
|
|
42
|
+
store: options.store,
|
|
43
|
+
registry,
|
|
44
|
+
});
|
|
45
|
+
const configPath = join(resolved.root, 'specflow', 'config.yaml');
|
|
46
|
+
let parsed = parseProjectConfig({});
|
|
47
|
+
try {
|
|
48
|
+
parsed = parseProjectConfig(yaml.load(fs.readFileSync(configPath, 'utf-8')));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// use empty config when project config is unavailable
|
|
52
|
+
}
|
|
53
|
+
const referencedStores = await buildReferencedStoreIndex({
|
|
54
|
+
references: parsed.references,
|
|
55
|
+
registry,
|
|
56
|
+
rootStoreId: resolved.storeId,
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
context: parsed.context,
|
|
60
|
+
artifactLanguage: parsed.artifactLanguage,
|
|
61
|
+
referencedStores,
|
|
62
|
+
rootProvenance: {
|
|
63
|
+
source: resolved.source,
|
|
64
|
+
storeId: resolved.storeId,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
28
68
|
/**
|
|
29
69
|
* Gets instructions for creating an artifact within a change.
|
|
30
70
|
*
|
|
@@ -34,36 +74,49 @@ function readProjectConfig(projectRoot) {
|
|
|
34
74
|
* @returns The artifact instructions with dependencies and output path
|
|
35
75
|
* @throws When the artifact or change is not found
|
|
36
76
|
*/
|
|
37
|
-
export async function getInstructions(artifactId, changeName, projectRoot) {
|
|
77
|
+
export async function getInstructions(artifactId, changeName, projectRoot, options = {}) {
|
|
38
78
|
const changeDir = join(projectRoot, 'specflow', 'changes', changeName);
|
|
39
79
|
if (!fs.existsSync(changeDir)) {
|
|
40
80
|
throw new Error(`Change "${changeName}" not found at ${changeDir}`);
|
|
41
81
|
}
|
|
42
|
-
const { schemaName
|
|
82
|
+
const { schemaName } = readProjectConfig(projectRoot);
|
|
43
83
|
const schema = resolveSchema(schemaName, projectRoot);
|
|
84
|
+
const config = await enrichInstructionConfig(projectRoot, options);
|
|
44
85
|
return loadInstructions(artifactId, changeDir, schema, config);
|
|
45
86
|
}
|
|
46
87
|
/**
|
|
47
88
|
* Registers the `instructions` command with Commander.
|
|
48
89
|
*/
|
|
49
90
|
export function registerInstructionsCommand(program) {
|
|
50
|
-
program
|
|
91
|
+
addStoreOption(program
|
|
51
92
|
.command('instructions <artifact-id> <change-name>')
|
|
52
93
|
.description('Show instructions for creating an artifact')
|
|
53
94
|
.option('--json', 'Output as JSON')
|
|
54
95
|
.action(async (artifactId, changeName, opts) => {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
96
|
+
const registry = await readRegistry(getStoreRegistryPath());
|
|
97
|
+
const resolved = await resolveCommandPlanningRoot({ store: opts.store, registry });
|
|
98
|
+
const projectRoot = resolved.root;
|
|
99
|
+
const instructions = await getInstructions(artifactId, changeName, projectRoot, { store: opts.store });
|
|
57
100
|
if (opts.json) {
|
|
58
101
|
console.info(JSON.stringify(instructions, null, 2));
|
|
59
102
|
}
|
|
60
103
|
else {
|
|
61
104
|
console.info(`Artifact: ${artifactId}`);
|
|
105
|
+
if (instructions.rootProvenance) {
|
|
106
|
+
console.info(`Planning root: ${instructions.rootProvenance.source}${instructions.rootProvenance.storeId ? ` (${instructions.rootProvenance.storeId})` : ''}`);
|
|
107
|
+
}
|
|
62
108
|
console.info(`Generates: ${instructions.generates}`);
|
|
63
109
|
console.info(`Description: ${instructions.description}`);
|
|
110
|
+
console.info(`Artifact language: ${instructions.artifactLanguage}`);
|
|
64
111
|
if (instructions.instruction) {
|
|
65
112
|
console.info(`\nInstruction:\n${instructions.instruction}`);
|
|
66
113
|
}
|
|
114
|
+
console.info(`\n${instructions.languageGuidance}`);
|
|
115
|
+
const referenceGuidance = collectReferenceGuidance({ referencedStores: instructions.referencedStores });
|
|
116
|
+
const guidance = referenceGuidance.claude;
|
|
117
|
+
if (guidance) {
|
|
118
|
+
console.info(`\n${guidance}`);
|
|
119
|
+
}
|
|
67
120
|
if (instructions.dependencies.length > 0) {
|
|
68
121
|
console.info('\nDependencies:');
|
|
69
122
|
for (const dep of instructions.dependencies) {
|
|
@@ -71,5 +124,5 @@ export function registerInstructionsCommand(program) {
|
|
|
71
124
|
}
|
|
72
125
|
}
|
|
73
126
|
}
|
|
74
|
-
});
|
|
127
|
+
}));
|
|
75
128
|
}
|
|
@@ -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;
|