cardbuddy 0.14.0 → 0.14.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.
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16-Cg6Nrosk.png" />
9
9
  <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32-B_1744sp.png" />
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-BQcES9QK.png" />
11
- <script type="module" crossorigin src="/assets/index-Bccog4uC.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-BN79pTkQ.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-cbTCHp8J.css">
13
13
  </head>
14
14
  <body>
@@ -22,8 +22,11 @@ import vue from '@vitejs/plugin-vue'
22
22
  import chokidar from 'chokidar'
23
23
  import chalk from 'chalk'
24
24
 
25
- import { mountCoreMiddlewares, installLogMirror, subscribeLog } from './core.js'
26
- import { refreshCardList } from '../../../server/core/cardFiles.js'
25
+ // 一律经 core.js 取 server/core 的东西:仓库内它桥到 ../../../server/core/index.js,
26
+ // 发布态由 build.mjs 生成 dist/core.js 桥到包内快照 ./vendored/server/core/index.js。
27
+ // 直接写仓库相对路径(如 ../../../server/core/cardFiles.js)在**安装态会解析到包外** —— 0.14.0
28
+ // 就是因此在全局安装后 ERR_MODULE_NOT_FOUND(连 --help 都起不来)。见 build.mjs 门禁 D。
29
+ import { mountCoreMiddlewares, installLogMirror, subscribeLog, refreshCardList } from './core.js'
27
30
  import { loadConfig } from './config.js'
28
31
  import { ensurePortFree } from './portGuard.js'
29
32
 
