flower-trellis 0.1.0

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 (94) hide show
  1. package/README.md +113 -0
  2. package/bin/flower-trellis.js +4 -0
  3. package/enhancements/0.5/.agents/skills/trellis-analyze-task/SKILL.md +142 -0
  4. package/enhancements/0.5/.agents/skills/trellis-check-all/SKILL.md +324 -0
  5. package/enhancements/0.5/.agents/skills/trellis-create-command/SKILL.md +258 -0
  6. package/enhancements/0.5/.agents/skills/trellis-create-prd/SKILL.md +197 -0
  7. package/enhancements/0.5/.agents/skills/trellis-draw-uml/SKILL.md +148 -0
  8. package/enhancements/0.5/.agents/skills/trellis-migrate-skill/SKILL.md +216 -0
  9. package/enhancements/0.5/.agents/skills/trellis-plan-version/SKILL.md +140 -0
  10. package/enhancements/0.5/.agents/skills/trellis-push/SKILL.md +240 -0
  11. package/enhancements/0.5/.agents/skills/trellis-re-implement/SKILL.md +166 -0
  12. package/enhancements/0.5/.agents/skills/trellis-route/SKILL.md +159 -0
  13. package/enhancements/0.5/.agents/skills/trellis-run-full-chain/SKILL.md +402 -0
  14. package/enhancements/0.5/.agents/skills/trellis-sync-prd/SKILL.md +150 -0
  15. package/enhancements/0.5/.agents/skills/trellis-verify-prd/SKILL.md +217 -0
  16. package/enhancements/0.5/.claude/skills/trellis-analyze-task/SKILL.md +142 -0
  17. package/enhancements/0.5/.claude/skills/trellis-check-all/SKILL.md +324 -0
  18. package/enhancements/0.5/.claude/skills/trellis-create-command/SKILL.md +258 -0
  19. package/enhancements/0.5/.claude/skills/trellis-create-prd/SKILL.md +197 -0
  20. package/enhancements/0.5/.claude/skills/trellis-draw-uml/SKILL.md +148 -0
  21. package/enhancements/0.5/.claude/skills/trellis-migrate-skill/SKILL.md +216 -0
  22. package/enhancements/0.5/.claude/skills/trellis-plan-version/SKILL.md +140 -0
  23. package/enhancements/0.5/.claude/skills/trellis-push/SKILL.md +240 -0
  24. package/enhancements/0.5/.claude/skills/trellis-re-implement/SKILL.md +166 -0
  25. package/enhancements/0.5/.claude/skills/trellis-route/SKILL.md +159 -0
  26. package/enhancements/0.5/.claude/skills/trellis-run-full-chain/SKILL.md +402 -0
  27. package/enhancements/0.5/.claude/skills/trellis-sync-prd/SKILL.md +150 -0
  28. package/enhancements/0.5/.claude/skills/trellis-verify-prd/SKILL.md +217 -0
  29. package/enhancements/0.5/overrides/trellis-route.md +52 -0
  30. package/enhancements/0.6/.agents/skills/trellis-check-all/SKILL.md +342 -0
  31. package/enhancements/0.6/.agents/skills/trellis-create-command/SKILL.md +293 -0
  32. package/enhancements/0.6/.agents/skills/trellis-draw-uml/SKILL.md +148 -0
  33. package/enhancements/0.6/.agents/skills/trellis-extract-prd/SKILL.md +197 -0
  34. package/enhancements/0.6/.agents/skills/trellis-plan-version/SKILL.md +140 -0
  35. package/enhancements/0.6/.agents/skills/trellis-push/SKILL.md +316 -0
  36. package/enhancements/0.6/.agents/skills/trellis-route/SKILL.md +159 -0
  37. package/enhancements/0.6/.agents/skills/trellis-run-full-chain/SKILL.md +402 -0
  38. package/enhancements/0.6/.agents/skills/trellis-verify-task/SKILL.md +360 -0
  39. package/enhancements/0.6/.claude/skills/trellis-check-all/SKILL.md +342 -0
  40. package/enhancements/0.6/.claude/skills/trellis-create-command/SKILL.md +293 -0
  41. package/enhancements/0.6/.claude/skills/trellis-draw-uml/SKILL.md +148 -0
  42. package/enhancements/0.6/.claude/skills/trellis-extract-prd/SKILL.md +197 -0
  43. package/enhancements/0.6/.claude/skills/trellis-plan-version/SKILL.md +140 -0
  44. package/enhancements/0.6/.claude/skills/trellis-push/SKILL.md +316 -0
  45. package/enhancements/0.6/.claude/skills/trellis-route/SKILL.md +159 -0
  46. package/enhancements/0.6/.claude/skills/trellis-run-full-chain/SKILL.md +402 -0
  47. package/enhancements/0.6/.claude/skills/trellis-verify-task/SKILL.md +360 -0
  48. package/enhancements/0.6/overrides/workflow-states/in_progress-inline.md +5 -0
  49. package/enhancements/0.6/overrides/workflow-states/in_progress.md +7 -0
  50. package/enhancements/0.6/overrides/workflow-states/no_task.md +6 -0
  51. package/enhancements/0.6/overrides/workflow-states/planning.md +6 -0
  52. package/enhancements/0.6/overrides/workflow.md +53 -0
  53. package/enhancements/MANIFEST.json +109 -0
  54. package/enhancements/old/.agents/skills/analyze-task/SKILL.md +143 -0
  55. package/enhancements/old/.agents/skills/check-all/SKILL.md +128 -0
  56. package/enhancements/old/.agents/skills/check-impl/SKILL.md +159 -0
  57. package/enhancements/old/.agents/skills/check-prd/SKILL.md +219 -0
  58. package/enhancements/old/.agents/skills/check-prd-impl/SKILL.md +190 -0
  59. package/enhancements/old/.agents/skills/create-prd/SKILL.md +154 -0
  60. package/enhancements/old/.agents/skills/draw-uml/SKILL.md +148 -0
  61. package/enhancements/old/.agents/skills/plan-version/SKILL.md +140 -0
  62. package/enhancements/old/.agents/skills/push/SKILL.md +191 -0
  63. package/enhancements/old/.agents/skills/re-implement/SKILL.md +166 -0
  64. package/enhancements/old/.agents/skills/sync-prd/SKILL.md +146 -0
  65. package/enhancements/old/.claude/commands/trellis/analyze-task.md +139 -0
  66. package/enhancements/old/.claude/commands/trellis/check-all.md +124 -0
  67. package/enhancements/old/.claude/commands/trellis/check-impl.md +154 -0
  68. package/enhancements/old/.claude/commands/trellis/check-prd-impl.md +186 -0
  69. package/enhancements/old/.claude/commands/trellis/check-prd.md +215 -0
  70. package/enhancements/old/.claude/commands/trellis/create-prd.md +150 -0
  71. package/enhancements/old/.claude/commands/trellis/draw-uml.md +144 -0
  72. package/enhancements/old/.claude/commands/trellis/plan-version.md +136 -0
  73. package/enhancements/old/.claude/commands/trellis/push.md +187 -0
  74. package/enhancements/old/.claude/commands/trellis/re-implement.md +162 -0
  75. package/enhancements/old/.claude/commands/trellis/sync-prd.md +142 -0
  76. package/package.json +39 -0
  77. package/src/cli.js +151 -0
  78. package/src/commands/init.js +66 -0
  79. package/src/commands/uninstall.js +85 -0
  80. package/src/commands/update.js +42 -0
  81. package/src/constants.js +50 -0
  82. package/src/lib/apply-enhancements.js +133 -0
  83. package/src/lib/banner.js +45 -0
  84. package/src/lib/codex-tweaks.js +112 -0
  85. package/src/lib/copy-skills.js +91 -0
  86. package/src/lib/fs-utils.js +60 -0
  87. package/src/lib/legacy-blocks.js +70 -0
  88. package/src/lib/manifest.js +32 -0
  89. package/src/lib/paths.js +16 -0
  90. package/src/lib/pick-platforms.js +57 -0
  91. package/src/lib/trellis-runner.js +190 -0
  92. package/src/lib/variant.js +40 -0
  93. package/src/lib/versions.js +30 -0
  94. package/src/lib/workflow-inject.js +193 -0
