flower-trellis 0.6.3 → 0.6.5

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 (51) 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 +33 -0
  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/scripts/decision_log.py +29 -24
  13. package/enhancements/0.6/scripts/task_progress.py +54 -13
  14. package/enhancements/0.6/scripts/untracked_flow.py +13 -5
  15. package/enhancements/MANIFEST.json +9 -2
  16. package/package.json +4 -4
  17. package/src/assets/flower_session_start.py +195 -0
  18. package/src/assets/flower_telemetry_hook.py +98 -0
  19. package/src/builtin-plugins/skill-garden/content-adapter.js +15 -1
  20. package/src/cli.js +3 -1
  21. package/src/commands/init.js +36 -1
  22. package/src/commands/plugin-remote.js +118 -20
  23. package/src/commands/plugin.js +25 -3
  24. package/src/commands/self-check.js +20 -0
  25. package/src/commands/self-update.js +38 -3
  26. package/src/commands/telemetry.js +30 -1
  27. package/src/commands/uninstall.js +20 -0
  28. package/src/commands/update-check.js +19 -0
  29. package/src/commands/update.js +59 -8
  30. package/src/commands/worktree.js +114 -0
  31. package/src/lib/cli-args.js +10 -0
  32. package/src/lib/developer.js +3 -2
  33. package/src/lib/flower-assets.js +21 -6
  34. package/src/lib/platform-patch-adapters.js +46 -8
  35. package/src/lib/telemetry-context.js +275 -0
  36. package/src/lib/telemetry-files.js +144 -0
  37. package/src/lib/telemetry-operation.js +84 -0
  38. package/src/lib/telemetry-queue.js +310 -0
  39. package/src/lib/telemetry-sender.js +7 -0
  40. package/src/lib/telemetry.js +12 -361
  41. package/src/patches/bundles/flower-platform-integration.json +4 -1
  42. package/src/patches/platforms/claude/session-start-hooks/patch.json +28 -0
  43. package/src/patches/platforms/claude/telemetry-hooks/patch.json +59 -0
  44. package/src/patches/platforms/codex/session-start-hooks/patch.json +3 -2
  45. package/src/patches/platforms/codex/telemetry-hooks/patch.json +59 -0
  46. package/src/plugin/application-service.js +170 -53
  47. package/src/plugin/contracts.js +1 -0
  48. package/src/plugin/lock-reachability.js +37 -0
  49. package/src/plugin/resolver/dependency-resolver.js +58 -7
  50. package/src/plugin/schemas/project-files.js +1 -0
  51. 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
  }
@@ -747,7 +751,7 @@ function pluginExitCode(error) {
747
751
  * 执行 Plugin 生命周期命令。
748
752
  *
749
753
  * @param {object} ctx cli-args.js 的执行上下文
750
- * @param {{cwd?:string,providers?:object[],output?:{log:(message:string)=>void,error:(message:string)=>void},interactive?:boolean,prompts?:object,confirmApproval?:(requests:object[])=>Promise<boolean>|boolean,compact?:boolean,onPreflight?:(result:object)=>void,trellisControlMode?:"materialized"|"restoring"}} [options] 测试、Provider、输出与交互确认注入
754
+ * @param {{cwd?:string,providers?:object[],output?:{log:(message:string)=>void,error:(message:string)=>void},interactive?:boolean,prompts?:object,confirmApproval?:(requests:object[])=>Promise<boolean>|boolean,compact?:boolean,preserveIds?:string[],onPreflight?:(result:object)=>void,trellisControlMode?:"materialized"|"restoring"}} [options] 测试、Provider、输出与交互确认注入
751
755
  * @returns {Promise<number>} 进程退出码
752
756
  */
753
757
  export async function plugin(ctx, options = {}) {
@@ -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,8 @@ export async function plugin(ctx, options = {}) {
834
844
  options,
835
845
  registry,
836
846
  lock,
847
+ pluginsFile,
848
+ preserveIds: options.preserveIds || [],
837
849
  });
838
850
 
839
851
  if (parsed.command !== "add" && canonicalId && !canonicalId.includes("/")) {
@@ -889,7 +901,14 @@ export async function plugin(ctx, options = {}) {
889
901
  }));