@@ -440,6 +443,11 @@ export async function startServer(userConfig = {}) {
440
443
  // (首次 AI 请求才 import),故这里在启动期注入即可;已有的显式环境变量优先,不覆盖。
441
444
  if (!process.env.CARD_ROOT) process.env.CARD_ROOT = config.root
442
445
  if (!process.env.CARD_API_BASE) process.env.CARD_API_BASE = apiBase
446
+ // 技能层(skills/*/scripts,发布态为包内快照)认宿主工程根用的是 DSL_CARD_PROJECT_ROOT。
447
+ // 这里显式桥一次:脚本不能靠「自身位置往上推」认工程根 —— 发布态它住在
448
+ // <pkg>/dist/vendored/skills/... 里,推出来的是包内快照目录,UI/ 产物会写进 node_modules
449
+ // (0.14.1 的 route_input 事故)。技能层的 AI 工具与 skill_exec 子进程都继承本进程 env。
450
+ if (!process.env.DSL_CARD_PROJECT_ROOT) process.env.DSL_CARD_PROJECT_ROOT = config.root
443
451
  // upstream(FINMALL_BASE_URL)透传给资源代理中间件,使其能回源 FinMall 取站内资源
444
452
  mountCoreMiddlewares(app, {
445
453
  root: config.root,
@@ -19,6 +19,12 @@ import { devApiBase } from '../tools/finmall-env.mjs'
19
19
 
20
20
  const __dirname = dirname(fileURLToPath(import.meta.url))
21
21
  const ROOT = process.env.CARD_ROOT ? resolve(process.env.CARD_ROOT) : resolve(__dirname, '..')
22
+ // 本模块下面会 import 技能层脚本(route_input / scaffold / …),它们按 DSL_CARD_PROJECT_ROOT
23
+ // 认宿主工程根。必须在**任何** skills/* 模块被加载之前落定 —— 那些模块在 import 期就算好路径常量
24
+ // (晚一步注入等于没注入)。CARD_ROOT 给了就以它为准;没有 CARD_ROOT 时 ROOT 已是本模块的上一级,
25
+ // 仓库内即仓库根。这样即使外部 MCP 客户端只配了命令、没配 cwd,技能层也不会把 UI/ 产物写进
26
+ // node_modules 里的包内快照(0.14.1 的 route_input 事故)。
27
+ if (!process.env.DSL_CARD_PROJECT_ROOT) process.env.DSL_CARD_PROJECT_ROOT = ROOT
22
28
  // dev server 端口由 .env.cb 的 CARDBUDDY_DEV_PORT 控制(默认 5180);
23
29
  // 仍可用 CARD_API_BASE 覆盖(如 dev server 跑在非默认端口)。
24
30
  const BASE = (process.env.CARD_API_BASE || devApiBase()).replace(/\/$/, '')
@@ -240,7 +240,7 @@ function buildSystemPrompt({ shellAllowed = false, vision = false, autoApprove =
240
240
  4. 改完用人话说明:改了什么、为什么、需要用户确认什么。
241
241
  5. 信息足够后立即收敛:直接调用工具落盘或回复方案;同一内容不要重复读取。**思考里不要逐字草拟 schema / IR / 长 JSON,也不要复述卡片数据**——那会同时吃掉思考与输出额度,想清结构就立刻调工具把内容写出去。
242
242
  6. 读文件用 fs_read(仓库内任意文本文件 / 挂载附件),列目录用 fs_list —— fs_read 只能读文件,对目录一定报错,别拿它试目录;被中止过的轮次按上下文里的中止标记从中断处继续,不要从零重做。
243
- 7. 跑技能脚本用 skill_exec(script 传 skills/<skill>/scripts/<脚本>,args 传参数数组):设计稿前处理管线与 spec_audit / crop_slices / expand_ir / iterate / check_fidelity 等建卡脚本都靠它。${skillAuto ? '本仓库已设「技能脚本免确认」:调用即执行(审计写 .cache/ai-audit.log),不要停下来等用户批准,也不要因为怕要授权而改用 shell_exec 绕路、或把命令甩给用户自己跑。' : '每次调用会先请用户确认(用户可选「本回合不再询问」),被拒就把命令原样写给用户自行运行。'}${shellAllowed ? `\n8. 技能脚本之外的本地命令(npm / node / git / rg / cd 等)用 shell_exec:经真实 shell 执行,管道 |、链接 && / ;、重定向、引号与循环 / 条件 / 子shell 都可用(其中真正执行的每个外部命令需在白名单),不必再为元字符把命令拆开;复制文件用 fs_copy。${shellAuto ? '本仓库已设「本地命令免确认」:调用即执行。' : '每次调用都会弹给用户确认,被拒返回错误。'}cwd 已是仓库根、技能脚本自己按仓库根推算落点,不要写 cd;白名单不含 sips / convert / magick / python3 —— 图像裁剪派生一律 crop_slices.mjs,Python 脚本一律 skill_exec,别去试系统图像命令。` : ''}
243
+ 7. 跑技能脚本用 skill_exec(script 传 skills/<skill>/scripts/<脚本>,args 传参数数组):设计稿前处理管线与 spec_audit / crop_slices / expand_ir / iterate / check_fidelity 等建卡脚本都靠它。${skillAuto ? '本仓库已设「技能脚本免确认」:调用即执行(审计写 .cache/ai-audit.log),不要停下来等用户批准,也不要因为怕要授权而改用 shell_exec 绕路、或把命令甩给用户自己跑。' : '每次调用会先请用户确认(用户可选「本回合不再询问」),被拒就把命令原样写给用户自行运行。'}${shellAllowed ? `\n8. 技能脚本之外的本地命令(npm / node / git / rg / cd 等)用 shell_exec:经真实 shell 执行,管道 |、链接 && / ;、重定向、引号与循环 / 条件 / 子shell 都可用(其中真正执行的每个外部命令需在白名单),不必再为元字符把命令拆开;复制文件用 fs_copy。${shellAuto ? '本仓库已设「本地命令免确认」:调用即执行。' : '每次调用都会弹给用户确认,被拒返回错误。'}cwd 已是工程根(技能脚本按 DSL_CARD_PROJECT_ROOT / cwd 认工程根,不按自身位置推算,落点由服务端注入),不要写 cd;白名单不含 sips / convert / magick / python3 —— 图像裁剪派生一律 crop_slices.mjs,Python 脚本一律 skill_exec,别去试系统图像命令。` : ''}
244
244
 
245
245
  ## 设计稿建卡协议(Stage Machine:逐阶段推进,上一阶段产物齐了才进下一阶段;用户挂载设计稿目录 / 图片并要求建卡时严格执行,不要自由发挥)
246
246
  S0 路由:输入形态已由服务端机械判定,写在「用户挂载的本地文件」段(kind / preprocess / next):照它执行,不要再调 card_route_input;该段缺失才自己调 card_route_input({ path })。
@@ -1306,7 +1306,9 @@ async function runShellExec(root, args, res, approved) {
1306
1306
  )
1307
1307
  }
1308
1308
 
1309
- /** 技能脚本执行:路径形状校验 → 解释器解析 → 审批(或按策略免确认)→ 跑(cwd=仓库根,脚本自己按仓库根推算落点) */
1309
+ /** 技能脚本执行:路径形状校验 → 解释器解析 → 审批(或按策略免确认)→ 跑(cwd=工程根;
1310
+ * 脚本认工程根走 DSL_CARD_PROJECT_ROOT > cwd,**不按自身位置推算** —— 发布态脚本住在包内快照里,
1311
+ * 按自身位置推会把 UI/ 产物写进 node_modules,见 tools/tests/cardbuddy-project-anchor.test.mjs) */
1310
1312
  async function runSkillExec(root, args, res, approved) {
1311
1313
  const script = resolveSkillScript(root, args.script)
1312
1314
  const argv = normalizeSkillArgs(args.args)
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { createSyncMiddleware, createPreviewMiddleware, createLoginCredentialsMiddleware } from './cardSync.js'
14
14
  import { createSliceUploadMiddleware } from './sliceUpload.js'
15
- import { createCardFilesMiddleware } from './cardFiles.js'
15
+ import { createCardFilesMiddleware, refreshCardList } from './cardFiles.js'
16
16
  import { createSaveMiddleware } from './cardSave.js'
17
17
  import { createImportMetaMiddleware } from './cardImportMeta.js'
18
18
  import { createHistoryMiddleware } from './cardHistory.js'
@@ -57,6 +57,12 @@ export {
57
57
  createCardImageMiddleware,
58
58
  createOpenFolderMiddleware,
59
59
  createCodeBridgeMiddleware,
60
+ // refreshCardList:standalone / --preview 服务在文件变动后重建卡片列表快照要用它。
61
+ // 必须经本入口导出,**不要**让包外调用方直接 import './cardFiles.js' —— 发布态
62
+ // dist/core.js 被构造成桥到包内快照(./vendored/server/core/index.js),而写死的仓库相对路径
63
+ // 在安装态会解析到包外(0.14.0 的 standalone.js 就因此 ERR_MODULE_NOT_FOUND)。
64
+ // build.mjs 门禁 D 会静态拦住这类「dist 里解析不到的相对引用」。
65
+ refreshCardList,
60
66
  createLogMiddleware,
61
67
  installLogMirror,
62
68
  subscribeLog,
@@ -52,9 +52,29 @@ export const baselinesDir =
52
52
  /** 兼容旧脚本的别名:宿主项目根目录。 */
53
53
  export const repoRoot = projectRoot
54
54
 
55
- /** skills 父目录(供需要跨 skill 的脚本使用)。 */
55
+ /**
56
+ * 本 skill 所在的 skills 父目录 —— **按脚本自身位置**推算。
57
+ * 仓库内即 <repo>/skills;发布态快照里是 <pkg>/dist/vendored/skills。
58
+ * 只用于「找不到宿主技能库时的兜底」,别拿它推宿主工程根(见 hostSkillsDir)。
59
+ */
56
60
  export const skillsDir = path.dirname(skillRoot)
57
61
 
62
+ /**
63
+ * 宿主工程的 skills 根 —— 给用户 / 模型看的命令里该用的那一份。
64
+ *
65
+ * 与 skillsDir 的区别(0.14.1 事故):发布态脚本住在 `<pkg>/dist/vendored/skills/...`,
66
+ * 那不是宿主的技能库;命令里出现 node_modules 路径既是错的、也误导模型去改包内文件。
67
+ * 解析顺序与 server/core/skillPaths.js 的只读候选一致:
68
+ * DSL_SKILLS_ROOT(显式)> <projectRoot>/skills(存在)> 本 skill 所在快照的 skills/(兜底)。
69
+ */
70
+ export const hostSkillsDir = (() => {
71
+ const explicit = process.env.DSL_SKILLS_ROOT
72
+ if (explicit && fs.existsSync(explicit)) return path.resolve(explicit)
73
+ const inProject = path.join(projectRoot, 'skills')
74
+ if (fs.existsSync(inProject)) return inProject
75
+ return skillsDir
76
+ })()
77
+
58
78
  /**
59
79
  * 解析卡片目录的绝对路径。
60
80
  * @param {string} id 卡片 id
@@ -64,9 +64,32 @@ baselines_dir = Path(
64
64
  repo_root = project_root
65
65
 
66
66
  # skills 父目录。
67
+ # 按脚本自身位置推算:仓库内即 <repo>/skills,发布态快照里是 <pkg>/dist/vendored/skills。
68
+ # 只用于「找不到宿主技能库时的兜底」,别拿它推宿主工程根(见 host_skills_dir)。
67
69
  skills_dir = skill_root.parent
68
70
 
69
71
 
72
+ def _host_skills_dir() -> Path:
73
+ """
74
+ 宿主工程的 skills 根 —— 给用户 / 模型看的命令里该用的那一份。
75
+
76
+ 与 skills_dir 的区别(0.14.1 事故):发布态脚本住在 <pkg>/dist/vendored/skills/...,
77
+ 那不是宿主的技能库;命令里出现 node_modules 路径既是错的、也误导模型去改包内文件。
78
+ 解析顺序与 server/core/skillPaths.js 的只读候选一致:
79
+ DSL_SKILLS_ROOT(显式)> <project_root>/skills(存在)> 本 skill 所在快照的 skills/(兜底)。
80
+ """
81
+ explicit = os.environ.get("DSL_SKILLS_ROOT")
82
+ if explicit and Path(explicit).is_dir():
83
+ return Path(explicit).resolve()
84
+ in_project = project_root / "skills"
85
+ if in_project.is_dir():
86
+ return in_project
87
+ return skills_dir
88
+
89
+
90
+ host_skills_dir = _host_skills_dir()
91
+
92
+
70
93
  def card_dir(card_id: str) -> Path:
71
94
  """解析卡片目录的绝对路径:`<cards_dir>/<id>`。"""
72
95
  return cards_dir / card_id
@@ -22,12 +22,12 @@ import fs from 'node:fs'
22
22
  import path from 'node:path'
23
23
  import { spawnSync } from 'node:child_process'
24
24
  import { fileURLToPath } from 'node:url'
25
- import { skillsDir } from './lib/paths.mjs'
25
+ import { hostSkillsDir, projectRoot } from './lib/paths.mjs'
26
26
 
27
27
  const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp'])
28
28
  /** 已知入口文件:判定交给它所在目录(index.html=MeaXure 导出;manifest/clusters/spec/prompt=要素包;schema=卡片目录)。 */
29
29
  const ENTRY_FILES = new Set(['index.html', 'manifest.json', 'clusters.json', 'spec.json', 'spec.md', 'prompt.md', 'schema.json'])
30
- const SKETCH_SKILL = path.join(skillsDir, 'sketch-spec-to-card')
30
+ const SKETCH_SKILL = path.join(hostSkillsDir, 'sketch-spec-to-card')
31
31
  /**
32
32
  * 多画板选择门禁(铁律见 references/rules.md §十三):UI 产物允许全量生成,
33
33
  * 但卡片生成必须以用户指定的单个画板 / 单个 cardKey 为单位。
@@ -36,13 +36,18 @@ const SKETCH_SKILL = path.join(skillsDir, 'sketch-spec-to-card')
36
36
  const SELECTION_GATE =
37
37
  '# 多画板门禁(rules.md §十三 + §13.4):用 spec_tree.mjs --list-cards 把全量画板按「画板序号.卡片序号」列出(一个画板含 N 张卡则展开为 K.1 ~ K.N)+ 末尾加「全部」;让用户选的方式 = 正文逐条写清单 + 一句提问后停止输出、等用户回序号再继续(**没有** request_user_input 这个工具,不要去找弹窗,也不要替用户挑);序号格式「画板序号.卡片序号」(如 1.2),单独回画板序号默认取该画板第 1 张卡;UI 产物可回「全部」全量生成,产卡默认单卡片(回「全部」才算显式批量授权);禁止自行挑画板或挑卡、禁止擅自批量产卡;清单逐行配图:每个卡片行下紧跟它自己的 card-N.png 绝对路径,同一画板的卡共享画板预览图(artboard.png)但各有聚焦图;与序号一一对应,缺图显式标缺失,禁止把图堆在清单末尾。'
38
38
  /**
39
- * 要素包根目录 UI/:与上游 `sketch-spec-to-card/scripts/parse_meaxure.py` 的 `REPO_ROOT/UI`
40
- * (按脚本自身路径推算,非 cwd)保持同一锚点,避免「建议命令」与「实际落点」分叉。
41
- * 可用 DSL_CARD_UI_DIR 直接指定,或用 DSL_CARD_PROJECT_ROOT 指定宿主工程根。
39
+ * 要素包根目录 UI/ —— **锚在宿主工程根,不锚在脚本自身位置**。
40
+ *
41
+ * 曾经的写法(`path.resolve(skillsDir, '..')`)在仓库内恰好等于工程根,怎么跑都对;
42
+ * 可发布态脚本住在 `<pkg>/dist/vendored/skills/...`,往上两级是包内快照目录,于是
43
+ * `card_route_input`(MCP 里直接吃包内快照那份)会把 uiDir 算成
44
+ * `node_modules/cardbuddy/dist/vendored/UI/<稿名>` —— 产物写进 node_modules、重装即丢,
45
+ * 同时把「建议命令」和实际落点一起带偏(0.14.1 的 npx 工程事故)。
46
+ * 工程根一律取 `projectRoot`(= DSL_CARD_PROJECT_ROOT || cwd,与 parse_meaxure.py /
47
+ * build_prompt.py / lib/paths.py 同一契约);可用 DSL_CARD_UI_DIR 直接指定 UI 根。
42
48
  */
43
- const REPO_ROOT = path.resolve(skillsDir, '..')
44
- const UI_ROOT =
45
- process.env.DSL_CARD_UI_DIR || path.join(process.env.DSL_CARD_PROJECT_ROOT || REPO_ROOT, 'UI')
49
+ const REPO_ROOT = projectRoot
50
+ const UI_ROOT = process.env.DSL_CARD_UI_DIR || path.join(REPO_ROOT, 'UI')
46
51
 
47
52
  function exists(target) {
48
53
  try {
@@ -250,8 +255,8 @@ function route(inputAbs) {
250
255
  skillChain: ['dsl-card-generator'],
251
256
  workflow: 'Workflow B — 编辑已有卡片',
252
257
  next: [
253
- `node ${shortPath(path.join(skillsDir, 'dsl-card-generator', 'scripts', 'describe_card.mjs'))} --id ${path.basename(inputAbs)}`,
254
- `node ${shortPath(path.join(skillsDir, 'dsl-card-generator', 'scripts', 'diff_card.mjs'))} --id ${path.basename(inputAbs)}`,
258
+ `node ${shortPath(path.join(hostSkillsDir, 'dsl-card-generator', 'scripts', 'describe_card.mjs'))} --id ${path.basename(inputAbs)}`,
259
+ `node ${shortPath(path.join(hostSkillsDir, 'dsl-card-generator', 'scripts', 'diff_card.mjs'))} --id ${path.basename(inputAbs)}`,
255
260
  ],
256
261
  note: '输入已是卡片目录,走编辑链路(card_node_models 定位 + add_component 增改 + diff_card 兜底),不需要设计稿前处理。',
257
262
  }
@@ -291,7 +296,7 @@ function route(inputAbs) {
291
296
  SELECTION_GATE,
292
297
  `# 拿到回复后(产物已存在,${done.via},已生成 ${done.generated}/${done.total} 画板),跳过前处理,只消费用户指定的那个画板:`,
293
298
  `cat ${shortPath(done.manifest)}`,
294
- `node ${shortPath(path.join(skillsDir, 'dsl-card-generator', 'scripts', 'spec_tree.mjs'))} --spec ${shortPath(path.join(uiDir, '<画板>', 'spec.json'))}`,
299
+ `node ${shortPath(path.join(hostSkillsDir, 'dsl-card-generator', 'scripts', 'spec_tree.mjs'))} --spec ${shortPath(path.join(uiDir, '<画板>', 'spec.json'))}`,
295
300
  ...(done.alsoFound && done.alsoFound.length
296
301
  ? [`# 同一导出另有产物目录(目标画板在主 manifest 里是 pending 时先去这些目录找,找到就直接用,别重跑管线):${done.alsoFound.map((m) => `${m.uiDir}(已生成 ${m.generated}/${m.total})`).join('、')}`]
297
302
  : []),
@@ -313,7 +318,7 @@ function route(inputAbs) {
313
318
  const bundle = detectUiBundle(inputAbs)
314
319
  if (bundle.matched) {
315
320
  const specHint = bundle.specJson
316
- ? `node ${shortPath(path.join(skillsDir, 'dsl-card-generator', 'scripts', 'spec_tree.mjs'))} --spec ${shortPath(bundle.specJson)}`
321
+ ? `node ${shortPath(path.join(hostSkillsDir, 'dsl-card-generator', 'scripts', 'spec_tree.mjs'))} --spec ${shortPath(bundle.specJson)}`
317
322
  : `cat ${shortPath(bundle.manifest || inputAbs)}`
318
323
  return {
319
324
  kind: 'ui-bundle',
@@ -332,7 +337,7 @@ function route(inputAbs) {
332
337
  specHint,
333
338
  ...(bundle.artboardBundle ? [] : [
334
339
  `# 先用 --list-cards 列出每个画板下的卡片清单(画板序号.卡片序号),交给用户选:`,
335
- `node ${shortPath(path.join(skillsDir, 'dsl-card-generator', 'scripts', 'spec_tree.mjs'))} --list-cards ${shortPath(inputAbs)}`,
340
+ `node ${shortPath(path.join(hostSkillsDir, 'dsl-card-generator', 'scripts', 'spec_tree.mjs'))} --list-cards ${shortPath(inputAbs)}`,
336
341
  SELECTION_GATE,
337
342
  ]),
338
343
  `# 再按 SKILL.md §8 Workflow A 六步闭环生成 schema(scaffold_card → add_component/expand_ir → validate+lint → render_diff)`,
@@ -24,13 +24,12 @@
24
24
  */
25
25
  import fs from 'node:fs'
26
26
  import path from 'node:path'
27
- import { fileURLToPath } from 'node:url'
28
27
  import { buildContentIndex, checkSliceName, sha256File } from './lib/slice_dedupe.mjs'
28
+ import { cardsDir as CARDS_DIR, projectRoot as REPO_ROOT, slicesDir as SLICES_DIR } from './lib/paths.mjs'
29
29
 
30
- const HERE = path.dirname(fileURLToPath(import.meta.url))
31
- const REPO_ROOT = path.resolve(HERE, '../../..')
32
- const SLICES_DIR = path.join(REPO_ROOT, 'src/slices')
33
- const CARDS_DIR = path.join(REPO_ROOT, 'src/cards')
30
+ // 工程根一律走 lib/paths.mjs(DSL_CARD_PROJECT_ROOT || cwd)—— 原先按脚本自身位置
31
+ // (`HERE/../../..`)推算,仓库内恰好等于工程根,发布态却会指到 <pkg>/dist/vendored,
32
+ // 于是切图改名 / 卡片引用改写全落到包内快照上(重装即丢)。
34
33
  const MANIFEST = path.join(SLICES_DIR, 'manifest.json')
35
34
 
36
35
  /**
@@ -29,7 +29,17 @@ function findRepoRoot(startDir) {
29
29
  return path.resolve(__dirname, '..', '..')
30
30
  }
31
31
 
32
- const PROJECT_ROOT = findRepoRoot(__dirname)
32
+ /**
33
+ * 宿主工程根:优先显式注入(cardbuddy 的 standalone.js / mcp.js / client.mjs 会按工程根设
34
+ * `DSL_CARD_PROJECT_ROOT`),其次才按自身位置向上找 package.json。
35
+ * 为什么要有第一优先级:发布态本文件住在 `<pkg>/dist/vendored/skills/shared/`,
36
+ * 往上第一个带 package.json 的目录是**包自己**(node_modules/cardbuddy),
37
+ * 于是 .env.cb 会去读包内那份(不存在)→ 静默退化成默认值,消费者工程的
38
+ * FINMALL_BASE_URL / APP_ID / 端口全被忽略。与 route_input.mjs / parse_meaxure.py 同一契约。
39
+ */
40
+ const PROJECT_ROOT = process.env.DSL_CARD_PROJECT_ROOT
41
+ ? path.resolve(process.env.DSL_CARD_PROJECT_ROOT)
42
+ : findRepoRoot(__dirname)
33
43
 
34
44
  /**
35
45
  * 解析 .env 文件,返回 key-value 对象。
@@ -119,7 +119,7 @@ python skills/sketch-spec-to-card/scripts/build_prompt.py --only "未登录"
119
119
  python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录>
120
120
  python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --auto-refs # 从 src/cards 动态探测参考卡
121
121
  ```
122
- - `--src` 缺省取环境变量 `MEAXURE_SRC`,否则为当前目录;实际请显式传入 `--src`。默认 `--out` = 仓库根/`UI`(脚本按自身路径推算,确保落在工程根而非 skills/ 下)。
122
+ - `--src` 缺省取环境变量 `MEAXURE_SRC`,否则为当前目录;实际请显式传入 `--src`。默认 `--out` = **工程根**/`UI`,工程根取 `DSL_CARD_PROJECT_ROOT`、缺省当前工作目录(cwd)—— **不按脚本自身位置推算**:发布态脚本住在 `<pkg>/dist/vendored/skills/…` 里,按自身位置推会把 `UI/` 写进 node_modules(0.14.1 事故;回归见 `tools/tests/cardbuddy-project-anchor.test.mjs`)。
123
123
 
124
124
  ## 4. 与 dsl-card-generator 的衔接(边界)
125
125
  - **本 skill 只产出要素,不调用 dsl-card-generator**。职责分离:解析/标注/切图归本 skill,建卡 schema 归 dsl-card-generator。
@@ -12,6 +12,7 @@ build_prompt.py — 由 parse_meaxure.py 的 spec.json(视觉驱动)生成
12
12
  """
13
13
  import argparse
14
14
  import json
15
+ import os
15
16
  import re
16
17
  import sys
17
18
  from collections import Counter
@@ -19,8 +20,12 @@ from pathlib import Path
19
20
 
20
21
  SCRIPT_DIR = Path(__file__).resolve().parent
21
22
  SKILL_DIR = SCRIPT_DIR.parent
22
- REPO_ROOT = SKILL_DIR.parent.parent
23
- DEFAULT_OUT = REPO_ROOT / "UI"
23
+ # 宿主工程根:与 parse_meaxure.py / dsl-card-generator/scripts/lib/paths.py 同一契约
24
+ # (DSL_CARD_PROJECT_ROOT 优先,缺省 cwd)—— **不要**按脚本自身位置往上推,
25
+ # 发布态那是 <pkg>/dist/vendored,会把 UI/ 产物写进 node_modules。
26
+ PROJECT_ROOT = Path(os.environ.get("DSL_CARD_PROJECT_ROOT") or Path.cwd()).resolve()
27
+ REPO_ROOT = PROJECT_ROOT
28
+ DEFAULT_OUT = PROJECT_ROOT / "UI"
24
29
  VALIDATE_REL = "skills/dsl-card-generator/scripts/validate_schema.py"
25
30
 
26
31
  # 人力卡学习规则库(与 dsl-card-generator 共享,要素侧只读)
@@ -46,8 +46,12 @@ from urllib.parse import unquote
46
46
 
47
47
  SCRIPT_DIR = Path(__file__).resolve().parent
48
48
  SKILL_DIR = SCRIPT_DIR.parent
49
- REPO_ROOT = SKILL_DIR.parent.parent
50
- DEFAULT_OUT = REPO_ROOT / "UI"
49
+ # 宿主工程根:与 dsl-card-generator/scripts/lib/paths.py 同一契约
50
+ # (DSL_CARD_PROJECT_ROOT 优先,缺省 cwd)—— **不要**按脚本自身位置往上推。
51
+ # 发布态脚本住在 <pkg>/dist/vendored/skills/sketch-spec-to-card/scripts/,
52
+ # 往上两级是包内快照目录,UI/ 产物会被写进 node_modules(重装即丢、多项目互相污染)。
53
+ PROJECT_ROOT = Path(os.environ.get("DSL_CARD_PROJECT_ROOT") or Path.cwd()).resolve()
54
+ DEFAULT_OUT = PROJECT_ROOT / "UI"
51
55
  DEFAULT_SRC = Path(os.environ.get('MEAXURE_SRC') or str(Path.cwd())) # 可用环境变量 MEAXURE_SRC 指定,缺省当前目录
52
56
 
53
57
  sys.path.insert(0, str(SCRIPT_DIR))
@@ -1724,12 +1728,15 @@ def main():
1724
1728
  cdir.mkdir(parents=True, exist_ok=True)
1725
1729
 
1726
1730
  # 整图截图(参考)
1727
- preview_rel = unquote(ab.get("imagePath", ""))
1728
- src_png = src / preview_rel
1729
- if not src_png.exists():
1730
- cand = list(src.glob(f"preview/*{name.split('_', 1)[-1]}*.png"))
1731
+ # imagePath 缺失时 `src / ""` 会等于**导出目录本身**:exists() 为真 → 下面按画板名兜底的
1732
+ # glob 永远不跑,copy2(目录, 文件) 直接抛 IsADirectoryError(整份管线在写 artboard.png 时崩)。
1733
+ # 故必须显式判「非空且是文件」,让兜底真正生效。
1734
+ preview_rel = unquote(ab.get("imagePath", "") or "")
1735
+ src_png = (src / preview_rel) if preview_rel else None
1736
+ if not (src_png and src_png.is_file()):
1737
+ cand = [c for c in src.glob(f"preview/*{name.split('_', 1)[-1]}*.png") if c.is_file()]
1731
1738
  src_png = cand[0] if cand else None
1732
- if src_png and src_png.exists():
1739
+ if src_png:
1733
1740
  shutil.copy2(src_png, cdir / "artboard.png")
1734
1741
  entry["preview"] = f"{name}/artboard.png"
1735
1742
  # —— P1 优化(RC5):整屏 artboard.png 降采样,降低多模态 token 成本 ——
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "cardbuddy",
3
- "version": "0.14.0",
3
+ "version": "0.14.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": {
7
- "cardbuddy": "./dist/cli.js"
7
+ "cardbuddy": "dist/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "dist",
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "start": "node src/cli.js",
21
21
  "build": "node build.mjs",
22
- "prepublishOnly": "node build.mjs"
22
+ "prepublishOnly": "node build.mjs && node publishSim.mjs"
23
23
  },
24
24
  "dependencies": {
25
25
  "@ckpack/vue-color": "^1.6.0",