@my-life-buddies/cli 0.15.1 → 0.17.0

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,14 +90,14 @@ 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` | 不需要 | 管理账号会话 |
93
97
  | `models` | 不需要,也不登录 | 始终联网读取 npm 最新 Runtime 的模型目录,提示工程版本差异;支持 `--directory`、`--json` |
94
98
  | `init` | 目标目录可以是空目录或已有官方工程 | 登记或恢复云端身份,准备本地文件 |
95
99
  | `dev`、`preview`、`push` | 需要有效清单和完整工程文件 | 运行、调试或打包该工程 |
96
- | `status` | 需要含 `buddyId` 的 `buddy.agent.json` | 查询该搭子的云端状态;创作尚未完成时也可使用 |
100
+ | `status` | 需要含 `buddyId` 的 `package.json` | 查询该搭子的云端状态;创作尚未完成时也可使用 |
97
101
  | `avatar` | 从工程读取身份,或用 `--buddy-id` 指定 | 上传本地图片或清除搭子头像,保留其他资料;无需 Runtime |
98
102
  | `datasets` | 不需要,也不登录 | 与 models 使用同一 Runtime 来源,读取它的数据目录;支持 `--directory`、`--json` |
99
103
 
@@ -151,10 +155,10 @@ CLI 只生成工程骨架,不分析手册或自动生成业务代码,也不
151
155
 
152
156
  ```text
153
157
  study-buddy/
154
- ├── buddy.agent.json 平台 buddyId、模型与数据需求
158
+ ├── package.json 顶层 buddyId、npm 依赖与启动命令
155
159
  ├── agent.md 应用 Prompt,只包含自然语言
156
160
  ├── start.mjs 本地和云端共用的启动入口
157
- ├── package.json 包含 npm start
161
+ ├── data-access.json 数据集 ID 和用途声明
158
162
  ├── package-lock.json 依赖锁文件
159
163
  ├── src/
160
164
  │ ├── buddy-options.mjs BuddyFactory,组装工具与回合钩子
@@ -221,15 +225,10 @@ Runtime 0.12.2 的小挂件工具按操作区分:
221
225
  `start.mjs` 是 CLI 生成的启动与停止入口;`src/buddy-options.mjs` 负责业务配置。启动文件通过以下方式接入组装器:
222
226
 
223
227
  ```js
224
- import { readFileSync } from "node:fs";
225
228
  import { BuddyServer } from "@my-life-buddies/buddy-runtime";
226
229
  import createBuddyOptions from "./src/buddy-options.mjs";
227
230
 
228
- const config = JSON.parse(readFileSync(new URL("./buddy.agent.json", import.meta.url), "utf8"));
229
- await BuddyServer.start({
230
- dataRequirements: config.datasets ?? [],
231
- buddy: createBuddyOptions,
232
- });
231
+ await BuddyServer.start(createBuddyOptions);
233
232
  ```
234
233
 
235
234
  工程在 `package.json` 声明准确 Runtime 版本,锁文件固定依赖;上传不包含 `node_modules` 或 Runtime 打包副本,部署执行同一份 `npm start`。首次生成 package.json 和 package-lock.json 时查询公共 npm latest,生成准确版本与锁文件。查询或锁文件生成失败时停止,不回退内置旧版本。已有工程再次初始化时保留其依赖;安装验收直接从公共 npm 安装。
@@ -288,9 +287,9 @@ BUDDY_CREATOR_SKILL_PATH=/absolute/path/buddy-creator-skill \
288
287
  buddy-cli init --mode direct --directory ./study-buddy
