@my-life-buddies/cli 0.16.0 → 0.17.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.
package/README.md CHANGED
@@ -31,7 +31,7 @@ buddy-cli init --help
31
31
  buddy-cli guide
32
32
  ```
33
33
 
34
- `--help` 列出命令,`<命令> --help` 说明当前命令的参数、限制和示例。`guide` 输出 Coding Agent 的完整开发指引,包括资料确认、Skill 接续、实现和交付规则;无需工程、登录或联网,不写文件。让 Agent 开始工作前先读取 `guide`,再按 CLI 返回的 `HANDOFF.md` 继续当前工程。`guide --help` 只显示该命令的简短用法。
34
+ `--help` 列出命令,`<命令> --help` 说明当前命令的参数、限制和示例。`guide` 输出 Coding Agent 的完整开发指引,包括资料确认、Skill 接续、实现和交付规则;无需工程或登录,先联网自检并按需自动更新 CLI,再输出新版指引,不修改工程文件。让 Agent 开始工作前先读取 `guide`,再按 CLI 返回的 `HANDOFF.md` 继续当前工程。HANDOFF 只包含当前工程、Skill 路径和接续方式,通用规则以 guide 为准;已生成文件在正常恢复创作时校验、备份并更新,CLI 升级不会直接覆盖工程里的 HANDOFF。`guide --help` 只显示该命令的简短用法。
35
35
 
36
36
  命令执行结果只提示当前状态和下一步;完整规则不再重复出现在顶层帮助和成功输出中。后台同一份入口说明见 [CLI 使用指南](../web/web/docs/cli/index.html)。
37
37
 
@@ -45,7 +45,11 @@ buddy-cli update
45
45
 
46
46
  `--version`(也可写作 `-v`)显示当前运行的 CLI 版本,无需联网。`update --check` 查询公共 npm 的 `latest` 版本,只检查、不安装;`update` 有新版本时安装检查到的准确版本,完成后核验安装结果。版本相同或本机版本更高时不安装、不降级。这三个用法均无需登录搭搭账号或进入搭子工程,从 CLI 0.8.0 开始支持。旧版用户先用上面的 npm 命令升级。
47
47
 
48
- 自动更新支持 npm 全局安装。源码、`npm link`、局部安装和 `npx` 请沿用原来的安装方式更新;使用 nvm 时先切换到安装该 CLI Node.js 环境。更新只针对 CLI,已有工程的 Runtime 依赖和锁文件需要另行修改与验证。
48
+ CLI 0.17.0 起,每条正常命令(含 `guide` 和无参数入口)先查询公共 npm `latest`,当前版本落后时自动更新,并由新版执行原命令。无需手动先运行 `update`,等待当前命令完成即可。更新发生在创建工程、领取凭据、启动 DEV 或上传之前;保留参数、工作目录、终端输入输出和退出状态,一次调用最多自动切换一次。版本相同或更高时继续执行,不降级;已经运行的 DEV 不因新版本发布而中断。
49
+
50
+ 全局安装更新当前 npm 环境中对应的 CLI;源码、`npm link`、项目内安装和 `npx` 自动下载准确版本到 `~/.cache/buddy-cli/updates/releases/`,从缓存运行,不改写原安装或工程依赖。并发更新共用安装锁。手动 `update` 仍只用于 npm 全局安装。查询最多等待 10 秒,安装最多等待 180 秒;失败或取消时停止原业务命令,不回退旧版。帮助、各命令 `--help` 和 `--version` 不联网。自动更新进度写入 stderr,不混入业务命令的 stdout 或 JSON;更新失败时 `--json` 返回 `code=CLI_UPDATE_FAILED` 和 `error`。
51
+
52
+ 更新只针对 CLI,已有工程的 Runtime 依赖、锁文件和业务适配仍需另行修改与验证。旧 CLI 不会自动获得自检能力,需要先用 npm 安装命令升级一次。
49
53
 
50
54
  可追加 `--json` 供 Coding Agent 或脚本读取,例如:
51
55
 
@@ -86,7 +90,7 @@ CLI 会把换取到的会话保存到 macOS Keychain。有效会话会复用,
86
90
  | 命令 | 是否需要 Agent 目录 | 会发生什么 |
87
91
  | --- | --- | --- |
88
92
  | `help`、`--help` | 不需要,也不登录 | 列出命令;参数见 `<命令> --help` |
89
- | `guide` | 不需要,也不登录 | 离线输出 Coding Agent 开发指引,不写文件 |
93
+ | `guide` | 不需要,也不登录 | 自检并按需更新 CLI 后输出当前 Coding Agent 开发指引,不修改工程 |
90
94
  | `--version [--json]` | 不需要,也不登录 | 显示当前运行的 CLI 版本,无需联网 |
91
95
  | `update [--check] [--json]` | 不需要,也不登录 | 检查公共 npm 最新版,或更新 npm 全局 CLI |
92
96
  | `login`、`logout` | 不需要 | 管理账号会话 |
@@ -438,11 +442,10 @@ CLI 不提供 `release`、`publish`、`disable`、`enable` 或 `unpublish`。最
438
442
 
439
443
  ## 官方工程如何在云端启动
440
444
 
441
- 平台在部署阶段解包、准备依赖,再执行 `npm start`。官方模板的启动入口读取:
445
+ 平台在部署阶段解包、准备依赖,再执行 `npm start`。Runtime 从包内 `package.json.buddyId` 读取身份;Server 收包时校验它与上传目标一致。连接凭据从环境变量读取:
442
446
 
443
447
  | 环境变量 | 作用 |
444
448
  | --- | --- |
445
- | `BUDDY_ID` | 平台部署的搭子身份,须与工程匹配 |
446
449
  | `BUDDY_TOKEN` | CLI 注入的共享开发 Token;平台托管时由 Server 注入正式 Token |
447
450
  | `MLB_GATEWAY_URL` | 平台网关,由部署环境显式注入;未设置时沿用 Runtime 的默认地址 |
448
451
 
@@ -479,19 +482,19 @@ Preview 文本发送采用 App WebSocket 的 `messages` 数组,并校验逐项
479
482
 
480
483
  ## 开发凭据复用
481
484
 
482
- CLI 0.14.1 起,开发 Token 优先从工程 `.env` 读取;首次启动时可迁移旧版 macOS Keychain 中的开发凭据,或通过 `GET /cli/buddies/:id` 领取,再写入 `.env`。更换工程目录不会生成新 Token。新电脑登录并取得搭子开发权限后,可以领取同一把开发 Token。
485
+ 开发 Token 从工程 `.env` 读取,并通过 `GET /cli/buddies/:id` Server 返回的当前搭子开发 Token 比对;不匹配时停止启动,确认工程后可用 `--refresh-token` 重新领取。首次启动时直接领取,再写入 `.env`。更换工程目录不会生成新 Token。新电脑登录并取得搭子开发权限后,可以领取同一把开发 Token。
483
486
 
484
487
  同一个搭子的开发者共享开发 Token 和 `<buddyId>-dev` 身份;同一开发身份仍只能保持一条 Runtime 事件流。正式身份使用另一把 Token,两者互不顶替。开发凭据没有自动过期时间,Server 重置或删除搭子后才失效。
485
488
 
486
- 开发 Token 被平台重置后,停止当前 CLI,再运行 `buddy-cli dev --refresh-token` 或 `buddy-cli preview --refresh-token`。这会跳过本地开发凭据,重新领取服务端当前的开发 Token 并更新 `.env`,不在 Server 生成新密钥。读取旧版 Keychain 凭据失败时会明确报错,不将读取失败当成没有凭据。
489
+ 开发 Token 被平台重置后,停止当前 CLI,再运行 `buddy-cli dev --refresh-token` 或 `buddy-cli preview --refresh-token`。这会跳过本地开发凭据,重新领取服务端当前的开发 Token 并更新 `.env`,不在 Server 生成新密钥。工程启动不再从旧版 Keychain 的开发 Token 缓存领取凭据。
487
490
 
488
491
  账号登录凭据仍保存在 macOS Keychain,不写入工程。开发 Token 保存在权限为 `0600` 的工程 `.env`,不传给 Web,也不进入上传包或命令行参数。
489
492
 
490
- ### Runtime 0.17.0 本地配置
493
+ ### 本地身份与连接配置
491
494
 
492
- 首次启动 DEV 时,CLI `BUDDY_ID`、开发版 `BUDDY_TOKEN` 和 `MLB_GATEWAY_URL` 写入工程 `.env`,保留其他配置和注释。后续优先复用;`--refresh-token` 重新领取后更新此文件。开发者登录凭据仍使用系统凭据存储。`.env` 加入 Git 忽略,且始终排除在上传包之外。
495
+ 首次启动 DEV 时,CLI 将开发版 `BUDDY_TOKEN` 和 `MLB_GATEWAY_URL` 写入工程 `.env`,保留其他配置和注释,移除旧 `BUDDY_ID` 赋值。身份始终来自 `package.json.buddyId`;只配置 Token 的 `.env` 也可使用。后续复用前向 Server 核对 Token;`--refresh-token` 重新领取后更新此文件。开发者登录凭据仍使用系统凭据存储。`.env` 加入 Git 忽略,且始终排除在上传包之外。
493
496
 
494
- CLI、Preview 与标准 `npm start` 均由 Node 的 `--env-file-if-exists=.env` 加载本地配置,Runtime 自行读取环境变量;托管时平台注入的同名环境变量优先。已有工程显式升级 Runtime 后,CLI 只自动迁移能精确识别的生成入口及标准启动命令,自定义业务代码和脚本保留。
497
+ CLI、Preview 与标准 `npm start` 均由 Node 的 `--env-file-if-exists=.env` 加载本地配置,Runtime `package.json.buddyId` 读取身份、从环境变量读取 Token 和网关;托管时平台注入的同名环境变量优先。已有工程显式升级 Runtime 后,CLI 只自动迁移能精确识别的生成入口及标准启动命令,自定义业务代码和脚本保留。
495
498
 
496
499
  ### CLI 0.15.1 的 Skill 包名
497
500
 
@@ -501,4 +504,10 @@ Creator 使用 `@my-life-buddies/buddy-creator-skill`,头像使用 `@my-life-b
501
504
 
