cardbuddy 0.19.1 → 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 +32 -0
- package/dist/cli.js +65 -3
- package/dist/init.js +27 -0
- package/dist/skills-upgrade.js +220 -0
- package/package.json +1 -1
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
|
-
|
|
112
|
-
|
|
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
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cardbuddy skills upgrade` —— 把已装 cardbuddy 包内最新的技能库同步进当前工程的 skills/。
|
|
3
|
+
*
|
|
4
|
+
* 背景:init 时把 dist/vendored/skills 复制进 <root>/skills/(增量幂等,见 init.js)。
|
|
5
|
+
* 运行时 skillPaths 解析「项目 skills/ 优先于包内快照」(server/core/skillPaths.js),
|
|
6
|
+
* 故只 `npm i cardbuddy@latest` 不会更新工程里的 skills 副本——必须显式拉一次。本命令
|
|
7
|
+
* 就是为这个场景而生,且**只动 skills/**,绝不碰 .env.cb / package.json / README.md
|
|
8
|
+
* (那是 `init --force` 的危险面,可能冲掉端口 / FinMall 配置 / AI 网关占位)。
|
|
9
|
+
*
|
|
10
|
+
* 四类 diff(内容比对,非 mtime):
|
|
11
|
+
* ADDED 源有、目标无 → 复制(安全)
|
|
12
|
+
* SAME 两边内容一致 → 跳过
|
|
13
|
+
* MODIFIED 两边都有、内容不同 → 默认跳过 + 黄色警告(覆盖风险:消费者可能改过该 shipped skill)
|
|
14
|
+
* ORPHAN 目标有、源无 → 默认保留不删(可能是消费者私有 skill,或上游已下架该 skill)
|
|
15
|
+
*
|
|
16
|
+
* 标志:--force 覆盖 MODIFIED;--prune 额外删除 ORPHAN(危险,需显式);--dry-run 只报告;
|
|
17
|
+
* --strict 存在 MODIFIED 且未 --force 时返回非 0(便于 CI 感知未决覆盖)。
|
|
18
|
+
* 同步后写 skills/.synced-from = 当前装包版本,供下次 drift 检测与启动提示。
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
import chalk from 'chalk'
|
|
24
|
+
import { loadConfig } from './config.js'
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
|
|
28
|
+
const SKIP_DIRS = new Set(['node_modules', '__pycache__', '.git', '.local', '.cache', '.history'])
|
|
29
|
+
const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db'])
|
|
30
|
+
const MARKER = '.synced-from'
|
|
31
|
+
|
|
32
|
+
function isSkipped(name, isDir) {
|
|
33
|
+
if (isDir) return SKIP_DIRS.has(name)
|
|
34
|
+
return SKIP_FILES.has(name) || name.endsWith('.pyc')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtBytes(n) {
|
|
38
|
+
if (n < 1024) return `${n} B`
|
|
39
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
40
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 当前装包版本:发布态/源码态都读 <pkg>/package.json(dist 与 src 同深度)。 */
|
|
44
|
+
function ownVersion() {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')).version || '*'
|
|
47
|
+
} catch {
|
|
48
|
+
return '*'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function contentEquals(a, b) {
|
|
53
|
+
try {
|
|
54
|
+
const sa = fs.statSync(a)
|
|
55
|
+
const sb = fs.statSync(b)
|
|
56
|
+
if (sa.size !== sb.size) return false
|
|
57
|
+
return fs.readFileSync(a).equals(fs.readFileSync(b))
|
|
58
|
+
} catch {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 递归列出 dir 下所有文件(跳过私有/缓存),返回相对 base 的正斜杠路径。 */
|
|
64
|
+
function walkFiles(dir, base = dir, out = []) {
|
|
65
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
66
|
+
const fp = path.join(dir, entry.name)
|
|
67
|
+
if (isSkipped(entry.name, entry.isDirectory())) continue
|
|
68
|
+
if (entry.isDirectory()) walkFiles(fp, base, out)
|
|
69
|
+
else if (entry.isFile()) out.push(path.relative(base, fp).split(path.sep).join('/'))
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 比对源/目标技能树(内容比对)。
|
|
76
|
+
* @returns {{ added:string[], same:string[], modified:string[], orphan:string[] }}
|
|
77
|
+
*/
|
|
78
|
+
export function diffSkills(src, dest) {
|
|
79
|
+
const res = { added: [], same: [], modified: [], orphan: [] }
|
|
80
|
+
if (fs.existsSync(src)) {
|
|
81
|
+
for (const rel of walkFiles(src)) {
|
|
82
|
+
const sp = path.join(src, rel)
|
|
83
|
+
const dp = path.join(dest, rel)
|
|
84
|
+
if (!fs.existsSync(dp)) res.added.push(rel)
|
|
85
|
+
else if (contentEquals(sp, dp)) res.same.push(rel)
|
|
86
|
+
else res.modified.push(rel)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (fs.existsSync(dest)) {
|
|
90
|
+
for (const rel of walkFiles(dest)) {
|
|
91
|
+
if (rel === MARKER) continue
|
|
92
|
+
if (!fs.existsSync(path.join(src, rel))) res.orphan.push(rel)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return res
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 执行 skills 升级。
|
|
100
|
+
* @param {object} opts
|
|
101
|
+
* @param {string} [opts.targetDir='.'] 目标工程根(skills 落在 <targetDir>/skills)
|
|
102
|
+
* @param {boolean} [opts.force] 覆盖本地已修改的 shipped skill
|
|
103
|
+
* @param {boolean} [opts.dryRun] 只报告不落盘
|
|
104
|
+
* @param {boolean} [opts.prune] 额外删除包内已不存在的技能文件(默认保留)
|
|
105
|
+
* @param {boolean} [opts.strict] 存在未覆盖 MODIFIED 时返回非 0
|
|
106
|
+
* @param {ReturnType<import('./config.js').loadConfig>} [opts.config]
|
|
107
|
+
* @param {(msg: string) => void} [opts.log]
|
|
108
|
+
* @returns {Promise<number>} 进程退出码
|
|
109
|
+
*/
|
|
110
|
+
export async function runSkillsUpgrade({
|
|
111
|
+
targetDir = '.',
|
|
112
|
+
force = false,
|
|
113
|
+
dryRun = false,
|
|
114
|
+
prune = false,
|
|
115
|
+
strict = false,
|
|
116
|
+
config,
|
|
117
|
+
log = (m) => console.log(m),
|
|
118
|
+
} = {}) {
|
|
119
|
+
const cfg = config || loadConfig()
|
|
120
|
+
const src = cfg.vendoredSkillsDir
|
|
121
|
+
const target = path.resolve(targetDir)
|
|
122
|
+
const destSkills = path.join(target, 'skills')
|
|
123
|
+
|
|
124
|
+
const tag = chalk.magenta('▌ ') + chalk.magenta.bold('cardbuddy skills upgrade') + ' '
|
|
125
|
+
const say = (m) => log(tag + m)
|
|
126
|
+
const ok = (m) => say(chalk.green(`• ${m}`))
|
|
127
|
+
const warn = (m) => say(chalk.yellow(`• ${m}`))
|
|
128
|
+
const dim = (m) => say(chalk.dim(`• ${m}`))
|
|
129
|
+
|
|
130
|
+
if (dryRun) say(chalk.yellow('(dry-run:只报告将要做的事,不落盘)'))
|
|
131
|
+
|
|
132
|
+
if (!src || !fs.existsSync(src)) {
|
|
133
|
+
log(chalk.red(`✖ 找不到包内技能库快照:vendoredSkillsDir 缺失(cardbuddy 安装不完整?)`))
|
|
134
|
+
return 1
|
|
135
|
+
}
|
|
136
|
+
if (path.resolve(src) === path.resolve(destSkills)) {
|
|
137
|
+
dim(`技能库源与目标同路径(${src}),无需升级`)
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const version = ownVersion()
|
|
142
|
+
const diff = diffSkills(src, destSkills)
|
|
143
|
+
|
|
144
|
+
let copied = 0
|
|
145
|
+
let bytes = 0
|
|
146
|
+
let overwritten = 0
|
|
147
|
+
let deleted = 0
|
|
148
|
+
|
|
149
|
+
// ADDED 总是复制;MODIFIED 仅 --force 时复制
|
|
150
|
+
const toCopy = [...diff.added, ...(force ? diff.modified : [])]
|
|
151
|
+
for (const rel of toCopy) {
|
|
152
|
+
const sp = path.join(src, rel)
|
|
153
|
+
const dp = path.join(destSkills, rel)
|
|
154
|
+
const isModified = diff.modified.includes(rel)
|
|
155
|
+
if (!dryRun) {
|
|
156
|
+
fs.mkdirSync(path.dirname(dp), { recursive: true })
|
|
157
|
+
fs.copyFileSync(sp, dp)
|
|
158
|
+
}
|
|
159
|
+
bytes += fs.statSync(sp).size
|
|
160
|
+
copied++
|
|
161
|
+
if (isModified) overwritten++
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MODIFIED 未 --force:警告(不复制,防丢改动)
|
|
165
|
+
if (!force && diff.modified.length) {
|
|
166
|
+
warn(`⚠ 检测到 ${diff.modified.length} 个已修改的技能文件(本地副本与包内不一致),默认不覆盖,避免丢失你的改动:`)
|
|
167
|
+
for (const rel of diff.modified) say(chalk.yellow(` ${rel}`))
|
|
168
|
+
dim(' 如需用包内版本覆盖,加 --force;想先看差异,加 --dry-run 后人工对比。')
|
|
169
|
+
} else if (force && diff.modified.length) {
|
|
170
|
+
warn(`--force:已用包内版本覆盖 ${diff.modified.length} 个本地修改的技能文件(你的改动将被丢弃)`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ORPHAN:默认保留;--prune 才删除
|
|
174
|
+
if (diff.orphan.length) {
|
|
175
|
+
if (prune) {
|
|
176
|
+
warn(`⚠ --prune:将删除 ${diff.orphan.length} 个包内已不存在的技能文件(本地私有或上游已下架):`)
|
|
177
|
+
for (const rel of diff.orphan) {
|
|
178
|
+
const dp = path.join(destSkills, rel)
|
|
179
|
+
say(chalk.yellow(` ${rel}`))
|
|
180
|
+
if (!dryRun) {
|
|
181
|
+
fs.rmSync(dp, { force: true })
|
|
182
|
+
deleted++
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
dim(`${diff.orphan.length} 个本地独有技能文件(包内无对应,可能是你私有的或上游已下架)—— 已保留不删`)
|
|
187
|
+
if (diff.orphan.length <= 20) for (const rel of diff.orphan) dim(` ${rel}`)
|
|
188
|
+
else dim(` (仅显示前 20,共 ${diff.orphan.length} 个;加 --prune 会删除它们,谨慎)`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 写同步标记(best-effort)
|
|
193
|
+
if (!dryRun) {
|
|
194
|
+
try {
|
|
195
|
+
fs.mkdirSync(destSkills, { recursive: true })
|
|
196
|
+
fs.writeFileSync(path.join(destSkills, MARKER), version + '\n', 'utf8')
|
|
197
|
+
} catch {
|
|
198
|
+
/* best-effort */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 汇总
|
|
203
|
+
log('')
|
|
204
|
+
say(chalk.green(dryRun ? 'dry-run 完成(未落盘)' : 'skills 升级完成'))
|
|
205
|
+
say(chalk.dim(` 源:包内 cardbuddy@${version}`))
|
|
206
|
+
const targetNote = fs.existsSync(destSkills) ? destSkills : `${destSkills}(将新建)`
|
|
207
|
+
say(chalk.dim(` 目标:${targetNote}`))
|
|
208
|
+
say(
|
|
209
|
+
chalk.dim(
|
|
210
|
+
` 新增 ${diff.added.length} · 相同 ${diff.same.length} · 覆盖 ${overwritten} · 保留私有 ${diff.orphan.length}` +
|
|
211
|
+
`${prune ? ` · 删除孤儿 ${deleted}` : ''} · 体积 ${fmtBytes(bytes)}`
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if (strict && diff.modified.length && !force) {
|
|
216
|
+
say(chalk.red('(--strict:存在未覆盖的 MODIFIED 文件,返回非 0)'))
|
|
217
|
+
return 2
|
|
218
|
+
}
|
|
219
|
+
return 0
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cardbuddy",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Local dev service for the CardBuddy card editor — decouples card CRUD (sync/save/validate/render-status/screenshot) from the static public editor, so the editor can be deployed to any static host while each developer runs a local service against their own card source.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|