@keyou007/agent-bot 0.1.14-alpha.1 → 0.1.14-alpha.3

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 (80) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +48 -15
  3. package/README.zh.md +48 -15
  4. package/config.example.yaml +1 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +21 -13
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/Initializer.js +35 -0
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/SelfUpdateRunner.js +552 -0
  12. package/dist/cli/SelfUpdateRunner.js.map +1 -0
  13. package/dist/cli/SelfUpdater.js +431 -0
  14. package/dist/cli/SelfUpdater.js.map +1 -0
  15. package/dist/cli/autostartOutput.js +63 -0
  16. package/dist/cli/autostartOutput.js.map +1 -0
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +94 -18
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +14 -11
  21. package/dist/cli/taskGroupOptions.js.map +1 -1
  22. package/dist/cli/taskListOutput.js +3 -0
  23. package/dist/cli/taskListOutput.js.map +1 -1
  24. package/dist/cli/taskTarget.js +99 -0
  25. package/dist/cli/taskTarget.js.map +1 -0
  26. package/dist/cli.js +661 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +8 -2
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +16 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/feishu/CardRenderer.js +266 -38
  33. package/dist/feishu/CardRenderer.js.map +1 -1
  34. package/dist/feishu/FeishuMarkdown.js +25 -1
  35. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  36. package/dist/feishu/FeishuMessageClient.js +16 -0
  37. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  38. package/dist/feishu/FeishuTurnPresenter.js +42 -6
  39. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  40. package/dist/index.js +136 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/presentation/OutboundRouter.js +7 -0
  43. package/dist/presentation/OutboundRouter.js.map +1 -1
  44. package/dist/presentation/TurnStateReducer.js +21 -7
  45. package/dist/presentation/TurnStateReducer.js.map +1 -1
  46. package/dist/proxy/ProxySessionController.js +537 -13
  47. package/dist/proxy/ProxySessionController.js.map +1 -1
  48. package/dist/state/StateStore.js +53 -0
  49. package/dist/state/StateStore.js.map +1 -1
  50. package/docs/technical-reference.md +17 -7
  51. package/docs/technical-reference.zh.md +17 -7
  52. package/npm-shrinkwrap.json +2 -2
  53. package/package.json +2 -2
  54. package/skills/agent-bot/SKILL.md +109 -188
  55. package/src/cli/AutostartManager.ts +579 -0
  56. package/src/cli/FeishuAppConfiguration.ts +26 -7
  57. package/src/cli/Initializer.ts +57 -0
  58. package/src/cli/SelfUpdateRunner.ts +817 -0
  59. package/src/cli/SelfUpdater.ts +688 -0
  60. package/src/cli/autostartOutput.ts +75 -0
  61. package/src/cli/controlProtocol.ts +119 -1
  62. package/src/cli/help.ts +94 -18
  63. package/src/cli/taskGroupOptions.ts +27 -17
  64. package/src/cli/taskListOutput.ts +10 -0
  65. package/src/cli/taskTarget.ts +168 -0
  66. package/src/cli.ts +809 -65
  67. package/src/codex/CodexRuntime.ts +8 -2
  68. package/src/commands/CommandRouter.ts +14 -1
  69. package/src/commands/commandTypes.ts +2 -0
  70. package/src/feishu/CardRenderer.ts +313 -35
  71. package/src/feishu/FeishuMarkdown.ts +27 -1
  72. package/src/feishu/FeishuMessageClient.ts +33 -0
  73. package/src/feishu/FeishuTurnPresenter.ts +43 -6
  74. package/src/feishu/types.ts +1 -0
  75. package/src/index.ts +174 -2
  76. package/src/presentation/OutboundRouter.ts +7 -0
  77. package/src/presentation/TurnStateReducer.ts +21 -7
  78. package/src/proxy/ProxySessionController.ts +661 -16
  79. package/src/runtime/types.ts +1 -0
  80. package/src/state/StateStore.ts +56 -0