502
505
  新工程以 `package.json.buddyId` 作为唯一工程绑定,不生成 `buddy.agent.json`,也不需要工程配置的 `schemaVersion`。创建阶段先保存 `{ "buddyId": "平台返回的 ID" }`,生成代码时在同一文件补齐 npm 字段,不覆盖已有工程配置。模型直接维护在业务工厂中,数据声明独立放在 `data-access.json`。本次不提供旧工程自动迁移。
503
506
 
504
- 本地 CLI 校验绑定与 `.env.BUDDY_ID`,准备开发凭据;托管身份由 MLB Server 注入环境变量。生成的 `start.mjs` 不读取 `package.json` 或比较 ID。Server 对包中 ID 的部署校验由 Server 实现,CLI 不代替服务端鉴权。
507
+ CLI Runtime 统一使用 `package.json.buddyId`。CLI 启动前校验本地 Token 与 Server 返回的当前搭子 Token 一致;Server 托管只注入正式 Token,收包时比对包内 ID 与上传目标。`start.mjs` 直接调用 Runtime,不重复读取或比较 ID。
508
+
509
+ ### Widget 卡片打开目标
510
+
511
+ Runtime 0.19.0 支持在 `schema.json.openTargets` 声明入口。业务通过 `conversation.widget.send(id, { idempotencyKey, openTarget })` 或模型工具发送 `version: 1`、`entry` 以及可选 `resource`、`state`;记录必须已经存在。H5 读取 `window.widget.openTarget` 定位,无目标时显示默认页。
512
+
513
+ 后台和本地模拟器保留每条消息的目标,通过 `POST /app/widgets/:widgetId/presentation` 获取鉴权页面;列表入口不传目标。同一 Widget 的不同卡片、加载重试与 DEV 重载分别保留自己的目标;目标记录删除或权限变化时显示打开失败,不回退默认页。
package/dist/bin/buddy.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // apps/cli/src/bin/buddy.ts
4
- import { execFile as execFile2 } from "node:child_process";
5
- import { promisify as promisify2 } from "node:util";
4
+ import { execFile as execFile3 } from "node:child_process";
5
+ import { promisify as promisify3 } from "node:util";
6
6
  import { stdin, stderr, stdout } from "node:process";