890
902
  }
891
903
 
892
- await remoteRuntime?.prepareRemotePluginCandidates({ parsed, canonicalId, registry, lock });
904
+ await remoteRuntime?.prepareRemotePluginCandidates({
905
+ parsed,
906
+ canonicalId,
907
+ registry,
908
+ lock,
909
+ pluginsFile,
910
+ preserveIds: options.preserveIds || [],
911
+ });
893
912
 
894
913
  const service = new PluginApplicationService(ctx.target, { registry, store });
895
914
  let result;
@@ -906,6 +925,7 @@ export async function plugin(ctx, options = {}) {
906
925
  platforms: parsed.platforms,
907
926
  ...(parsed.contentSelection ? { contentSelection: parsed.contentSelection } : {}),
908
927
  dryRun: parsed.dryRun,
928
+ preserveIds: options.preserveIds || [],
909
929
  onPreflight: options.onPreflight,
910
930
  };
911
931
  result = parsed.dryRun ? service.add(addOptions) : await executeWithCapabilityApproval(
@@ -925,6 +945,7 @@ export async function plugin(ctx, options = {}) {
925
945
  platforms: parsed.platforms,
926
946
  ...(parsed.contentSelection ? { contentSelection: parsed.contentSelection } : {}),
927
947
  dryRun: parsed.dryRun,
948
+ preserveIds: options.preserveIds || [],
928
949
  onPreflight: options.onPreflight,
929
950
  };
930
951
  result = parsed.dryRun ? service.update(updateOptions) : await executeWithCapabilityApproval(
@@ -955,6 +976,7 @@ export async function plugin(ctx, options = {}) {
955
976
  printResult(result, parsed.json, output, { compact, verbose });
956
977
  return result.ok === false ? 3 : 0;
957
978
  } catch (error) {
979
+ noteTelemetryError(ctx, error);
958
980
  const json = parsed?.json || ctx.passthrough?.includes("--json");
959
981
  const code = error instanceof PluginError ? error.code : "PLUGIN_UNEXPECTED_ERROR";
960
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
  *
@@ -89,6 +108,17 @@ function pluginPlatformArgs(platforms) {
89
108
  */
90
109
  export async function replayPlugins(ctx, target, dryRun, compensationSnapshot = null) {
91
110
  const output = ctx.trellisControlQuiet ? SILENT_OUTPUT : console;
111
+ const store = new ProjectStore(target);
112
+ const pluginsFile = store.readPlugins();
113
+ const lock = store.readLock();
114
+ const { reachableIds } = classifyLockReachability(pluginsFile.plugins, lock);
115
+ const preserveIds = (lock?.plugins || [])
116
+ .filter(({ id, source }) => (
117
+ reachableIds.has(id) &&
118
+ id !== SKILL_GARDEN_PLUGIN_ID &&
119
+ ["gitlab", "github"].includes(source.type)
120
+ ))
121
+ .map(({ id }) => id);
92
122
  const onPreflight = compensationSnapshot || ctx.trellisControlExtendSnapshot
93
123
  ? ({ plan }) => {
94
124
  const targets = [
@@ -100,7 +130,7 @@ export async function replayPlugins(ctx, target, dryRun, compensationSnapshot =
100
130
  }
101
131
  : undefined;
102
132
  if (ctx.enhance) {
103
- const declared = new ProjectStore(target).readPlugins().plugins
133
+ const declared = pluginsFile.plugins
104
134
  .some(({ id }) => id === SKILL_GARDEN_PLUGIN_ID);
105
135
  const code = await plugin({
106
136
  ...ctx,
@@ -113,6 +143,7 @@ export async function replayPlugins(ctx, target, dryRun, compensationSnapshot =
113
143
  ],
114
144
  }, {
115
145
  skillGarden: { variant: ctx.variant, skills: ctx.skills },
146
+ preserveIds,
116
147
  compact: true,
117
148
  onPreflight,
118
149
  output,
@@ -122,15 +153,17 @@ export async function replayPlugins(ctx, target, dryRun, compensationSnapshot =
122
153
  }
123
154
 
124
155
  output.log("· --no-enhance:跳过 Skill-Garden,仅重放其它已声明 Plugin");
125
- const preserveSkillGarden = new ProjectStore(target).readLock()?.plugins
126
- .some(({ id }) => id === SKILL_GARDEN_PLUGIN_ID) === true;
156
+ const preserveSkillGarden = reachableIds.has(SKILL_GARDEN_PLUGIN_ID);
127
157
  const code = await plugin({
128
158
  ...ctx,
129
159
  target,
130
160
  passthrough: ["replay", ...(dryRun ? ["--dry-run"] : [])],
131
161
  }, {
132
162
  skillGarden: { preserve: preserveSkillGarden },
133
- preserveIds: preserveSkillGarden ? [SKILL_GARDEN_PLUGIN_ID] : [],
163
+ preserveIds: [
164
+ ...preserveIds,
165
+ ...(preserveSkillGarden ? [SKILL_GARDEN_PLUGIN_ID] : []),
166
+ ],
134
167
  compact: true,
135
168
  onPreflight,
136
169
  output,
@@ -207,13 +240,30 @@ function printBackupRetentionResult(result, output = console) {
207
240
  * @returns {Promise<void>} 升级、强化叠加与备份保留处理完成后返回
208
241
  */
209
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
+ }
210
255
  const dryRun = ctx.passthrough.includes("--dry-run");
256
+ if (!dryRun) beginTelemetryOperation(ctx);
211
257
  if (!dryRun && ctx.trellisControlMode !== "materialized" && ctx.trellisControlMode !== "restoring") {
212
- return runWithTrellisIntegrationEnabled(ctx.target, ({ extendSnapshot }) => update({
258
+ const result = await runWithTrellisIntegrationEnabled(ctx.target, ({ extendSnapshot }) => update({
213
259
  ...ctx,
214
260
  trellisControlMode: "materialized",
261
+ telemetryDeferCompletion: true,
215
262
  trellisControlExtendSnapshot: extendSnapshot,
216
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;
217
267
  }
218
268
  const { target } = ctx;
219
269
  const quiet = ctx.trellisControlQuiet === true;
@@ -258,7 +308,7 @@ export async function update(ctx) {
258
308
  { stripBanner: true, ...(quiet ? { stdout: SILENT_OUTPUT } : {}) },
259
309
  );
260
310
  if (code !== 0) {
261
- throw new Error(`trellis update 失败(退出码 ${code}),已中止,未重新叠加`);
311
+ throw Object.assign(new Error(`trellis update 失败(退出码 ${code}),已中止,未重新叠加`), { code: code === 130 ? "FLOWER_OPERATION_CANCELLED" : "FLOWER_UPSTREAM_FAILED" });
262
312
  }
263
313
  await replayPlugins(ctx, target, dryRun, compensationSnapshot);
264
314
  updateSucceeded = true;
@@ -307,10 +357,11 @@ export async function update(ctx) {
307
357
  output.log(" · --backup-retention 0:保留全部升级备份");
308
358
  }
309
359
 
360
+ if (!ctx.telemetryDeferCompletion) completeTelemetryOperation(ctx, "update");
310
361
  const telemetryPromise = dryRun
311
362
  ? null
312
363
  : reportTelemetry(target, "update_completed", { force: true });
313
- await showCommandCompletion("update", target, {
364
+ if (!ctx.telemetryDeferCompletion) await showCommandCompletion("update", target, {
314
365
  passthrough: ctx.passthrough,
315
366
  outcome: dryRun ? "preview" : "success",
316
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 = {}) {