@@ -0,0 +1,190 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { spawn } from "node:child_process";
5
+ import { createInterface } from "node:readline";
6
+ import figlet from "figlet";
7
+ import * as pty from "node-pty";
8
+
9
+ /**
10
+ * 定位捆绑的 @mindfoldhq/trellis 可执行 bin 的绝对路径。
11
+ * 解析依赖的 package.json(必随包发布)再读其 bin 字段,而非 main。
12
+ */
13
+ export function resolveTrellisBin() {
14
+ const require = createRequire(import.meta.url);
15
+ const pkgJsonPath = require.resolve("@mindfoldhq/trellis/package.json");
16
+ const pkgRoot = path.dirname(pkgJsonPath);
17
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
18
+ const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin.trellis;
19
+ return path.resolve(pkgRoot, binRel);
20
+ }
21
+
22
+ /** 去除 ANSI 颜色码,便于按文本匹配。 */
23
+ function stripAnsi(s) {
24
+ // eslint-disable-next-line no-control-regex
25
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
26
+ }
27
+
28
+ /** Trellis 启动横幅(figlet Rebel "Trellis")的实体行集合,用于过滤掉重复 banner。 */
29
+ function trellisBannerLines() {
30
+ try {
31
+ return new Set(
32
+ figlet
33
+ .textSync("Trellis", { font: "Rebel" })
34
+ .split("\n")
35
+ .map((l) => l.replace(/\s+$/, ""))
36
+ .filter((l) => l.trim()),
37
+ );
38
+ } catch {
39
+ return new Set();
40
+ }
41
+ }
42
+
43
+ /**
44
+ * 判断 trellis 输出的一行属于「头部」的哪类:
45
+ * - "skip":banner 实体行 / 空行 / 副标题 / Developer / Mode → 过滤掉
46
+ * - "keep":proxy 行 → 保留输出,但仍处于头部(继续过滤后续 Mode/Developer)
47
+ * - "content":其它 → 头部结束,正文开始
48
+ */
49
+ function classifyHeaderLine(line, bannerSet) {
50
+ const plain = stripAnsi(line);
51
+ const t = plain.trim();
52
+ const key = plain.replace(/\s+$/, "");
53
+ if (!t) return "skip";
54
+ if (bannerSet.has(key)) return "skip";
55
+ if (t.startsWith("All-in-one AI framework")) return "skip";
56
+ if (t.startsWith("👤 Developer:")) return "skip";
57
+ if (t.startsWith("Mode:")) return "skip";
58
+ if (t.startsWith("Using proxy:")) return "keep";
59
+ return "content";
60
+ }
61
+
62
+ /**
63
+ * 用当前 node 执行 trellis bin(普通 spawn),透传子命令与参数。
64
+ *
65
+ * 用于:兜底透传其它命令(inherit);或非交互场景下捕获 stdout 过滤 banner(pipe)。
66
+ *
67
+ * @param {string[]} args
68
+ * @param {string} cwd
69
+ * @param {object} [opts] { stripBanner }
70
+ * @returns {Promise<number>} 退出码(信号终止返回 128)
71
+ */
72
+ export function runTrellis(args, cwd, opts = {}) {
73
+ const bin = resolveTrellisBin();
74
+ const strip = opts.stripBanner;
75
+ return new Promise((resolve, reject) => {
76
+ const child = spawn(process.execPath, [bin, ...args], {
77
+ cwd,
78
+ stdio: strip ? ["inherit", "pipe", "inherit"] : "inherit",
79
+ env: strip ? { ...process.env, FORCE_COLOR: "1" } : process.env,
80
+ });
81
+ if (strip && child.stdout) {
82
+ const banner = trellisBannerLines();
83
+ const rl = createInterface({ input: child.stdout });
84
+ rl.on("line", (line) => {
85
+ const kind = classifyHeaderLine(line, banner);
86
+ // 普通(非 pty)场景一直按行过滤即可:trellis 此时是 -y 非交互,无 inquirer
87
+ if (kind === "skip") return;
88
+ process.stdout.write(line + "\n");
89
+ });
90
+ }
91
+ child.on("error", reject);
92
+ child.on("exit", (code, signal) => {
93
+ if (signal) resolve(128);
94
+ else resolve(code ?? 0);
95
+ });
96
+ });
97
+ }
98
+
99
+ /**
100
+ * 在伪终端(node-pty)里运行 trellis,**保留其全部交互**(模板 / monorepo / 冲突菜单),
101
+ * 同时过滤掉它开头重复打印的启动 banner / 副标题 / Developer / Mode。
102
+ *
103
+ * 过滤只作用于「头部阶段」:逐行识别 banner 类行并丢弃(proxy 保留),
104
+ * 一旦遇到正文行或 inquirer 渲染(隐藏光标序列 ESC[?25l)立即停止过滤、转为完全透传,
105
+ * 以免破坏交互菜单的光标控制。
106
+ *
107
+ * @param {string[]} args
108
+ * @param {string} cwd
109
+ * @param {object} [opts] { stripBanner }
110
+ * @returns {Promise<number>} 退出码(信号终止返回 128)
111
+ */
112
+ export function runTrellisPty(args, cwd, opts = {}) {
113
+ const bin = resolveTrellisBin();
114
+ return new Promise((resolve, reject) => {
115
+ let child;
116
+ try {
117
+ child = pty.spawn(process.execPath, [bin, ...args], {
118
+ name: "xterm-256color",
119
+ cols: process.stdout.columns || 80,
120
+ rows: process.stdout.rows || 30,
121
+ cwd,
122
+ env: { ...process.env, FORCE_COLOR: "1" },
123
+ });
124
+ } catch (e) {
125
+ reject(e);
126
+ return;
127
+ }
128
+
129
+ const bannerSet = trellisBannerLines();
130
+ let filtering = !!opts.stripBanner;
131
+ let buf = "";
132
+
133
+ child.onData((data) => {
134
+ if (!filtering) {
135
+ process.stdout.write(data);
136
+ return;
137
+ }
138
+ // inquirer 开始渲染(隐藏光标)→ 立即停止过滤,整体透传,保住交互菜单
139
+ if (data.includes("\x1b[?25l")) {
140
+ filtering = false;
141
+ process.stdout.write(buf + data);
142
+ buf = "";
143
+ return;
144
+ }
145
+ buf += data;
146
+ const parts = buf.split(/\r?\n/);
147
+ buf = parts.pop(); // 末尾不完整行留待下次
148
+ for (const line of parts) {
149
+ const kind = classifyHeaderLine(line, bannerSet);
150
+ if (kind === "skip") continue;
151
+ if (kind === "keep") {
152
+ process.stdout.write(line + "\r\n");
153
+ continue;
154
+ }
155
+ filtering = false; // 正文开始
156
+ process.stdout.write(line + "\r\n");
157
+ }
158
+ if (!filtering && buf) {
159
+ process.stdout.write(buf);
160
+ buf = "";
161
+ }
162
+ });
163
+
164
+ // 把真终端的输入转发进 pty(让用户能操作 trellis 的交互菜单)
165
+ const stdin = process.stdin;
166
+ const wasRaw = !!stdin.isRaw;
167
+ if (stdin.isTTY) stdin.setRawMode(true);
168
+ stdin.resume();
169
+ const onStdin = (d) => child.write(d.toString("utf8"));
170
+ stdin.on("data", onStdin);
171
+
172
+ const onResize = () => {
173
+ try {
174
+ child.resize(process.stdout.columns || 80, process.stdout.rows || 30);
175
+ } catch {
176
+ // 忽略 resize 失败
177
+ }
178
+ };
179
+ process.stdout.on("resize", onResize);
180
+
181
+ child.onExit(({ exitCode, signal }) => {
182
+ stdin.off("data", onStdin);
183
+ if (stdin.isTTY) stdin.setRawMode(wasRaw);
184
+ stdin.pause();
185
+ process.stdout.off("resize", onResize);
186
+ if (signal) resolve(128);
187
+ else resolve(exitCode ?? 0);
188
+ });
189
+ });
190
+ }
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * 根据目标项目的 `.trellis/.version` 选择强化包变体。
6
+ *
7
+ * 规则(逐字符移植 skill-garden install.sh 263-274):
8
+ * - 主版本 >= 1 或 次版本 >= 6 → "0.6"
9
+ * - 次版本 >= 5 → "0.5"
10
+ * - 其它(文件缺失 / 解析失败 / 更低)→ "old"
11
+ *
12
+ * 次版本会先剥掉 `-beta.x` 之类后缀(只取开头连续数字),
13
+ * 因此 `0.6.0-beta.1` 也归入 0.6。
14
+ *
15
+ * @param {string} target 目标项目根目录
16
+ * @returns {{ variant: string, version: string }}
17
+ */
18
+ export function selectVariant(target) {
19
+ const versionFile = path.join(target, ".trellis", ".version");
20
+ let version = "";
21
+ let variant = "old";
22
+
23
+ try {
24
+ version = fs.readFileSync(versionFile, "utf8").replace(/\s/g, "");
25
+ } catch {
26
+ return { variant, version }; // 文件不存在 → old
27
+ }
28
+
29
+ const parts = version.split(".");
30
+ const major = parseInt(parts[0], 10);
31
+ const minorMatch = (parts[1] || "").match(/^(\d+)/);
32
+ const minor = minorMatch ? parseInt(minorMatch[1], 10) : NaN;
33
+
34
+ if (Number.isInteger(major) && Number.isInteger(minor)) {
35
+ if (major >= 1 || minor >= 6) variant = "0.6";
36
+ else if (minor >= 5) variant = "0.5";
37
+ }
38
+
39
+ return { variant, version };
40
+ }
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { PKG_ROOT } from "./paths.js";
5
+
6
+ /**
7
+ * 读取 flower-trellis 自身版本(来自包根 package.json)。
8
+ * @returns {string}
9
+ */
10
+ export function flowerVersion() {
11
+ const p = path.join(PKG_ROOT, "package.json");
12
+ return JSON.parse(fs.readFileSync(p, "utf8")).version;
13
+ }
14
+
15
+ /**
16
+ * 读取捆绑的 @mindfoldhq/trellis 版本。
17
+ *
18
+ * 与定位 bin 同源:解析依赖的 package.json(必随包发布)。依赖缺失时返回占位串,
19
+ * 不抛错 —— `-v` 在任何环境下都应能打印。
20
+ * @returns {string}
21
+ */
22
+ export function trellisVersion() {
23
+ try {
24
+ const require = createRequire(import.meta.url);
25
+ const p = require.resolve("@mindfoldhq/trellis/package.json");
26
+ return JSON.parse(fs.readFileSync(p, "utf8")).version;
27
+ } catch {
28
+ return "(未安装)";
29
+ }
30
+ }
@@ -0,0 +1,193 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ LEGACY_NO_TASK_BLOCK,
5
+ LEGACY_PLANNING_BLOCK,
6
+ LEGACY_PUSH_PROGRESS_BLOCK,
7
+ LEGACY_IN_PROGRESS_BLOCK,
8
+ LEGACY_PUSH_SNAPSHOT_BLOCK,
9
+ } from "./legacy-blocks.js";
10
+
11
+ /**
12
+ * workflow.md 强化块注入。
13
+ *
14
+ * 纯 JS 移植 skill-garden install.sh 362-557 的内嵌 Python:
15
+ * 1. 首次注入前备份 .bak(已存在则保留);
16
+ * 2. 先清掉所有旧的 skill-garden 段(3 个 SECTION + 12 个 sentinel),保证可重复升级;
17
+ * 3. 把 hub(0.6)/ route(0.5)块注入到 `## Phase Index` 之后(找不到则顶部 fallback);
18
+ * 4. 替换 4 个 workflow-state 块的内容(0.6 读 overrides 文件,0.5 用 legacy 常量);
19
+ * 5. 处理后内容与原文件相同则不写盘(幂等)。
20
+ *
21
+ * Python re.DOTALL|re.MULTILINE → JS 用 `[\s\S]` 代替 `.`(免 s flag)+ `m` flag;
22
+ * SECTION/SENTINEL 全局替换用 `g`,state 替换只替首个(对应 Python count=1)故不加 `g`。
23
+ */
24
+
25
+ /** 等价 Python re.escape(用于把字面量安全嵌入正则)。 */
26
+ function escapeRe(s) {
27
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
29
+
30
+ // 3 个 SECTION 段(heading + BEGIN/END 注释包裹的整块)
31
+ const SECTION_PATTERNS = [
32
+ "^#{2,4} HIGHEST PRIORITY: skill-garden overrides[^\\n]*\\n+" +
33
+ "^<!-- BEGIN skill-garden overrides[^\\n]*-->\\n[\\s\\S]*?" +
34
+ "^<!-- END skill-garden overrides[^\\n]*-->\\n*",
35
+ "^#{2,4} (?:skill-garden Override: trellis-route routing|HIGHEST PRIORITY: skill-garden trellis-route routing gate)[^\\n]*\\n+" +
36
+ "^<!-- BEGIN skill-garden enhancement[^\\n]*-->\\n[\\s\\S]*?" +
37
+ "^<!-- END skill-garden enhancement[^\\n]*-->\\n*",
38
+ "^#{2,4} HIGHEST PRIORITY: skill-garden finish-work bookkeeping guard[^\\n]*\\n+" +
39
+ "^<!-- BEGIN skill-garden finish-work override[^\\n]*-->\\n[\\s\\S]*?" +
40
+ "^<!-- END skill-garden finish-work override[^\\n]*-->\\n*",
41
+ ];
42
+ const SECTION_RES = SECTION_PATTERNS.map((p) => new RegExp(p, "gm"));
43
+
44
+ // 12 个 sentinel 名(每个对应一对 BEGIN/END 注释块)
45
+ const SENTINEL_NAMES = [
46
+ "skill-garden overrides",
47
+ "skill-garden enhancement",
48
+ "skill-garden finish-work override",
49
+ "skill-garden workflow-state no-task-gate",
50
+ "skill-garden workflow-state planning-handoff",
51
+ "skill-garden workflow-state trellis-route",
52
+ "skill-garden workflow-state push-progress-recovery",
53
+ "skill-garden workflow-state in-progress-push-snapshot",
54
+ "skill-garden workflow-state no_task",
55
+ "skill-garden workflow-state planning",
56
+ "skill-garden workflow-state in_progress",
57
+ "skill-garden workflow-state in_progress_inline",
58
+ ];
59
+ const SENTINEL_RES = SENTINEL_NAMES.map(
60
+ (name) =>
61
+ new RegExp(
62
+ "^<!-- BEGIN " +
63
+ escapeRe(name) +
64
+ "[^\\n]*-->\\n[\\s\\S]*?^<!-- END " +
65
+ escapeRe(name) +
66
+ "[^\\n]*-->\\n*",
67
+ "gm",
68
+ ),
69
+ );
70
+
71
+ const PHASE_INDEX_RE = /^(## Phase Index[^\n]*\n)/m;
72
+
73
+ /** 构造某 workflow-state 块的匹配正则(只替首个,不加 g)。 */
74
+ function stateRe(state) {
75
+ return new RegExp(
76
+ "^(\\[workflow-state:" +
77
+ escapeRe(state) +
78
+ "\\]\\n)([\\s\\S]*?)(^\\[/workflow-state:" +
79
+ escapeRe(state) +
80
+ "\\])",
81
+ "m",
82
+ );
83
+ }
84
+
85
+ /** 删除所有旧的 skill-garden 段(SECTION + sentinel)。 */
86
+ function stripBlocks(value) {
87
+ for (const re of SECTION_RES) value = value.replace(re, "");
88
+ for (const re of SENTINEL_RES) value = value.replace(re, "");
89
+ return value;
90
+ }
91
+
92
+ /** 把 block 注入到 `## Phase Index` 锚点之后;找不到锚点则注入顶部。 */
93
+ function injectAfterPhaseIndex(value, block) {
94
+ const b = block.replace(/\s+$/, ""); // rstrip
95
+ const m = PHASE_INDEX_RE.exec(value);
96
+ if (m) {
97
+ const end = m.index + m[0].length;
98
+ const after = value.slice(end).replace(/^\n+/, ""); // lstrip("\n")
99
+ return {
100
+ value: value.slice(0, end) + "\n" + b + "\n\n" + after,
101
+ action: "插入到 ## Phase Index 顶部",
102
+ };
103
+ }
104
+ return {
105
+ value: b + "\n\n" + value,
106
+ action: "注入顶部 (fallback: 未找到 Phase Index 锚点)",
107
+ };
108
+ }
109
+
110
+ /** 替换某 workflow-state 块:open + 新 block + 原 body(去首换行/尾空白) + close。 */
111
+ function replaceState(value, re, block) {
112
+ let replaced = false;
113
+ const out = value.replace(re, (_full, open, body, close) => {
114
+ replaced = true;
115
+ const t = body.replace(/^\n+/, "").replace(/\s+$/, "");
116
+ const bodyPart = t ? t + "\n" : "";
117
+ return open + block + bodyPart + close;
118
+ });
119
+ return [out, replaced];
120
+ }
121
+
122
+ /** 读取 0.6 的 workflow-state override 文件,语义同 Python:strip() + "\n\n"。 */
123
+ function readStateBlock(stateDir, filename) {
124
+ const p = path.join(stateDir, filename);
125
+ return fs.readFileSync(p, "utf8").trim() + "\n\n";
126
+ }
127
+
128
+ /**
129
+ * 对目标项目的 .trellis/workflow.md 执行强化注入。
130
+ *
131
+ * @param {string} target 目标项目根
132
+ * @param {string} variantDir 该变体在 enhancements/ 下的目录
133
+ * @param {string} variant "old" | "0.5" | "0.6"
134
+ * @returns {{skipped?:boolean, reason?:string, changed?:boolean, action?:string, backupNote?:string}}
135
+ */
136
+ export function injectWorkflow(target, variantDir, variant) {
137
+ const dst = path.join(target, ".trellis", "workflow.md");
138
+ if (!fs.existsSync(dst)) {
139
+ return { skipped: true, reason: "目标无 .trellis/workflow.md" };
140
+ }
141
+
142
+ const hubSrc = path.join(variantDir, "overrides", "workflow.md");
143
+ const routeSrc = path.join(variantDir, "overrides", "trellis-route.md");
144
+ const stateDir = path.join(variantDir, "overrides", "workflow-states");
145
+ const isV06 = variant === "0.6" && fs.existsSync(hubSrc);
146
+
147
+ let source;
148
+ if (isV06) source = hubSrc;
149
+ else if (fs.existsSync(routeSrc)) source = routeSrc;
150
+ else return { skipped: true, reason: `变体 ${variant} 无 overrides` }; // old
151
+
152
+ const text = fs.readFileSync(dst, "utf8");
153
+
154
+ // 备份(首次创建,后续保留 → .bak 永远是首次注入前的原文)
155
+ const bak = dst + ".bak";
156
+ let backupNote;
157
+ if (!fs.existsSync(bak)) {
158
+ fs.copyFileSync(dst, bak);
159
+ backupNote = "(已创建 workflow.md.bak)";
160
+ } else {
161
+ backupNote = "(保留已有 workflow.md.bak)";
162
+ }
163
+
164
+ const clean = stripBlocks(text);
165
+ const block = fs.readFileSync(source, "utf8").replace(/\s+$/, "");
166
+ const { value: phaseNew, action } = injectAfterPhaseIndex(clean, block);
167
+
168
+ const stateSpecs = isV06
169
+ ? [
170
+ ["no_task", readStateBlock(stateDir, "no_task.md")],
171
+ ["planning", readStateBlock(stateDir, "planning.md")],
172
+ ["in_progress", readStateBlock(stateDir, "in_progress.md")],
173
+ ["in_progress-inline", readStateBlock(stateDir, "in_progress-inline.md")],
174
+ ]
175
+ : [
176
+ ["no_task", LEGACY_NO_TASK_BLOCK + LEGACY_PUSH_PROGRESS_BLOCK],
177
+ ["planning", LEGACY_PLANNING_BLOCK],
178
+ ["in_progress", LEGACY_IN_PROGRESS_BLOCK + LEGACY_PUSH_SNAPSHOT_BLOCK],
179
+ ["in_progress-inline", LEGACY_PUSH_SNAPSHOT_BLOCK],
180
+ ];
181
+
182
+ let newText = phaseNew;
183
+ for (const [state, blk] of stateSpecs) {
184
+ [newText] = replaceState(newText, stateRe(state), blk);
185
+ }
186
+ newText = newText.replace(/\s+$/, "") + "\n";
187
+
188
+ if (newText === text) {
189
+ return { changed: false, backupNote };
190
+ }
191
+ fs.writeFileSync(dst, newText);
192
+ return { changed: true, action, backupNote };
193
+ }