289
288
  ```
290
289
 
291
- 创作阶段的 `buddy.agent.json` 只有 `schemaVersion` 和 `buddyId`;CLI 用它从平台读取最新资料并重新校验归属,不另建搭子。创作中断后可以在原目录重试 `--mode create`,继续使用原作品;手册完成后执行 `--mode direct`,在保留已有模型和数据选择的前提下补齐工程文件,并为尚未填写的数据需求写入空数组。旧 `--mode interview` 已停止使用。
290
+ 创作阶段的 `package.json` 只有顶层 `buddyId`;CLI 用它从平台读取最新资料并重新校验归属,不另建搭子。创作中断后可以在原目录重试 `--mode create`,继续使用原作品;手册完成后执行 `--mode direct`,保留已有业务代码与数据声明,补齐 npm 字段和缺失的工程文件;缺失 `data-access.json` 时生成空 requirements 数组。旧 `--mode interview` 已停止使用。
292
291
 
293
- 两条主线都遵循同一个登记规则:名称、`tagline`(一句话介绍)、`intro`(完整介绍)先同步到平台,返回的 ID 写入 `buddy.agent.json.buddyId`。公开资料不在本地留副本。若平台已创建成功、本地写盘失败,可用 `--buddy-id <平台返回的 ID>` 恢复;它不是自定义 ID 的入口。
292
+ 两条主线都遵循同一个登记规则:名称、`tagline`(一句话介绍)、`intro`(完整介绍)先同步到平台,返回的 ID 写入 `package.json.buddyId`。公开资料不在本地留副本。若平台已创建成功、本地写盘失败,可用 `--buddy-id <平台返回的 ID>` 恢复;它不是自定义 ID 的入口。
294
293
 
295
294
  Creator 例外保留首次创作背景快照,用于接续同一作品;恢复时使用它校验接续说明,云端后来修改名称或介绍不会因此阻断创作,也不会被首次快照覆盖。
296
295
 
@@ -315,7 +314,7 @@ buddy-cli avatar --clear
315
314
 
316
315
  `--file` 与 `--clear` 二选一。支持静态 PNG、JPEG、WebP,最大 5 MiB。Server 校验图片并裁剪成 512 × 512 WebP,生成头像地址;CLI 不再提供 `--url`。服务端处理失败时保留原头像。
317
316
 
318
- 命令默认向上寻找 `buddy.agent.json`,可用 `--directory ./my-buddy` 指定工程;图片路径始终相对执行命令的当前目录。创作阶段只有身份清单时也能使用,无需安装 Runtime。工程外可直接指定平台 ID:
317
+ 命令默认向上寻找 `package.json`,可用 `--directory ./my-buddy` 指定工程;图片路径始终相对执行命令的当前目录。创作阶段只有身份清单时也能使用,无需安装 Runtime。工程外可直接指定平台 ID:
319
318
 
320
319
  ```bash
321
320
  buddy-cli avatar --buddy-id b_example --file ./avatar.png
@@ -337,44 +336,36 @@ buddy-cli models
337
336
 
338
337
  此限制由本版 CLI 执行,不修改服务端策略或已部署进程;直接运行工程 `npm start`、旧 CLI 或服务端发布入口不受本版 CLI 检查约束。
339
338
 
340
- 选择一个写进现有 `buddy.agent.json`,例如:
339
+ 将选定的模型直接写进 `src/buddy-options.mjs` 返回值的 `initialState.model`:
341
340
 
342
- ```json
343
- {
344
- "schemaVersion": 1,
345
- "buddyId": "平台返回的搭子 ID",
346
- "model": "deepseek-v4-pro",
347
- "datasets": []
341
+ ```js
342
+ initialState: {
343
+ model: "deepseek-v4-pro",
344
+ systemPrompt,
345
+ tools: [],
348
346
  }
349
347
  ```
350
348
 
351
- 其他身份和工程字段要保留,不要整份替换配置。模板的会话工厂读取 `model` 并放入 `initialState.model`,重启后生效。自定义工厂也可按会话选择受支持模型。
352
-
353
- 省略 `model` 时模板使用 `kimi-k2.6`;不可用时返回错误,不自动换模型。列表也不等于上游服务实时健康检查。
354
-
355
- 官方 Runtime 统一通过平台代理调用选定模型,不把外部模型密钥带进项目。
349
+ 模板默认生成 `kimi-k2.6`,后续可直接修改业务代码;自定义工厂也可按会话选择受支持模型。CLI 不执行业务工厂来推断模型;模型有效性由 Runtime 在创建会话时检查。模型不可用时返回错误,不自动换模型。目录不代表上游实时健康状态。官方 Runtime 通过平台代理调用模型,工程不保存外部模型密钥。
356
350
 
357
351
  ## 声明这个搭子需要的数据
358
352
 
359
- `buddy.agent.json` 的 `datasets` 中声明数据集及用途。新工程默认 `[]`;省略该字段也表示不申请数据。每个 ID 只能出现一次,`purpose` 是展示给用户的用途说明,必须包含非空白内容,最多 200 字。
353
+ 在工程根目录 `data-access.json` 的 `requirements` 中声明数据集及用途:
360
354
 
361
355
  ```json
