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.
- package/README.md +20 -1
- package/enhancements/0.6/overrides/bundles/control-plane-integrity.json +2 -0
- package/enhancements/0.6/overrides/bundles/intent-routing.json +2 -1
- package/enhancements/0.6/overrides/bundles/task-reference-contract.json +24 -0
- package/enhancements/0.6/overrides/conflicts.json +37 -4
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/patch.json +19 -0
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-content.py +39 -0
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-selector.py +38 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/patch.json +20 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-content.py +64 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-selector.py +2 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/content.md +2 -4
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/planning-contract-content.md +1 -1
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/readiness-content.md +2 -2
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/review-content.md +2 -2
- package/enhancements/0.6/scripts/decision_log.py +29 -24
- package/enhancements/0.6/scripts/task_progress.py +54 -13
- package/enhancements/0.6/scripts/untracked_flow.py +13 -5
- package/enhancements/MANIFEST.json +9 -2
- package/package.json +4 -4
- package/src/assets/flower_session_start.py +195 -0
- package/src/assets/flower_telemetry_hook.py +98 -0
- package/src/builtin-plugins/skill-garden/content-adapter.js +15 -1
- package/src/cli.js +3 -1
- package/src/commands/init.js +36 -1
- package/src/commands/plugin-remote.js +61 -14
- package/src/commands/plugin.js +14 -1
- package/src/commands/self-check.js +20 -0
- package/src/commands/self-update.js +38 -3
- package/src/commands/telemetry.js +30 -1
- package/src/commands/uninstall.js +20 -0
- package/src/commands/update-check.js +19 -0
- package/src/commands/update.js +52 -9
- package/src/commands/worktree.js +114 -0
- package/src/lib/cli-args.js +10 -0
- package/src/lib/developer.js +3 -2
- package/src/lib/flower-assets.js +21 -6
- package/src/lib/platform-patch-adapters.js +46 -8
- package/src/lib/telemetry-context.js +275 -0
- package/src/lib/telemetry-files.js +144 -0
- package/src/lib/telemetry-operation.js +84 -0
- package/src/lib/telemetry-queue.js +310 -0
- package/src/lib/telemetry-sender.js +7 -0
- package/src/lib/telemetry.js +12 -361
- package/src/patches/bundles/flower-platform-integration.json +4 -1
- package/src/patches/platforms/claude/session-start-hooks/patch.json +28 -0
- package/src/patches/platforms/claude/telemetry-hooks/patch.json +59 -0
- package/src/patches/platforms/codex/session-start-hooks/patch.json +3 -2
- package/src/patches/platforms/codex/telemetry-hooks/patch.json +59 -0
- package/src/plugin/application-service.js +5 -22
- package/src/plugin/contracts.js +1 -0
- package/src/plugin/lock-reachability.js +37 -0
- package/src/plugin/schemas/project-files.js +1 -0
- package/src/plugin/sources/gitlab-provider.js +50 -9
package/src/commands/plugin.js
CHANGED
|
@@ -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)
|
|
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
|
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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,
|
package/src/commands/worktree.js
CHANGED
|
@@ -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");
|
package/src/lib/cli-args.js
CHANGED
|
@@ -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
|
*
|
package/src/lib/developer.js
CHANGED
|
@@ -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 = {}) {
|
package/src/lib/flower-assets.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
27
|
-
paths:
|
|
41
|
+
installed: assets.map((asset) => `script:${asset}`),
|
|
42
|
+
paths: assets.map((asset) => `.trellis/scripts/${asset}`),
|
|
28
43
|
};
|
|
29
44
|
}
|