flower-trellis 0.6.4 → 0.6.6

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 (54) hide show
  1. package/README.md +20 -1
  2. package/enhancements/0.6/overrides/bundles/control-plane-integrity.json +2 -0
  3. package/enhancements/0.6/overrides/bundles/intent-routing.json +2 -1
  4. package/enhancements/0.6/overrides/bundles/task-reference-contract.json +24 -0
  5. package/enhancements/0.6/overrides/conflicts.json +37 -4
  6. package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/patch.json +19 -0
  7. package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-content.py +39 -0
  8. package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-selector.py +38 -0
  9. package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/patch.json +20 -0
  10. package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-content.py +64 -0
  11. package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-selector.py +2 -0
  12. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/content.md +2 -4
  13. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/planning-contract-content.md +1 -1
  14. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/readiness-content.md +2 -2
  15. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/review-content.md +2 -2
  16. package/enhancements/0.6/scripts/decision_log.py +29 -24
  17. package/enhancements/0.6/scripts/task_progress.py +54 -13
  18. package/enhancements/0.6/scripts/untracked_flow.py +13 -5
  19. package/enhancements/MANIFEST.json +9 -2
  20. package/package.json +4 -4
  21. package/src/assets/flower_session_start.py +195 -0
  22. package/src/assets/flower_telemetry_hook.py +98 -0
  23. package/src/builtin-plugins/skill-garden/content-adapter.js +15 -1
  24. package/src/cli.js +3 -1
  25. package/src/commands/init.js +36 -1
  26. package/src/commands/plugin-remote.js +61 -14
  27. package/src/commands/plugin.js +14 -1
  28. package/src/commands/self-check.js +20 -0
  29. package/src/commands/self-update.js +38 -3
  30. package/src/commands/telemetry.js +30 -1
  31. package/src/commands/uninstall.js +20 -0
  32. package/src/commands/update-check.js +19 -0
  33. package/src/commands/update.js +52 -9
  34. package/src/commands/worktree.js +114 -0
  35. package/src/lib/cli-args.js +10 -0
  36. package/src/lib/developer.js +3 -2
  37. package/src/lib/flower-assets.js +21 -6
  38. package/src/lib/platform-patch-adapters.js +46 -8
  39. package/src/lib/telemetry-context.js +275 -0
  40. package/src/lib/telemetry-files.js +144 -0
  41. package/src/lib/telemetry-operation.js +84 -0
  42. package/src/lib/telemetry-queue.js +310 -0
  43. package/src/lib/telemetry-sender.js +7 -0
  44. package/src/lib/telemetry.js +12 -361
  45. package/src/patches/bundles/flower-platform-integration.json +4 -1
  46. package/src/patches/platforms/claude/session-start-hooks/patch.json +28 -0
  47. package/src/patches/platforms/claude/telemetry-hooks/patch.json +59 -0
  48. package/src/patches/platforms/codex/session-start-hooks/patch.json +3 -2
  49. package/src/patches/platforms/codex/telemetry-hooks/patch.json +59 -0
  50. package/src/plugin/application-service.js +5 -22
  51. package/src/plugin/contracts.js +1 -0
  52. package/src/plugin/lock-reachability.js +37 -0
  53. package/src/plugin/schemas/project-files.js +1 -0
  54. package/src/plugin/sources/gitlab-provider.js +50 -9
@@ -1,3 +1,4 @@
1
+ import { observeTelemetryOperation, beginTelemetryOperation, noteTelemetryError } from "../lib/telemetry-operation.js";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { PLUGIN_ERROR_CODES, PluginError } from "../plugin/errors.js";
@@ -636,7 +637,10 @@ async function executeWithCapabilityApproval(execute, preview, options, output)
636
637
  default: false,
637
638
  });
638
639
  }
639
- if (!approved) throw error;
640
+ if (!approved) {
641
+ error.telemetryCancelled = true;
642
+ throw error;
643
+ }
640
644
  return execute(previewResult.approvalRequests.map(({ pluginId }) => pluginId));
641
645
  }
642
646
  }
@@ -778,6 +782,12 @@ export async function plugin(ctx, options = {}) {
778
782
  const remoteRuntime = await loadRemotePluginRuntime();
779
783
  return await remoteRuntime.runPluginManagementCommand(parsed, ctx, options, output);
780
784
  }
785
+ if (parsed.command === "add" && !parsed.dryRun) {
786
+ if (!ctx.telemetryOperation && !process.env.FLOWER_TELEMETRY_PARENT_OPERATION) {
787
+ return await observeTelemetryOperation(ctx, "plugin_add", nested => plugin(nested, options), options.telemetry || {});
788
+ }
789
+ beginTelemetryOperation(ctx);
790
+ }
781
791
  const trellisAlreadyMaterialized = ["materialized", "restoring"].includes(ctx.trellisControlMode) ||