362
356
  {
363
- "schemaVersion": 1,
364
- "buddyId": "b_example",
365
- "model": "kimi-k2.6",
366
- "datasets": [
357
+ "requirements": [
367
358
  { "id": "health.sleep", "purpose": "根据最近睡眠调整作息建议" },
368
359
  { "id": "health.workouts", "purpose": "结合运动记录安排训练和恢复" }
369
360
  ]
370
361
  }
371
362
  ```
372
363
 
373
- 完整选项以工程目录运行 `buddy-cli datasets` 的输出为准;脚本可加 `--json`。目录由 npm 最新 Runtime 提供,具体条目以查询结果为准;使用新版条目前需完成工程升级与适配。
364
+ 新工程生成 `{ "requirements": [] }`。最多 32 项,ID 不能重复;`id`、`purpose` 会去除首尾空白,用途必须为 1~200 字。完整选项由 `buddy-cli datasets [--json]` npm 最新 Runtime 读取,DEV 与 push 使用工程安装的 SDK 校验 ID。
374
365
 
375
- CLI 校验声明,并把清单原样放入源码包。服务端从包中解析申请清单、绑定版本,以及 App 展示和取得用户授权的流程仍需接入;当前 CLI 不会额外调用接口修改平台权限。工程中的声明不代表用户已经授权,也不会自动启用查询工具。
366
+ Runtime 0.17.0 启动时自动读取该文件,通过 `PUT /internal/v1/data-access/requirements` 登记;文件缺失或空数组会上传空声明,清除旧声明。登记失败阻止启动,不会带着未同步的配置上线。DEV 监听该文件,保存后重启 Runtime 并重新登记。部署环境需提供对应登记接口。
376
367
 
377
- `start.mjs` `config.datasets ?? []` 传给 `BuddyServer.start` `dataRequirements`。模型通过 `data_access_query` 查询,通过 `data_access_request` 发授权卡;HealthKit 授权后重查 Dataset,位置和日历用 `data_access_read_result` 读取一次性结果。业务代码使用 `conversation.dataAccess.query/request/readResult`。Runtime 随实际业务请求携带完整声明,Server 校验用户权限;上传包解析与发布版本绑定仍待实现。
368
+ CLI 将文件原样放入源码包,启动入口只调用 `BuddyServer.start(createBuddyOptions)`。声明不代表用户已授权,也不会自动启用工具。模型查询需要注册 `data_access_query`;HealthKit 授权后重新查询,位置和日历使用一次性授权与 `data_access_read_result`,不放进 `requirements`。
378
369
 
379
370
  ## 在模拟器里聊一次
380
371
 
@@ -393,7 +384,7 @@ buddy-cli preview --directory ./study-buddy
393
384
 
394
385
  `dev` 启动本地 Agent 并连接 MLB;`preview` 连接当前工程并打开对应搭子的创造者后台「开发调试」,等待开发者在后台启动 DEV。本地进程准备好之后,CLI 通过平台通知接收消息,不需要公网隧道。
395
386
 
396
- 平台身份保存在 `buddy.agent.json.buddyId`,模拟器访问的是对应的 `<buddyId>-dev`。MLB 决定当前账号是否可以访问这只开发版搭子。不要把本地 `buddyId` 改成 `-dev`,也不需要传 `x-mlb-dev`。
387
+ 平台身份保存在 `package.json.buddyId`,模拟器访问的是对应的 `<buddyId>-dev`。MLB 决定当前账号是否可以访问这只开发版搭子。不要把本地 `buddyId` 改成 `-dev`,也不需要传 `x-mlb-dev`。
397
388
 
398
389
  打开模拟器就会查询 `GET /app/buddies/:buddyId`,不必先启动 DEV。名称来自云端资料,预览标识来自服务端 `isDev`;详情查询不会添加搭子或触发欢迎。
399
390
 
@@ -433,7 +424,7 @@ buddy-cli push --directory ./study-buddy --commit "根据完成情况调整练
433
424
  buddy-cli status --directory ./study-buddy
434
425
  ```
435
426
 
436
- `push` 收集 `buddy.agent.json`、源码和依赖锁文件,生成 `tar.gz`,上传到 `/cli/buddies/:id/packages/push`。`commit` 是改动说明,与本地 Git 是否提交无关。版本由 MLB 返回,CLI 不上传自己生成的 tag 或提审时间戳。
427
+ `push` 收集包含顶层 `buddyId` 的 `package.json`、`data-access.json`、源码和依赖锁文件,生成 `tar.gz`,上传到 `/cli/buddies/:id/packages/push`。`commit` 是改动说明,与本地 Git 是否提交无关。版本由 MLB 返回,CLI 不上传自己生成的 tag 或提审时间戳。
437
428
 
438
429
  审核状态以平台响应为准,可能已经是 `approved`,不能仅因命令名叫“提审”就假定一定进入等待审核。`status` 查询搭子资料和最近 5 次程序包记录,显示线上版本、运行状态、审核结果及说明。
439
430
 
@@ -441,7 +432,7 @@ CLI 不提供 `release`、`publish`、`disable`、`enable` 或 `unpublish`。最
441
432
 
442
433
  ### 上传前检查
443
434
 
444
- - 必须包含非空且不超过 64 KiB 的 `agent.md`、业务入口 `src/buddy-options.mjs`、`start.mjs`,以及 `scripts.start` 为 `node --env-file-if-exists=.env start.mjs`(0.16.0 及以上 Runtime)、在 dependencies 中声明准确 Runtime 版本的 `package.json`。
435
+ - 必须包含非空且不超过 64 KiB 的 `agent.md`、业务入口 `src/buddy-options.mjs`、`start.mjs`,以及 `scripts.start` 为 `node --env-file-if-exists=.env start.mjs`(0.17.0 及以上 Runtime)、在 dependencies 中声明准确 Runtime 版本的 `package.json`。
445
436
  - 保留一份有效的依赖锁文件(`package-lock.json`、`pnpm-lock.yaml` 或 `yarn.lock`),并确保 `src/buddy-options.mjs` 与它引用的业务源码实际进入程序包。模板恢复会沿用已有锁文件,不另加一份 npm 锁文件。
446
437
  - `.buddy/`、依赖安装目录、常见凭据文件和私钥不会进入包。默认排除包括 `.env`、`.env.*`(含示例)、`*.env`、`.envrc`、`.npmrc`、`.yarnrc`、`.yarnrc.yml`,以及 `.ssh`、`.aws` 等目录;完整规则见 [打包实现](../../packages/cli-core/src/push/preflight.ts)。
447
438
  - 排除不依赖 Git,也不继承 `.gitignore`。放进 Git 忽略列表,不代表不会上传。
@@ -486,7 +477,7 @@ buddy-cli datasets --directory ./my-buddy --json
486
477
 
487
478
  目录来自 npm 最新 Runtime 的 `DATA_ACCESS_CAPABILITIES`,包括 ID、名称、来源和 Schema 版本。JSON 返回版本信息、适配状态和 `datasets` 数组(字段见上方模型目录说明)。命令不登录、不启动搭子、不申请权限,也不修改配置;从子目录运行会向上找到工程。每次都需要联网检查 latest,下载失败时明确报错,不把旧缓存称为最新版。
488
479
 
489
- 开发者将实际选择的 ID 和用途写进 `buddy.agent.json.datasets`。JSON Schema 检查形状与用途;DEV 和上传预检按工程安装的 Runtime 检查所选模型与非空数据声明。目录缺失、依赖未安装或版本不匹配时明确报错。Runtime 0.12.2 有 10 项,但单次数据查询仍最多 8 项。
480
+ 开发者将实际选择的 ID 和用途写进 `data-access.json.requirements`。JSON Schema 检查形状与用途;DEV 和上传预检按工程安装的 Runtime 检查所选模型与非空数据声明。目录缺失、依赖未安装或版本不匹配时明确报错。Runtime 0.12.2 有 10 项,但单次数据查询仍最多 8 项。
490
481
 
491
482
  Preview 文本发送采用 App WebSocket 的 `messages` 数组,并校验逐项回执。`ask_question` 的全部选项以文字显示,开发者直接回复选择;权限卡提示到搭搭 App 处理,浏览器不模拟系统授权。
492
483
 
@@ -500,7 +491,7 @@ CLI 0.14.1 起,开发 Token 优先从工程 `.env` 读取;首次启动时可
500
491
 
501
492
  账号登录凭据仍保存在 macOS Keychain,不写入工程。开发 Token 保存在权限为 `0600` 的工程 `.env`,不传给 Web,也不进入上传包或命令行参数。
502
493
 
503
- ### Runtime 0.16.0 本地配置
494
+ ### Runtime 0.17.0 本地配置
504
495
 
505
496
  首次启动 DEV 时,CLI 将 `BUDDY_ID`、开发版 `BUDDY_TOKEN` 和 `MLB_GATEWAY_URL` 写入工程 `.env`,保留其他配置和注释。后续优先复用;`--refresh-token` 重新领取后更新此文件。开发者登录凭据仍使用系统凭据存储。`.env` 加入 Git 忽略,且始终排除在上传包之外。
506
497
 
@@ -509,3 +500,9 @@ CLI、Preview 与标准 `npm start` 均由 Node 的 `--env-file-if-exists=.env`
509
500
  ### CLI 0.15.1 的 Skill 包名
510
501
 
511
502
  Creator 使用 `@my-life-buddies/buddy-creator-skill`,头像使用 `@my-life-buddies/avatar-creator-skill`。缓存按新包名分别保存;本次不提供旧包或旧锁文件迁移。
503
+
504
+ ### CLI 0.16 工程配置
505
+
506
+ 新工程以 `package.json.buddyId` 作为唯一工程绑定,不生成 `buddy.agent.json`,也不需要工程配置的 `schemaVersion`。创建阶段先保存 `{ "buddyId": "平台返回的 ID" }`,生成代码时在同一文件补齐 npm 字段,不覆盖已有工程配置。模型直接维护在业务工厂中,数据声明独立放在 `data-access.json`。本次不提供旧工程自动迁移。
507
+
508
+ 本地 CLI 校验绑定与 `.env.BUDDY_ID`,准备开发凭据;托管身份由 MLB Server 注入环境变量。生成的 `start.mjs` 不读取 `package.json` 或比较 ID。Server 对包中 ID 的部署校验由 Server 实现,CLI 不代替服务端鉴权。
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,
@@ -438,9 +438,42 @@ function assertSupportedDeveloperPlatform(platform = process.platform) {
438
438
  // apps/cli/src/self-update.ts
439
439
  import { execFile } from "node:child_process";
440
440
  import { lstat, readFile, realpath } from "node:fs/promises";
441
- import { homedir } from "node:os";
442
- import { isAbsolute, join } from "node:path";
441
+ import { homedir as homedir2 } from "node:os";
442
+ import { isAbsolute, join as join2 } from "node:path";
443
443
  import { promisify } from "node:util";
444
+
445
+ // apps/cli/src/update-lock.ts
446
+ import { mkdir, rm, writeFile } from "node:fs/promises";
447
+ import { dirname, join } from "node:path";
448
+ import { homedir } from "node:os";
449
+ import { createHash as createHash2 } from "node:crypto";
450
+ import { setTimeout as delay } from "node:timers/promises";
451
+ var defaultUpdateCache = () => join(homedir(), ".cache", "buddy-cli", "updates");
452
+ var globalUpdateLock = (cache, prefix) => join(cache, "locks", createHash2("sha256").update(prefix).digest("hex"));
453
+ async function withUpdateLock(path, signal, action) {
454
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
455
+ const deadline = Date.now() + 2e5;
456
+ for (; ; ) {
457
+ signal?.throwIfAborted();
458
+ try {
459
+ await mkdir(path, { mode: 448 });
460
+ break;
461
+ } catch (cause) {
462
+ if (cause.code !== "EEXIST") throw cause;
463
+ 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}`);
464
+ await delay(100, void 0, { signal });
465
+ }
466
+ }
467
+ try {
468
+ await writeFile(`${path}/owner.json`, JSON.stringify({ pid: process.pid, startedAt: Date.now() }), { mode: 384 });
469
+ signal?.throwIfAborted();
470
+ return await action();
471
+ } finally {
472
+ await rm(path, { recursive: true, force: true });
473
+ }
474
+ }
475
+
476
+ // apps/cli/src/self-update.ts
444
477
  var PACKAGE = "@my-life-buddies/cli";
445
478
  var REGISTRY = "https://registry.npmjs.org/";
446
479
  var MANUAL_UPDATE = `npm install --global ${PACKAGE}@latest --registry=${REGISTRY}`;
@@ -455,9 +488,12 @@ var HELP = `\u7528\u6CD5:
455
488
  --json \u8F93\u51FA JSON\uFF0C\u4F9B Coding Agent \u6216\u811A\u672C\u8BFB\u53D6
456
489
 
457
490
  \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
491
+ \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
492
+ --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
493
+ 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
494
  \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
495
+ \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
496
+ \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
497
  \u65E7\u7248 CLI \u5C1A\u65E0 update \u547D\u4EE4\u65F6\uFF0C\u8FD0\u884C\uFF1A
462
498
  ${MANUAL_UPDATE}`;
463
499
  function parseVersion(version) {
@@ -510,11 +546,11 @@ async function latestVersion(options) {
510
546
  }
511
547
  async function installUpdate(version, options) {
512
548
  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 };
549
+ const settings = { cwd: homedir2(), signal: options.signal, timeout: 1e4 };
514
550
  const { stdout: stdout2 } = await runNpm(["prefix", "--global"], settings);
515
551
  const prefix = stdout2.trim();
516
552
  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);
553
+ const installedRoot = join2(prefix, "lib", "node_modules", PACKAGE);
518
554
  let sameInstallation = false;
519
555
  try {
520
556
  sameInstallation = !(await lstat(installedRoot)).isSymbolicLink() && await realpath(installedRoot) === await realpath(options.packageRoot);
@@ -523,28 +559,33 @@ async function installUpdate(version, options) {
523
559
  if (!sameInstallation) {
524
560
  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
561
  }
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
- }
562
+ await withUpdateLock(globalUpdateLock(options.cacheDirectory ?? defaultUpdateCache(), prefix), options.signal, async () => {
563
+ const current = JSON.parse(await readFile(join2(installedRoot, "package.json"), "utf8"));
564
+ if (current.name === PACKAGE && current.version === version) return;
565
+ 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");
566
+ try {
567
+ await runNpm([
568
+ "install",
569
+ "--global",
570
+ `${PACKAGE}@${version}`,
571
+ "--prefix",
572
+ prefix,
573
+ `--registry=${REGISTRY}`,
574
+ `--@my-life-buddies:registry=${REGISTRY}`,
575
+ "--engine-strict",
576
+ "--no-audit",
577
+ "--no-fund"
578
+ ], { ...settings, timeout: 18e4 });
579
+ } catch (cause) {
580
+ 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");
581
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "unknown";
582
+ 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}`);
583
+ }
584
+ const installed = JSON.parse(await readFile(join2(installedRoot, "package.json"), "utf8"));
585
+ if (installed.name !== PACKAGE || installed.version !== version) {
586
+ 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}`);
587
+ }
588
+ });
548
589
  }
549
590
  async function runSelfUpdateCommand(args, io, options) {
550
591
  const versionOnly = args[0] === "--version" || args[0] === "-v";
@@ -599,6 +640,143 @@ async function runSelfUpdateCommand(args, io, options) {
599
640
  }
600
641
  }
601
642
 
643
+ // apps/cli/src/auto-update.ts
644
+ import { execFile as execFile2, spawn } from "node:child_process";
645
+ 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";
646
+ import { constants, homedir as homedir3 } from "node:os";
647
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
648
+ import { promisify as promisify2 } from "node:util";
649
+ var PACKAGE2 = "@my-life-buddies/cli";
650
+ var REGISTRY2 = "https://registry.npmjs.org/";
651
+ var execute2 = promisify2(execFile2);
652
+ var HANDOFF_ENV = "BUDDY_CLI_UPDATE_HANDOFF";
653
+ async function installedVersion(root) {
654
+ try {
655
+ const pkg = JSON.parse(await readFile2(join3(root, "package.json"), "utf8"));
656
+ return pkg.name === PACKAGE2 && typeof pkg.version === "string" ? pkg.version : void 0;
657
+ } catch {
658
+ return void 0;
659
+ }
660
+ }
661
+ async function checkedEntry(root, version) {
662
+ 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`);
663
+ const entry = join3(root, "dist", "bin", "buddy.js");
664
+ if (!(await lstat2(entry)).isFile()) throw new Error("\u65B0\u7248 CLI \u7F3A\u5C11\u6709\u6548\u7684\u6267\u884C\u5165\u53E3\u3002");
665
+ return entry;
666
+ }
667
+ async function prepareLatest(version, options) {
668
+ const cache = options.cacheDirectory ?? defaultUpdateCache();
669
+ const runNpm = options.runNpm ?? ((args, settings2) => execute2("npm", args, { ...settings2, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }));
670
+ const settings = { cwd: homedir3(), signal: options.signal, timeout: 1e4 };
671
+ const { stdout: stdout2 } = await runNpm(["prefix", "--global"], settings);
672
+ const prefix = stdout2.trim();
673
+ if (!isAbsolute2(prefix) || /[\r\n]/u.test(prefix)) throw new Error("\u65E0\u6CD5\u786E\u5B9A npm \u5168\u5C40\u5B89\u88C5\u76EE\u5F55\u3002");
674
+ const globalRoot = join3(prefix, "lib", "node_modules", PACKAGE2);
675
+ let isGlobal = false;
676
+ try {
677
+ isGlobal = !(await lstat2(globalRoot)).isSymbolicLink() && await realpath2(globalRoot) === await realpath2(options.packageRoot);
678
+ } catch {
679
+ }
680
+ if (isGlobal) {
681
+ await installUpdate(version, options);
682
+ return checkedEntry(globalRoot, version);
683
+ }
684
+ const destination = join3(cache, "releases", version);
685
+ return withUpdateLock(join3(cache, "locks", `release-${version}`), options.signal, async () => {
686
+ const root = join3(destination, "node_modules", PACKAGE2);
687
+ try {
688
+ return await checkedEntry(root, version);
689
+ } catch {
690
+ }
691
+ await mkdir2(join3(cache, "releases"), { recursive: true, mode: 448 });
692
+ const temporary = await mkdtemp(join3(cache, "releases", ".install-"));
693
+ try {
694
+ await writeFile2(join3(temporary, "package.json"), JSON.stringify({ private: true }));
695
+ try {
696
+ await runNpm(
697
+ [
698
+ "install",
699
+ "--prefix",
700
+ temporary,
701
+ "--save-exact",
702
+ `${PACKAGE2}@${version}`,
703
+ `--registry=${REGISTRY2}`,
704
+ `--@my-life-buddies:registry=${REGISTRY2}`,
705
+ "--engine-strict",
706
+ "--no-audit",
707
+ "--no-fund"
708
+ ],
709
+ { cwd: temporary, signal: options.signal, timeout: 18e4 }
710
+ );
711
+ } catch (cause) {
712
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "unknown";
713
+ 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`);
714
+ }
715
+ await checkedEntry(join3(temporary, "node_modules", PACKAGE2), version);
716
+ options.signal?.throwIfAborted();
717
+ await rm2(destination, { recursive: true, force: true });
718
+ await rename(temporary, destination);
719
+ return checkedEntry(root, version);
720
+ } finally {
721
+ await rm2(temporary, { recursive: true, force: true });
722
+ }
723
+ });
724
+ }
725
+ async function runUpdatedCli(entry, args, options) {
726
+ options.signal?.throwIfAborted();
727
+ return new Promise((resolve2, reject) => {
728
+ const child = spawn(process.execPath, [entry, ...args], { cwd: options.cwd, env: options.env, stdio: "inherit" });
729
+ let killTimer;
730
+ const abort = () => {
731
+ const signal = options.signal?.reason?.message === "SIGINT" ? "SIGINT" : "SIGTERM";
732
+ child.kill(signal);
733
+ killTimer ??= setTimeout(() => child.kill("SIGKILL"), 5e3);
734
+ killTimer.unref();
735
+ };
736
+ const cleanup = () => {
737
+ options.signal?.removeEventListener("abort", abort);
738
+ clearTimeout(killTimer);
739
+ };
740
+ child.once("error", (error) => {
741
+ cleanup();
742
+ reject(error);
743
+ });
744
+ child.once("exit", (code, signal) => {
745
+ cleanup();
746
+ resolve2({ exitCode: code ?? 128 + (signal ? constants.signals[signal] : 1) });
747
+ });
748
+ options.signal?.addEventListener("abort", abort, { once: true });
749
+ if (options.signal?.aborted) abort();
750
+ });
751
+ }
752
+ async function runAutoUpdate(args, io, options) {
753
+ if (args[0] === "help" || args[0] === "update" || args[0] === "--version" || args[0] === "-v" || args.includes("--help") || args.includes("-h")) return void 0;
754
+ try {
755
+ const env = { ...options.env ?? process.env };
756
+ const handoff = env[HANDOFF_ENV];
757
+ if (handoff !== void 0) {
758
+ delete env[HANDOFF_ENV];
759
+ if (!options.env) delete process.env[HANDOFF_ENV];
760
+ const expected = JSON.parse(handoff);
761
+ const entry2 = await realpath2(join3(options.packageRoot, "dist", "bin", "buddy.js"));
762
+ 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");
763
+ return void 0;
764
+ }
765
+ const latest = await latestVersion(options);
766
+ if (compareVersions(latest, options.version) <= 0) return void 0;
767
+ 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`);
768
+ const entry = await realpath2(await prepareLatest(latest, options));
769
+ options.signal?.throwIfAborted();
770
+ env[HANDOFF_ENV] = JSON.stringify({ parent: process.pid, version: latest, entry, args });
771
+ return await (options.runCli ?? runUpdatedCli)(entry, args, { cwd: options.cwd ?? process.cwd(), env, signal: options.signal });
772
+ } catch (cause) {
773
+ 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)}`;
774
+ if (args.includes("--json")) io.write(JSON.stringify({ code: "CLI_UPDATE_FAILED", error }));
775
+ else io.writeError(error);
776
+ return { exitCode: 1 };
777
+ }
778
+ }
779
+
602
780
  // apps/cli/src/bin/buddy.ts
603
781
  import { latestRuntimeTemplate, scaffoldPlatformAgent } from "./core.js";
604
782
  var TerminalIO = class {
@@ -616,7 +794,7 @@ var TerminalIO = class {
616
794
  }
617
795
  async openMetaPreview(path) {
618
796
  try {
619
- await promisify2(execFile2)("open", [path]);
797
+ await promisify3(execFile3)("open", [path]);
620
798
  } catch {
621
799
  this.write(`\u8BF7\u6253\u5F00\u672C\u5730\u5934\u50CF\u9884\u89C8\u540E\u518D\u786E\u8BA4\uFF1A${path}`);
622
800
  }
@@ -683,11 +861,12 @@ try {
683
861
  const args = process.argv.slice(2);
684
862
  const require2 = createRequire(import.meta.url);
685
863
  const manifestPath = require2.resolve("@my-life-buddies/cli/package.json");
686
- const maintenance = await runSelfUpdateCommand(args, terminal, {
864
+ const updateOptions = {
687
865
  version: require2(manifestPath).version,
688
- packageRoot: dirname(manifestPath),
866
+ packageRoot: dirname2(manifestPath),
689
867
  signal: lifecycle.signal
690
- });
868
+ };
869
+ const maintenance = await runSelfUpdateCommand(args, terminal, updateOptions) ?? await runAutoUpdate(args, terminal, updateOptions);
691
870
  const command = args[0];
692
871
  let platformHost;
693
872
  const host = () => platformHost ??= new BuddyPlatformHost({ signal: lifecycle.signal });