@qfeius/everyline-cli 0.1.0 → 0.1.3-test.1

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.
@@ -1,15 +1,506 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { chmodSync, existsSync } = require("node:fs");
4
- const { join } = require("node:path");
3
+ const { randomUUID } = require("node:crypto");
4
+ const { execFileSync } = require("node:child_process");
5
+ const {
6
+ chmodSync,
7
+ existsSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ readlinkSync,
11
+ readFileSync,
12
+ realpathSync,
13
+ symlinkSync,
14
+ unlinkSync,
15
+ } = require("node:fs");
16
+ const { homedir } = require("node:os");
17
+ const { basename, dirname, join, resolve } = require("node:path");
5
18
  const { resolvePlatformTarget } = require("./platform");
19
+ const { buildDoubaoSkillPlans, inspectDoubaoSkillRegistration, installDoubaoSkill, finishDoubaoSkill } = require("./doubao-skills");
6
20
 
7
- // npm 安装必须确认当前平台产物真实存在,防止留下“安装成功但无法运行”的包。
8
- const executable = process.platform === "win32" ? "everyline-cli.exe" : "everyline-cli";
9
- const binary = join(__dirname, "..", "bin", resolvePlatformTarget(), executable);
10
- if (!existsSync(binary)) {
11
- throw new Error(`npm 包缺少当前平台二进制: ${binary}`);
21
+ // skillNames 是同一份 npm 包向 Codex、WorkBuddy 和豆包发布的三项职责分离 Skill。
22
+ const skillNames = ["everyline-cli", "everyline-review", "everyline-review-config"];
23
+ const deprecatedSkillNames = ["everyline-shared"];
24
+ const installStateSchema = "everyline.install-state.v1";
25
+ // 安装提示与 Agent 事件共用文案;先说明可协助授权,用户要求登录后再选择 user/app。
26
+ const firstInstallMessage = "EveryLine CLI 已安装完成。目前支持合同审查,以及审查清单、规则和规则分组配置。使用前需要先完成账号授权,我现在可以为你打开授权页面或生成授权链接。";
27
+ const updateMessage = "EveryLine CLI 已更新完成。目前支持合同审查,以及审查清单、规则和规则分组配置。";
28
+ const authorizationRequiredMessage = "使用前需要先完成账号授权,我现在可以为你打开授权页面或生成授权链接。";
29
+ const authorizedMessage = "当前已存在生效授权,可直接调用cli能力;";
30
+
31
+ /**
32
+ * shouldInstallCodexSkill 判断本次 npm 生命周期是否应登记 Codex Skill。
33
+ * 入参:environment(NodeJS.ProcessEnv),当前进程环境变量。
34
+ * 返回值:boolean,仅全局安装且未显式跳过时为 true。
35
+ */
36
+ function shouldInstallCodexSkill(environment) {
37
+ return environment.npm_config_global === "true" && environment.EVERYLINE_SKIP_SKILL_INSTALL !== "1";
38
+ }
39
+
40
+ /**
41
+ * shouldInstallWorkBuddySkills 判断本次 npm 生命周期是否应登记 WorkBuddy Skills。
42
+ * 入参:environment(NodeJS.ProcessEnv),当前进程环境变量。
43
+ * 返回值:boolean,仅全局安装且未显式跳过全部 Skill 或 WorkBuddy Skill 时为 true。
44
+ */
45
+ function shouldInstallWorkBuddySkills(environment) {
46
+ return shouldInstallCodexSkill(environment) && environment.EVERYLINE_SKIP_WORKBUDDY_SKILL_INSTALL !== "1";
47
+ }
48
+
49
+ /**
50
+ * isEverylineSkillSource 根据包内路径和 npm manifest 确认旧链接属于 EveryLine,避免接管用户同名 Skill。
51
+ * 入参:source(string)为旧链接解析后的来源路径。
52
+ * 返回值:boolean,仅当前或废弃的已知 Skill 且所属包声明正确的 CLI 入口时为 true;读取权限等异常向外抛出。
53
+ */
54
+ function isEverylineSkillSource(source) {
55
+ if ((!skillNames.includes(basename(source)) && !deprecatedSkillNames.includes(basename(source))) || basename(dirname(source)) !== "skills") {
56
+ return false;
57
+ }
58
+ try {
59
+ const manifest = JSON.parse(readFileSync(join(dirname(dirname(source)), "package.json"), "utf8"));
60
+ return ["everyline-cli", "@qfeius/everyline-cli"].includes(manifest?.name) && manifest.bin?.["everyline-cli"] === "scripts/run.js";
61
+ } catch (error) {
62
+ if (error.code === "ENOENT" || error.code === "ENOTDIR" || error instanceof SyntaxError) {
63
+ return false;
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * inspectAgentSkillRegistration 只读校验 Skill 来源和目标,并生成后续登记计划。
71
+ * 入参:source(string)为包内 Skill 路径;target(string)为宿主目标路径;hostName(string)为宿主名;metadata(object)为需透传的名称与分组。
72
+ * 返回值:object,包含规范化来源、目标、预期状态;跨安装来源更新时保留原链接供失败回滚。
73
+ */
74
+ function inspectAgentSkillRegistration(source, target, hostName = "Agent", metadata = {}) {
75
+ const resolvedSource = resolve(source);
76
+ if (!existsSync(join(resolvedSource, "SKILL.md"))) {
77
+ throw new Error(`npm 包缺少 EveryLine Skill: ${resolvedSource}`);
78
+ }
79
+
80
+ // 先检查目标本身而非其指向内容,确保悬空链接也不会被静默覆盖。
81
+ let targetState;
82
+ try {
83
+ targetState = lstatSync(target);
84
+ } catch (error) {
85
+ if (error.code !== "ENOENT") {
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ if (targetState) {
91
+ if (targetState.isSymbolicLink()) {
92
+ const previousLink = readlinkSync(target);
93
+ let previousSource = resolve(dirname(target), previousLink);
94
+ try {
95
+ previousSource = realpathSync(target);
96
+ } catch (error) {
97
+ // 旧包可能已移除 Skill 文件;只有仍可验证的 manifest 才允许迁移悬空链接。
98
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") {
99
+ throw error;
100
+ }
101
+ }
102
+ // realpath 同时兼容 POSIX 符号链接和 Windows 目录联接的路径表示差异。
103
+ if (previousSource === realpathSync(resolvedSource)) {
104
+ return { ...metadata, source: resolvedSource, target, hostName, status: "existing" };
105
+ }
106
+ if (basename(previousSource) === basename(resolvedSource) && isEverylineSkillSource(previousSource)) {
107
+ return { ...metadata, source: resolvedSource, target, hostName, status: "updated", previousLink };
108
+ }
109
+ }
110
+ throw new Error(`${hostName} Skill 目标已存在且未确认属于 EveryLine;请将需保留的内容移到 Skill 扫描目录之外再重试,不要仅在原目录内添加 .bak 后缀: ${target}`);
111
+ }
112
+
113
+ return { ...metadata, source: resolvedSource, target, hostName, status: "created" };
114
+ }
115
+
116
+ /**
117
+ * registerAgentSkillPlans 预检全部宿主,登记当前技能并迁出旧入口,最终状态提交失败时撤回本轮变更。
118
+ * 入参:plans(Array<object>)为登记或迁出计划;platform(string)为 Node 平台名;beforeRegister(Function,可选)在预检后写入首次安装意图;commit(Function,可选)接收预检结果,作为事务的最终状态提交。
119
+ * 返回值:Array<object>,保留计划元数据及 created/existing/updated/removed/skipped 状态。
120
+ */
121
+ function registerAgentSkillPlans(plans, platform = process.platform, beforeRegister, commit) {
122
+ // 全量预检发生在任何写入前,常见的同名目录冲突不会留下半套登记结果。
123
+ const inspected = plans.map((plan) => {
124
+ if (plan.installMode === "deprecated-link") return plan;
125
+ if (plan.installMode === "directory") return inspectDoubaoSkillRegistration(plan);
126
+ return inspectAgentSkillRegistration(plan.source, plan.target, plan.hostName, plan);
127
+ });
128
+ // 首次安装意图必须先可靠落盘;预检冲突不会创建门禁,登记中断也不会丢失待授权状态。
129
+ beforeRegister?.(inspected);
130
+ const changed = [];
131
+ try {
132
+ for (const registration of inspected) {
133
+ if (registration.status === "existing" || registration.status === "skipped") {
134
+ continue;
135
+ }
136
+ if (registration.installMode === "directory") {
137
+ changed.push(registration);
138
+ installDoubaoSkill(registration);
139
+ continue;
140
+ }
141
+ mkdirSync(dirname(registration.target), { recursive: true });
142
+ if (registration.status === "updated" || registration.status === "removed") {
143
+ // 写入前再次核对旧链接,避免预检之后出现的用户目录或其他来源被覆盖。
144
+ if (!lstatSync(registration.target).isSymbolicLink() || readlinkSync(registration.target) !== registration.previousLink) {
145
+ throw new Error(`Skill 目标在安装期间发生变化,请重试: ${registration.target}`);
146
+ }
147
+ unlinkSync(registration.target);
148
+ // 在新链接创建前记入回滚列表,即使 symlink 失败也能恢复原链接。
149
+ changed.push(registration);
150
+ if (registration.status === "removed") continue;
151
+ }
152
+ symlinkSync(registration.source, registration.target, platform === "win32" ? "junction" : "dir");
153
+ if (registration.status === "created") {
154
+ changed.push(registration);
155
+ }
156
+ }
157
+ for (const registration of changed) {
158
+ if (registration.installMode === "directory") finishDoubaoSkill(registration);
159
+ }
160
+ // 旧目录备份与链接撤销信息仍在;状态提交是最后一个失败点,失败后统一恢复各宿主。
161
+ commit?.(inspected);
162
+ return inspected;
163
+ } catch (error) {
164
+ // 只撤回本轮仍指向新来源的链接;旧目标保留在内存中,不在扫描目录里创建 .bak 副本。
165
+ const rollbackErrors = [];
166
+ for (const registration of changed.reverse()) {
167
+ try {
168
+ if (registration.installMode === "directory") {
169
+ finishDoubaoSkill(registration, true);
170
+ continue;
171
+ }
172
+ let targetState;
173
+ try {
174
+ targetState = lstatSync(registration.target);
175
+ } catch (error) {
176
+ if (error.code !== "ENOENT") {
177
+ throw error;
178
+ }
179
+ }
180
+ if (targetState) {
181
+ // 旧链接迁出后出现的新目标由其他进程拥有,回滚不覆盖它。
182
+ if (registration.status === "removed") continue;
183
+ if (!targetState.isSymbolicLink() || realpathSync(registration.target) !== realpathSync(registration.source)) {
184
+ continue;
185
+ }
186
+ unlinkSync(registration.target);
187
+ }
188
+ if (registration.status === "updated" || registration.status === "removed") {
189
+ symlinkSync(registration.previousLink, registration.target, platform === "win32" ? "junction" : "dir");
190
+ }
191
+ } catch (rollbackError) {
192
+ // 继续恢复其他宿主,同时保留未恢复目录的备份位置供用户处理。
193
+ rollbackErrors.push(rollbackError.message);
194
+ }
195
+ }
196
+ if (rollbackErrors.length > 0) {
197
+ throw new Error(`${error.message};部分 Skill 回滚失败: ${rollbackErrors.join(";")}`, { cause: error });
198
+ }
199
+ throw error;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * registerAgentSkill 将 npm 包内单项 Skill 以目录链接登记到指定 Agent 宿主目录。
205
+ * 入参:source(string)为包内 Skill 绝对路径;target(string)为宿主 Skill 目标路径;platform(string)为 Node 平台名;hostName(string)为错误提示中的宿主名。
206
+ * 返回值:"created" | "existing" | "updated",分别表示新建、已存在同源链接或跨安装来源更新。
207
+ */
208
+ function registerAgentSkill(source, target, platform = process.platform, hostName = "Agent") {
209
+ const [registration] = registerAgentSkillPlans([{ source, target, hostName }], platform);
210
+ return registration.status;
211
+ }
212
+
213
+ /**
214
+ * buildSkillSetPlans 构造一个宿主下三项职责分离 Skill 的无副作用登记计划。
215
+ * 入参:packageRoot(string)为 npm 包根目录;skillRoot(string)为宿主 Skill 根目录;hostName(string)为宿主名;hostKey(string)为返回结果分组键。
216
+ * 返回值:Array<object>,每项包含来源、目标、Skill 名称和宿主分组。
217
+ */
218
+ function buildSkillSetPlans(packageRoot, skillRoot, hostName, hostKey = "") {
219
+ return skillNames.map((name) => ({
220
+ name,
221
+ hostKey,
222
+ hostName,
223
+ source: join(packageRoot, "skills", name),
224
+ target: join(skillRoot, name),
225
+ }));
226
+ }
227
+
228
+ /**
229
+ * registerCodexSkill 保留既有单项登记接口,兼容安装器调用方和发布测试。
230
+ * 入参:source(string)为包内 Skill 绝对路径;target(string)为 Codex Skill 目标路径;platform(string)为 Node 平台名。
231
+ * 返回值:"created" | "existing" | "updated",含义与 registerAgentSkill 一致。
232
+ */
233
+ function registerCodexSkill(source, target, platform = process.platform) {
234
+ return registerAgentSkill(source, target, platform, "Codex");
235
+ }
236
+
237
+ /**
238
+ * registerSkillSet 将职责分离的三项 EveryLine Skill 登记到一个宿主根目录。
239
+ * 入参:packageRoot(string)为 npm 包根目录;skillRoot(string)为宿主 Skill 根目录;platform(string)为 Node 平台名;hostName(string)为宿主名。
240
+ * 返回值:Array<object>,每项包含 name、target 和 created/existing/updated 状态。
241
+ */
242
+ function registerSkillSet(packageRoot, skillRoot, platform, hostName) {
243
+ return registerAgentSkillPlans(buildSkillSetPlans(packageRoot, skillRoot, hostName), platform)
244
+ .map(({ name, target, status }) => ({ name, target, status }));
12
245
  }
13
- if (process.platform !== "win32") {
14
- chmodSync(binary, 0o755);
246
+
247
+ /**
248
+ * inspectDeprecatedSkillRegistrations 只读识别本包同源或可验证旧包中的废弃 Skill 链接。
249
+ * 入参:packageRoot(string)为 npm 包根目录;skillRoot(string)为宿主 Skill 根目录。
250
+ * 返回值:Array<object>,包含 name、target 和 previousLink,供旧安装识别及清理前核对;不包含用户目录或未确认来源。
251
+ */
252
+ function inspectDeprecatedSkillRegistrations(packageRoot, skillRoot) {
253
+ const registrations = [];
254
+ for (const name of deprecatedSkillNames) {
255
+ const target = join(skillRoot, name);
256
+ let targetState;
257
+ try {
258
+ targetState = lstatSync(target);
259
+ } catch (error) {
260
+ if (error.code === "ENOENT") {
261
+ continue;
262
+ }
263
+ throw error;
264
+ }
265
+ if (!targetState.isSymbolicLink()) {
266
+ continue;
267
+ }
268
+
269
+ // 同源升级允许源目录已删除;跨 Node 目录必须额外验证旧包 manifest,不接管未知来源。
270
+ const previousLink = readlinkSync(target);
271
+ const linkedSource = resolve(dirname(target), previousLink);
272
+ const expectedSource = resolve(packageRoot, "skills", name);
273
+ if (linkedSource !== expectedSource && (basename(linkedSource) !== name || !isEverylineSkillSource(linkedSource))) {
274
+ continue;
275
+ }
276
+ registrations.push({ name, target, previousLink });
277
+ }
278
+ return registrations;
279
+ }
280
+
281
+ /**
282
+ * removeDeprecatedSkillRegistrations 清理已确认属于 EveryLine 的废弃链接,包括其他 Node/npm 前缀。
283
+ * 入参:packageRoot(string)为本次包根目录;skillRoot(string)为宿主 Skill 根目录。
284
+ * 返回值:string[],为已移除的旧 Skill 名称;目标发生变化时抛出错误并保留新目标。
285
+ */
286
+ function removeDeprecatedSkillRegistrations(packageRoot, skillRoot) {
287
+ const registrations = inspectDeprecatedSkillRegistrations(packageRoot, skillRoot);
288
+ for (const { target, previousLink } of registrations) {
289
+ if (!lstatSync(target).isSymbolicLink() || readlinkSync(target) !== previousLink) {
290
+ throw new Error(`Skill 目标在安装期间发生变化,请重试: ${target}`);
291
+ }
292
+ unlinkSync(target);
293
+ }
294
+ return registrations.map(({ name }) => name);
295
+ }
296
+
297
+ /**
298
+ * resolveInstallStatePath 解析安装器与原生 CLI 共享的首次安装状态路径。
299
+ * 入参:environment(NodeJS.ProcessEnv)为环境变量;userHome(string)为用户目录。
300
+ * 返回值:string,为 install-state.json 的绝对路径。
301
+ */
302
+ function resolveInstallStatePath(environment, userHome) {
303
+ const configured = String(environment.EVERYLINE_CONFIG_DIR || "").trim();
304
+ return join(configured ? resolve(userHome, configured) : join(userHome, ".everyline-cli"), "install-state.json");
305
+ }
306
+
307
+ /**
308
+ * loadInstallState 读取并校验已有首次安装状态,文件缺失时返回 null。
309
+ * 入参:statePath(string)为状态文件路径。
310
+ * 返回值:object|null,为已校验状态或文件缺失。
311
+ */
312
+ function loadInstallState(statePath) {
313
+ if (!existsSync(statePath)) {
314
+ return null;
315
+ }
316
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
317
+ if (state.schema !== installStateSchema || typeof state.eventId !== "string" || state.eventId.length === 0) {
318
+ throw new Error(`EveryLine 首次安装状态格式错误: ${statePath}`);
319
+ }
320
+ return state;
321
+ }
322
+
323
+ /**
324
+ * ensureFirstInstallState 通过包内原生 CLI 登记安装,和授权完成流程共用文件锁,避免覆盖最新状态。
325
+ * 入参:packageRoot(string)为包根;environment(NodeJS.ProcessEnv)为环境;userHome(string)为用户目录;platform(string)为平台;registrations(Array<object>)为 Skill 登记结果;hadDeprecatedRegistrations(boolean,可选)为已确认的旧链接;nativeBinary(string,可选)为本次安装的平台二进制。
326
+ * 返回值:object,包含状态路径和原生 CLI 在锁内计算的安装及更新状态;登记失败时抛出 Error。
327
+ */
328
+ function ensureFirstInstallState(packageRoot, environment, userHome, platform, registrations, hadDeprecatedRegistrations = false, nativeBinary) {
329
+ const statePath = resolveInstallStatePath(environment, userHome);
330
+ const packageData = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
331
+ const installedVersion = String(packageData.version || "");
332
+ // existing 来自写入前的同源预检;旧链接的清理结果同样证明此前已安装,单项 created 不代表首次安装。
333
+ const previouslyInstalled = hadDeprecatedRegistrations || registrations.some((registration) => registration.status === "existing" || registration.status === "updated");
334
+ if (!existsSync(statePath) && !previouslyInstalled && !registrations.some((registration) => registration.status === "created")) {
335
+ return { path: statePath, firstInstall: false, authorizationRequired: false, nextAction: "", updated: false };
336
+ }
337
+ const binary = nativeBinary || join(packageRoot, "bin", resolvePlatformTarget(platform, process.arch), platform === "win32" ? "everyline-cli.exe" : "everyline-cli");
338
+ // 直接运行包内二进制,不依赖 PATH 中可能仍为旧版的 CLI;绝对配置目录保证父子进程写入同一文件。
339
+ const output = execFileSync(binary, [
340
+ "_record-install", "--installed-version", installedVersion, "--event-id", randomUUID(),
341
+ `--previously-installed=${previouslyInstalled}`,
342
+ ], {
343
+ encoding: "utf8",
344
+ env: { ...process.env, ...environment, EVERYLINE_CONFIG_DIR: dirname(statePath) },
345
+ });
346
+ return { path: statePath, ...JSON.parse(output) };
15
347
  }
348
+
349
+ /**
350
+ * installPackage 保留首次安装意图,将三宿主同步、废弃入口迁出和最终安装状态提交放在同一恢复流程中。
351
+ * 入参:options(object,可选),可注入 packageRoot、platform、architecture、environment 和 userHome 供安装与测试使用。
352
+ * 返回值:object,包含 binary、Skill 登记结果及机器可读的首次安装、授权和更新状态。
353
+ */
354
+ function installPackage(options = {}) {
355
+ const packageRoot = options.packageRoot || join(__dirname, "..");
356
+ const platform = options.platform || process.platform;
357
+ const architecture = options.architecture || process.arch;
358
+ const environment = options.environment || process.env;
359
+ const userHome = options.userHome || homedir();
360
+ const executable = platform === "win32" ? "everyline-cli.exe" : "everyline-cli";
361
+ const binary = join(packageRoot, "bin", resolvePlatformTarget(platform, architecture), executable);
362
+
363
+ // npm 安装必须确认当前平台产物真实存在,防止留下“安装成功但 CLI 不可运行”的包。
364
+ if (!existsSync(binary)) {
365
+ throw new Error(`npm 包缺少当前平台二进制: ${binary}`);
366
+ }
367
+ if (platform !== "win32") {
368
+ chmodSync(binary, 0o755);
369
+ }
370
+
371
+ if (!shouldInstallCodexSkill(environment)) {
372
+ return {
373
+ binary,
374
+ skillTarget: "",
375
+ skillStatus: "",
376
+ skills: { codex: [], workBuddy: [], doubao: [] },
377
+ doubaoSkillReloadRequired: false,
378
+ firstInstall: false,
379
+ authorizationRequired: false,
380
+ nextAction: "",
381
+ updated: false,
382
+ };
383
+ }
384
+
385
+ const codexSkillRoot = environment.EVERYLINE_CODEX_SKILLS_DIR || join(userHome, ".agents", "skills");
386
+ const plans = buildSkillSetPlans(packageRoot, codexSkillRoot, "Codex", "codex");
387
+ const installedSkillRoots = [codexSkillRoot];
388
+ if (shouldInstallWorkBuddySkills(environment)) {
389
+ const workBuddySkillRoot = environment.EVERYLINE_WORKBUDDY_SKILLS_DIR || join(userHome, ".workbuddy", "skills");
390
+ plans.push(...buildSkillSetPlans(
391
+ packageRoot,
392
+ workBuddySkillRoot,
393
+ "WorkBuddy",
394
+ "workBuddy",
395
+ ));
396
+ installedSkillRoots.push(workBuddySkillRoot);
397
+ }
398
+ plans.push(...buildDoubaoSkillPlans(packageRoot, skillNames, environment, platform, userHome, deprecatedSkillNames));
399
+ // 已校验的旧链接进入同一撤销列表,避免最终提交失败后丢失原来的公共授权入口。
400
+ for (const skillRoot of installedSkillRoots) {
401
+ plans.push(...inspectDeprecatedSkillRegistrations(packageRoot, skillRoot).map((registration) => ({
402
+ ...registration, deprecated: true, installMode: "deprecated-link", status: "removed",
403
+ })));
404
+ }
405
+ let hadDeprecatedRegistrations = false;
406
+ let installState;
407
+ const registrations = registerAgentSkillPlans(plans, platform, (inspected) => {
408
+ // 仅已确认需要迁出的目录或链接证明旧安装存在,跳过的未知目录不影响首次授权判定。
409
+ hadDeprecatedRegistrations = inspected.some((registration) => registration.deprecated && registration.status === "removed");
410
+ const existingState = loadInstallState(resolveInstallStatePath(environment, userHome));
411
+ if (!existingState && !hadDeprecatedRegistrations && inspected.filter((registration) => !registration.deprecated).every(({ status }) => status === "created")) {
412
+ // 只预提交全新安装的待授权意图;失败或进程中断后,重试仍读取该门禁,旧安装的版本更新留到登记成功后。
413
+ ensureFirstInstallState(packageRoot, environment, userHome, platform, inspected, false, binary);
414
+ }
415
+ }, (inspected) => {
416
+ installState = ensureFirstInstallState(packageRoot, environment, userHome, platform, inspected, hadDeprecatedRegistrations, binary);
417
+ });
418
+ const hostSkills = (hostKey) => registrations
419
+ .filter((registration) => registration.hostKey === hostKey && !registration.deprecated)
420
+ .map(({ name, target, status, backupPath }) => ({ name, target, status, ...(backupPath ? { backupPath } : {}) }));
421
+ const codexSkills = hostSkills("codex");
422
+ const workBuddySkills = hostSkills("workBuddy");
423
+ const doubaoSkills = hostSkills("doubao");
424
+ return {
425
+ binary,
426
+ skillTarget: codexSkills[0].target,
427
+ skillStatus: codexSkills[0].status,
428
+ skills: { codex: codexSkills, workBuddy: workBuddySkills, doubao: doubaoSkills },
429
+ doubaoSkillReloadRequired: registrations.some((skill) => skill.hostKey === "doubao" && skill.status !== "existing" && skill.status !== "skipped"),
430
+ installStatePath: installState.path,
431
+ firstInstall: installState.firstInstall === true,
432
+ authorizationRequired: installState.authorizationRequired === true,
433
+ nextAction: installState.nextAction || "",
434
+ updated: installState.updated === true,
435
+ };
436
+ }
437
+
438
+ /**
439
+ * formatInstallOutput 生成全局安装成功后的用户可见输出和机器可读首次安装或更新事件。
440
+ * 入参:result(object)为 installPackage 返回的安装结果。
441
+ * 返回值:string,为可直接写入 stdout 的完整文本;未登记 Skill 时为空字符串。
442
+ */
443
+ function formatInstallOutput(result) {
444
+ if (!result.skillTarget) {
445
+ return "";
446
+ }
447
+ const lines = [result.updated
448
+ ? updateMessage
449
+ : (result.authorizationRequired ? firstInstallMessage : "EveryLine CLI 与 Agent Skills 安装完成")];
450
+ for (const [hostName, skills] of Object.entries(result.skills)) {
451
+ for (const skill of skills) {
452
+ lines.push(`${hostName}: ${skill.target} (${skill.status})`);
453
+ }
454
+ }
455
+ if (result.doubaoSkillReloadRequired) {
456
+ lines.push("豆包本地 Skill 已同步;请重新读取三项 SKILL.md,或新建任务加载新版。历史对话不会自动重载。");
457
+ lines.push(JSON.stringify({
458
+ schema: "everyline.skill-event.v1", event: "skills_updated", host: "doubao",
459
+ reloadRequired: true, nextAction: "reload_skills", skills: result.skills.doubao,
460
+ }));
461
+ }
462
+ if (result.updated) {
463
+ lines.push(JSON.stringify({
464
+ schema: "everyline.skill-event.v1",
465
+ event: "updated",
466
+ authCheckRequired: true,
467
+ nextAction: "auth_status",
468
+ recommendedSkill: "everyline-cli",
469
+ message: updateMessage,
470
+ authorizationRequiredMessage,
471
+ authorizedMessage,
472
+ }));
473
+ } else if (result.authorizationRequired) {
474
+ lines.push(JSON.stringify({
475
+ schema: "everyline.skill-event.v1",
476
+ event: "first_install",
477
+ authorizationRequired: true,
478
+ nextAction: "authorize",
479
+ recommendedSkill: "everyline-cli",
480
+ message: firstInstallMessage,
481
+ }));
482
+ }
483
+ return `${lines.join("\n")}\n`;
484
+ }
485
+
486
+ if (require.main === module) {
487
+ const result = installPackage();
488
+ const output = formatInstallOutput(result);
489
+ if (output) {
490
+ process.stdout.write(output);
491
+ }
492
+ }
493
+
494
+ module.exports = {
495
+ formatInstallOutput,
496
+ installPackage,
497
+ ensureFirstInstallState,
498
+ loadInstallState,
499
+ registerAgentSkill,
500
+ registerCodexSkill,
501
+ registerSkillSet,
502
+ removeDeprecatedSkillRegistrations,
503
+ shouldInstallCodexSkill,
504
+ shouldInstallWorkBuddySkills,
505
+ skillNames,
506
+ };
package/scripts/run.js CHANGED
@@ -26,7 +26,10 @@ function resolveBinary() {
26
26
  }
27
27
 
28
28
  try {
29
- const result = spawnSync(resolveBinary(), process.argv.slice(2), { stdio: "inherit" });
29
+ const result = spawnSync(resolveBinary(), process.argv.slice(2), {
30
+ stdio: "inherit",
31
+ env: { ...process.env, EVERYLINE_CLI_WRAPPER: "1" },
32
+ });
30
33
  if (result.error) {
31
34
  throw result.error;
32
35
  }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ const { readFileSync, writeFileSync } = require("node:fs");
3
+ const { join } = require("node:path");
4
+ const { normalizePackageVersion } = require("./package-version");
5
+
6
+ /**
7
+ * syncSkillVersions 将安装包版本写入三项 Skill 的 metadata。
8
+ * 入参:root string 为包根目录。
9
+ * 返回值:无;版本或 frontmatter 无效时抛出错误,阻止发布。
10
+ */
11
+ function syncSkillVersions(root) {
12
+ const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
13
+ if (version === "0.0.0-development" || normalizePackageVersion(version) !== version) {
14
+ throw new Error(`无效发布版本: ${version}`);
15
+ }
16
+ // 先验证全部文件再写入,避免其中一项格式异常造成部分同步。
17
+ const changes = ["everyline-cli", "everyline-review", "everyline-review-config"].map(name => {
18
+ const path = join(root, "skills", name, "SKILL.md");
19
+ const source = readFileSync(path, "utf8");
20
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
21
+ if (!match || !/^metadata:$/m.test(match[1])) throw new Error(`缺少 metadata: ${name}`);
22
+ const header = match[1].replace(/^ version:.*\n?/m, "").replace(/^metadata:$/m, `metadata:\n version: "${version}"`);
23
+ return { path, source, content: source.replace(match[0], `---\n${header}\n---`) };
24
+ });
25
+ for (const { path, source, content } of changes) {
26
+ if (content !== source) writeFileSync(path, content);
27
+ }
28
+ }
29
+ if (require.main === module) syncSkillVersions(join(__dirname, ".."));
30
+ module.exports = { syncSkillVersions };