cardbuddy 0.19.0 → 0.19.2

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
@@ -67,6 +67,38 @@ npm run preview # 本地服务(8787) + 内置编辑器前端 +
67
67
 
68
68
  ---
69
69
 
70
+ ## 升级 cardbuddy 后同步 skills(`cardbuddy skills upgrade`)
71
+
72
+ `init` 会把技能库复制进工程的 `skills/`;而运行时**项目 `skills/` 优先于包内快照**
73
+ (见 `server/core/skillPaths.js`),所以 `npm i cardbuddy@latest` 只是刷新了包内快照,
74
+ **不会**更新工程里正在用的 `skills/` 副本——必须显式拉一次。本命令就是为这个场景而生,
75
+ 且**只动 `skills/`**,绝不碰 `.env.cb` / `package.json` / `README.md`(那得用 `init --force`,有冲配置风险)。
76
+
77
+ ```bash
78
+ npm i cardbuddy@latest
79
+ npx cardbuddy skills upgrade # 新增的 skill 复制进来;本地改过的 shipped skill 默认跳过并黄色警告
80
+ npx cardbuddy skills upgrade --force # 确认要抛弃本地改动、用包内版本覆盖时
81
+ npx cardbuddy skills upgrade --dry-run # 先看会动哪些文件(四类统计,不落盘)
82
+ npx cardbuddy skills upgrade --prune # 额外删除包内已不存在的技能文件(默认保留,防误删你的私有 skill)
83
+ ```
84
+
85
+ 按**内容比对**(非 mtime)分四类处理:
86
+
87
+ | 分类 | 判定 | 默认行为 | 风险 |
88
+ | --- | --- | --- | --- |
89
+ | `ADDED` | 包内有、本地无 | 复制 | 无(新 skill) |
90
+ | `SAME` | 两边内容一致 | 跳过 | 无 |
91
+ | `MODIFIED` | 两边都有、内容不同 | **跳过 + 黄色警告列清单** | ⚠️ 你可能改过该 shipped skill;`--force` 才覆盖 |
92
+ | `ORPHAN` | 本地有、包内无 | **保留不删** | ⚠️ 可能是你私有的 skill,或上游已下架该 skill;`--prune` 才删 |
93
+
94
+ > `init` 生成的工程已带 `npm run upgrade:skills` 脚本(等价 `cardbuddy skills upgrade`),
95
+ > 要覆盖时用 `npm run upgrade:skills -- --force`。
96
+
97
+ 同步后写 `skills/.synced-from = 当前装包版本`,供下次 drift 检测(Phase 2:服务启动时可据此提示
98
+ "skills 停留在 X,包已 Y")。
99
+
100
+ ---
101
+
70
102
  ## Preview 模式(本地独立调试)
71
103
 
