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/lib/telemetry.js
CHANGED
|
@@ -1,367 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { flowerConfigDirectory } from "../plugin/sources/user-source-store.js";
|
|
5
|
-
import { ProjectStore } from "../plugin/state/project-store.js";
|
|
6
|
-
import { SKILL_GARDEN_PLUGIN_ID } from "../builtin-plugins/skill-garden/provider.js";
|
|
7
|
-
import { readGitDeveloper } from "./developer.js";
|
|
8
|
-
import { readManifest, readUpdateCheck } from "./manifest.js";
|
|
9
|
-
import { flowerVersion, trellisVersion } from "./versions.js";
|
|
1
|
+
import { queueLegacyTelemetry } from "./telemetry-queue.js";
|
|
10
2
|
|
|
11
|
-
|
|
12
|
-
export
|
|
3
|
+
// 对外保留旧模块入口;队列只依赖 context,避免兼容入口反向形成循环。
|
|
4
|
+
export {
|
|
5
|
+
FLOWER_TELEMETRY_ENDPOINT, telemetryStatePath, readTelemetryState,
|
|
6
|
+
setTelemetryEnabled, buildTelemetryPayload,
|
|
7
|
+
} from "./telemetry-context.js";
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
/** 遥测请求保留 10 秒公网预算,避免慢速 TLS 建连或服务端响应被过早中止。 */
|
|
16
|
-
const DEFAULT_TIMEOUT_MS = 10000;
|
|
17
|
-
const VERSION_PATTERN = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/;
|
|
18
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
19
|
-
const EVENTS = new Set(["version_check", "init_completed", "update_completed"]);
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 规范化可上报的开发者名称。
|
|
23
|
-
*
|
|
24
|
-
* @param {unknown} value 原始名称
|
|
25
|
-
* @returns {string|null} 有效名称,无法使用时返回 null
|
|
26
|
-
*/
|
|
27
|
-
function normalizeDeveloperName(value) {
|
|
28
|
-
if (typeof value !== "string") return null;
|
|
29
|
-
const name = value.trim();
|
|
30
|
-
return name && name.length <= 100 ? name : null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 返回 Flower 用户级遥测状态文件路径。
|
|
35
|
-
*
|
|
36
|
-
* @param {NodeJS.ProcessEnv} [env] 环境变量
|
|
37
|
-
* @returns {string} 状态文件绝对路径
|
|
38
|
-
*/
|
|
39
|
-
export function telemetryStatePath(env = process.env) {
|
|
40
|
-
return path.join(flowerConfigDirectory(env), "telemetry.json");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 校验并规范化遥测状态。
|
|
45
|
-
*
|
|
46
|
-
* @param {unknown} value 原始状态
|
|
47
|
-
* @returns {object} 规范化状态
|
|
48
|
-
*/
|
|
49
|
-
function normalizeTelemetryState(value) {
|
|
50
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
51
|
-
throw new TypeError("遥测状态必须是对象");
|
|
52
|
-
}
|
|
53
|
-
if (value.schemaVersion !== TELEMETRY_SCHEMA_VERSION) {
|
|
54
|
-
throw new TypeError("遥测状态 schemaVersion 无效");
|
|
55
|
-
}
|
|
56
|
-
if (!UUID_PATTERN.test(String(value.deviceId || ""))) {
|
|
57
|
-
throw new TypeError("遥测设备 ID 无效");
|
|
58
|
-
}
|
|
59
|
-
if (typeof value.enabled !== "boolean") {
|
|
60
|
-
throw new TypeError("遥测 enabled 无效");
|
|
61
|
-
}
|
|
62
|
-
const developerName = normalizeDeveloperName(value.developerName);
|
|
63
|
-
if (value.developerName !== undefined && value.developerName !== null && !developerName) {
|
|
64
|
-
throw new TypeError("遥测 developerName 无效");
|
|
65
|
-
}
|
|
66
|
-
for (const field of ["lastAttemptAt", "lastSuccessAt"]) {
|
|
67
|
-
if (value[field] !== null && (
|
|
68
|
-
typeof value[field] !== "string" || Number.isNaN(Date.parse(value[field]))
|
|
69
|
-
)) {
|
|
70
|
-
throw new TypeError(`遥测 ${field} 无效`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
75
|
-
deviceId: value.deviceId,
|
|
76
|
-
developerName,
|
|
77
|
-
enabled: value.enabled,
|
|
78
|
-
lastAttemptAt: value.lastAttemptAt,
|
|
79
|
-
lastSuccessAt: value.lastSuccessAt,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 读取用户级遥测状态,并区分缺失与损坏。
|
|
85
|
-
*
|
|
86
|
-
* @param {{env?:NodeJS.ProcessEnv}} [options] 读取选项
|
|
87
|
-
* @returns {{status:"missing"|"valid"|"corrupt",state:object|null,path:string,error?:Error}} 状态读取结果
|
|
88
|
-
*/
|
|
89
|
-
export function readTelemetryState(options = {}) {
|
|
90
|
-
const filePath = telemetryStatePath(options.env);
|
|
91
|
-
try {
|
|
92
|
-
const stat = fs.lstatSync(filePath);
|
|
93
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
94
|
-
throw new TypeError("遥测状态必须是普通文件");
|
|
95
|
-
}
|
|
96
|
-
const state = normalizeTelemetryState(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
97
|
-
return { status: "valid", state, path: filePath };
|
|
98
|
-
} catch (error) {
|
|
99
|
-
if (error?.code === "ENOENT") return { status: "missing", state: null, path: filePath };
|
|
100
|
-
return { status: "corrupt", state: null, path: filePath, error };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 原子写入用户级遥测状态。
|
|
106
|
-
*
|
|
107
|
-
* @param {object} state 已规范化状态
|
|
108
|
-
* @param {{env?:NodeJS.ProcessEnv,randomBytes?:(size:number)=>Buffer}} [options] 写入选项
|
|
109
|
-
* @returns {object} 写入后的状态
|
|
110
|
-
*/
|
|
111
|
-
function writeTelemetryState(state, options = {}) {
|
|
112
|
-
const filePath = telemetryStatePath(options.env);
|
|
113
|
-
const parent = path.dirname(filePath);
|
|
114
|
-
const randomBytes = options.randomBytes || crypto.randomBytes;
|
|
115
|
-
const temporary = path.join(
|
|
116
|
-
parent,
|
|
117
|
-
`.${path.basename(filePath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`,
|
|
118
|
-
);
|
|
119
|
-
const normalized = normalizeTelemetryState(state);
|
|
120
|
-
let descriptor = null;
|
|
121
|
-
|
|
122
|
-
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
123
|
-
try {
|
|
124
|
-
fs.chmodSync(parent, 0o700);
|
|
125
|
-
} catch {
|
|
126
|
-
// Windows 等平台可能不支持 POSIX 权限,目录仍由当前用户配置目录边界保护。
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
const existing = fs.lstatSync(filePath);
|
|
130
|
-
if (existing.isSymbolicLink() || !existing.isFile()) {
|
|
131
|
-
throw new TypeError(`遥测状态必须是普通文件:${filePath}`);
|
|
132
|
-
}
|
|
133
|
-
} catch (error) {
|
|
134
|
-
if (error?.code !== "ENOENT") throw error;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
descriptor = fs.openSync(temporary, "wx", 0o600);
|
|
139
|
-
fs.writeFileSync(descriptor, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
140
|
-
fs.fsyncSync(descriptor);
|
|
141
|
-
fs.closeSync(descriptor);
|
|
142
|
-
descriptor = null;
|
|
143
|
-
fs.renameSync(temporary, filePath);
|
|
144
|
-
try {
|
|
145
|
-
fs.chmodSync(filePath, 0o600);
|
|
146
|
-
} catch {
|
|
147
|
-
// Windows 等平台可能不支持 POSIX 权限,原子写入模式仍限制了 Unix 新文件权限。
|
|
148
|
-
}
|
|
149
|
-
return normalized;
|
|
150
|
-
} catch (error) {
|
|
151
|
-
if (descriptor !== null) {
|
|
152
|
-
try {
|
|
153
|
-
fs.closeSync(descriptor);
|
|
154
|
-
} catch {
|
|
155
|
-
// 保留原始写入错误。
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
fs.unlinkSync(temporary);
|
|
160
|
-
} catch (cleanupError) {
|
|
161
|
-
if (cleanupError?.code !== "ENOENT") throw cleanupError;
|
|
162
|
-
}
|
|
163
|
-
throw error;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* 创建新的默认遥测状态。
|
|
169
|
-
*
|
|
170
|
-
* @param {{randomUUID?:()=>string}} [options] 随机 ID 注入
|
|
171
|
-
* @returns {object} 默认启用的遥测状态
|
|
172
|
-
*/
|
|
173
|
-
function createTelemetryState(options = {}) {
|
|
174
|
-
const randomUUID = options.randomUUID || crypto.randomUUID;
|
|
175
|
-
return {
|
|
176
|
-
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
177
|
-
deviceId: randomUUID(),
|
|
178
|
-
developerName: null,
|
|
179
|
-
enabled: true,
|
|
180
|
-
lastAttemptAt: null,
|
|
181
|
-
lastSuccessAt: null,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 显式启用或停用用户级遥测。
|
|
187
|
-
*
|
|
188
|
-
* 损坏状态只允许通过显式设置修复,普通后台上报不会覆盖现场证据。
|
|
189
|
-
*
|
|
190
|
-
* @param {boolean} enabled 是否启用
|
|
191
|
-
* @param {{env?:NodeJS.ProcessEnv,randomUUID?:()=>string,randomBytes?:(size:number)=>Buffer}} [options] 状态选项
|
|
192
|
-
* @returns {object} 写入后的遥测状态
|
|
193
|
-
*/
|
|
194
|
-
export function setTelemetryEnabled(enabled, options = {}) {
|
|
195
|
-
const result = readTelemetryState(options);
|
|
196
|
-
const state = result.status === "valid"
|
|
197
|
-
? result.state
|
|
198
|
-
: createTelemetryState(options);
|
|
199
|
-
return writeTelemetryState({ ...state, enabled }, options);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* 读取项目 `.trellis/.developer` 中由 Trellis 明确记录的开发者名称。
|
|
204
|
-
*
|
|
205
|
-
* @param {string} target 项目根目录
|
|
206
|
-
* @returns {string|null} 开发者名称
|
|
207
|
-
*/
|
|
208
|
-
function readProjectDeveloper(target) {
|
|
209
|
-
try {
|
|
210
|
-
const content = fs.readFileSync(path.join(target, ".trellis", ".developer"), "utf8");
|
|
211
|
-
const line = content.split(/\r?\n/).find((entry) => entry.startsWith("name="));
|
|
212
|
-
return normalizeDeveloperName(line?.slice("name=".length));
|
|
213
|
-
} catch {
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 按项目自报、Git 配置、用户级缓存的顺序解析开发者名称。
|
|
220
|
-
*
|
|
221
|
-
* @param {string} target 项目根目录
|
|
222
|
-
* @param {{developerName?:string|null,env?:NodeJS.ProcessEnv}} [options] 回退选项
|
|
223
|
-
* @returns {string|null} 开发者名称
|
|
224
|
-
*/
|
|
225
|
-
function resolveDeveloperName(target, options = {}) {
|
|
226
|
-
return readProjectDeveloper(target) ||
|
|
227
|
-
normalizeDeveloperName(readGitDeveloper(target, { env: options.env })) ||
|
|
228
|
-
normalizeDeveloperName(options.developerName);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* 读取项目当前 Flower 与 Trellis 版本,不采集任何项目路径或仓库信息。
|
|
233
|
-
*
|
|
9
|
+
/** 将兼容安装快照入队;网络由独立 sender 执行。
|
|
234
10
|
* @param {string} target 项目根目录
|
|
235
|
-
* @
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
let projectFlower = null;
|
|
239
|
-
let projectTrellis = null;
|
|
240
|
-
try {
|
|
241
|
-
const lock = new ProjectStore(target).readLock();
|
|
242
|
-
const skillGarden = lock?.plugins.find(({ id }) => id === SKILL_GARDEN_PLUGIN_ID);
|
|
243
|
-
projectFlower = skillGarden?.version || readManifest(target)?.flowerVersion || null;
|
|
244
|
-
} catch {
|
|
245
|
-
projectFlower = readManifest(target)?.flowerVersion || null;
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
projectTrellis = fs.readFileSync(path.join(target, ".trellis", ".version"), "utf8").trim();
|
|
249
|
-
} catch {
|
|
250
|
-
projectTrellis = null;
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
flower: VERSION_PATTERN.test(String(projectFlower || "")) ? projectFlower : null,
|
|
254
|
-
trellis: VERSION_PATTERN.test(String(projectTrellis || "")) ? projectTrellis : null,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* 构造严格白名单的匿名遥测载荷。
|
|
260
|
-
*
|
|
261
|
-
* @param {string} target 项目根目录
|
|
262
|
-
* @param {string} event 遥测事件
|
|
263
|
-
* @param {{deviceId:string,developerName?:string|null,env?:NodeJS.ProcessEnv,now?:Date}} options 设备、身份与时间参数
|
|
264
|
-
* @returns {object|null} 可上报载荷;全部身份来源缺失时返回 null
|
|
265
|
-
*/
|
|
266
|
-
export function buildTelemetryPayload(target, event, options) {
|
|
267
|
-
if (!EVENTS.has(event)) throw new TypeError(`未知遥测事件:${event}`);
|
|
268
|
-
if (!UUID_PATTERN.test(String(options?.deviceId || ""))) {
|
|
269
|
-
throw new TypeError("遥测设备 ID 无效");
|
|
270
|
-
}
|
|
271
|
-
const now = options.now instanceof Date ? options.now : new Date();
|
|
272
|
-
const resolvedTarget = path.resolve(target);
|
|
273
|
-
const developerName = resolveDeveloperName(resolvedTarget, options);
|
|
274
|
-
// 三种可信身份来源都缺失时无法诚实构造开发者名称,保留静默降级作为最后边界。
|
|
275
|
-
if (!developerName) return null;
|
|
276
|
-
const project = readProjectVersions(resolvedTarget);
|
|
277
|
-
const bundledTrellis = trellisVersion();
|
|
278
|
-
return {
|
|
279
|
-
schema_version: TELEMETRY_SCHEMA_VERSION,
|
|
280
|
-
device_id: options.deviceId,
|
|
281
|
-
event,
|
|
282
|
-
flower_version: flowerVersion(),
|
|
283
|
-
bundled_trellis_version: VERSION_PATTERN.test(bundledTrellis) ? bundledTrellis : null,
|
|
284
|
-
project_flower_version: project.flower,
|
|
285
|
-
project_trellis_version: project.trellis,
|
|
286
|
-
developer_name: developerName,
|
|
287
|
-
platform: process.platform,
|
|
288
|
-
arch: process.arch,
|
|
289
|
-
client_time: now.toISOString(),
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* 判断常规遥测是否仍处于上报间隔内。
|
|
295
|
-
*
|
|
296
|
-
* @param {object} state 遥测状态
|
|
297
|
-
* @param {Date} now 当前时间
|
|
298
|
-
* @param {number} intervalHours 上报间隔小时数
|
|
299
|
-
* @returns {boolean} 是否应跳过
|
|
300
|
-
*/
|
|
301
|
-
function isWithinInterval(state, now, intervalHours) {
|
|
302
|
-
if (!state.lastAttemptAt) return false;
|
|
303
|
-
const attemptedAt = new Date(state.lastAttemptAt).getTime();
|
|
304
|
-
if (!Number.isFinite(attemptedAt)) return false;
|
|
305
|
-
return now.getTime() - attemptedAt < intervalHours * 60 * 60 * 1000;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* 尽力而为地上报 Flower 匿名安装遥测。
|
|
310
|
-
*
|
|
311
|
-
* 网络、状态文件或服务端失败均只返回状态,不输出日志也不影响调用命令。
|
|
312
|
-
*
|
|
313
|
-
* @param {string} target 项目根目录
|
|
314
|
-
* @param {"version_check"|"init_completed"|"update_completed"} event 遥测事件
|
|
315
|
-
* @param {{force?:boolean,env?:NodeJS.ProcessEnv,fetch?:typeof fetch,now?:Date,timeoutMs?:number,endpoint?:string,randomUUID?:()=>string,randomBytes?:(size:number)=>Buffer}} [options] 上报选项
|
|
316
|
-
* @returns {Promise<{status:string}>} 上报结果
|
|
11
|
+
* @param {string} event v1 事件
|
|
12
|
+
* @param {object} options 本地采集依赖
|
|
13
|
+
* @returns {Promise<{status:string}>} 入队结果
|
|
317
14
|
*/
|
|
318
15
|
export async function reportTelemetry(target, event, options = {}) {
|
|
319
|
-
try {
|
|
320
|
-
|
|
321
|
-
if (env.FLOWER_NO_TELEMETRY) return { status: "disabled_by_env" };
|
|
322
|
-
|
|
323
|
-
const readResult = readTelemetryState({ env });
|
|
324
|
-
if (readResult.status === "corrupt") return { status: "corrupt_state" };
|
|
325
|
-
let state = readResult.status === "valid"
|
|
326
|
-
? readResult.state
|
|
327
|
-
: createTelemetryState(options);
|
|
328
|
-
if (!state.enabled) return { status: "disabled" };
|
|
329
|
-
|
|
330
|
-
const now = options.now instanceof Date ? options.now : new Date();
|
|
331
|
-
const payload = buildTelemetryPayload(target, event, {
|
|
332
|
-
deviceId: state.deviceId,
|
|
333
|
-
developerName: state.developerName,
|
|
334
|
-
env,
|
|
335
|
-
now,
|
|
336
|
-
});
|
|
337
|
-
if (!payload) return { status: "missing_developer" };
|
|
338
|
-
const intervalHours = readUpdateCheck(path.resolve(target)).intervalHours;
|
|
339
|
-
if (!options.force && isWithinInterval(state, now, intervalHours)) {
|
|
340
|
-
return { status: "throttled" };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
state = writeTelemetryState({
|
|
344
|
-
...state,
|
|
345
|
-
developerName: payload.developer_name,
|
|
346
|
-
lastAttemptAt: now.toISOString(),
|
|
347
|
-
}, options);
|
|
348
|
-
const controller = new AbortController();
|
|
349
|
-
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
350
|
-
try {
|
|
351
|
-
const fetchRequest = options.fetch || fetch;
|
|
352
|
-
const response = await fetchRequest(options.endpoint || FLOWER_TELEMETRY_ENDPOINT, {
|
|
353
|
-
method: "POST",
|
|
354
|
-
headers: { "Content-Type": "application/json" },
|
|
355
|
-
body: JSON.stringify(payload),
|
|
356
|
-
signal: controller.signal,
|
|
357
|
-
});
|
|
358
|
-
if (!response.ok) return { status: "failed" };
|
|
359
|
-
writeTelemetryState({ ...state, lastSuccessAt: now.toISOString() }, options);
|
|
360
|
-
return { status: "reported" };
|
|
361
|
-
} finally {
|
|
362
|
-
clearTimeout(timer);
|
|
363
|
-
}
|
|
364
|
-
} catch {
|
|
365
|
-
return { status: "failed" };
|
|
366
|
-
}
|
|
16
|
+
try { return queueLegacyTelemetry(target, event, options); }
|
|
17
|
+
catch { return { status: "failed" }; }
|
|
367
18
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "flower-claude-session-start-hooks",
|
|
4
|
+
"purpose": "platform_config",
|
|
5
|
+
"operations": [{
|
|
6
|
+
"id": "flower-claude-trellis-session-start",
|
|
7
|
+
"operation": "replace",
|
|
8
|
+
"targets": [{
|
|
9
|
+
"kind": "json",
|
|
10
|
+
"path": ".claude/settings.json",
|
|
11
|
+
"missing": "create",
|
|
12
|
+
"requires": [".claude"]
|
|
13
|
+
}],
|
|
14
|
+
"selector": {
|
|
15
|
+
"type": "json-hook-command",
|
|
16
|
+
"event": "SessionStart",
|
|
17
|
+
"commandNeedle": ".claude/hooks/session-start.py"
|
|
18
|
+
},
|
|
19
|
+
"content": {
|
|
20
|
+
"value": {
|
|
21
|
+
"matcher": "startup|clear|compact",
|
|
22
|
+
"timeout": 30,
|
|
23
|
+
"commandResolver": "claude-session-start",
|
|
24
|
+
"sessionParts": ["state", "rules", "stages"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}]
|
|
28
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "flower-claude-telemetry-hooks",
|
|
4
|
+
"purpose": "platform_config",
|
|
5
|
+
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"id": "flower-claude-telemetry-sessionstart",
|
|
8
|
+
"operation": "replace",
|
|
9
|
+
"targets": [
|
|
10
|
+
{
|
|
11
|
+
"kind": "json",
|
|
12
|
+
"path": ".claude/settings.json",
|
|
13
|
+
"missing": "create",
|
|
14
|
+
"requires": [
|
|
15
|
+
".claude"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"selector": {
|
|
20
|
+
"type": "json-hook-command",
|
|
21
|
+
"event": "SessionStart",
|
|
22
|
+
"commandNeedle": ".trellis/scripts/flower_telemetry_hook.py"
|
|
23
|
+
},
|
|
24
|
+
"content": {
|
|
25
|
+
"value": {
|
|
26
|
+
"matcher": "startup|resume|clear|compact",
|
|
27
|
+
"timeout": 5,
|
|
28
|
+
"commandResolver": "claude-flower-telemetry"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "flower-claude-telemetry-userpromptsubmit",
|
|
34
|
+
"operation": "replace",
|
|
35
|
+
"targets": [
|
|
36
|
+
{
|
|
37
|
+
"kind": "json",
|
|
38
|
+
"path": ".claude/settings.json",
|
|
39
|
+
"missing": "create",
|
|
40
|
+
"requires": [
|
|
41
|
+
".claude"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"selector": {
|
|
46
|
+
"type": "json-hook-command",
|
|
47
|
+
"event": "UserPromptSubmit",
|
|
48
|
+
"commandNeedle": ".trellis/scripts/flower_telemetry_hook.py"
|
|
49
|
+
},
|
|
50
|
+
"content": {
|
|
51
|
+
"value": {
|
|
52
|
+
"matcher": "*",
|
|
53
|
+
"timeout": 5,
|
|
54
|
+
"commandResolver": "claude-flower-telemetry"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
},
|
|
20
20
|
"content": {
|
|
21
21
|
"value": {
|
|
22
|
-
"matcher": "startup|
|
|
22
|
+
"matcher": "startup|clear|compact",
|
|
23
23
|
"timeout": 30,
|
|
24
|
-
"commandResolver": "codex-session-start"
|
|
24
|
+
"commandResolver": "codex-session-start",
|
|
25
|
+
"sessionParts": ["state", "rules", "stages"]
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
},
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "flower-codex-telemetry-hooks",
|
|
4
|
+
"purpose": "platform_config",
|
|
5
|
+
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"id": "flower-codex-telemetry-sessionstart",
|
|
8
|
+
"operation": "replace",
|
|
9
|
+
"targets": [
|
|
10
|
+
{
|
|
11
|
+
"kind": "json",
|
|
12
|
+
"path": ".codex/hooks.json",
|
|
13
|
+
"missing": "create",
|
|
14
|
+
"requires": [
|
|
15
|
+
".codex"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"selector": {
|
|
20
|
+
"type": "json-hook-command",
|
|
21
|
+
"event": "SessionStart",
|
|
22
|
+
"commandNeedle": ".trellis/scripts/flower_telemetry_hook.py"
|
|
23
|
+
},
|
|
24
|
+
"content": {
|
|
25
|
+
"value": {
|
|
26
|
+
"matcher": "startup|resume|clear|compact",
|
|
27
|
+
"timeout": 5,
|
|
28
|
+
"commandResolver": "codex-flower-telemetry"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "flower-codex-telemetry-userpromptsubmit",
|
|
34
|
+
"operation": "replace",
|
|
35
|
+
"targets": [
|
|
36
|
+
{
|
|
37
|
+
"kind": "json",
|
|
38
|
+
"path": ".codex/hooks.json",
|
|
39
|
+
"missing": "create",
|
|
40
|
+
"requires": [
|
|
41
|
+
".codex"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"selector": {
|
|
46
|
+
"type": "json-hook-command",
|
|
47
|
+
"event": "UserPromptSubmit",
|
|
48
|
+
"commandNeedle": ".trellis/scripts/flower_telemetry_hook.py"
|
|
49
|
+
},
|
|
50
|
+
"content": {
|
|
51
|
+
"value": {
|
|
52
|
+
"matcher": "*",
|
|
53
|
+
"timeout": 5,
|
|
54
|
+
"commandResolver": "codex-flower-telemetry"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -21,29 +21,10 @@ import {
|
|
|
21
21
|
normalizeContentSelection,
|
|
22
22
|
selectContentSkillEntries,
|
|
23
23
|
} from "./content-selection.js";
|
|
24
|
+
import { classifyLockReachability } from "./lock-reachability.js";
|
|
24
25
|
|
|
25
26
|
const SKILL_GARDEN_MIGRATION_OWNER_ID = "flower/skill-garden";
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
* 计算 lock roots 可达的全部 Plugin。
|
|
29
|
-
*
|
|
30
|
-
* @param {import("./contracts.js").PluginLock} lock Plugin lock
|
|
31
|
-
* @returns {Set<string>} 可达 Plugin ID
|
|
32
|
-
*/
|
|
33
|
-
function reachableLockIds(lock) {
|
|
34
|
-
const byId = new Map(lock.plugins.map((plugin) => [plugin.id, plugin]));
|
|
35
|
-
const reachable = new Set();
|
|
36
|
-
const visit = (id) => {
|
|
37
|
-
if (reachable.has(id)) return;
|
|
38
|
-
reachable.add(id);
|
|
39
|
-
const plugin = byId.get(id);
|
|
40
|
-
if (!plugin) return;
|
|
41
|
-
Object.keys(plugin.dependencies).sort(compareUtf8).forEach(visit);
|
|
42
|
-
};
|
|
43
|
-
[...lock.roots].sort(compareUtf8).forEach(visit);
|
|
44
|
-
return reachable;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
28
|
/**
|
|
48
29
|
* 汇总既有 Plugin state 的实际投影平台,供无显式选择的生命周期继续复用。
|
|
49
30
|
*
|
|
@@ -489,10 +470,10 @@ export class PluginApplicationService {
|
|
|
489
470
|
}
|
|
490
471
|
}
|
|
491
472
|
if (lock) {
|
|
492
|
-
const
|
|
473
|
+
const { reachableIds } = classifyLockReachability(pluginsFile.plugins, lock);
|
|
493
474
|
for (const id of [...lockById.keys()].sort(compareUtf8)) {
|
|
494
475
|
if (options.id && id !== options.id) continue;
|
|
495
|
-
if (!
|
|
476
|
+
if (!reachableIds.has(id)) {
|
|
496
477
|
diagnostics.push({
|
|
497
478
|
code: "verify.lock-orphan",
|
|
498
479
|
path: id,
|
|
@@ -788,6 +769,7 @@ export class PluginApplicationService {
|
|
|
788
769
|
if (desiredDirectoryPaths.has(entry.path)) continue;
|
|
789
770
|
const target = path.join(this.projectRoot, ...entry.path.split("/"));
|
|
790
771
|
const currentHash = hashDirectoryIfExists(target);
|
|
772
|
+
if (currentHash === null) continue;
|
|
791
773
|
if (currentHash !== entry.hash) {
|
|
792
774
|
throw new PluginRuntimeError(`受管 Plugin 目录已被用户修改,拒绝删除:${entry.path}`, {
|
|
793
775
|
code: PLUGIN_RUNTIME_ERROR_CODES.CONTENT_CONFLICT,
|
|
@@ -803,6 +785,7 @@ export class PluginApplicationService {
|
|
|
803
785
|
if (desiredPaths.has(entry.path)) continue;
|
|
804
786
|
const target = path.join(this.projectRoot, ...entry.path.split("/"));
|
|
805
787
|
const currentHash = hashFileIfExists(target);
|
|
788
|
+
if (currentHash === null) continue;
|
|
806
789
|
if (currentHash !== entry.hash) {
|
|
807
790
|
throw new PluginRuntimeError(`受管 Plugin 文件已被用户修改,拒绝删除:${entry.path}`, {
|
|
808
791
|
code: PLUGIN_RUNTIME_ERROR_CODES.CONTENT_CONFLICT,
|
package/src/plugin/contracts.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { compareUtf8 } from "./stable-order.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 从当前直接声明出发,分类既有 lock 中的可达、孤立和缺失节点。
|
|
5
|
+
*
|
|
6
|
+
* lock roots 可能来自半清理的旧项目,不能作为活跃 Plugin 的事实入口。调用方应使用当前
|
|
7
|
+
* `plugins.json` 声明开始遍历,才能在不访问外部来源的前提下识别可冻结节点和孤立状态。
|
|
8
|
+
*
|
|
9
|
+
* @param {import("./contracts.js").ProjectPluginDeclaration[]} declarations 当前直接声明
|
|
10
|
+
* @param {import("./contracts.js").PluginLock|null} lock 既有 Plugin lock
|
|
11
|
+
* @returns {{reachableIds:Set<string>,orphanIds:Set<string>,missingIds:Set<string>}} lock 节点分类
|
|
12
|
+
*/
|
|
13
|
+
export function classifyLockReachability(declarations, lock) {
|
|
14
|
+
const plugins = lock?.plugins || [];
|
|
15
|
+
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
|
16
|
+
const visited = new Set();
|
|
17
|
+
const reachableIds = new Set();
|
|
18
|
+
const missingIds = new Set();
|
|
19
|
+
|
|
20
|
+
const visit = (id) => {
|
|
21
|
+
if (visited.has(id)) return;
|
|
22
|
+
visited.add(id);
|
|
23
|
+
const plugin = byId.get(id);
|
|
24
|
+
if (!plugin) {
|
|
25
|
+
missingIds.add(id);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
reachableIds.add(id);
|
|
29
|
+
Object.keys(plugin.dependencies).sort(compareUtf8).forEach(visit);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
declarations.map(({ id }) => id).sort(compareUtf8).forEach(visit);
|
|
33
|
+
const orphanIds = new Set(
|
|
34
|
+
plugins.map(({ id }) => id).filter((id) => !reachableIds.has(id)).sort(compareUtf8),
|
|
35
|
+
);
|
|
36
|
+
return { reachableIds, orphanIds, missingIds };
|
|
37
|
+
}
|