782
792
  ["materialized", "restoring"].includes(options.trellisControlMode);
783
793
  if (
@@ -834,6 +844,7 @@ export async function plugin(ctx, options = {}) {
834
844
  options,
835
845
  registry,
836
846
  lock,
847
+ pluginsFile,
837
848
  preserveIds: options.preserveIds || [],
838
849
  });
839
850
 
@@ -895,6 +906,7 @@ export async function plugin(ctx, options = {}) {
895
906
  canonicalId,
896
907
  registry,
897
908
  lock,
909
+ pluginsFile,
898
910
  preserveIds: options.preserveIds || [],
899
911
  });
900
912
 
@@ -964,6 +976,7 @@ export async function plugin(ctx, options = {}) {
964
976
  printResult(result, parsed.json, output, { compact, verbose });
965
977
  return result.ok === false ? 3 : 0;
966
978
  } catch (error) {
979
+ noteTelemetryError(ctx, error);
967
980
  const json = parsed?.json || ctx.passthrough?.includes("--json");
968
981
  const code = error instanceof PluginError ? error.code : "PLUGIN_UNEXPECTED_ERROR";
969
982
  const publicPath = error.path && !path.isAbsolute(error.path) ? error.path : "";
@@ -1,5 +1,21 @@
1
1
  import { buildSelfCheck } from "../lib/self-check.js";
2
2
  import { reportTelemetry } from "../lib/telemetry.js";
3
+ import { hasHelpFlag } from "../lib/cli-args.js";
4
+
5
+ /** 打印 self-check 命令帮助。 */
6
+ function printSelfCheckHelp() {
7
+ console.log(`flower-trellis self-check — 输出稳定的启动更新检查 JSON
8
+
9
+ 用法:
10
+ flower-trellis self-check [--manual] [--force-remote] [--target <dir>]
11
+
12
+ 选项:
13
+ --manual 绕过提示节流,但不强制绕过远端缓存
14
+ --force-remote 强制检查远端版本
15
+ --target <dir> 要检查的项目目录
16
+
17
+ 该命令供 SessionStart hook 和自动化读取,成功时始终输出 JSON。`);
18
+ }
3
19
 
4
20
  /**
5
21
  * flower-trellis self-check:输出结构化启动更新检查结果。
@@ -11,6 +27,10 @@ import { reportTelemetry } from "../lib/telemetry.js";
11
27
  * @returns {Promise<void>}
12
28
  */
13
29
  export async function selfCheck(ctx) {
30
+ if (hasHelpFlag(ctx.passthrough)) {
31
+ printSelfCheckHelp();
32
+ return;
33
+ }
14
34
  const forceRemote = ctx.passthrough.includes("--force-remote");
15
35
  const manual = ctx.passthrough.includes("--manual") ||
16
36
  ctx.passthrough.includes("--ignore-prompt-suppression");
@@ -1,3 +1,4 @@
1
+ import { observeTelemetryOperation, beginTelemetryOperation, completeTelemetryOperation } from "../lib/telemetry-operation.js";
1
2
  import { spawnSync } from "node:child_process";
2
3
  import {
3
4
  buildSelfCheck,
@@ -6,6 +7,24 @@ import {
6
7
  safetyState,
7
8
  } from "../lib/self-check.js";
8
9
  import { installFlowerVersion } from "../lib/update-check.js";
10
+ import { hasHelpFlag } from "../lib/cli-args.js";
11
+
12
+ /** 打印 self-update 命令帮助。 */
13
+ function printSelfUpdateHelp() {
14
+ console.log(`flower-trellis self-update — 更新全局 Flower 并重叠加当前项目
15
+
16
+ 用法:
17
+ flower-trellis self-update --target <dir> --dry-run
18
+ flower-trellis self-update --target <dir> --yes [--project-only] [-- <update flags>]
19
+
20
+ 选项:
21
+ --dry-run 只展示全局升级、项目更新与安全检查
22
+ -y, --yes 确认真实写入
23
+ --project-only 跳过全局包升级,仅修复项目版本差异
24
+ -- 把后续冲突策略转发给项目 update
25
+
26
+ 建议先运行 --dry-run;真实更新完成后按输出进入 trellis-push。`);
27
+ }
9
28
 
10
29
  /** 判断参数里是否包含指定 flag。 */
11
30
  function hasFlag(args, name) {
@@ -13,15 +32,16 @@ function hasFlag(args, name) {
13
32
  }
14
33
 
15
34
  /** 执行命令并在失败时给出统一错误。 */
16
- function runCommand(command, args, cwd, failureMessage) {
35
+ function runCommand(command, args, cwd, failureMessage, env = process.env) {
17
36
  const res = spawnSync(command, args, {
18
37
  cwd,
38
+ env,
19
39
  stdio: "inherit",
20
40
  shell: process.platform === "win32",
21
41
  });
22
42
  if (res.status !== 0) {
23
43
  const reason = res.error ? res.error.message : `退出码 ${res.status ?? 1}`;
24
- throw new Error(`${failureMessage}(${reason})`);
44
+ throw Object.assign(new Error(`${failureMessage}(${reason})`), { code: res.status === 130 ? "FLOWER_OPERATION_CANCELLED" : "FLOWER_UPSTREAM_FAILED" });
25
45
  }
26
46
  }
27
47
 
@@ -109,6 +129,18 @@ function printFlowerUpdateResult(fields) {
109
129
  * @returns {Promise<void>}
110
130
  */
111
131
  export async function selfUpdate(ctx) {
132
+ return observeTelemetryOperation(ctx, "self_update", executeSelfUpdate);
133
+ }
134
+
135
+ /** 执行已建立外部操作上下文的命令。
136
+ * @param {object} ctx 命令上下文
137
+ * @returns {Promise<void>} 完成
138
+ */
139
+ async function executeSelfUpdate(ctx) {
140
+ if (hasHelpFlag(ctx.passthrough)) {
141
+ printSelfUpdateHelp();
142
+ return;
143
+ }
112
144
  const dryRun = hasFlag(ctx.passthrough, "--dry-run");
113
145
  const yes = hasFlag(ctx.passthrough, "--yes") || hasFlag(ctx.passthrough, "-y");
114
146
  const projectOnly = hasFlag(ctx.passthrough, "--project-only");
@@ -171,11 +203,12 @@ export async function selfUpdate(ctx) {
171
203
  return;
172
204
  }
173
205
 
206
+ beginTelemetryOperation(ctx);
174
207
  if (shouldInstallFlower) {
175
208
  const res = installFlowerVersion(check.recommendation.version, { cwd: ctx.target });
176
209
  if (res.status !== 0) {
177
210
  const reason = res.error ? res.error.message : `退出码 ${res.status ?? 1}`;
178
- throw new Error(`全局 flower-trellis 升级失败(${reason})`);
211
+ throw Object.assign(new Error(`全局 flower-trellis 升级失败(${reason})`), { code: res.status === 130 ? "FLOWER_OPERATION_CANCELLED" : "FLOWER_UPSTREAM_FAILED" });
179
212
  }
180
213
  } else {
181
214
  console.log(" · 跳过全局 flower-trellis 升级");
@@ -186,8 +219,10 @@ export async function selfUpdate(ctx) {
186
219
  projectArgs,
187
220
  ctx.target,
188
221
  "目标项目重叠加失败,请手动运行:" + projectUpdateCommand(ctx.target, forwarded),
222
+ { ...process.env, FLOWER_TELEMETRY_PARENT_OPERATION: ctx.telemetryOperation?.id || "self_update" },
189
223
  );
190
224
 
225
+ completeTelemetryOperation(ctx, "self_update");
191
226
  const dirty = gitDirtySummary(ctx.target);
192
227
  console.log(`\n🌸 flower-trellis self-update 完成 → ${ctx.target}`);
193
228
  printFlowerUpdateResult({
@@ -1,4 +1,19 @@
1
1
  import { readTelemetryState, setTelemetryEnabled } from "../lib/telemetry.js";
2
+ import { queueTelemetryEvent, telemetryQueueStatus } from "../lib/telemetry-queue.js";
3
+ import { hasHelpFlag } from "../lib/cli-args.js";
4
+
5
+ /** 打印 telemetry 命令帮助。 */
6
+ function printTelemetryHelp() {
7
+ console.log(`flower-trellis telemetry — 管理匿名安装遥测
8
+
9
+ 用法:
10
+ flower-trellis telemetry status
11
+ flower-trellis telemetry enable
12
+ flower-trellis telemetry disable
13
+
14
+ status 只读取用户级状态;enable/disable 会更新用户级遥测开关。
15
+ 环境变量 FLOWER_NO_TELEMETRY=1 可临时停用上报。`);
16
+ }
2
17
 
3
18
  /**
4
19
  * 格式化可空时间。
@@ -32,16 +47,30 @@ function printStatus(env) {
32
47
  console.log(` 开发者名称:${state?.developerName || "首次有效上报时识别"}`);
33
48
  console.log(` 最近尝试:${formatTime(state?.lastAttemptAt || null)}`);
34
49
  console.log(` 最近成功:${formatTime(state?.lastSuccessAt || null)}`);
50
+ const queue = telemetryQueueStatus({ env });
51
+ console.log(` 待发送:${queue.pending ?? "未知"};本地丢弃:${queue.dropped ?? "未知"}`);
52
+ console.log(` v2 最近成功:${formatTime(queue.lastSuccessAt)};最近诊断:${queue.diagnostic || "无"}`);
53
+ console.log(` 最早重试:${formatTime(queue.nextRetryAt)}`);
35
54
  }
36
55
 
37
56
  /**
38
57
  * flower-trellis telemetry:查询或修改用户级匿名遥测开关。
39
58
  *
40
59
  * @param {object} ctx 见 cli-args.js 的 parseCliArgs()
60
+ * @param {object} options 活动采集依赖,供隔离验证使用
41
61
  * @returns {Promise<void>} 命令执行完成后返回
42
62
  */
43
- export async function telemetry(ctx) {
63
+ export async function telemetry(ctx, options = {}) {
64
+ if (hasHelpFlag(ctx.passthrough)) {
65
+ printTelemetryHelp();
66
+ return;
67
+ }
44
68
  const action = ctx.passthrough[0] || "status";
69
+ if (action === "record-activity") {
70
+ const platform = ctx.passthrough[1];
71
+ if (["claude", "codex"].includes(platform)) queueTelemetryEvent(ctx.target, { event: "activity_daily", ai_platform: platform }, options);
72
+ return;
73
+ }
45
74
  if (action === "status") {
46
75
  printStatus(process.env);
47
76
  return;
@@ -3,6 +3,22 @@ import {
3
3
  applySkillGardenUninstall,
4
4
  planSkillGardenUninstall,
5
5
  } from "../builtin-plugins/skill-garden/uninstall.js";
6
+ import { hasHelpFlag } from "../lib/cli-args.js";
7
+
8
+ /** 打印 uninstall 命令帮助。 */
9
+ function printUninstallHelp() {
10
+ console.log(`flower-trellis uninstall — 卸载 Trellis 并清理受管强化资产
11
+
12
+ 用法:
13
+ flower-trellis uninstall [--target <dir>] [--dry-run] [-y|--yes]
14
+
15
+ 选项:
16
+ --dry-run 只预览 Trellis 与 Plugin 清理计划
17
+ -y, --yes 传给 Trellis 的非交互确认
18
+ --target <dir> 目标项目目录
19
+
20
+ 若仍有其它 Plugin 依赖 flower/skill-garden,命令会拒绝清理并列出依赖方。`);
21
+ }
6
22
 
7
23
  /**
8
24
  * flower-trellis uninstall:由 Trellis 删除自身内容,再按 Plugin state 清理平台资产。
@@ -11,6 +27,10 @@ import {
11
27
  * @returns {Promise<void>} 卸载完成后返回
12
28
  */
13
29
  export async function uninstall(ctx) {
30
+ if (hasHelpFlag(ctx.passthrough)) {
31
+ printUninstallHelp();
32
+ return;
33
+ }
14
34
  const { target } = ctx;
15
35
  const dryRun = ctx.passthrough.includes("--dry-run");
16
36
  const cleanupPlan = planSkillGardenUninstall(target);
@@ -12,10 +12,25 @@ import {
12
12
  buildSelfCheck,
13
13
  DEFAULT_UPDATE_PROMPT_SNOOZE_HOURS,
14
14
  } from "../lib/self-check.js";
15
+ import { hasHelpFlag } from "../lib/cli-args.js";
15
16
 
16
17
  const POLICIES = new Set(["off", "notify", "ask", "auto"]);
17
18
  const ACTIONABLE_STATUSES = new Set(["update_available", "project_out_of_sync"]);
18
19
 
20
+ /** 打印 update-check 命令帮助。 */
21
+ function printUpdateCheckHelp() {
22
+ console.log(`flower-trellis update-check — 管理启动更新检查策略
23
+
24
+ 用法:
25
+ flower-trellis update-check get [--target <dir>]
26
+ flower-trellis update-check set [--policy <off|notify|ask|auto>] [--interval-hours <n>]
27
+ flower-trellis update-check <disable|enable|reset>
28
+ flower-trellis update-check snooze [--hours <n>|--days <n>]
29
+ flower-trellis update-check skip
30
+
31
+ set 至少需要一个策略选项;snooze 与 skip 仅在当前存在可操作更新提示时有效。`);
32
+ }
33
+
19
34
  /** 读取 flag 后面的取值。 */
20
35
  function optionValue(args, name) {
21
36
  const index = args.indexOf(name);
@@ -79,6 +94,10 @@ function assertTrellisProject(target) {
79
94
  */
80
95
  export async function updateCheck(ctx) {
81
96
  const args = ctx.passthrough;
97
+ if (hasHelpFlag(args)) {
98
+ printUpdateCheckHelp();
99
+ return;
100
+ }
82
101
  const action = args.find((arg) => !arg.startsWith("-")) || "get";
83
102
  assertTrellisProject(ctx.target);
84
103
 
@@ -1,5 +1,6 @@
1
+ import { observeTelemetryOperation, beginTelemetryOperation, completeTelemetryOperation } from "../lib/telemetry-operation.js";
1
2
  import { runTrellis, runTrellisPty } from "../lib/trellis-runner.js";
2
- import { trellisUpdatePassthroughArgs } from "../lib/cli-args.js";
3
+ import { hasHelpFlag, trellisUpdatePassthroughArgs } from "../lib/cli-args.js";
3
4
  import { plugin } from "./plugin.js";
4
5
  import { printBanner, getDeveloper } from "../lib/banner.js";
5
6
  import { checkForUpdate } from "../lib/update-check.js";
@@ -29,6 +30,7 @@ import {
29
30
  restoreUpdateSnapshot,
30
31
  } from "../lib/update-transaction.js";
31
32
  import { runWithTrellisIntegrationEnabled } from "../lib/trellis-control.js";
33
+ import { classifyLockReachability } from "../plugin/lock-reachability.js";
32
34
 
33
35
  const SILENT_OUTPUT = Object.freeze({
34
36
  columns: 80,
@@ -40,6 +42,23 @@ const SILENT_OUTPUT = Object.freeze({
40
42
  off() {},
41
43
  });
42
44
 
45
+ /** 打印 update 命令帮助。 */
46
+ function printUpdateHelp() {
47
+ console.log(`flower-trellis update — 升级 Trellis 并重放已声明 Plugin
48
+
49
+ 用法:
50
+ flower-trellis update [trellis flags] [flower flags]
51
+
52
+ 常用选项:
53
+ --target <dir> 目标 Trellis 项目
54
+ --dry-run 仅预览升级与 Plugin 变化
55
+ --enhance-only 跳过 Trellis update,仅重放 Plugin
56
+ --backup-retention <n> 成功后保留最近 n 份升级备份,0 表示不清理
57
+ --no-update-check 本次跳过 flower-trellis 版本检查
58
+
59
+ 跨版本预览会在项目外沙箱执行;真实更新失败时会尝试恢复受管状态。`);
60
+ }
61
+
43
62
  /**
44
63
  * 判断普通跨版本 dry-run 是否需要进入项目外升级沙箱。
45
64
  *
@@ -90,10 +109,14 @@ function pluginPlatformArgs(platforms) {
90
109
  export async function replayPlugins(ctx, target, dryRun, compensationSnapshot = null) {
91
110
  const output = ctx.trellisControlQuiet ? SILENT_OUTPUT : console;
92
111
  const store = new ProjectStore(target);
112
+ const pluginsFile = store.readPlugins();
93
113
  const lock = store.readLock();
114
+ const { reachableIds } = classifyLockReachability(pluginsFile.plugins, lock);
94
115
  const preserveIds = (lock?.plugins || [])
95
116
  .filter(({ id, source }) => (
96
- id !== SKILL_GARDEN_PLUGIN_ID && ["gitlab", "github"].includes(source.type)
117
+ reachableIds.has(id) &&
118
+ id !== SKILL_GARDEN_PLUGIN_ID &&
119
+ ["gitlab", "github"].includes(source.type)
97
120
  ))
98
121
  .map(({ id }) => id);
99
122
  const onPreflight = compensationSnapshot || ctx.trellisControlExtendSnapshot
@@ -107,7 +130,7 @@ export async function replayPlugins(ctx, target, dryRun, compensationSnapshot =
107
130
  }
108
131
  : undefined;
109
132
  if (ctx.enhance) {
110
- const declared = store.readPlugins().plugins
133
+ const declared = pluginsFile.plugins
111
134
  .some(({ id }) => id === SKILL_GARDEN_PLUGIN_ID);
112
135
  const code = await plugin({
113
136
  ...ctx,
@@ -130,15 +153,17 @@ export async function replayPlugins(ctx, target, dryRun, compensationSnapshot =
130
153
  }
131
154
 
132
155
  output.log("· --no-enhance:跳过 Skill-Garden,仅重放其它已声明 Plugin");
133
- const preserveSkillGarden = lock?.plugins
134
- .some(({ id }) => id === SKILL_GARDEN_PLUGIN_ID) === true;
156
+ const preserveSkillGarden = reachableIds.has(SKILL_GARDEN_PLUGIN_ID);
135
157
  const code = await plugin({
136
158
  ...ctx,
137
159
  target,
138
160
  passthrough: ["replay", ...(dryRun ? ["--dry-run"] : [])],
139
161
  }, {
140
162
  skillGarden: { preserve: preserveSkillGarden },
141
- preserveIds: preserveSkillGarden ? [SKILL_GARDEN_PLUGIN_ID] : [],
163
+ preserveIds: [
164
+ ...preserveIds,
165
+ ...(preserveSkillGarden ? [SKILL_GARDEN_PLUGIN_ID] : []),
166
+ ],
142
167
  compact: true,
143
168
  onPreflight,
144
169
  output,
@@ -215,13 +240,30 @@ function printBackupRetentionResult(result, output = console) {
215
240
  * @returns {Promise<void>} 升级、强化叠加与备份保留处理完成后返回
216
241
  */
217
242
  export async function update(ctx) {
243
+ return observeTelemetryOperation(ctx, "update", executeUpdate);
244
+ }
245
+
246
+ /** 执行已建立外部操作上下文的命令。
247
+ * @param {object} ctx 命令上下文
248
+ * @returns {Promise<void>} 完成
249
+ */
250
+ async function executeUpdate(ctx) {
251
+ if (hasHelpFlag(ctx.passthrough)) {
252
+ printUpdateHelp();
253
+ return;
254
+ }
218
255
  const dryRun = ctx.passthrough.includes("--dry-run");
256
+ if (!dryRun) beginTelemetryOperation(ctx);
219
257
  if (!dryRun && ctx.trellisControlMode !== "materialized" && ctx.trellisControlMode !== "restoring") {
220
- return runWithTrellisIntegrationEnabled(ctx.target, ({ extendSnapshot }) => update({
258
+ const result = await runWithTrellisIntegrationEnabled(ctx.target, ({ extendSnapshot }) => update({
221
259
  ...ctx,
222
260
  trellisControlMode: "materialized",
261
+ telemetryDeferCompletion: true,
223
262
  trellisControlExtendSnapshot: extendSnapshot,
224
263
  }));
264
+ completeTelemetryOperation(ctx, "update");
265
+ await showCommandCompletion("update", ctx.target, { passthrough: ctx.passthrough, outcome: "success", output: ctx.trellisControlQuiet ? SILENT_OUTPUT : console });
266
+ return result;
225
267
  }
226
268
  const { target } = ctx;
227
269
  const quiet = ctx.trellisControlQuiet === true;
@@ -266,7 +308,7 @@ export async function update(ctx) {
266
308
  { stripBanner: true, ...(quiet ? { stdout: SILENT_OUTPUT } : {}) },
267
309
  );
268
310
  if (code !== 0) {
269
- throw new Error(`trellis update 失败(退出码 ${code}),已中止,未重新叠加`);
311
+ throw Object.assign(new Error(`trellis update 失败(退出码 ${code}),已中止,未重新叠加`), { code: code === 130 ? "FLOWER_OPERATION_CANCELLED" : "FLOWER_UPSTREAM_FAILED" });
270
312
  }
271
313
  await replayPlugins(ctx, target, dryRun, compensationSnapshot);
272
314
  updateSucceeded = true;
@@ -315,10 +357,11 @@ export async function update(ctx) {
315
357
  output.log(" · --backup-retention 0:保留全部升级备份");
316
358
  }
317
359
 
360
+ if (!ctx.telemetryDeferCompletion) completeTelemetryOperation(ctx, "update");
318
361
  const telemetryPromise = dryRun
319
362
  ? null
320
363
  : reportTelemetry(target, "update_completed", { force: true });
321
- await showCommandCompletion("update", target, {
364
+ if (!ctx.telemetryDeferCompletion) await showCommandCompletion("update", target, {
322
365
  passthrough: ctx.passthrough,
323
366
  outcome: dryRun ? "preview" : "success",
324
367
  output,
@@ -8,6 +8,7 @@ import {
8
8
  } from "../lib/trellis-python-command.js";
9
9
 
10
10
  const SUPPORTED_COMMANDS = new Set(["status", "prepare", "migrate", "create", "remove"]);
11
+ const HELP_OPTIONS = new Set(["-h", "--help"]);
11
12
  const VALUE_OPTIONS = new Set([
12
13
  "--branch",
13
14
  "--base",
@@ -19,6 +20,114 @@ const VALUE_OPTIONS = new Set([
19
20
  ]);
20
21
  const BOOLEAN_OPTIONS = new Set(["--json", "--dry-run", "--yes", "--inherit-route-prefs"]);
21
22
 
23
+ /**
24
+ * 识别 worktree 根级或子命令级帮助请求。
25
+ *
26
+ * @param {string[]} args `worktree` 之后的参数
27
+ * @returns {string|null|undefined} 子命令名、根级帮助标记,或非帮助请求
28
+ */
29
+ function requestedHelpCommand(args) {
30
+ const first = args[0];
31
+ if (first === "help" || HELP_OPTIONS.has(first)) {
32
+ const command = args[1];
33
+ if (command && !SUPPORTED_COMMANDS.has(command)) {
34
+ throw new Error(`worktree help 不支持子命令: ${command}`);
35
+ }
36
+ return command || null;
37
+ }
38
+ if (SUPPORTED_COMMANDS.has(first) && args.slice(1).some((token) => HELP_OPTIONS.has(token))) {
39
+ return first;
40
+ }
41
+ return undefined;
42
+ }
43
+
44
+ /**
45
+ * 打印 worktree 根级或指定子命令的场景化帮助。
46
+ *
47
+ * @param {string|null} command 可选子命令
48
+ * @returns {void}
49
+ */
50
+ function printWorktreeHelp(command) {
51
+ if (command === "status") {
52
+ console.log(`flower-trellis worktree status — 只读诊断目标 worktree
53
+
54
+ 用法:
55
+ flower-trellis worktree status [--target <dir>] [--json]
56
+
57
+ 先运行 status,再根据返回的 ready-local、needs-prepare、needs-init 或
58
+ needs-migration 状态选择下一步。`);
59
+ return;
60
+ }
61
+ if (command === "prepare") {
62
+ console.log(`flower-trellis worktree prepare — 准备分支本地运行状态
63
+
64
+ 用法:
65
+ flower-trellis worktree prepare [--target <dir>] [--developer <name>]
66
+ [--inherit-route-prefs] [--json]
67
+
68
+ prepare 不创建 Git worktree,只补齐目标分支本地的 developer、runtime 和 registry 状态。
69
+ 仅在确实需要继承当前控制 worktree 的个人路由偏好时使用 --inherit-route-prefs。`);
70
+ return;
71
+ }
72
+ if (command === "migrate") {
73
+ console.log(`flower-trellis worktree migrate — 迁移旧版 worktree 投影
74
+
75
+ 用法:
76
+ flower-trellis worktree migrate [--target <dir>] --dry-run [--json]
77
+ flower-trellis worktree migrate [--target <dir>] [--json]
78
+
79
+ 必须先用 --dry-run 检查迁移计划,再执行真实迁移。`);
80
+ return;
81
+ }
82
+ if (command === "create") {
83
+ console.log(`flower-trellis worktree create — 创建新分支、worktree 和 planning task
84
+
85
+ 只读预检:
86
+ flower-trellis worktree create --target <dir> --branch <new-branch> [--base <ref>]
87
+ --task-title <title> --task-slug <slug> [--task-description <text>]
88
+ [--developer <name>] [--json]
89
+
90
+ 确认执行:
91
+ flower-trellis worktree create ... --yes --plan-fingerprint <sha256>
92
+
93
+ 重要:
94
+ --branch 必须是尚不存在的新分支;create 不挂载已有分支。
95
+ 已有分支请改用:
96
+ git worktree add <target> <existing-branch>
97
+ flower-trellis worktree status --target <target>
98
+ 若 status 返回 needs-prepare,再运行:
99
+ flower-trellis worktree prepare --target <target> [--developer <name>]`);
100
+ return;
101
+ }
102
+ if (command === "remove") {
103
+ console.log(`flower-trellis worktree remove — 安全移除已登记的 linked worktree
104
+
105
+ 用法:
106
+ flower-trellis worktree remove [--target <dir>] [--json]
107
+
108
+ 目标必须无未提交修改、活动任务、会话或锁;remove 保留对应 Git 分支。`);
109
+ return;
110
+ }
111
+ console.log(`flower-trellis worktree — 管理分支本地化 Git worktree
112
+
113
+ 用法:
114
+ flower-trellis worktree <子命令> [选项]
115
+ flower-trellis worktree <子命令> --help
116
+
117
+ 子命令:
118
+ status 只读诊断目标 worktree 状态
119
+ prepare 准备分支本地 developer、runtime 和 registry 状态
120
+ migrate 迁移旧版 worktree 投影
121
+ create 创建新分支、worktree 和 planning task
122
+ remove 安全移除 worktree,保留 Git 分支
123
+
124
+ 推荐先运行:
125
+ flower-trellis worktree status --target <target>
126
+
127
+ 创建新分支请查看:
128
+ flower-trellis worktree create --help`);
129
+ }
130
+
22
131
  /**
23
132
  * 解析 Flower worktree 子命令参数。
24
133
  *
@@ -154,6 +263,11 @@ export function printWorktreeResult(payload) {
154
263
  * @returns {Promise<number>} 进程退出码
155
264
  */
156
265
  export async function worktree(ctx) {
266
+ const helpCommand = requestedHelpCommand(ctx.passthrough);
267
+ if (helpCommand !== undefined) {
268
+ printWorktreeHelp(helpCommand);
269
+ return 0;
270
+ }
157
271
  const parsed = parseWorktreeArgs(ctx.passthrough);
158
272
  if (parsed.command === "create" && !ctx.targetExplicit) {
159
273
  throw new Error("worktree create 必须显式传入 --target");
@@ -1,6 +1,16 @@
1
1
  import path from "node:path";
2
2
  import { DEFAULT_UPDATE_BACKUP_RETENTION } from "../constants.js";
3
3
 
4
+ /**
5
+ * 判断命令参数是否请求帮助。
6
+ *
7
+ * @param {string[]} args 命令自身收到的参数
8
+ * @returns {boolean} 是否包含 -h 或 --help
9
+ */
10
+ export function hasHelpFlag(args) {
11
+ return args.some((arg) => arg === "-h" || arg === "--help");
12
+ }
13
+
4
14
  /**
5
15
  * 解析 flower-trellis argv,分离自有参数与 Trellis 透传参数。
6
16
  *
@@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process";
4
4
  * 读取目标目录可见的 Git 开发者名称。
5
5
  *
6
6
  * @param {string} target 目标项目根目录
7
- * @param {{env?:NodeJS.ProcessEnv}} [options] 读取选项
7
+ * @param {{env?:NodeJS.ProcessEnv,timeoutMs?:number}} [options] 读取选项
8
8
  * @returns {string|null} Git 开发者名称,无法识别时返回 null
9
9
  */
10
10
  export function readGitDeveloper(target, options = {}) {
@@ -13,6 +13,7 @@ export function readGitDeveloper(target, options = {}) {
13
13
  execFileSync("git", ["-C", target, "config", "user.name"], {
14
14
  encoding: "utf8",
15
15
  env: options.env || process.env,
16
+ ...(options.timeoutMs ? { timeout: options.timeoutMs } : {}),
16
17
  }).trim() || null
17
18
  );
18
19
  } catch {
@@ -25,7 +26,7 @@ export function readGitDeveloper(target, options = {}) {
25
26
  *
26
27
  * @param {string[]} passthrough Trellis 透传参数
27
28
  * @param {string} target 目标项目根目录
28
- * @param {{env?:NodeJS.ProcessEnv}} [options] 读取选项
29
+ * @param {{env?:NodeJS.ProcessEnv,timeoutMs?:number}} [options] 读取选项
29
30
  * @returns {string|null} 开发者名称,无法识别时返回 null
30
31
  */
31
32
  export function getDeveloper(passthrough, target, options = {}) {
@@ -8,6 +8,18 @@ export const FLOWER_UPDATE_HOOK = "flower_update_hook.py";
8
8
  /** flower 自有启动更新 hook 在目标项目内的相对路径。 */
9
9
  export const FLOWER_UPDATE_HOOK_REL = `.trellis/scripts/${FLOWER_UPDATE_HOOK}`;
10
10
 
11
+ /** Flower 自有 SessionStart 分段脚本。 */
12
+ export const FLOWER_SESSION_HOOK = "flower_session_start.py";
13
+
14
+ /** SessionStart 分段脚本在目标项目中的位置。 */
15
+ export const FLOWER_SESSION_HOOK_REL = `.trellis/scripts/${FLOWER_SESSION_HOOK}`;
16
+
17
+ /** Flower 独立静默活动 hook。 */
18
+ export const FLOWER_TELEMETRY_HOOK = "flower_telemetry_hook.py";
19
+
20
+ /** 活动 hook 在项目内的受管位置。 */
21
+ export const FLOWER_TELEMETRY_HOOK_REL = `.trellis/scripts/${FLOWER_TELEMETRY_HOOK}`;
22
+
11
23
  /**
12
24
  * 复制 flower 自有脚本资产。
13
25
  *
@@ -18,12 +30,15 @@ export const FLOWER_UPDATE_HOOK_REL = `.trellis/scripts/${FLOWER_UPDATE_HOOK}`;
18
30
  * @returns {{installed:string[],paths:string[]}} 已安装资产和 manifest 路径
19
31
  */
20
32
  export function copyFlowerAssets(target) {
21
- copyPath(
22
- path.join(PKG_ROOT, "src", "assets", FLOWER_UPDATE_HOOK),
23
- path.join(target, ...FLOWER_UPDATE_HOOK_REL.split("/")),
24
- );
33
+ const assets = [FLOWER_UPDATE_HOOK, FLOWER_SESSION_HOOK, FLOWER_TELEMETRY_HOOK];
34
+ for (const asset of assets) {
35
+ copyPath(
36
+ path.join(PKG_ROOT, "src", "assets", asset),
37
+ path.join(target, ".trellis", "scripts", asset),
38
+ );
39
+ }
25
40
  return {
26
- installed: [`script:${FLOWER_UPDATE_HOOK}`],
27
- paths: [FLOWER_UPDATE_HOOK_REL],
41
+ installed: assets.map((asset) => `script:${asset}`),
42
+ paths: assets.map((asset) => `.trellis/scripts/${asset}`),
28
43
  };
29
44
  }