72
104
  ```bash
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { startServer } from './standalone.js'
19
19
  import { installLogMirror } from './core.js'
20
20
  import { loadConfig } from './config.js'
21
21
  import { runInit } from './init.js'
22
+ import { runSkillsUpgrade } from './skills-upgrade.js'
22
23
 
23
24
  const args = process.argv.slice(2)
24
25
  const mcpMode = args.includes('--mcp')
@@ -66,6 +67,7 @@ if (args.includes('--help') || args.includes('-h')) {
66
67
  用法:
67
68
  cardbuddy [--preview] [--mcp] [--version] [--help]
68
69
  cardbuddy init [目标目录] [--force] [--dry-run] [--no-skills] [--no-sample]
70
+ cardbuddy skills upgrade [目标目录] [--force] [--dry-run] [--prune] [--strict]
69
71
 
70
72
  命令:
71
73
  init 生成「立刻能开发调试」的卡片工程最小实例(增量、幂等,不覆盖已存在文件):
@@ -77,6 +79,20 @@ if (args.includes('--help') || args.includes('-h')) {
77
79
  --no-sample 不生成示例卡
78
80
  典型用法:npx cardbuddy init my-cards && cd my-cards && npm i && npm run preview
79
81
 
82
+ skills upgrade 升级 cardbuddy 包后,把包内最新技能库同步进当前工程的 skills/(增量拉取 skill 更新):
83
+ 仅动 skills/,不碰 .env.cb / package.json / README.md(那要用 init --force,有冲配置风险)。
84
+ 运行时「项目 skills/ 优先于包内快照」,故只 npm i cardbuddy@latest 不会更新工程里的
85
+ skills 副本,需显式拉一次。按内容比对分四类:
86
+ ADDED 包内有、本地无 → 复制(安全)
87
+ SAME 两边一致 → 跳过
88
+ MODIFIED 两边都有、内容不同 → 默认跳过并黄色警告(防丢你改过的 shipped skill)
89
+ ORPHAN 本地有、包内无 → 默认保留不删(可能是你私有的或上游已下架)
90
+ --force 覆盖本地已修改的 shipped skill(丢弃你的改动)
91
+ --dry-run 只报告四类统计,不落盘
92
+ --prune 额外删除包内已不存在的技能文件(默认保留,防误删你的私有 skill)
93
+ --strict 存在未覆盖的 MODIFIED 时返回非 0(便于 CI 感知)
94
+ 典型用法:npm i cardbuddy@latest && npx cardbuddy skills upgrade
95
+
80
96
  选项:
81
97
  --preview, -p 同时托管打包后的编辑器前端(本地预览),替代线上地址,并自动打开浏览器
82
98
  旧名 --offline / -o 仍可用,已废弃
@@ -106,10 +122,12 @@ if (args.includes('--help') || args.includes('-h')) {
106
122
  process.exit(0)
107
123
  }
108
124
 
109
- // 未知子命令:非 flag 的位置参数只有 init 合法(避免 `cardbuddy inti` 静默起服务)
125
+ // 未知子命令:非 flag 的位置参数只有 init / skills 合法(避免 `cardbuddy inti` 静默起服务)
110
126
  const subCommand = args.find((a) => !a.startsWith('-'))
111
- if (subCommand && subCommand !== 'init') {
112
- console.error(srcTag + chalk.red(`未知命令:${subCommand}(可用:init;或直接启动服务,见 --help)`))
127
+ const isSkillsUpgrade =
128
+ args.includes('upgrade-skills') || (subCommand === 'skills' && args.includes('upgrade'))
129
+ if (subCommand && subCommand !== 'init' && subCommand !== 'skills' && !isSkillsUpgrade) {
130
+ console.error(srcTag + chalk.red(`未知命令:${subCommand}(可用:init、skills upgrade;或直接启动服务,见 --help)`))
113
131
  process.exit(1)
114
132
  }
115
133
 
@@ -144,6 +162,50 @@ if (subCommand === 'init') {
144
162
  }
145
163
  }
146
164
 
165
+ // skills upgrade 子命令:把已装包内最新技能库同步进当前工程的 skills/(升级包后拉取 skill 更新)。
166
+ // 只动 skills/,不碰 .env.cb / package.json / README.md。支持 `skills upgrade` 与 `upgrade-skills` 两种写法。
167
+ if (isSkillsUpgrade) {
168
+ const marker = args.includes('upgrade-skills')
169
+ ? args.indexOf('upgrade-skills')
170
+ : args.indexOf('upgrade')
171
+ const rest = args.slice(marker + 1)
172
+ const flags2 = new Set(rest.filter((a) => a.startsWith('-')))
173
+ const positional2 = rest.filter((a) => !a.startsWith('-'))
174
+ const knownFlags = ['--force', '--dry-run', '--prune', '--strict']
175
+ const unknown = [...flags2].filter((f) => !knownFlags.includes(f))
176
+ if (unknown.length) {
177
+ console.error(srcTag + chalk.red(`skills upgrade 不支持的参数:${unknown.join(' ')}(可用:${knownFlags.join(' ')})`))
178
+ process.exit(1)
179
+ }
180
+ if (positional2.length > 1) {
181
+ console.error(srcTag + chalk.red('skills upgrade 只接受一个目标目录参数(收到:' + positional2.join(' ') + ')'))
182
+ process.exit(1)
183
+ }
184
+ try {
185
+ process.exit(
186
+ await runSkillsUpgrade({
187
+ targetDir: positional2[0] || '.',
188
+ force: flags2.has('--force'),
189
+ dryRun: flags2.has('--dry-run'),
190
+ prune: flags2.has('--prune'),
191
+ strict: flags2.has('--strict'),
192
+ config: loadConfig(),
193
+ })
194
+ )
195
+ } catch (e) {
196
+ console.error(srcTag + chalk.red('skills upgrade 失败:') + (e && e.stack ? e.stack : e))
197
+ process.exit(1)
198
+ }
199
+ }
200
+
201
+ // 仅 `skills` 而无 upgrade:提示唯一合法子命令
202
+ if (subCommand === 'skills' && !isSkillsUpgrade) {
203
+ console.error(
204
+ srcTag + chalk.red('skills 子命令仅支持 upgrade(用法:cardbuddy skills upgrade [目标目录] [--force] [--dry-run] [--prune] [--strict])')
205
+ )
206
+ process.exit(1)
207
+ }
208
+
147
209
  if (mcpMode) {
148
210
  // stdio MCP server:不碰端口 / TLS / Vite,由 server.mjs 常驻监听 stdin 直到对端关闭。
149
211
  const { startMcpServer } = await import('./mcp.js')
package/dist/init.js CHANGED
@@ -197,6 +197,21 @@ skills/ AI 协作技能库(校验 / 同步 / 建卡管线脚本
197
197
  npm run link:skills # 交互式选择 Codex / Claude / WorkBuddy 等,建立项目级技能软链
198
198
  \`\`\`
199
199
 
200
+ ## 升级 cardbuddy 后同步 skills
201
+
202
+ \`cardbuddy init\` 会把技能库复制进本工程的 \`skills/\`,而运行时**项目 skills/ 优先于包内快照**
203
+ (见 \`server/core/skillPaths.js\`),所以只 \`npm i cardbuddy@latest\` 不会更新工程里的 skills 副本。
204
+ 装完新版后拉一次:
205
+
206
+ \`\`\`bash
207
+ npm run upgrade:skills # 新增的 skill 复制进来;本地改过的 shipped skill 默认跳过并警告
208
+ npx cardbuddy skills upgrade --force # 确认要抛弃本地改动、用包内版本覆盖时
209
+ npx cardbuddy skills upgrade --dry-run # 先看会动哪些文件(四类:新增/相同/已修改/本地私有)
210
+ \`\`\`
211
+
212
+ 该命令**只动 \`skills/\`**,绝不碰 \`.env.cb\` / \`package.json\` / \`README.md\`
213
+ (那得用 \`init --force\`,有冲掉端口 / FinMall 配置 / AI 网关占位的风险)。
214
+
200
215
  ## 把卡片工具挂给外部 AI agent(MCP,可选)
201
216
 
202
217
  编辑器内置的网页 AI 助手走 \`POST /__ai/chat\`。若要让 Codex / Claude 这类**外部** agent
@@ -259,6 +274,10 @@ function projectPackageJson(projectName, version) {
259
274
  // 因为 `npm run` 的横幅会打到 stdout、污染 JSON-RPC 协议流(详见生成的 README)。
260
275
  mcp: 'cardbuddy --mcp',
261
276
  'link:skills': 'node tools/link-skills.mjs',
277
+ // 升级 cardbuddy 后,把包内最新技能库同步进本工程的 skills/(仅动 skills/,
278
+ // 不碰 .env.cb / package.json / README.md;本地改过的 shipped skill 默认跳过并警告,
279
+ // 确认要抛弃改动时加 -- --force)。
280
+ 'upgrade:skills': 'cardbuddy skills upgrade',
262
281
  },
263
282
  devDependencies: {
264
283
  cardbuddy: `^${version}`,
@@ -397,6 +416,14 @@ export async function runInit({
397
416
  chalk.dim(`(新增 ${stat.copied} 文件 / ${fmtBytes(stat.bytes)},保留已存在 ${stat.skipped} 个${force ? ',--force 覆盖' : ''})`)
398
417
  )
399
418
  if (names.length) say(chalk.dim(` ${names.join('、')}`))
419
+ // 写同步标记(best-effort):记录本次 init 基于的 cardbuddy 版本,供 `skills upgrade` 做 drift 检测
420
+ if (!dryRun && fs.existsSync(dest)) {
421
+ try {
422
+ fs.writeFileSync(path.join(dest, '.synced-from'), ownVersion() + '\n', 'utf8')
423
+ } catch {
424
+ /* best-effort */
425
+ }
426
+ }
400
427
  }
401
428
  }
402
429