7
7
  import { createRequire } from "node:module";
8
- import { dirname } from "node:path";
8
+ import { dirname as dirname2 } from "node:path";
9
9
  import { createInterface } from "node:readline/promises";
10
10
  import {
11
11
  PromptCancelledError,
@@ -101,7 +101,9 @@ var PREVIEW_COMMAND_HELP = `\u7528\u6CD5:
101
101
  \u4FDD\u6301\u7EC8\u7AEF\u8FD0\u884C\uFF1B\u9000\u51FA CLI \u4F1A\u505C\u6B62 DEV \u5E76\u65AD\u5F00\u8FDE\u63A5\uFF0C\u5237\u65B0\u540E\u53F0\u4E0D\u4F1A\u81EA\u52A8\u542F\u52A8 DEV\u3002
102
102
  \u672A\u914D\u7F6E\u5730\u5740\u65F6\u8FDE\u63A5 MLB \u6D4B\u8BD5\u73AF\u5883 ${DEFAULT_MLB_TEST_URL}\uFF08HTTP \u4EC5\u9650\u6D4B\u8BD5\uFF09\u3002
103
103
  CLI \u7EDF\u4E00\u542F\u52A8\u5E76\u7BA1\u7406\u5DE5\u7A0B\u4E2D\u7684\u5B98\u65B9 Runtime\uFF1B\u6BCF\u6B21\u542F\u52A8\u6216\u91CD\u8F7D\u5FC5\u987B\u901A\u8FC7 npm \u6700\u65B0\u7A33\u5B9A\u7248\u68C0\u67E5\u3002
104
+ \u8EAB\u4EFD\u8BFB\u53D6 package.json.buddyId\uFF1B.env \u4FDD\u5B58 Token\uFF0C\u542F\u52A8\u524D\u5411 Server \u6821\u9A8C\u6240\u5C5E\u642D\u5B50\u3002
104
105
  --refresh-token \u91CD\u65B0\u9886\u53D6\u5DF2\u6709\u5F00\u53D1 Token\uFF0C\u66F4\u65B0\u5DE5\u7A0B .env\uFF1B\u4E0D\u4F1A\u751F\u6210\u65B0\u5BC6\u94A5\u3002
106
+ Widget \u6D88\u606F\u5361\u7247\u4FDD\u7559\u6307\u5B9A\u8BB0\u5F55\u7684\u6253\u5F00\u76EE\u6807\uFF0C\u5C0F\u6302\u4EF6\u5217\u8868\u6253\u5F00\u9ED8\u8BA4\u89C6\u56FE\u3002
105
107
  \u6A21\u62DF\u5668\u4F7F\u7528\u5F53\u524D\u5F00\u53D1\u8005\u8D26\u53F7\u8BBF\u95EE <buddyId>-dev \u8C03\u8BD5\u5206\u8EAB\u3002`;
106
108
  function valueAfter(args, index, flag) {
107
109
  const value = args[index + 1];
@@ -438,9 +440,42 @@ function assertSupportedDeveloperPlatform(platform = process.platform) {
438
440
  // apps/cli/src/self-update.ts
439
441
  import { execFile } from "node:child_process";
440
442
  import { lstat, readFile, realpath } from "node:fs/promises";
441
- import { homedir } from "node:os";
442
- import { isAbsolute, join } from "node:path";
443
+ import { homedir as homedir2 } from "node:os";
444
+ import { isAbsolute, join as join2 } from "node:path";
443
445
  import { promisify } from "node:util";
446
+
447
+ // apps/cli/src/update-lock.ts
448
+ import { mkdir, rm, writeFile } from "node:fs/promises";
449
+ import { dirname, join } from "node:path";
450
+ import { homedir } from "node:os";
451
+ import { createHash as createHash2 } from "node:crypto";
452
+ import { setTimeout as delay } from "node:timers/promises";
453
+ var defaultUpdateCache = () => join(homedir(), ".cache", "buddy-cli", "updates");
454
+ var globalUpdateLock = (cache, prefix) => join(cache, "locks", createHash2("sha256").update(prefix).digest("hex"));
455
+ async function withUpdateLock(path, signal, action) {
456
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
457
+ const deadline = Date.now() + 2e5;
458
+ for (; ; ) {
459
+ signal?.throwIfAborted();
460
+ try {
461
+ await mkdir(path, { mode: 448 });
462
+ break;
463
+ } catch (cause) {
464
+ if (cause.code !== "EEXIST") throw cause;
465
+ if (Date.now() >= deadline) throw new Error(`\u7B49\u5F85\u5176\u4ED6 CLI \u66F4\u65B0\u8D85\u65F6\u3002\u8BF7\u786E\u8BA4\u66F4\u65B0\u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF0C\u518D\u6E05\u7406\u9501\u76EE\u5F55\u5E76\u91CD\u8BD5\uFF1A${path}`);
466
+ await delay(100, void 0, { signal });
467
+ }
468
+ }
469
+ try {
470
+ await writeFile(`${path}/owner.json`, JSON.stringify({ pid: process.pid, startedAt: Date.now() }), { mode: 384 });
471
+ signal?.throwIfAborted();
472
+ return await action();
473
+ } finally {
474
+ await rm(path, { recursive: true, force: true });
475
+ }
476
+ }
477
+
478
+ // apps/cli/src/self-update.ts
444
479
  var PACKAGE = "@my-life-buddies/cli";
445
480
  var REGISTRY = "https://registry.npmjs.org/";
446
481
  var MANUAL_UPDATE = `npm install --global ${PACKAGE}@latest --registry=${REGISTRY}`;
@@ -455,9 +490,12 @@ var HELP = `\u7528\u6CD5:
455
490
  --json \u8F93\u51FA JSON\uFF0C\u4F9B Coding Agent \u6216\u811A\u672C\u8BFB\u53D6
456
491
 
457
492
  \u65E0\u9700\u767B\u5F55\u642D\u642D\u8D26\u53F7\uFF0C\u4E5F\u65E0\u9700\u8FDB\u5165\u642D\u5B50\u5DE5\u7A0B\u3002
458
- update \u66F4\u65B0\u5F53\u524D npm \u5168\u5C40\u5B89\u88C5\u7684 CLI\uFF0C\u4E0D\u4FEE\u6539\u642D\u5B50\u5DE5\u7A0B\u7684 Runtime \u4F9D\u8D56\u3002
493
+ \u6B63\u5E38\u547D\u4EE4\uFF08\u542B guide\uFF09\u4F1A\u5148\u8054\u7F51\u81EA\u68C0\uFF0C\u81EA\u52A8\u66F4\u65B0\u540E\u7531\u65B0\u7248\u7EE7\u7EED\u6267\u884C\uFF1B\u7B49\u5F85\u5B8C\u6210\u5373\u53EF\u3002
494
+ --help\u3001\u5404\u547D\u4EE4 --help \u4E0E --version \u79BB\u7EBF\u53EF\u7528\uFF1B\u67E5\u8BE2\u6216\u66F4\u65B0\u5931\u8D25\u65F6\u505C\u6B62\u4E1A\u52A1\u64CD\u4F5C\u3002
495
+ update \u7528\u4E8E\u5355\u72EC\u66F4\u65B0\u5F53\u524D npm \u5168\u5C40\u5B89\u88C5\u7684 CLI\uFF0C\u4E0D\u4FEE\u6539\u642D\u5B50\u5DE5\u7A0B\u7684 Runtime \u4F9D\u8D56\u3002
459
496
  \u5DF2\u7ECF\u662F\u6700\u65B0\u7248\u6216\u5F53\u524D\u7248\u672C\u66F4\u9AD8\u65F6\u4E0D\u5B89\u88C5\u3001\u4E0D\u964D\u7EA7\u3002
460
- \u6E90\u7801\u3001npm link\u3001\u5C40\u90E8\u5B89\u88C5\u6216 npx \u8FD0\u884C\u65F6\uFF0C\u8BF7\u4F7F\u7528\u539F\u6765\u7684\u5B89\u88C5\u65B9\u5F0F\u66F4\u65B0\u3002
497
+ \u6E90\u7801\u3001npm link\u3001\u5C40\u90E8\u5B89\u88C5\u6216 npx \u7684\u6B63\u5E38\u547D\u4EE4\u4F7F\u7528\u7F13\u5B58\u7684\u65B0\u7248\u6267\u884C\uFF0C\u4E0D\u8986\u76D6\u6E90\u7801\u6216\u5DE5\u7A0B\u4F9D\u8D56\u3002
498
+ \u4E0A\u8FF0\u5B89\u88C5\u65B9\u5F0F\u4E0D\u80FD\u7528 update \u5355\u72EC\u4FEE\u6539\u539F\u5B89\u88C5\uFF0C\u8BF7\u6CBF\u7528\u539F\u6765\u7684\u5B89\u88C5\u65B9\u5F0F\u66F4\u65B0\u3002
461
499
  \u65E7\u7248 CLI \u5C1A\u65E0 update \u547D\u4EE4\u65F6\uFF0C\u8FD0\u884C\uFF1A
462
500
  ${MANUAL_UPDATE}`;
463
501
  function parseVersion(version) {
@@ -510,11 +548,11 @@ async function latestVersion(options) {
510
548
  }
511
549
  async function installUpdate(version, options) {
512
550
  const runNpm = options.runNpm ?? ((args, settings2) => execute("npm", args, { ...settings2, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }));
513
- const settings = { cwd: homedir(), signal: options.signal, timeout: 1e4 };
551
+ const settings = { cwd: homedir2(), signal: options.signal, timeout: 1e4 };
514
552
  const { stdout: stdout2 } = await runNpm(["prefix", "--global"], settings);
515
553
  const prefix = stdout2.trim();
516
554
  if (!isAbsolute(prefix) || /[\r\n]/u.test(prefix)) throw new Error("\u65E0\u6CD5\u786E\u5B9A npm \u5168\u5C40\u5B89\u88C5\u76EE\u5F55\u3002");
517
- const installedRoot = join(prefix, "lib", "node_modules", PACKAGE);
555
+ const installedRoot = join2(prefix, "lib", "node_modules", PACKAGE);
518
556
  let sameInstallation = false;
519
557
  try {
520
558
  sameInstallation = !(await lstat(installedRoot)).isSymbolicLink() && await realpath(installedRoot) === await realpath(options.packageRoot);
@@ -523,28 +561,33 @@ async function installUpdate(version, options) {
523
561
  if (!sameInstallation) {
524
562
  throw new Error("\u5F53\u524D CLI \u4E0D\u662F\u8FD9\u4E2A npm \u73AF\u5883\u7684\u5168\u5C40\u5B89\u88C5\uFF0C\u4E0D\u80FD\u76F4\u63A5\u66F4\u65B0\u3002\u8BF7\u7528\u539F\u6765\u7684\u5B89\u88C5\u65B9\u5F0F\u66F4\u65B0\uFF1B\u82E5\u4F7F\u7528 nvm\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u5B89\u88C5\u8BE5 CLI \u7684 Node.js \u73AF\u5883\u3002");
525
563
  }
526
- try {
527
- await runNpm([
528
- "install",
529
- "--global",
530
- `${PACKAGE}@${version}`,
531
- "--prefix",
532
- prefix,
533
- `--registry=${REGISTRY}`,
534
- `--@my-life-buddies:registry=${REGISTRY}`,
535
- "--engine-strict",
536
- "--no-audit",
537
- "--no-fund"
538
- ], { ...settings, timeout: 18e4 });
539
- } catch (cause) {
540
- if (options.signal?.aborted) throw new Error("\u66F4\u65B0\u5DF2\u53D6\u6D88\uFF0C\u8BF7\u8FD0\u884C buddy-cli --version \u68C0\u67E5\u5B89\u88C5\u72B6\u6001\u3002");
541
- const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "unknown";
542
- throw new Error(`npm \u5B89\u88C5\u672A\u5B8C\u6210\uFF08${code}\uFF09\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u548C\u5168\u5C40\u76EE\u5F55\u6743\u9650\uFF0C\u7136\u540E\u91CD\u8BD5\uFF1A${MANUAL_UPDATE}`);
543
- }
544
- const installed = JSON.parse(await readFile(join(installedRoot, "package.json"), "utf8"));
545
- if (installed.name !== PACKAGE || installed.version !== version) {
546
- throw new Error(`npm \u5DF2\u7ED3\u675F\uFF0C\u4F46\u672A\u786E\u8BA4\u5B89\u88C5\u5230 ${version}\u3002\u8BF7\u8FD0\u884C buddy-cli --version \u68C0\u67E5\uFF0C\u5FC5\u8981\u65F6\u91CD\u8BD5\uFF1A${MANUAL_UPDATE}`);
547
- }
564
+ await withUpdateLock(globalUpdateLock(options.cacheDirectory ?? defaultUpdateCache(), prefix), options.signal, async () => {
565
+ const current = JSON.parse(await readFile(join2(installedRoot, "package.json"), "utf8"));
566
+ if (current.name === PACKAGE && current.version === version) return;
567
+ if (current.name === PACKAGE && compareVersions(current.version, version) > 0) throw new Error("\u53E6\u4E00\u4E2A\u8FDB\u7A0B\u5DF2\u5B89\u88C5\u66F4\u65B0\u7684 CLI\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u547D\u4EE4\u3002");
568
+ try {
569
+ await runNpm([
570
+ "install",
571
+ "--global",
572
+ `${PACKAGE}@${version}`,
573
+ "--prefix",
574
+ prefix,
575
+ `--registry=${REGISTRY}`,
576
+ `--@my-life-buddies:registry=${REGISTRY}`,
577
+ "--engine-strict",
578
+ "--no-audit",
579
+ "--no-fund"
580
+ ], { ...settings, timeout: 18e4 });
581
+ } catch (cause) {
582
+ if (options.signal?.aborted) throw new Error("\u66F4\u65B0\u5DF2\u53D6\u6D88\uFF0C\u8BF7\u8FD0\u884C buddy-cli --version \u68C0\u67E5\u5B89\u88C5\u72B6\u6001\u3002");
583
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "unknown";
584
+ throw new Error(`npm \u5B89\u88C5\u672A\u5B8C\u6210\uFF08${code}\uFF09\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u548C\u5168\u5C40\u76EE\u5F55\u6743\u9650\uFF0C\u7136\u540E\u91CD\u8BD5\uFF1A${MANUAL_UPDATE}`);
585
+ }
586
+ const installed = JSON.parse(await readFile(join2(installedRoot, "package.json"), "utf8"));
587
+ if (installed.name !== PACKAGE || installed.version !== version) {
588
+ throw new Error(`npm \u5DF2\u7ED3\u675F\uFF0C\u4F46\u672A\u786E\u8BA4\u5B89\u88C5\u5230 ${version}\u3002\u8BF7\u8FD0\u884C buddy-cli --version \u68C0\u67E5\uFF0C\u5FC5\u8981\u65F6\u91CD\u8BD5\uFF1A${MANUAL_UPDATE}`);
589
+ }
590
+ });
548
591
  }
549
592
  async function runSelfUpdateCommand(args, io, options) {
550
593
  const versionOnly = args[0] === "--version" || args[0] === "-v";
@@ -599,6 +642,143 @@ async function runSelfUpdateCommand(args, io, options) {
599
642
  }
600
643
  }
601
644
 
645
+ // apps/cli/src/auto-update.ts
646
+ import { execFile as execFile2, spawn } from "node:child_process";
647
+ import { lstat as lstat2, mkdir as mkdir2, mkdtemp, readFile as readFile2, realpath as realpath2, rename, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
648
+ import { constants, homedir as homedir3 } from "node:os";
649
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
650
+ import { promisify as promisify2 } from "node:util";
651
+ var PACKAGE2 = "@my-life-buddies/cli";
652
+ var REGISTRY2 = "https://registry.npmjs.org/";
653
+ var execute2 = promisify2(execFile2);
654
+ var HANDOFF_ENV = "BUDDY_CLI_UPDATE_HANDOFF";
655
+ async function installedVersion(root) {
656
+ try {
657
+ const pkg = JSON.parse(await readFile2(join3(root, "package.json"), "utf8"));
658
+ return pkg.name === PACKAGE2 && typeof pkg.version === "string" ? pkg.version : void 0;
659
+ } catch {
660
+ return void 0;
661
+ }
662
+ }
663
+ async function checkedEntry(root, version) {
664
+ if (await installedVersion(root) !== version) throw new Error(`CLI \u5B89\u88C5\u7ED3\u679C\u4E0E\u672C\u6B21\u8981\u6C42\u7684 ${version} \u4E0D\u4E00\u81F4\uFF0C\u8BF7\u91CD\u8BD5\u3002`);
665
+ const entry = join3(root, "dist", "bin", "buddy.js");
666
+ if (!(await lstat2(entry)).isFile()) throw new Error("\u65B0\u7248 CLI \u7F3A\u5C11\u6709\u6548\u7684\u6267\u884C\u5165\u53E3\u3002");
667
+ return entry;
668
+ }
669
+ async function prepareLatest(version, options) {
670
+ const cache = options.cacheDirectory ?? defaultUpdateCache();
671
+ const runNpm = options.runNpm ?? ((args, settings2) => execute2("npm", args, { ...settings2, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }));
672
+ const settings = { cwd: homedir3(), signal: options.signal, timeout: 1e4 };
673
+ const { stdout: stdout2 } = await runNpm(["prefix", "--global"], settings);
674
+ const prefix = stdout2.trim();
675
+ if (!isAbsolute2(prefix) || /[\r\n]/u.test(prefix)) throw new Error("\u65E0\u6CD5\u786E\u5B9A npm \u5168\u5C40\u5B89\u88C5\u76EE\u5F55\u3002");
676
+ const globalRoot = join3(prefix, "lib", "node_modules", PACKAGE2);
677
+ let isGlobal = false;
678
+ try {
679
+ isGlobal = !(await lstat2(globalRoot)).isSymbolicLink() && await realpath2(globalRoot) === await realpath2(options.packageRoot);
680
+ } catch {
681
+ }
682
+ if (isGlobal) {
683
+ await installUpdate(version, options);
684
+ return checkedEntry(globalRoot, version);
685
+ }
686
+ const destination = join3(cache, "releases", version);
687
+ return withUpdateLock(join3(cache, "locks", `release-${version}`), options.signal, async () => {
688
+ const root = join3(destination, "node_modules", PACKAGE2);
689
+ try {
690
+ return await checkedEntry(root, version);
691
+ } catch {
692
+ }
693
+ await mkdir2(join3(cache, "releases"), { recursive: true, mode: 448 });
694
+ const temporary = await mkdtemp(join3(cache, "releases", ".install-"));
695
+ try {
696
+ await writeFile2(join3(temporary, "package.json"), JSON.stringify({ private: true }));
697
+ try {
698
+ await runNpm(
699
+ [
700
+ "install",
701
+ "--prefix",
702
+ temporary,
703
+ "--save-exact",
704
+ `${PACKAGE2}@${version}`,
705
+ `--registry=${REGISTRY2}`,
706
+ `--@my-life-buddies:registry=${REGISTRY2}`,
707
+ "--engine-strict",
708
+ "--no-audit",
709
+ "--no-fund"
710
+ ],
711
+ { cwd: temporary, signal: options.signal, timeout: 18e4 }
712
+ );
713
+ } catch (cause) {
714
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "unknown";
715
+ throw new Error(`\u65B0\u7248 CLI \u7F13\u5B58\u5B89\u88C5\u5931\u8D25\uFF08${code}\uFF09\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u3001Node.js \u7248\u672C\u548C\u76EE\u5F55\u6743\u9650\u540E\u91CD\u8BD5\u3002`);
716
+ }
717
+ await checkedEntry(join3(temporary, "node_modules", PACKAGE2), version);
718
+ options.signal?.throwIfAborted();
719
+ await rm2(destination, { recursive: true, force: true });
720
+ await rename(temporary, destination);
721
+ return checkedEntry(root, version);
722
+ } finally {
723
+ await rm2(temporary, { recursive: true, force: true });
724
+ }
725
+ });
726
+ }
727
+ async function runUpdatedCli(entry, args, options) {
728
+ options.signal?.throwIfAborted();
729
+ return new Promise((resolve2, reject) => {
730
+ const child = spawn(process.execPath, [entry, ...args], { cwd: options.cwd, env: options.env, stdio: "inherit" });
731
+ let killTimer;
732
+ const abort = () => {
733
+ const signal = options.signal?.reason?.message === "SIGINT" ? "SIGINT" : "SIGTERM";
734
+ child.kill(signal);
735
+ killTimer ??= setTimeout(() => child.kill("SIGKILL"), 5e3);
736
+ killTimer.unref();
737
+ };
738
+ const cleanup = () => {
739
+ options.signal?.removeEventListener("abort", abort);
740
+ clearTimeout(killTimer);
741
+ };
742
+ child.once("error", (error) => {
743
+ cleanup();
744
+ reject(error);
745
+ });
746
+ child.once("exit", (code, signal) => {
747
+ cleanup();
748
+ resolve2({ exitCode: code ?? 128 + (signal ? constants.signals[signal] : 1) });
749
+ });
750
+ options.signal?.addEventListener("abort", abort, { once: true });
751
+ if (options.signal?.aborted) abort();
752
+ });
753
+ }
754
+ async function runAutoUpdate(args, io, options) {
755
+ if (args[0] === "help" || args[0] === "update" || args[0] === "--version" || args[0] === "-v" || args.includes("--help") || args.includes("-h")) return void 0;
756
+ try {
757
+ const env = { ...options.env ?? process.env };
758
+ const handoff = env[HANDOFF_ENV];
759
+ if (handoff !== void 0) {
760
+ delete env[HANDOFF_ENV];
761
+ if (!options.env) delete process.env[HANDOFF_ENV];
762
+ const expected = JSON.parse(handoff);
763
+ const entry2 = await realpath2(join3(options.packageRoot, "dist", "bin", "buddy.js"));
764
+ if (expected.parent !== process.ppid || expected.version !== options.version || expected.entry !== entry2 || JSON.stringify(expected.args) !== JSON.stringify(args)) throw new Error("CLI \u81EA\u52A8\u66F4\u65B0\u63A5\u7EED\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C\u547D\u4EE4\u3002");
765
+ return void 0;
766
+ }
767
+ const latest = await latestVersion(options);
768
+ if (compareVersions(latest, options.version) <= 0) return void 0;
769
+ io.writeError(`CLI \u81EA\u52A8\u66F4\u65B0\uFF1A${options.version} \u2192 ${latest}\u3002\u5B8C\u6210\u540E\u7EE7\u7EED\u5F53\u524D\u547D\u4EE4\uFF0C\u8BF7\u52FF\u91CD\u590D\u6267\u884C\u3002`);
770
+ const entry = await realpath2(await prepareLatest(latest, options));
771
+ options.signal?.throwIfAborted();
772
+ env[HANDOFF_ENV] = JSON.stringify({ parent: process.pid, version: latest, entry, args });
773
+ return await (options.runCli ?? runUpdatedCli)(entry, args, { cwd: options.cwd ?? process.cwd(), env, signal: options.signal });
774
+ } catch (cause) {
775
+ const error = options.signal?.aborted ? "CLI \u81EA\u52A8\u66F4\u65B0\u5DF2\u53D6\u6D88\uFF0C\u539F\u547D\u4EE4\u672A\u6267\u884C\u3002" : `CLI \u81EA\u68C0\u6216\u66F4\u65B0\u5931\u8D25\uFF0C\u539F\u547D\u4EE4\u672A\u6267\u884C\uFF1A${cause instanceof Error ? cause.message : String(cause)}`;
776
+ if (args.includes("--json")) io.write(JSON.stringify({ code: "CLI_UPDATE_FAILED", error }));
777
+ else io.writeError(error);
778
+ return { exitCode: 1 };
779
+ }
780
+ }
781
+
602
782
  // apps/cli/src/bin/buddy.ts
603
783
  import { latestRuntimeTemplate, scaffoldPlatformAgent } from "./core.js";
604
784
  var TerminalIO = class {
@@ -616,7 +796,7 @@ var TerminalIO = class {
616
796
  }
617
797
  async openMetaPreview(path) {
618
798
  try {
619
- await promisify2(execFile2)("open", [path]);
799
+ await promisify3(execFile3)("open", [path]);
620
800
  } catch {
621
801
  this.write(`\u8BF7\u6253\u5F00\u672C\u5730\u5934\u50CF\u9884\u89C8\u540E\u518D\u786E\u8BA4\uFF1A${path}`);
622
802
  }
@@ -683,11 +863,12 @@ try {
683
863
  const args = process.argv.slice(2);
684
864
  const require2 = createRequire(import.meta.url);
685
865
  const manifestPath = require2.resolve("@my-life-buddies/cli/package.json");
686
- const maintenance = await runSelfUpdateCommand(args, terminal, {
866
+ const updateOptions = {
687
867
  version: require2(manifestPath).version,
688
- packageRoot: dirname(manifestPath),
868
+ packageRoot: dirname2(manifestPath),
689
869
  signal: lifecycle.signal
690
- });
870
+ };
871
+ const maintenance = await runSelfUpdateCommand(args, terminal, updateOptions) ?? await runAutoUpdate(args, terminal, updateOptions);
691
872
  const command = args[0];
692
873
  let platformHost;
693
874
  const host = () => platformHost ??= new BuddyPlatformHost({ signal: lifecycle.signal });