dream-wf 0.1.1 → 0.1.2
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 +91 -24
- package/core/grill-prd-policy.md +2 -0
- package/core/workflow-profile.md +1 -0
- package/package.json +4 -3
- package/src/cli/index.js +192 -65
- package/src/deps/index.js +50 -0
- package/src/lib/catalog.js +82 -0
- package/src/lib/mcp.js +305 -0
- package/src/lib/platforms.js +13 -3
- package/src/lib/trellis.js +22 -2
- package/src/platforms/claude-code/index.js +7 -3
- package/src/platforms/codex/index.js +83 -0
- package/src/platforms/cursor/index.js +7 -3
- package/src/platforms/opencode/index.js +7 -3
- package/src/platforms/shared.js +11 -0
- package/src/tui/index.js +445 -0
- package/templates/hooks/claude-code/__pycache__/dream-wf-guard.cpython-314.pyc +0 -0
- package/templates/hooks/claude-code/dream-wf-guard.py +33 -11
- package/templates/hooks/codex/__pycache__/dream-wf-guard.cpython-314.pyc +0 -0
- package/templates/hooks/codex/dream-wf-guard.py +150 -0
- package/templates/hooks/cursor/__pycache__/dream-wf-guard.cpython-314.pyc +0 -0
- package/templates/hooks/cursor/dream-wf-guard.py +30 -11
- package/templates/hooks/opencode/dream-wf-guard.js +28 -10
- package/templates/rules/claude-code/dream-wf-block.md +2 -0
- package/templates/rules/codex/dream-wf-block.md +43 -0
- package/templates/rules/cursor/dream-wf.mdc +2 -1
- package/templates/rules/opencode/dream-wf-block.md +2 -0
- package/templates/skills/dream-wf-grill-prd/SKILL.md +54 -2
- package/templates/spec/guides/dream-wf-prd-policy.md +14 -0
|
@@ -2,16 +2,20 @@ import path from 'node:path';
|
|
|
2
2
|
import { readFile, chmod } from 'node:fs/promises';
|
|
3
3
|
import { readJsonObject, writeJsonObject, pushUniqueByCommand } from '../../lib/json.js';
|
|
4
4
|
import { writeIfChanged } from '../../lib/files.js';
|
|
5
|
-
import { installCommonDreamWfFiles, installRuleFile,
|
|
5
|
+
import { installCommonDreamWfFiles, installRuleFile, installSelectedSkills } from '../shared.js';
|
|
6
|
+
import { installMcpServers } from '../../lib/mcp.js';
|
|
6
7
|
|
|
7
8
|
export async function installCursor(packageRoot, targetRoot, options) {
|
|
8
9
|
const results = [];
|
|
9
10
|
|
|
10
11
|
results.push(await installRuleFile(packageRoot, targetRoot, 'templates/rules/cursor/dream-wf.mdc', '.cursor/rules/dream-wf.mdc'));
|
|
11
|
-
results.push(await
|
|
12
|
-
results.push(await installSkill(packageRoot, targetRoot, '.cursor', 'dream-wf-mcp-policy'));
|
|
12
|
+
results.push(...await installSelectedSkills(packageRoot, targetRoot, '.cursor', options.skills));
|
|
13
13
|
results.push(...await installCommonDreamWfFiles(packageRoot, targetRoot));
|
|
14
14
|
|
|
15
|
+
if (options.mcps && options.mcps.length > 0) {
|
|
16
|
+
results.push(await installMcpServers(targetRoot, 'cursor', options.mcps));
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
if (options.mode === 'strict') {
|
|
16
20
|
results.push(await installCursorHook(packageRoot, targetRoot));
|
|
17
21
|
results.push(await mergeCursorHooks(targetRoot));
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { writeIfChanged } from '../../lib/files.js';
|
|
4
|
-
import { installCommonDreamWfFiles, installManagedBlock,
|
|
4
|
+
import { installCommonDreamWfFiles, installManagedBlock, installSelectedSkills } from '../shared.js';
|
|
5
|
+
import { installMcpServers } from '../../lib/mcp.js';
|
|
5
6
|
|
|
6
7
|
export async function installOpenCode(packageRoot, targetRoot, options) {
|
|
7
8
|
const results = [];
|
|
8
9
|
|
|
9
10
|
results.push(await installManagedBlock(packageRoot, targetRoot, 'templates/rules/opencode/dream-wf-block.md', 'AGENTS.md', '<!-- DREAM-WF:START -->', '<!-- DREAM-WF:END -->'));
|
|
10
|
-
results.push(await
|
|
11
|
-
results.push(await installSkill(packageRoot, targetRoot, '.opencode', 'dream-wf-mcp-policy'));
|
|
11
|
+
results.push(...await installSelectedSkills(packageRoot, targetRoot, '.opencode', options.skills));
|
|
12
12
|
results.push(...await installCommonDreamWfFiles(packageRoot, targetRoot));
|
|
13
13
|
|
|
14
|
+
if (options.mcps && options.mcps.length > 0) {
|
|
15
|
+
results.push(await installMcpServers(targetRoot, 'opencode', options.mcps));
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
if (options.mode === 'strict') {
|
|
15
19
|
results.push(await installOpenCodePlugin(packageRoot, targetRoot));
|
|
16
20
|
}
|
package/src/platforms/shared.js
CHANGED
|
@@ -9,6 +9,17 @@ export async function installSkill(packageRoot, targetRoot, platformDir, skillNa
|
|
|
9
9
|
return writeIfChanged(targetPath, contents);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// 按 catalog 选中的 skill 条目安装。skills 为 resolveSkills 返回的数组。
|
|
13
|
+
// 若 skills 为 undefined,则安装全部(向后兼容旧 CLI 行为)。
|
|
14
|
+
export async function installSelectedSkills(packageRoot, targetRoot, platformDir, skills) {
|
|
15
|
+
const list = skills ?? [];
|
|
16
|
+
const results = [];
|
|
17
|
+
for (const skill of list) {
|
|
18
|
+
results.push(await installSkill(packageRoot, targetRoot, platformDir, skill.name));
|
|
19
|
+
}
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export async function installSpecGuide(packageRoot, targetRoot, fileName) {
|
|
13
24
|
const sourcePath = path.join(packageRoot, 'templates', 'spec', 'guides', fileName);
|
|
14
25
|
const targetPath = path.join(targetRoot, '.trellis', 'spec', 'guides', fileName);
|
package/src/tui/index.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { SUPPORTED_PLATFORMS, PLATFORM_LABELS } from "../lib/platforms.js";
|
|
3
|
+
import {
|
|
4
|
+
SKILL_CATALOG,
|
|
5
|
+
MCP_CATALOG,
|
|
6
|
+
defaultSkillIds,
|
|
7
|
+
defaultMcpIds,
|
|
8
|
+
} from "../lib/catalog.js";
|
|
9
|
+
import { detectTrellis } from "../lib/trellis.js";
|
|
10
|
+
import { trellisPlatformFlag } from "../lib/platforms.js";
|
|
11
|
+
|
|
12
|
+
const COLORS = {
|
|
13
|
+
reset: "\x1B[0m",
|
|
14
|
+
dim: "\x1B[2m",
|
|
15
|
+
bold: "\x1B[1m",
|
|
16
|
+
magenta: "\x1B[35m",
|
|
17
|
+
cyan: "\x1B[36m",
|
|
18
|
+
green: "\x1B[32m",
|
|
19
|
+
yellow: "\x1B[33m",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function colorize(text, color) {
|
|
23
|
+
if (!process.stdout.isTTY || process.env.NO_COLOR) {
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
return `${COLORS[color] ?? ""}${text}${COLORS.reset}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderCheckboxList(items, cursorIndex, selected) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
items.forEach((item, index) => {
|
|
32
|
+
const pointer = index === cursorIndex ? colorize("❯", COLORS.cyan) : " ";
|
|
33
|
+
const check = selected.has(item.id)
|
|
34
|
+
? colorize("◉", COLORS.green)
|
|
35
|
+
: colorize("◯", COLORS.dim);
|
|
36
|
+
const label =
|
|
37
|
+
index === cursorIndex ? colorize(item.label, COLORS.bold) : item.label;
|
|
38
|
+
lines.push(`${pointer} ${check} ${label}`);
|
|
39
|
+
if (item.description) {
|
|
40
|
+
lines.push(colorize(` ${item.description}`, COLORS.dim));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderRadioList(items, cursorIndex) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
items.forEach((item, index) => {
|
|
49
|
+
const pointer = index === cursorIndex ? colorize("❯", COLORS.cyan) : " ";
|
|
50
|
+
const dot =
|
|
51
|
+
index === cursorIndex
|
|
52
|
+
? colorize("●", COLORS.green)
|
|
53
|
+
: colorize("○", COLORS.dim);
|
|
54
|
+
const label =
|
|
55
|
+
index === cursorIndex ? colorize(item.label, COLORS.bold) : item.label;
|
|
56
|
+
lines.push(`${pointer} ${dot} ${label}`);
|
|
57
|
+
});
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 在 raw 模式下读取按键。返回标准化按键名。
|
|
62
|
+
// 方向键转义序列 \x1B[A/B/C/D 可能分多个 data 事件到达,这里做拼接。
|
|
63
|
+
function readKeystroke() {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
let buffer = "";
|
|
66
|
+
function onRaw(data) {
|
|
67
|
+
buffer += data.toString();
|
|
68
|
+
// 完整的转义序列至少 3 字节;单字符按键 1 字节即可判定。
|
|
69
|
+
if (buffer.length === 1) {
|
|
70
|
+
const char = buffer;
|
|
71
|
+
if (char === "\x1B") {
|
|
72
|
+
// 等待后续字节。
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
process.stdin.removeListener("data", onRaw);
|
|
76
|
+
resolve(char);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (buffer.length >= 3 && buffer.startsWith("\x1B")) {
|
|
80
|
+
process.stdin.removeListener("data", onRaw);
|
|
81
|
+
resolve(buffer);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// 超时兜底,避免 hang。
|
|
85
|
+
if (buffer.length > 8) {
|
|
86
|
+
process.stdin.removeListener("data", onRaw);
|
|
87
|
+
resolve(buffer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
process.stdin.setRawMode(true);
|
|
91
|
+
process.stdin.resume();
|
|
92
|
+
process.stdin.once("data", onRaw);
|
|
93
|
+
process.stdin.on("data", onRaw);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopRaw() {
|
|
98
|
+
process.stdin.setRawMode(false);
|
|
99
|
+
process.stdin.pause();
|
|
100
|
+
process.stdin.removeAllListeners("data");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function multiSelect({ title, hint, items, defaults }) {
|
|
104
|
+
let cursor = 0;
|
|
105
|
+
const selected = new Set(defaults);
|
|
106
|
+
|
|
107
|
+
function render() {
|
|
108
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
109
|
+
process.stdout.write(`${colorize(title, COLORS.magenta)}\n`);
|
|
110
|
+
if (hint) {
|
|
111
|
+
process.stdout.write(`${colorize(hint, COLORS.dim)}\n`);
|
|
112
|
+
}
|
|
113
|
+
process.stdout.write("\n");
|
|
114
|
+
process.stdout.write(renderCheckboxList(items, cursor, selected));
|
|
115
|
+
process.stdout.write("\n\n");
|
|
116
|
+
process.stdout.write(
|
|
117
|
+
colorize(
|
|
118
|
+
" ↑/↓ 移动 · space 切换选中 · enter 确认 · a 全选/全不选 · ctrl+c 退出\n",
|
|
119
|
+
COLORS.dim,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
while (true) {
|
|
125
|
+
render();
|
|
126
|
+
const key = await readKeystroke();
|
|
127
|
+
|
|
128
|
+
if (key === "\x03") {
|
|
129
|
+
stopRaw();
|
|
130
|
+
process.stdout.write("\n已取消\n");
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
if (key === "q") {
|
|
134
|
+
stopRaw();
|
|
135
|
+
process.stdout.write("\n已退出\n");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
if (key === "\x1B[A") {
|
|
139
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (key === "\x1B[B") {
|
|
143
|
+
cursor = (cursor + 1) % items.length;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (key === " ") {
|
|
147
|
+
const item = items[cursor];
|
|
148
|
+
if (selected.has(item.id)) {
|
|
149
|
+
selected.delete(item.id);
|
|
150
|
+
} else {
|
|
151
|
+
selected.add(item.id);
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (key === "a") {
|
|
156
|
+
if (selected.size === items.length) {
|
|
157
|
+
selected.clear();
|
|
158
|
+
} else {
|
|
159
|
+
items.forEach((item) => selected.add(item.id));
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (key === "\r" || key === "\n") {
|
|
164
|
+
stopRaw();
|
|
165
|
+
return [...selected];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function singleSelect({ title, hint, items }) {
|
|
171
|
+
let cursor = 0;
|
|
172
|
+
|
|
173
|
+
function render() {
|
|
174
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
175
|
+
process.stdout.write(`${colorize(title, COLORS.magenta)}\n`);
|
|
176
|
+
if (hint) {
|
|
177
|
+
process.stdout.write(`${colorize(hint, COLORS.dim)}\n`);
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write("\n");
|
|
180
|
+
process.stdout.write(renderRadioList(items, cursor));
|
|
181
|
+
process.stdout.write("\n\n");
|
|
182
|
+
process.stdout.write(
|
|
183
|
+
colorize(" ↑/↓ 移动 · enter 选择 · ctrl+c 退出\n", COLORS.dim),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
while (true) {
|
|
188
|
+
render();
|
|
189
|
+
const key = await readKeystroke();
|
|
190
|
+
|
|
191
|
+
if (key === "\x03") {
|
|
192
|
+
stopRaw();
|
|
193
|
+
process.stdout.write("\n已取消\n");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
if (key === "\x1B[A") {
|
|
197
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (key === "\x1B[B") {
|
|
201
|
+
cursor = (cursor + 1) % items.length;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (key === "\r" || key === "\n") {
|
|
205
|
+
stopRaw();
|
|
206
|
+
return items[cursor].id;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function confirmPrompt(label) {
|
|
212
|
+
let cursor = 0;
|
|
213
|
+
const options = ["确认安装", "取消"];
|
|
214
|
+
|
|
215
|
+
function render() {
|
|
216
|
+
const line = options
|
|
217
|
+
.map((option, index) => {
|
|
218
|
+
const prefix = index === cursor ? colorize("❯", COLORS.cyan) : " ";
|
|
219
|
+
const text = index === cursor ? colorize(option, COLORS.bold) : option;
|
|
220
|
+
return `${prefix} ${text}`;
|
|
221
|
+
})
|
|
222
|
+
.join(" ");
|
|
223
|
+
process.stdout.write(`\r${colorize(label, COLORS.bold)} ${line}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
while (true) {
|
|
227
|
+
render();
|
|
228
|
+
const key = await readKeystroke();
|
|
229
|
+
|
|
230
|
+
if (key === "\x03") {
|
|
231
|
+
stopRaw();
|
|
232
|
+
process.stdout.write("\n已取消\n");
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
key === "\x1B[A" ||
|
|
237
|
+
key === "\x1B[B" ||
|
|
238
|
+
key === "\t" ||
|
|
239
|
+
key === "\x1B[C" ||
|
|
240
|
+
key === "\x1B[D"
|
|
241
|
+
) {
|
|
242
|
+
cursor = (cursor + 1) % options.length;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (key === "\r" || key === "\n") {
|
|
246
|
+
stopRaw();
|
|
247
|
+
process.stdout.write("\n");
|
|
248
|
+
return cursor === 0;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 文本输入 prompt:在 raw 模式下逐字符读取,回车确认。
|
|
254
|
+
async function textInput({ title, hint, placeholder }) {
|
|
255
|
+
let value = "";
|
|
256
|
+
|
|
257
|
+
function render() {
|
|
258
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
259
|
+
process.stdout.write(`${colorize(title, COLORS.magenta)}\n`);
|
|
260
|
+
if (hint) {
|
|
261
|
+
process.stdout.write(`${colorize(hint, COLORS.dim)}\n`);
|
|
262
|
+
}
|
|
263
|
+
process.stdout.write("\n");
|
|
264
|
+
const display = value || colorize(placeholder ?? "", COLORS.dim);
|
|
265
|
+
process.stdout.write(` ❯ ${display}\n`);
|
|
266
|
+
process.stdout.write("\n");
|
|
267
|
+
process.stdout.write(colorize(" 输入文字 · enter 确认 · ctrl+c 退出\n", COLORS.dim));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
while (true) {
|
|
271
|
+
render();
|
|
272
|
+
const key = await readKeystroke();
|
|
273
|
+
|
|
274
|
+
if (key === "\x03") {
|
|
275
|
+
stopRaw();
|
|
276
|
+
process.stdout.write("\n已取消\n");
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
if (key === "\r" || key === "\n") {
|
|
280
|
+
stopRaw();
|
|
281
|
+
return value.trim();
|
|
282
|
+
}
|
|
283
|
+
if (key === "\x7F" || key === "\b") {
|
|
284
|
+
value = value.slice(0, -1);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// 方向键等转义序列忽略。
|
|
288
|
+
if (key.startsWith("\x1B")) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
// 普通可打印字符。
|
|
292
|
+
if (key.length === 1 && key >= " " && key <= "~") {
|
|
293
|
+
value += key;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderSummary(platform, skills, mcps, mode, trellisAction) {
|
|
299
|
+
const lines = [];
|
|
300
|
+
lines.push(colorize("即将安装:", COLORS.magenta));
|
|
301
|
+
lines.push(` 平台: ${colorize(PLATFORM_LABELS[platform], COLORS.bold)}`);
|
|
302
|
+
lines.push(` 模式: ${mode === "strict" ? colorize("strict", COLORS.yellow) : colorize("advisory", COLORS.green)}`);
|
|
303
|
+
|
|
304
|
+
if (trellisAction) {
|
|
305
|
+
lines.push(` Trellis: ${colorize(trellisAction, COLORS.cyan)}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
lines.push(` Skills (${skills.length}):`);
|
|
309
|
+
if (skills.length === 0) {
|
|
310
|
+
lines.push(colorize(" (无)", COLORS.dim));
|
|
311
|
+
} else {
|
|
312
|
+
skills.forEach((s) =>
|
|
313
|
+
lines.push(` ${colorize("✓", COLORS.green)} ${s.name}`),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
lines.push(` MCPs (${mcps.length}):`);
|
|
317
|
+
if (mcps.length === 0) {
|
|
318
|
+
lines.push(colorize(" (无)", COLORS.dim));
|
|
319
|
+
} else {
|
|
320
|
+
mcps.forEach((m) =>
|
|
321
|
+
lines.push(` ${colorize("✓", COLORS.green)} ${m.name}`),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 交互式安装向导:平台 -> Trellis 基础依赖 -> skill -> mcp -> 确认安装。
|
|
328
|
+
export async function runInteractive() {
|
|
329
|
+
// 第一步:选择平台。
|
|
330
|
+
const platformItems = [...SUPPORTED_PLATFORMS].map((id) => ({
|
|
331
|
+
id,
|
|
332
|
+
label: PLATFORM_LABELS[id],
|
|
333
|
+
}));
|
|
334
|
+
const platform = await singleSelect({
|
|
335
|
+
title: "第一步 · 选择目标平台",
|
|
336
|
+
hint: "选择你要安装 dream-wf 的 AI 编码平台。",
|
|
337
|
+
items: platformItems,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 第二步:Trellis 基础依赖。
|
|
341
|
+
const trellisState = await detectTrellis(process.cwd());
|
|
342
|
+
let trellisAction = null;
|
|
343
|
+
let installDeps = false;
|
|
344
|
+
let developer = undefined;
|
|
345
|
+
|
|
346
|
+
if (trellisState.exists) {
|
|
347
|
+
trellisAction = "已初始化(跳过)";
|
|
348
|
+
} else if (!trellisState.cli) {
|
|
349
|
+
// trellis CLI 未安装。
|
|
350
|
+
const installChoice = await singleSelect({
|
|
351
|
+
title: "第二步 · Trellis 基础依赖",
|
|
352
|
+
hint: `检测到 trellis CLI 未安装。是否自动安装 @mindfoldhq/trellis?`,
|
|
353
|
+
items: [
|
|
354
|
+
{ id: "install", label: "自动安装 trellis CLI 并初始化" },
|
|
355
|
+
{ id: "skip", label: "跳过(稍后手动安装)" },
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (installChoice === "install") {
|
|
360
|
+
const name = await textInput({
|
|
361
|
+
title: "输入开发者名称",
|
|
362
|
+
hint: "trellis init -u <name> 需要一个开发者名称。",
|
|
363
|
+
placeholder: "your-name",
|
|
364
|
+
});
|
|
365
|
+
if (!name) {
|
|
366
|
+
process.stdout.write("开发者名称不能为空,已取消\n");
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
trellisAction = "安装 trellis CLI + 初始化项目";
|
|
370
|
+
installDeps = true;
|
|
371
|
+
developer = name;
|
|
372
|
+
} else {
|
|
373
|
+
trellisAction = "跳过(需手动安装 trellis CLI)";
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
// trellis CLI 已安装但项目未初始化。
|
|
377
|
+
const initChoice = await singleSelect({
|
|
378
|
+
title: "第二步 · Trellis 基础依赖",
|
|
379
|
+
hint: `trellis CLI 已安装,但当前项目未初始化。是否初始化?`,
|
|
380
|
+
items: [
|
|
381
|
+
{ id: "init", label: "初始化 Trellis 项目" },
|
|
382
|
+
{ id: "skip", label: "跳过(稍后手动初始化)" },
|
|
383
|
+
],
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (initChoice === "init") {
|
|
387
|
+
const name = await textInput({
|
|
388
|
+
title: "输入开发者名称",
|
|
389
|
+
hint: "trellis init -u <name> 需要一个开发者名称。",
|
|
390
|
+
placeholder: "your-name",
|
|
391
|
+
});
|
|
392
|
+
if (!name) {
|
|
393
|
+
process.stdout.write("开发者名称不能为空,已取消\n");
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
trellisAction = "初始化 Trellis 项目";
|
|
397
|
+
installDeps = true;
|
|
398
|
+
developer = name;
|
|
399
|
+
} else {
|
|
400
|
+
trellisAction = "跳过(需手动初始化)";
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 第三步:选择 skills。
|
|
405
|
+
const skillIds = await multiSelect({
|
|
406
|
+
title: "第三步 · 选择要安装的 Skills",
|
|
407
|
+
hint: "这些是 dream-wf 的 Trellis patch skills,默认全选。",
|
|
408
|
+
items: SKILL_CATALOG,
|
|
409
|
+
defaults: defaultSkillIds(),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// 第四步:选择 MCPs。
|
|
413
|
+
const mcpIds = await multiSelect({
|
|
414
|
+
title: "第四步 · 选择要配置的 MCP Servers",
|
|
415
|
+
hint: "这些 MCP 会被写入对应平台的 mcp 配置文件,默认全选。",
|
|
416
|
+
items: MCP_CATALOG,
|
|
417
|
+
defaults: defaultMcpIds(),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const skills = SKILL_CATALOG.filter((item) => skillIds.includes(item.id));
|
|
421
|
+
const mcps = MCP_CATALOG.filter((item) => mcpIds.includes(item.id));
|
|
422
|
+
const mode = "strict";
|
|
423
|
+
|
|
424
|
+
// 确认安装。
|
|
425
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
426
|
+
process.stdout.write(renderSummary(platform, skills, mcps, mode, trellisAction));
|
|
427
|
+
process.stdout.write("\n\n");
|
|
428
|
+
|
|
429
|
+
const confirmed = await confirmPrompt("确认开始安装?");
|
|
430
|
+
if (!confirmed) {
|
|
431
|
+
process.stdout.write("已取消安装\n");
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
platform,
|
|
437
|
+
mode,
|
|
438
|
+
skillIds,
|
|
439
|
+
mcpIds,
|
|
440
|
+
skills,
|
|
441
|
+
mcps,
|
|
442
|
+
installDeps,
|
|
443
|
+
developer,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
@@ -52,10 +52,12 @@ def is_planning_artifact(root, tool_input):
|
|
|
52
52
|
return False
|
|
53
53
|
|
|
54
54
|
try:
|
|
55
|
+
root_resolved = root.resolve()
|
|
55
56
|
file_path = Path(candidate)
|
|
56
57
|
if not file_path.is_absolute():
|
|
57
|
-
file_path = (
|
|
58
|
-
|
|
58
|
+
file_path = (root_resolved / file_path)
|
|
59
|
+
# 规范化符号链接,避免 /tmp vs /private/tmp 导致 relative_to 失败。
|
|
60
|
+
relative = file_path.resolve().relative_to(root_resolved).as_posix()
|
|
59
61
|
except Exception:
|
|
60
62
|
return False
|
|
61
63
|
|
|
@@ -91,6 +93,10 @@ def allow():
|
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
def main():
|
|
96
|
+
# 逃生舱:DREAM_WF_MODE=advisory 时跳过 strict 检查。
|
|
97
|
+
if os.environ.get("DREAM_WF_MODE", "").lower() == "advisory":
|
|
98
|
+
allow()
|
|
99
|
+
|
|
94
100
|
try:
|
|
95
101
|
payload = json.load(sys.stdin)
|
|
96
102
|
except Exception:
|
|
@@ -111,15 +117,31 @@ def main():
|
|
|
111
117
|
|
|
112
118
|
tasks = active_tasks(root)
|
|
113
119
|
if not tasks:
|
|
114
|
-
deny("dream-wf strict: mutating actions require an active Trellis task. Create or start a Trellis task first, or switch dream-wf to advisory mode.")
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
deny("dream-wf strict: mutating actions require an active Trellis task. Create or start a Trellis task first, or switch dream-wf to advisory mode (DREAM_WF_MODE=advisory).")
|
|
121
|
+
|
|
122
|
+
# 规划产物始终允许,便于在 planning 阶段编写 prd/design 等。
|
|
123
|
+
if is_planning_artifact(root, tool_input):
|
|
124
|
+
allow()
|
|
125
|
+
|
|
126
|
+
# 若存在已确认的 in_progress 任务,允许实现操作,不被其它 stale planning 任务阻塞。
|
|
127
|
+
in_progress_confirmed = any(
|
|
128
|
+
task.get("status") == "in_progress" and is_prd_confirmed(task_dir)
|
|
129
|
+
for task_dir, task in tasks
|
|
130
|
+
)
|
|
131
|
+
if in_progress_confirmed:
|
|
132
|
+
allow()
|
|
133
|
+
|
|
134
|
+
in_progress_any = any(task.get("status") == "in_progress" for _, task in tasks)
|
|
135
|
+
if in_progress_any:
|
|
136
|
+
allow()
|
|
137
|
+
|
|
138
|
+
# 剩余情况:所有活跃任务都是 planning。若任一未确认 PRD,则阻塞实现。
|
|
139
|
+
planning_unconfirmed = [
|
|
140
|
+
task_dir for task_dir, task in tasks
|
|
141
|
+
if task.get("status") == "planning" and not is_prd_confirmed(task_dir)
|
|
142
|
+
]
|
|
143
|
+
if planning_unconfirmed:
|
|
144
|
+
deny("dream-wf strict: implementation is blocked while all active tasks are in planning and at least one PRD is not confirmed. Continue grill-me PRD clarification first. Planning artifacts under .trellis/tasks/** are allowed.")
|
|
123
145
|
|
|
124
146
|
allow()
|
|
125
147
|
|
|
Binary file
|