@@ -0,0 +1,688 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { agentBotHome } from "../config/paths.js";
7
+ import { cliText } from "./i18n.js";
8
+ import { readPackageVersion } from "./packageVersion.js";
9
+ import {
10
+ runNpmCommand,
11
+ SELF_UPDATE_PLAN_VERSION,
12
+ validateInstalledPackage,
13
+ type SelfUpdatePlan,
14
+ } from "./SelfUpdateRunner.js";
15
+
16
+ export const AGENT_BOT_PACKAGE_NAME = "@keyou007/agent-bot";
17
+
18
+ export type SelfUpdateChannel = "alpha" | "latest";
19
+
20
+ export interface SelfUpdateOptions {
21
+ channel?: SelfUpdateChannel;
22
+ version?: string;
23
+ allowDowngrade?: boolean;
24
+ }
25
+
26
+ export interface PendingSelfUpdate {
27
+ planPath: string;
28
+ plan: SelfUpdatePlan;
29
+ }
30
+
31
+ export type PrepareSelfUpdateResult =
32
+ | {
33
+ status: "current";
34
+ currentVersion: string;
35
+ targetVersion: string;
36
+ channel: SelfUpdateChannel;
37
+ }
38
+ | {
39
+ status: "prepared";
40
+ currentVersion: string;
41
+ targetVersion: string;
42
+ channel: SelfUpdateChannel;
43
+ planPath: string;
44
+ runnerPath: string;
45
+ logPath: string;
46
+ resultPath: string;
47
+ };
48
+
49
+ interface CommandResult {
50
+ status: number;
51
+ stdout: string;
52
+ stderr: string;
53
+ error?: Error;
54
+ }
55
+
56
+ export interface SelfUpdaterDependencies {
57
+ packageRoot: string;
58
+ home: string;
59
+ runnerEntry: string;
60
+ runNpm(args: string[], cwd: string): CommandResult;
61
+ validatePackage(
62
+ packageRoot: string,
63
+ packageName: string,
64
+ version: string,
65
+ ): void;
66
+ now(): Date;
67
+ randomId(): string;
68
+ }
69
+
70
+ export function parseSelfUpdateOptions(input: string[]): SelfUpdateOptions & {
71
+ json: boolean;
72
+ taskReference?: string;
73
+ } {
74
+ const options: SelfUpdateOptions & { json: boolean; taskReference?: string } =
75
+ { json: false };
76
+ for (let index = 0; index < input.length; index += 1) {
77
+ const value = input[index]!;
78
+ if (value === "--json") {
79
+ options.json = true;
80
+ continue;
81
+ }
82
+ if (value === "--alpha") {
83
+ if (options.channel && options.channel !== "alpha")
84
+ throw conflictingChannelError();
85
+ options.channel = "alpha";
86
+ continue;
87
+ }
88
+ if (value === "--stable") {
89
+ if (options.channel && options.channel !== "latest")
90
+ throw conflictingChannelError();
91
+ options.channel = "latest";
92
+ continue;
93
+ }
94
+ if (value === "--allow-downgrade") {
95
+ options.allowDowngrade = true;
96
+ continue;
97
+ }
98
+ if (value === "--version" || value === "--task") {
99
+ const next = input[index + 1]?.trim();
100
+ if (!next || next.startsWith("--"))
101
+ throw new Error(
102
+ cliText(`${value} requires a value.`, `${value} 需要提供值。`),
103
+ );
104
+ if (value === "--version") options.version = next;
105
+ else options.taskReference = next;
106
+ index += 1;
107
+ continue;
108
+ }
109
+ throw new Error(
110
+ cliText(
111
+ `Unsupported update option: ${value}`,
112
+ `不支持的 update 选项:${value}`,
113
+ ),
114
+ );
115
+ }
116
+ if (options.version && options.channel)
117
+ throw new Error(
118
+ cliText(
119
+ "--version cannot be combined with --alpha or --stable.",
120
+ "--version 不能与 --alpha 或 --stable 同时使用。",
121
+ ),
122
+ );
123
+ return options;
124
+ }
125
+
126
+ export async function prepareSelfUpdate(
127
+ options: SelfUpdateOptions = {},
128
+ dependencies: SelfUpdaterDependencies = defaultDependencies(),
129
+ ): Promise<PrepareSelfUpdateResult> {
130
+ const currentVersion = readVersionFromRoot(dependencies.packageRoot);
131
+ const installation = requireNpmSelfUpdateInstallation(
132
+ dependencies.packageRoot,
133
+ dependencies.runNpm,
134
+ );
135
+
136
+ const channel = options.channel ?? defaultUpdateChannel(currentVersion);
137
+ const targetVersion = options.version
138
+ ? normalizeVersion(options.version)
139
+ : resolvePublishedVersion(
140
+ channel,
141
+ dependencies.runNpm,
142
+ dependencies.packageRoot,
143
+ );
144
+ const comparison = compareSemver(targetVersion, currentVersion);
145
+ if (comparison === 0)
146
+ return { status: "current", currentVersion, targetVersion, channel };
147
+ if (comparison < 0 && !options.allowDowngrade)
148
+ throw new Error(
149
+ cliText(
150
+ `Refusing to downgrade Agent Bot from ${currentVersion} to ${targetVersion}. Pass --allow-downgrade to confirm.`,
151
+ `拒绝将 Agent Bot 从 ${currentVersion} 降级到 ${targetVersion}。如需确认降级,请传入 --allow-downgrade。`,
152
+ ),
153
+ );
154
+
155
+ const lockToken = dependencies.randomId();
156
+ const lockPath = acquireUpdateLock(
157
+ installation.packageRoot,
158
+ lockToken,
159
+ dependencies.now(),
160
+ );
161
+
162
+ const operationName = `${timestamp(dependencies.now())}-${dependencies.randomId()}`;
163
+ const operationRoot = path.join(dependencies.home, "updates", operationName);
164
+ const downloadRoot = path.join(operationRoot, "packages");
165
+ const candidateInstallRoot = path.join(operationRoot, "candidate");
166
+ const backupPackageRoot = path.join(operationRoot, "backup", "package");
167
+ const pendingMarkerPath = path.join(
168
+ dependencies.home,
169
+ "updates",
170
+ "pending-update.json",
171
+ );
172
+ try {
173
+ fs.mkdirSync(downloadRoot, { recursive: true });
174
+ fs.mkdirSync(candidateInstallRoot, { recursive: true });
175
+ fs.mkdirSync(path.dirname(backupPackageRoot), { recursive: true });
176
+
177
+ const candidateTarball = packPackage(
178
+ `${AGENT_BOT_PACKAGE_NAME}@${targetVersion}`,
179
+ downloadRoot,
180
+ dependencies.packageRoot,
181
+ dependencies.runNpm,
182
+ );
183
+ runRequiredNpm(
184
+ [
185
+ "install",
186
+ "--prefix",
187
+ candidateInstallRoot,
188
+ "--no-save",
189
+ "--no-package-lock",
190
+ "--no-audit",
191
+ "--no-fund",
192
+ candidateTarball,
193
+ ],
194
+ dependencies.packageRoot,
195
+ dependencies.runNpm,
196
+ );
197
+ const candidatePackageRoot = path.join(
198
+ candidateInstallRoot,
199
+ "node_modules",
200
+ ...AGENT_BOT_PACKAGE_NAME.split("/"),
201
+ );
202
+ dependencies.validatePackage(
203
+ candidatePackageRoot,
204
+ AGENT_BOT_PACKAGE_NAME,
205
+ targetVersion,
206
+ );
207
+
208
+ fs.cpSync(dependencies.packageRoot, backupPackageRoot, {
209
+ recursive: true,
210
+ dereference: false,
211
+ errorOnExist: true,
212
+ });
213
+ dependencies.validatePackage(
214
+ backupPackageRoot,
215
+ AGENT_BOT_PACKAGE_NAME,
216
+ currentVersion,
217
+ );
218
+ const backupTarball = packPackage(
219
+ dependencies.packageRoot,
220
+ downloadRoot,
221
+ dependencies.packageRoot,
222
+ dependencies.runNpm,
223
+ ["--ignore-scripts"],
224
+ );
225
+
226
+ const runnerPath = path.join(operationRoot, "update-runner.mjs");
227
+ fs.copyFileSync(dependencies.runnerEntry, runnerPath);
228
+ const planPath = path.join(operationRoot, "plan.json");
229
+ const resultPath = path.join(operationRoot, "result.json");
230
+ const logPath = path.join(operationRoot, "update.log");
231
+ const plan: SelfUpdatePlan = {
232
+ schemaVersion: SELF_UPDATE_PLAN_VERSION,
233
+ packageName: AGENT_BOT_PACKAGE_NAME,
234
+ fromVersion: currentVersion,
235
+ toVersion: targetVersion,
236
+ packageRoot: installation.packageRoot,
237
+ candidateTarball,
238
+ backupTarball,
239
+ backupPackageRoot,
240
+ lockPath,
241
+ lockToken,
242
+ pendingMarkerPath,
243
+ databasePath: "pending",
244
+ controlEndpoint: "pending",
245
+ workingDirectory: process.cwd(),
246
+ restartService: false,
247
+ resultPath,
248
+ logPath,
249
+ };
250
+ fs.writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
251
+ fs.writeFileSync(
252
+ pendingMarkerPath,
253
+ `${JSON.stringify(
254
+ {
255
+ token: lockToken,
256
+ phase: "prepared",
257
+ planPath,
258
+ createdAt: dependencies.now().toISOString(),
259
+ },
260
+ null,
261
+ 2,
262
+ )}\n`,
263
+ );
264
+ writeUpdateLock(lockPath, {
265
+ token: lockToken,
266
+ phase: "prepared",
267
+ pid: process.pid,
268
+ planPath,
269
+ createdAt: dependencies.now().toISOString(),
270
+ });
271
+ return {
272
+ status: "prepared",
273
+ currentVersion,
274
+ targetVersion,
275
+ channel,
276
+ planPath,
277
+ runnerPath,
278
+ logPath,
279
+ resultPath,
280
+ };
281
+ } catch (error) {
282
+ releaseSelfUpdateLock(lockPath, lockToken);
283
+ releasePendingMarker(pendingMarkerPath, lockToken);
284
+ throw error;
285
+ }
286
+ }
287
+
288
+ export function requireNpmSelfUpdateInstallation(
289
+ packageRoot = fileURLToPath(new URL("../../", import.meta.url)),
290
+ runNpm: (args: string[], cwd: string) => CommandResult = runNpmCommand,
291
+ ): { kind: "global"; packageRoot: string } {
292
+ const installation = inspectNpmInstallation(packageRoot, runNpm);
293
+ if (installation.kind === "linked")
294
+ throw new Error(
295
+ cliText(
296
+ "Agent Bot is running from npm link or a linked source directory and will not update itself. Update the source checkout manually.",
297
+ "Agent Bot 当前通过 npm link 或链接的源码目录运行,不会执行自更新。请手动更新源码目录。",
298
+ ),
299
+ );
300
+ if (installation.kind !== "global")
301
+ throw new Error(
302
+ cliText(
303
+ "Agent Bot is not running from a global npm installation and will not update itself. Update the source checkout manually.",
304
+ "Agent Bot 当前不是通过 npm 全局安装运行,不会执行自更新。请手动更新源码目录。",
305
+ ),
306
+ );
307
+ return { kind: "global", packageRoot: installation.packageRoot };
308
+ }
309
+
310
+ export function finalizeSelfUpdatePlan(
311
+ planPath: string,
312
+ values: {
313
+ controlEndpoint: string;
314
+ databasePath: string;
315
+ restartService: boolean;
316
+ workingDirectory?: string;
317
+ reason?: string;
318
+ notificationSessionId?: string;
319
+ },
320
+ ): void {
321
+ const plan = readSelfUpdatePlan(planPath);
322
+ plan.controlEndpoint = values.controlEndpoint;
323
+ plan.databasePath = values.databasePath;
324
+ plan.restartService = values.restartService;
325
+ if (values.workingDirectory) plan.workingDirectory = values.workingDirectory;
326
+ if (values.reason) plan.reason = values.reason;
327
+ if (values.notificationSessionId)
328
+ plan.notificationSessionId = values.notificationSessionId;
329
+ fs.writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
330
+ }
331
+
332
+ export function readSelfUpdatePlan(planPath: string): SelfUpdatePlan {
333
+ return JSON.parse(fs.readFileSync(planPath, "utf8")) as SelfUpdatePlan;
334
+ }
335
+
336
+ export function releaseSelfUpdatePlan(planPath: string): void {
337
+ try {
338
+ const plan = readSelfUpdatePlan(planPath);
339
+ releaseSelfUpdateLock(plan.lockPath, plan.lockToken);
340
+ releasePendingMarker(plan.pendingMarkerPath, plan.lockToken);
341
+ } catch {
342
+ // An incomplete plan may not have acquired a durable update reservation.
343
+ }
344
+ }
345
+
346
+ export function readPendingSelfUpdate(
347
+ home = agentBotHome(),
348
+ ): PendingSelfUpdate | undefined {
349
+ const markerPath = path.join(home, "updates", "pending-update.json");
350
+ try {
351
+ const marker = JSON.parse(fs.readFileSync(markerPath, "utf8")) as {
352
+ token?: unknown;
353
+ phase?: unknown;
354
+ planPath?: unknown;
355
+ };
356
+ if (
357
+ marker.phase !== "prepared" ||
358
+ typeof marker.token !== "string" ||
359
+ typeof marker.planPath !== "string"
360
+ )
361
+ return undefined;
362
+ assertSelfUpdatePlanPath(marker.planPath, home);
363
+ const plan = readSelfUpdatePlan(marker.planPath);
364
+ if (
365
+ plan.lockToken !== marker.token ||
366
+ plan.pendingMarkerPath !== markerPath
367
+ )
368
+ return undefined;
369
+ const lock = JSON.parse(fs.readFileSync(plan.lockPath, "utf8")) as {
370
+ token?: unknown;
371
+ };
372
+ if (lock.token !== marker.token) return undefined;
373
+ return { planPath: marker.planPath, plan };
374
+ } catch {
375
+ return undefined;
376
+ }
377
+ }
378
+
379
+ export function assertSelfUpdatePlanPath(
380
+ planPath: string,
381
+ home = agentBotHome(),
382
+ ): void {
383
+ const updatesRoot = path.resolve(home, "updates");
384
+ const resolved = path.resolve(planPath);
385
+ const relative = path.relative(updatesRoot, resolved);
386
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
387
+ throw new Error(
388
+ "Agent Bot update plan must be stored under the active Profile's updates directory.",
389
+ );
390
+ }
391
+ const plan = readSelfUpdatePlan(resolved);
392
+ if (
393
+ plan.schemaVersion !== SELF_UPDATE_PLAN_VERSION ||
394
+ plan.packageName !== AGENT_BOT_PACKAGE_NAME
395
+ ) {
396
+ throw new Error("Invalid Agent Bot update plan.");
397
+ }
398
+ const expectedRunner = path.join(path.dirname(resolved), "update-runner.mjs");
399
+ if (!fs.existsSync(expectedRunner))
400
+ throw new Error("Agent Bot update runner is missing.");
401
+ }
402
+
403
+ export function launchSelfUpdateRunner(
404
+ planPath: string,
405
+ options: { waitPids?: number[]; environment?: NodeJS.ProcessEnv } = {},
406
+ ): number | undefined {
407
+ const runnerPath = path.join(path.dirname(planPath), "update-runner.mjs");
408
+ const waitArguments = (options.waitPids ?? []).flatMap((pid) => [
409
+ "--wait-pid",
410
+ String(pid),
411
+ ]);
412
+ const child = spawn(
413
+ process.execPath,
414
+ [runnerPath, planPath, ...waitArguments],
415
+ {
416
+ cwd: path.dirname(planPath),
417
+ detached: true,
418
+ windowsHide: true,
419
+ stdio: "ignore",
420
+ env: options.environment ?? process.env,
421
+ },
422
+ );
423
+ child.unref();
424
+ return child.pid;
425
+ }
426
+
427
+ export function inspectNpmInstallation(
428
+ packageRoot: string,
429
+ runNpm: (args: string[], cwd: string) => CommandResult = runNpmCommand,
430
+ ): { kind: "global" | "linked" | "source"; packageRoot: string } {
431
+ const result = runNpm(["root", "--global"], packageRoot);
432
+ if (result.error || result.status !== 0)
433
+ return { kind: "source", packageRoot };
434
+ const globalRoot = lastMatchingLine(result.stdout, () => true);
435
+ if (!globalRoot) return { kind: "source", packageRoot };
436
+ const expected = path.join(globalRoot, ...AGENT_BOT_PACKAGE_NAME.split("/"));
437
+ if (!fs.existsSync(expected)) return { kind: "source", packageRoot };
438
+ const linked = fs.lstatSync(expected).isSymbolicLink();
439
+ const expectedReal = fs.realpathSync.native(expected);
440
+ const packageReal = fs.realpathSync.native(packageRoot);
441
+ if (!samePath(expectedReal, packageReal))
442
+ return { kind: "source", packageRoot };
443
+ return { kind: linked ? "linked" : "global", packageRoot: expectedReal };
444
+ }
445
+
446
+ export function defaultUpdateChannel(version: string): SelfUpdateChannel {
447
+ return version.includes("-") ? "alpha" : "latest";
448
+ }
449
+
450
+ export function compareSemver(left: string, right: string): number {
451
+ const a = parseSemver(left);
452
+ const b = parseSemver(right);
453
+ for (let index = 0; index < 3; index += 1) {
454
+ if (a.core[index] !== b.core[index])
455
+ return a.core[index]! > b.core[index]! ? 1 : -1;
456
+ }
457
+ if (a.pre.length === 0 || b.pre.length === 0) {
458
+ if (a.pre.length === b.pre.length) return 0;
459
+ return a.pre.length === 0 ? 1 : -1;
460
+ }
461
+ const length = Math.max(a.pre.length, b.pre.length);
462
+ for (let index = 0; index < length; index += 1) {
463
+ const leftPart = a.pre[index];
464
+ const rightPart = b.pre[index];
465
+ if (leftPart === undefined) return -1;
466
+ if (rightPart === undefined) return 1;
467
+ if (leftPart === rightPart) continue;
468
+ const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : undefined;
469
+ const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : undefined;
470
+ if (leftNumber !== undefined && rightNumber !== undefined)
471
+ return leftNumber > rightNumber ? 1 : -1;
472
+ if (leftNumber !== undefined) return -1;
473
+ if (rightNumber !== undefined) return 1;
474
+ return leftPart > rightPart ? 1 : -1;
475
+ }
476
+ return 0;
477
+ }
478
+
479
+ function resolvePublishedVersion(
480
+ channel: SelfUpdateChannel,
481
+ runNpm: (args: string[], cwd: string) => CommandResult,
482
+ cwd: string,
483
+ ): string {
484
+ const result = runNpm(
485
+ ["view", `${AGENT_BOT_PACKAGE_NAME}@${channel}`, "version", "--json"],
486
+ cwd,
487
+ );
488
+ if (result.error) throw result.error;
489
+ if (result.status !== 0)
490
+ throw new Error(
491
+ result.stderr.trim() || `npm view exited with ${result.status}.`,
492
+ );
493
+ let value: unknown;
494
+ try {
495
+ value = JSON.parse(result.stdout);
496
+ } catch {
497
+ value = result.stdout.trim();
498
+ }
499
+ if (Array.isArray(value)) value = value.at(-1);
500
+ if (typeof value !== "string")
501
+ throw new Error("npm did not return a valid Agent Bot version.");
502
+ return normalizeVersion(value);
503
+ }
504
+
505
+ function packPackage(
506
+ spec: string,
507
+ destination: string,
508
+ cwd: string,
509
+ runNpm: (args: string[], cwd: string) => CommandResult,
510
+ extra: string[] = [],
511
+ ): string {
512
+ const result = runRequiredNpm(
513
+ ["pack", spec, "--pack-destination", destination, "--silent", ...extra],
514
+ cwd,
515
+ runNpm,
516
+ );
517
+ const filename = lastMatchingLine(result.stdout, (line) =>
518
+ line.endsWith(".tgz"),
519
+ );
520
+ if (!filename)
521
+ throw new Error(
522
+ `Could not determine npm package archive from: ${result.stdout.trim()}`,
523
+ );
524
+ const tarball = path.resolve(destination, filename);
525
+ if (!fs.existsSync(tarball))
526
+ throw new Error(`npm package archive was not created: ${tarball}`);
527
+ return tarball;
528
+ }
529
+
530
+ function runRequiredNpm(
531
+ args: string[],
532
+ cwd: string,
533
+ runNpm: (args: string[], cwd: string) => CommandResult,
534
+ ): CommandResult {
535
+ const result = runNpm(args, cwd);
536
+ if (result.error) throw result.error;
537
+ if (result.status !== 0) {
538
+ throw new Error(
539
+ result.stderr.trim() ||
540
+ result.stdout.trim() ||
541
+ `npm exited with ${result.status}.`,
542
+ );
543
+ }
544
+ return result;
545
+ }
546
+
547
+ function readVersionFromRoot(packageRoot: string): string {
548
+ return normalizeVersion(
549
+ readPackageVersion(pathToFileURL(path.join(packageRoot, "package.json"))),
550
+ );
551
+ }
552
+
553
+ function normalizeVersion(version: string): string {
554
+ const normalized = version.trim().replace(/^v/, "");
555
+ parseSemver(normalized);
556
+ return normalized;
557
+ }
558
+
559
+ function parseSemver(version: string): {
560
+ core: [number, number, number];
561
+ pre: string[];
562
+ } {
563
+ const match =
564
+ /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
565
+ version,
566
+ );
567
+ if (!match) throw new Error(`Invalid Agent Bot version: ${version}.`);
568
+ return {
569
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
570
+ pre: match[4]?.split(".") ?? [],
571
+ };
572
+ }
573
+
574
+ function samePath(left: string, right: string): boolean {
575
+ return process.platform === "win32"
576
+ ? left.toLowerCase() === right.toLowerCase()
577
+ : left === right;
578
+ }
579
+
580
+ function timestamp(value: Date): string {
581
+ return value.toISOString().replace(/[:.]/g, "-");
582
+ }
583
+
584
+ function acquireUpdateLock(
585
+ packageRoot: string,
586
+ token: string,
587
+ now: Date,
588
+ ): string {
589
+ const globalModulesRoot = path.dirname(path.dirname(packageRoot));
590
+ const lockPath = path.join(globalModulesRoot, ".agent-bot-update.lock");
591
+ try {
592
+ const handle = fs.openSync(lockPath, "wx");
593
+ try {
594
+ fs.writeFileSync(
595
+ handle,
596
+ `${JSON.stringify(
597
+ {
598
+ token,
599
+ phase: "preparing",
600
+ pid: process.pid,
601
+ createdAt: now.toISOString(),
602
+ },
603
+ null,
604
+ 2,
605
+ )}\n`,
606
+ );
607
+ } finally {
608
+ fs.closeSync(handle);
609
+ }
610
+ return lockPath;
611
+ } catch (error) {
612
+ if (!isNodeError(error) || error.code !== "EEXIST") throw error;
613
+ throw new Error(
614
+ cliText(
615
+ `Another Agent Bot update is already prepared or running. Lock: ${lockPath}`,
616
+ `另一个 Agent Bot 更新已在准备或执行中。锁文件:${lockPath}`,
617
+ ),
618
+ );
619
+ }
620
+ }
621
+
622
+ function writeUpdateLock(
623
+ lockPath: string,
624
+ value: Record<string, unknown>,
625
+ ): void {
626
+ fs.writeFileSync(lockPath, `${JSON.stringify(value, null, 2)}\n`);
627
+ }
628
+
629
+ function releaseSelfUpdateLock(lockPath: string, token: string): void {
630
+ try {
631
+ const lock = JSON.parse(fs.readFileSync(lockPath, "utf8")) as {
632
+ token?: unknown;
633
+ };
634
+ if (lock.token === token) fs.rmSync(lockPath, { force: true });
635
+ } catch {
636
+ // Never remove a lock that cannot be verified as belonging to this operation.
637
+ }
638
+ }
639
+
640
+ function releasePendingMarker(markerPath: string, token: string): void {
641
+ try {
642
+ const marker = JSON.parse(fs.readFileSync(markerPath, "utf8")) as {
643
+ token?: unknown;
644
+ };
645
+ if (marker.token === token) fs.rmSync(markerPath, { force: true });
646
+ } catch {
647
+ // Never remove a marker that cannot be verified as belonging to this operation.
648
+ }
649
+ }
650
+
651
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
652
+ return error instanceof Error && "code" in error;
653
+ }
654
+
655
+ function lastMatchingLine(
656
+ value: string,
657
+ predicate: (line: string) => boolean,
658
+ ): string | undefined {
659
+ const lines = value.trim().split(/\r?\n/);
660
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
661
+ const line = lines[index]?.trim();
662
+ if (line && predicate(line)) return line;
663
+ }
664
+ return undefined;
665
+ }
666
+
667
+ function conflictingChannelError(): Error {
668
+ return new Error(
669
+ cliText(
670
+ "--alpha and --stable cannot be combined.",
671
+ "--alpha 和 --stable 不能同时使用。",
672
+ ),
673
+ );
674
+ }
675
+
676
+ function defaultDependencies(): SelfUpdaterDependencies {
677
+ return {
678
+ packageRoot: fileURLToPath(new URL("../../", import.meta.url)),
679
+ home: agentBotHome(),
680
+ runnerEntry: fileURLToPath(
681
+ new URL("./SelfUpdateRunner.js", import.meta.url),
682
+ ),
683
+ runNpm: runNpmCommand,
684
+ validatePackage: validateInstalledPackage,
685
+ now: () => new Date(),
686
+ randomId: randomUUID,
687
+ };
688
+ }