draftgo-cli 2.0.3 → 2.0.9

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.
@@ -67,7 +67,14 @@ def _lessons_reminder(cfg):
67
67
 
68
68
  def api_call(method, server, token, path, body=None):
69
69
  data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
70
- headers = {"Authorization": f"Bearer {token}"}
70
+ headers = {
71
+ "Authorization": f"Bearer {token}",
72
+ "User-Agent": (
73
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
74
+ "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
75
+ ),
76
+ "Accept": "application/json",
77
+ }
71
78
  if data is not None:
72
79
  headers["Content-Type"] = "application/json"
73
80
  req = urllib.request.Request(
@@ -287,36 +294,81 @@ def sync_nav(server, token, ids=None):
287
294
 
288
295
 
289
296
  def sync_aihub(server, token, ids=None):
290
- items = _load_index("aihub/index.json")
297
+ all_items = _load_index("aihub/index.json")
298
+ items = all_items
291
299
  if ids:
292
- items = [it for it in items if str(it.get("id")) in ids]
300
+ items = [it for it in all_items if str(it.get("id")) in ids]
301
+ dirty = False
293
302
  for it in items:
294
303
  iid = it.get("id")
295
- name = it.get("name", iid)
304
+ name = it.get("name", iid or "(new)")
296
305
  # 服务端 AIHubUpdate 接受的字段子集
297
306
  payload = {k: it.get(k) for k in (
298
307
  "type", "name", "data", "priority", "version",
299
308
  "tags", "describe", "permission", "status",
300
309
  ) if it.get(k) is not None}
301
- ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload)
302
- print(f" {'OK' if ok else 'ERR'} [{name}] aihub_id={iid}{'' if ok else ' -> ' + _info(ok, status, body)}")
310
+ if iid:
311
+ ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload)
312
+ if not ok and status == 404:
313
+ print(f" WARN [{name}] aihub_id={iid} 不存在,尝试创建")
314
+ iid = None
315
+ else:
316
+ print(f" {'OK' if ok else 'ERR'} [{name}] aihub_id={iid}{'' if ok else ' -> ' + _info(ok, status, body)}")
317
+ if not iid:
318
+ create_payload = {k: payload.get(k) for k in (
319
+ "type", "name", "data", "priority", "version",
320
+ "tags", "describe", "permission",
321
+ ) if payload.get(k) is not None}
322
+ ok, status, body = api_call("POST", server, token, "/api/aihub", create_payload)
323
+ new_id = _extract_created_id(body) if ok else None
324
+ if ok and new_id:
325
+ it["id"] = new_id
326
+ dirty = True
327
+ print(f" OK [{name}] 已创建 aihub_id={new_id}(已回写 index)")
328
+ else:
329
+ print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
330
+ if dirty:
331
+ _writeback_index("aihub/index.json", all_items)
303
332
 
304
333
 
305
334
  def sync_external_apis(server, token, ids=None):
306
- items = _load_index("external_apis/index.json")
335
+ all_items = _load_index("external_apis/index.json")
336
+ items = all_items
307
337
  if ids:
308
- items = [it for it in items if str(it.get("id")) in ids]
338
+ items = [it for it in all_items if str(it.get("id")) in ids]
339
+ dirty = False
309
340
  for it in items:
310
341
  iid = it.get("id")
311
- code = it.get("code", iid)
342
+ code = it.get("code", iid or "(new)")
312
343
  # 服务端 ExternalAPIUpdate 接受字段
313
344
  payload = {k: it.get(k) for k in (
314
345
  "name", "base_url", "method", "path", "headers",
315
346
  "auth_type", "auth_config", "timeout_ms", "permission",
316
347
  "param_schema", "tags", "description", "status",
317
348
  ) if it.get(k) is not None}
318
- ok, status, body = api_call("PUT", server, token, f"/api/external-apis/{iid}", payload)
319
- print(f" {'OK' if ok else 'ERR'} [{code}] api_id={iid}{'' if ok else ' -> ' + _info(ok, status, body)}")
349
+ if iid:
350
+ ok, status, body = api_call("PUT", server, token, f"/api/external-apis/{iid}", payload)
351
+ if not ok and status == 404:
352
+ print(f" WARN [{code}] api_id={iid} 不存在,尝试创建")
353
+ iid = None
354
+ else:
355
+ print(f" {'OK' if ok else 'ERR'} [{code}] api_id={iid}{'' if ok else ' -> ' + _info(ok, status, body)}")
356
+ if not iid:
357
+ create_payload = {k: it.get(k) for k in (
358
+ "code", "name", "base_url", "method", "path", "headers",
359
+ "auth_type", "auth_config", "timeout_ms", "permission",
360
+ "param_schema", "tags", "description",
361
+ ) if it.get(k) is not None}
362
+ ok, status, body = api_call("POST", server, token, "/api/external-apis", create_payload)
363
+ new_id = _extract_created_id(body) if ok else None
364
+ if ok and new_id:
365
+ it["id"] = new_id
366
+ dirty = True
367
+ print(f" OK [{code}] 已创建 api_id={new_id}(已回写 index)")
368
+ else:
369
+ print(f" ERR [{code}] 创建失败 -> {_info(ok, status, body)}")
370
+ if dirty:
371
+ _writeback_index("external_apis/index.json", all_items)
320
372
 
321
373
 
322
374
  def sync_system_config(server, token, keys=None):
@@ -339,7 +391,13 @@ def sync_system_config(server, token, keys=None):
339
391
  }
340
392
  payload = {k: v for k, v in payload.items() if v is not None}
341
393
  ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", payload)
342
- print(f" {'OK' if ok else 'ERR'} [{ck}]{'' if ok else ' -> ' + _info(ok, status, body)}")
394
+ if not ok and status == 404:
395
+ print(f" WARN [{ck}] system_config 不存在,尝试创建")
396
+ create_payload = {"config_key": ck, **payload}
397
+ ok, status, body = api_call("POST", server, token, "/api/system/", create_payload)
398
+ print(f" {'OK' if ok else 'ERR'} [{ck}] 已创建{'' if ok else ' -> ' + _info(ok, status, body)}")
399
+ else:
400
+ print(f" {'OK' if ok else 'ERR'} [{ck}]{'' if ok else ' -> ' + _info(ok, status, body)}")
343
401
 
344
402
 
345
403
  def sync_roles(server, token, ids=None):
@@ -421,18 +479,48 @@ def sync_docs(server, token, ids=None):
421
479
 
422
480
 
423
481
  def sync_doc_categories(server, token, ids=None):
424
- items = _load_index("doc_categories/index.json")
482
+ all_items = _load_index("doc_categories/index.json")
483
+ items = all_items
425
484
  if ids:
426
- items = [it for it in items if str(it.get("id")) in ids]
485
+ items = [it for it in all_items if str(it.get("id")) in ids]
486
+ dirty = False
427
487
  for it in items:
428
488
  cid = it.get("id")
429
- name = it.get("name", cid)
489
+ name = it.get("name", cid or "(new)")
430
490
  # CategoryUpdate 字段
431
491
  payload = {k: it.get(k) for k in (
432
492
  "name", "slug", "description", "icon", "parent_id", "sort_order", "status",
433
493
  ) if it.get(k) is not None}
434
- ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload)
435
- print(f" {'OK' if ok else 'ERR'} [{name}] category_id={cid}{'' if ok else ' -> ' + _info(ok, status, body)}")
494
+ if cid:
495
+ ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload)
496
+ if not ok and status == 404:
497
+ print(f" WARN [{name}] category_id={cid} 不存在,尝试创建")
498
+ cid = None
499
+ else:
500
+ print(f" {'OK' if ok else 'ERR'} [{name}] category_id={cid}{'' if ok else ' -> ' + _info(ok, status, body)}")
501
+ if not cid:
502
+ ok, status, body = api_call("POST", server, token, "/api/docs/categories", payload)
503
+ new_id = _extract_created_id(body) if ok else None
504
+ if ok and new_id:
505
+ it["id"] = new_id
506
+ dirty = True
507
+ print(f" OK [{name}] 已创建 category_id={new_id}(已回写 index)")
508
+ else:
509
+ print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
510
+ if dirty:
511
+ _writeback_index("doc_categories/index.json", all_items)
512
+
513
+
514
+ def _normalize_script_triggers(mode, triggers):
515
+ """归一化 custom_script.triggers。
516
+
517
+ - scheduled:实际调度只读代码里的 @scheduled(...),CLI push 主动回写空 dict,
518
+ 避免把误导性的 cron 元数据继续推回云端。
519
+ - 其他模式:保留本地 triggers 元数据。
520
+ """
521
+ if str(mode or "").lower() == "scheduled":
522
+ return {}
523
+ return triggers
436
524
 
437
525
 
438
526
  def sync_custom_scripts(server, token, ids=None):
@@ -451,11 +539,13 @@ def sync_custom_scripts(server, token, ids=None):
451
539
  print(f" SKIP [{name}] code_file not found: {code_rel}")
452
540
  continue
453
541
  code = code_path.read_text(encoding="utf-8")
542
+ normalized_triggers = _normalize_script_triggers(it.get("mode"), it.get("triggers"))
454
543
  if sid:
455
544
  # ScriptUpdate 接受字段(不含 slug/mode,避免误改启停/路由)
456
545
  payload = {k: it.get(k) for k in (
457
- "name", "description", "triggers", "config", "permission",
546
+ "name", "description", "config", "permission",
458
547
  ) if it.get(k) is not None}
548
+ payload["triggers"] = normalized_triggers
459
549
  payload["code"] = code
460
550
  ok, status, body = api_call("PUT", server, token, f"/api/scripts/{sid}", payload)
461
551
  if not ok and status == 404:
@@ -464,13 +554,13 @@ def sync_custom_scripts(server, token, ids=None):
464
554
  else:
465
555
  print(f" {'OK' if ok else 'ERR'} [{name}] script_id={sid}{'' if ok else ' -> ' + _info(ok, status, body)}")
466
556
  if not sid:
467
- # ScriptCreate 必填 name/slug/code/mode/triggers
557
+ # ScriptCreate 必填 name/slug/code/mode;scheduled 的 cron 以代码装饰器为准
468
558
  payload = {
469
559
  "name": it.get("name", ""),
470
560
  "slug": it.get("slug", ""),
471
561
  "code": code,
472
562
  "mode": it.get("mode", "route"),
473
- "triggers": it.get("triggers") or {},
563
+ "triggers": normalized_triggers,
474
564
  "config": it.get("config"),
475
565
  "permission": it.get("permission"),
476
566
  "description": it.get("description"),
@@ -57,14 +57,6 @@ design:
57
57
  modules:
58
58
  - name: "模块名"
59
59
  role: "这个模块在系统里的定位(核心/辅助/基座),一句话"
60
- style:
61
- personality: "系统'说话'像谁?一句话描述调性人格"
62
- keywords:
63
- - "设计关键词(如:叙事性交互、情感化、微动效、仪式感)"
64
- do:
65
- - "正面指引(什么该做)"
66
- dont:
67
- - "禁区(什么绝对不做)"
68
60
 
69
61
  # === 第三层:决策日志(只增不删)===
70
62
  decisions:
@@ -174,7 +166,7 @@ Q5: 第一版能用,最少要包含什么?
174
166
  ```
175
167
  1. 读取 .draftgo/story.yaml 全文 → 作为最高优先级上下文
176
168
  2. 解析 design → 理解系统核心流转和模块定位
177
- 3. 解析 decisions(status: active)→ 提取品味/取舍依据
169
+ 3. 解析 decisions(status: active)→ 提取产品取舍依据
178
170
  4. 解析 identity.not → 建立硬边界
179
171
  5. 解析 now → 知道当前重点
180
172
  6. open_questions → 适时主动追问
@@ -217,8 +209,7 @@ AI 在开发过程中检测到"方向性决策"时,主动提议沉淀:
217
209
  **触发条件:**
218
210
  - 开发者明确拒绝了某个建议("不要这样做")
219
211
  - 开发者在两个方案中做了选择
220
- - 开发者表达了对系统调性的偏好
221
- - 开发者描述了 UI/UX 的感觉、风格、动效、文案语气等审美偏好
212
+ - 开发者表达了对系统定位、功能边界或文案语气的偏好
222
213
 
223
214
  **AI 行为:**
224
215
 
@@ -229,23 +220,6 @@ AI 在开发过程中检测到"方向性决策"时,主动提议沉淀:
229
220
  → [是] / [否] / [改改措辞]
230
221
  ```
231
222
 
232
- **style 沉淀(渐进式,不单独提问):**
233
-
234
- 当开发者在 UI/UX 相关对话中表达了审美偏好时(如描述交互节奏、文案风格、动效喜好、视觉调性),AI 提议:
235
-
236
- ```
237
- 从你刚才的描述里,我提炼了几个设计偏好:
238
- - keyword: "叙事性交互"
239
- - do: "关键流程用动效创造起承转合"
240
- - dont: "不用系统腔提示语"
241
-
242
- 要写进 Story 的 design.style 里吗?以后做 UI 我会按这个调性来。
243
-
244
- → [写入] / [调整措辞] / [这次不记]
245
- ```
246
-
247
- style 不是一次性采集的,而是在持续开发中不断丰富——每次用户表达审美偏好都是一次沉淀机会。
248
-
249
223
  ---
250
224
 
251
225
  ## now 的更新
@@ -268,7 +242,6 @@ style 不是一次性采集的,而是在持续开发中不断丰富——每
268
242
  | `identity.*` | 极少 | 仅重大方向转向时(冲突检测中确认) |
269
243
  | `design.overview` | 里程碑级 | 核心流转发生结构性变化时更新 |
270
244
  | `design.modules` | 新增模块时 | 只增/改,不删已有模块(模块下线标注即可) |
271
- | `design.style` | 渐进沉淀 | 不在初始化时采集;开发过程中从用户的描述、选择、审美反馈中提炼 |
272
245
  | `decisions` | 只增不删 | ID 永不复用,supersede 时不删旧的只标记 |
273
246
  | `now.focus` | 周/里程碑 | 对话结束时 AI 提议更新 |
274
247
  | `now.next` | 周 | 同上 |
@@ -34,27 +34,6 @@ design:
34
34
  role: "数据可视化,让开发者快速感知趋势"
35
35
  - name: "数据源配置"
36
36
  role: "基座模块,连接开发者自己的后端 API"
37
- style:
38
- personality: "像一个有品味的年轻朋友,不是客服也不是机器"
39
- keywords:
40
- - "叙事性交互"
41
- - "情感化对话"
42
- - "微动效驱动节奏"
43
- - "仪式感"
44
- - "克制的丰富"
45
- do:
46
- - "用人话跟用户说话,有温度"
47
- - "关键流程要有起承转合的时序编排"
48
- - "用动效创造记忆点(渐显渐隐、灯亮进度、礼花)"
49
- - "重要时刻要有仪式感(注册=相识、完成=庆祝)"
50
- - "空态用一句有温度的话引导,不用冷冰冰的'暂无数据'"
51
- dont:
52
- - "不堆 emoji"
53
- - "不用系统腔提示语(如'操作成功'、'请稍后重试')"
54
- - "不极简到无趣"
55
- - "不炫技到喧宾夺主"
56
- - "敏感场景(支付/删除/错误)不抖机灵"
57
-
58
37
  # === 第三层:决策日志(只增不删)===
59
38
  decisions:
60
39
  - id: D001
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { analyzeProject } = require('../projectMap');
5
+
6
+ function check(projectDir, flags = {}) {
7
+ const result = analyzeProject(projectDir);
8
+
9
+ if (flags.output === 'json') {
10
+ console.log(JSON.stringify(result, null, 2));
11
+ return result.errors.length || (flags.strict && result.warnings.length) ? 1 : 0;
12
+ }
13
+
14
+ log.title('draftgo check');
15
+ log.info(`项目目录:${projectDir}`);
16
+
17
+ console.log('');
18
+ log.info(`资源概览:pages=${result.map.pages.length}, nav=${result.map.navigations.length}, db=${result.map.db_meta.length}, scripts=${result.map.custom_scripts.length}, aihub=${result.map.aihub.length}, external_apis=${result.map.external_apis.length}, docs=${result.map.docs.length}, doc_categories=${result.map.doc_categories.length}, system_config=${result.map.system_config.length}`);
19
+
20
+ if (result.errors.length) {
21
+ console.log('');
22
+ console.log(`${log.c.red('x')} 错误:`);
23
+ result.errors.forEach((msg) => console.log(` - ${msg}`));
24
+ }
25
+
26
+ if (result.warnings.length) {
27
+ console.log('');
28
+ console.log(`${log.c.yellow('!')} 提醒:`);
29
+ result.warnings.forEach((msg) => console.log(` - ${msg}`));
30
+ }
31
+
32
+ if (!result.errors.length && !result.warnings.length) {
33
+ console.log('');
34
+ log.ok('未发现明显闭环问题。');
35
+ }
36
+
37
+ if (result.errors.length) {
38
+ console.log('');
39
+ console.log(`${log.c.red('x')} 检查未通过:请先修复错误。`);
40
+ return 1;
41
+ }
42
+ if (flags.strict && result.warnings.length) {
43
+ console.log('');
44
+ console.log(`${log.c.red('x')} strict 模式:存在提醒项,检查未通过。`);
45
+ return 1;
46
+ }
47
+
48
+ console.log('');
49
+ log.ok(result.warnings.length ? '检查完成:存在提醒项,建议开发代理处理后再 push。' : '检查通过。');
50
+ return 0;
51
+ }
52
+
53
+ module.exports = check;
@@ -5,7 +5,11 @@ const { getPackageVersion } = require('../skill');
5
5
 
6
6
  function help() {
7
7
  const targets = all.map((i) => ` ${i.name.padEnd(12)} ${i.displayName}`).join('\n');
8
- console.log(`draftgo v${getPackageVersion()} — manage the DraftGo skill across AI coding agents
8
+ console.log(`draftgo v${getPackageVersion()} — DraftGo Next workbench CLI for AI coding agents
9
+
10
+ DraftGo Next frontend baseline: React + Vite + shadcn/ui + Tailwind.
11
+ Database pages use dg-* as the shadcn HTML runtime protocol.
12
+ The CLI is the workbench layer for local runtime, resource sync, checks, and push/pull flows.
9
13
 
10
14
  Usage:
11
15
  draftgo init [<target>...] Install skill. No target = auto-detect.
@@ -19,6 +23,19 @@ Usage:
19
23
  draftgo status Show installed targets and skill version.
20
24
  draftgo doctor Diagnose environment (python, targets,
21
25
  CLI freshness).
26
+ draftgo map Print a local DraftGo project resource map
27
+ for fast AI orientation.
28
+ draftgo check Check local resource closure: routes,
29
+ entry binding, files, obvious mock risks.
30
+ draftgo dev Run this project's npm dev script.
31
+ draftgo build Run this project's npm build script.
32
+ draftgo pull [type] [id...] Pull DraftGo resources via the bundled
33
+ sync script. Defaults to --all.
34
+ draftgo push <type> [id...] Push DraftGo resources via the bundled
35
+ sync script.
36
+ draftgo local up|down|logs|status
37
+ Manage .draftgo/docker/docker-compose.yaml
38
+ generated by draftgo local-dev.
22
39
  draftgo list-targets List supported AI tools.
23
40
  draftgo connect Bind this project to an existing DraftGo
24
41
  server. Prompts for BaseURL + access
@@ -31,6 +48,11 @@ Usage:
31
48
  draftgo -v | --version Print CLI version.
32
49
  draftgo -h | --help Show this help.
33
50
 
51
+ v3 Workbench:
52
+ draftgo local up|down|logs Local stack lifecycle commands.
53
+ draftgo dev|build|check Project workflow gates.
54
+ draftgo pull|push First-class resource sync wrappers.
55
+
34
56
  Flags:
35
57
  --project <dir> Operate on <dir> instead of the current directory.
36
58
  --force Overwrite existing skill body during install/update.
@@ -42,6 +64,8 @@ Flags:
42
64
  --no-setup (init) Don't offer either flow after installing.
43
65
  --server <url> (connect) Provide the DraftGo BaseURL non-interactively.
44
66
  --token <sat> (connect) Provide the access token non-interactively.
67
+ --output <json> (map/check) Print machine-readable JSON.
68
+ --strict (check) Treat warnings as failures.
45
69
 
46
70
  Environment:
47
71
  DRAFTGO_NO_UPDATE_CHECK=1 Disable the automatic CLI-freshness check.
@@ -55,6 +79,8 @@ Examples:
55
79
  draftgo init claudecode kiro # install for both
56
80
  draftgo init all # install for every supported target
57
81
  draftgo update # upgrade CLI if needed, then refresh skill
82
+ draftgo map # inspect pages/nav/db/scripts before development
83
+ draftgo check --strict # fail on closure warnings before push
58
84
  draftgo uninstall all --purge # full removal incl. runtime data
59
85
  `);
60
86
  }
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+ const log = require('../logger');
6
+ const { exists } = require('../fsx');
7
+ const { detectDocker } = require('../localdev/detect');
8
+
9
+ function composeFile(projectDir) {
10
+ return path.join(projectDir, '.draftgo', 'docker', 'docker-compose.yaml');
11
+ }
12
+
13
+ function runCompose(projectDir, args, stdio = 'inherit') {
14
+ const docker = detectDocker();
15
+ if (!docker.ok) {
16
+ log.err(docker.reason === 'compose-missing'
17
+ ? '检测到 Docker,但缺少 compose 插件。'
18
+ : '未检测到可用 Docker。');
19
+ return 1;
20
+ }
21
+
22
+ const file = composeFile(projectDir);
23
+ if (!exists(file)) {
24
+ log.err('未找到 .draftgo/docker/docker-compose.yaml。');
25
+ log.dim(' 请先运行 `draftgo local-dev` 生成本地 DraftGo stack。');
26
+ return 1;
27
+ }
28
+
29
+ const r = spawnSync(docker.composeCmd, [...docker.composeArgs, '-f', file, ...args], {
30
+ cwd: path.dirname(file),
31
+ stdio,
32
+ shell: false,
33
+ });
34
+ return r.status || 0;
35
+ }
36
+
37
+ function local(projectDir, positional) {
38
+ const action = positional[0] || 'status';
39
+
40
+ switch (action) {
41
+ case 'up':
42
+ return runCompose(projectDir, ['up', '-d']);
43
+ case 'down':
44
+ return runCompose(projectDir, ['down']);
45
+ case 'logs':
46
+ return runCompose(projectDir, ['logs', ...(positional.slice(1).length ? positional.slice(1) : ['-f', 'app'])]);
47
+ case 'status':
48
+ case 'ps':
49
+ return runCompose(projectDir, ['ps']);
50
+ default:
51
+ log.err(`未知 local 子命令:${action}`);
52
+ log.dim(' 可用:draftgo local up | down | logs | status');
53
+ return 1;
54
+ }
55
+ }
56
+
57
+ module.exports = local;
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { buildProjectMap } = require('../projectMap');
5
+
6
+ function printList(title, rows, render) {
7
+ console.log('');
8
+ log.info(`${title}:${rows.length}`);
9
+ if (rows.length === 0) {
10
+ log.dim(' (无)');
11
+ return;
12
+ }
13
+ for (const row of rows) log.dim(` • ${render(row)}`);
14
+ }
15
+
16
+ function mapCommand(projectDir, flags = {}) {
17
+ const map = buildProjectMap(projectDir);
18
+
19
+ if (flags.output === 'json') {
20
+ console.log(JSON.stringify(map, null, 2));
21
+ return 0;
22
+ }
23
+
24
+ log.title('draftgo map');
25
+ log.info(`项目目录:${projectDir}`);
26
+
27
+ printList('页面', map.pages, (p) => {
28
+ const id = p.id == null ? 'new' : p.id;
29
+ return `${String(id).padEnd(4)} ${p.route || '(无 route)'} ${p.title || '未命名'}${p.html_file ? ` ${p.html_file}` : ''}`;
30
+ });
31
+
32
+ printList('导航', map.navigations, (n) => {
33
+ const id = n.id == null ? 'new' : n.id;
34
+ return `${String(id).padEnd(4)} ${n.code || 'default'} ${n.name || '未命名'}${n.html_file ? ` ${n.html_file}` : ''}`;
35
+ });
36
+
37
+ printList('动态 DB', map.db_meta, (m) => {
38
+ const fields = m.fields.length ? ` (${m.fields.slice(0, 8).join(', ')}${m.fields.length > 8 ? ', ...' : ''})` : '';
39
+ return `${m.type || '(无 type)'} ${m.label || ''}${fields}`;
40
+ });
41
+
42
+ printList('自定义脚本', map.custom_scripts, (s) => {
43
+ return `${s.slug || '(无 slug)'} ${s.mode || 'mode?'} ${s.name || ''}${s.code_file ? ` ${s.code_file}` : ''}`;
44
+ });
45
+
46
+ printList('外部 API', map.external_apis, (a) => {
47
+ const route = `${a.method || 'GET'} ${a.path || '/'}`;
48
+ const tags = a.tags && a.tags.length ? ` #${a.tags.join(',#')}` : '';
49
+ return `${a.id || 'new'} ${a.code || '(无 code)'} ${route} ${a.name || ''}${a.base_url ? ` ${a.base_url}` : ''}${tags}`;
50
+ });
51
+
52
+ printList('文档', map.docs, (d) => {
53
+ return `${d.id || 'new'} ${d.slug || '(无 slug)'} ${d.title || '未命名'}${d.content_file ? ` ${d.content_file}` : ''}`;
54
+ });
55
+
56
+ printList('文档分类', map.doc_categories, (c) => {
57
+ const parent = c.parent_id == null ? 'root' : `parent:${c.parent_id}`;
58
+ return `${c.id || 'new'} ${c.slug || '(无 slug)'} ${c.name || '未命名'} ${parent}`;
59
+ });
60
+
61
+ printList('系统配置', map.system_config, (c) => {
62
+ const sensitive = c.is_sensitive ? ' sensitive' : '';
63
+ return `${c.config_key || '(无 key)'} ${c.category || 'default'} ${c.value_type || 'value'}${sensitive}`;
64
+ });
65
+
66
+ printList('AIHub', map.aihub, (a) => {
67
+ const extra = [];
68
+ if (a.type === 'agent') {
69
+ if (a.mode) extra.push(a.mode);
70
+ if (a.output_format && a.output_format.mode === 'json') extra.push(`json:${a.output_format.json_strategy || 'auto'}`);
71
+ if (a.model_selection && a.model_selection.user_selectable) extra.push('user-model');
72
+ if (a.tools && a.tools.sources.length) extra.push(`tools:${a.tools.sources.length}`);
73
+ } else if (a.type === 'model') {
74
+ extra.push(`models:${a.models_count || 0}`);
75
+ if (a.supports_response_format === true) extra.push('response_format');
76
+ if (a.supports_json_schema === true) extra.push('json_schema');
77
+ } else if (a.type === 'mcp') {
78
+ extra.push(a.transport || 'transport?');
79
+ extra.push(`tools:${a.tools_count || 0}`);
80
+ }
81
+ return `${a.id || 'new'} ${a.type || ''} ${a.name || ''}${extra.length ? ` [${extra.join(', ')}]` : ''}`;
82
+ });
83
+ printList('角色', map.roles, (r) => `${r.code || r.id || 'new'} ${r.name || ''}`);
84
+
85
+ console.log('');
86
+ log.info('入口引用:');
87
+ const routes = Object.keys(map.routeRefs).sort();
88
+ if (routes.length === 0) log.dim(' (未发现 data-page-route / href 引用)');
89
+ else routes.forEach((route) => log.dim(` • ${route} ← ${map.routeRefs[route].join(', ')}`));
90
+
91
+ return 0;
92
+ }
93
+
94
+ module.exports = mapCommand;
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const log = require('../logger');
7
+
8
+ function projectScript(projectDir, scriptName) {
9
+ const pkgPath = path.join(projectDir, 'package.json');
10
+ if (!fs.existsSync(pkgPath)) {
11
+ log.err(`当前项目没有 package.json,无法运行 draftgo ${scriptName}。`);
12
+ return 1;
13
+ }
14
+
15
+ let pkg;
16
+ try {
17
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
18
+ } catch (err) {
19
+ log.err(`package.json 解析失败:${err.message}`);
20
+ return 1;
21
+ }
22
+
23
+ if (!pkg.scripts || !pkg.scripts[scriptName]) {
24
+ log.err(`package.json 中没有 scripts.${scriptName}。`);
25
+ return 1;
26
+ }
27
+
28
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
29
+ const r = spawnSync(npmCmd, ['run', scriptName], {
30
+ cwd: projectDir,
31
+ stdio: 'inherit',
32
+ shell: false,
33
+ });
34
+ return r.status || 0;
35
+ }
36
+
37
+ module.exports = projectScript;
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+ const log = require('../logger');
6
+ const { exists } = require('../fsx');
7
+ const { findPython } = require('../python');
8
+ const { platforms } = require('../platforms');
9
+
10
+ function findSyncScript(projectDir, command) {
11
+ const scriptName = command === 'pull' ? 'draftgo_pull.py' : 'draftgo_push.py';
12
+
13
+ for (const platform of platforms) {
14
+ const candidate = path.join(projectDir, platform.assetDir, 'scripts', scriptName);
15
+ if (exists(candidate)) return candidate;
16
+ }
17
+
18
+ const bundled = path.resolve(__dirname, '..', '..', 'resources', 'skill', 'scripts', scriptName);
19
+ return exists(bundled) ? bundled : null;
20
+ }
21
+
22
+ function sync(projectDir, command, positional) {
23
+ const py = findPython();
24
+ if (!py) {
25
+ log.err('未检测到 Python,无法运行 DraftGo 同步脚本。');
26
+ return 1;
27
+ }
28
+
29
+ const script = findSyncScript(projectDir, command);
30
+ if (!script) {
31
+ log.err(`未找到 ${command} 同步脚本。`);
32
+ log.dim(' 请先运行 `draftgo init` 安装 DraftGo skill,或重新安装 CLI。');
33
+ return 1;
34
+ }
35
+
36
+ if (!exists(path.join(projectDir, '.draftgo', 'config.json'))) {
37
+ log.err('未找到 .draftgo/config.json。');
38
+ log.dim(' 请先运行 `draftgo connect` 或 `/draftgo init`。');
39
+ return 1;
40
+ }
41
+
42
+ const args = positional.length ? positional : ['--all'];
43
+ const r = spawnSync(py.bin, [script, ...args], {
44
+ cwd: projectDir,
45
+ stdio: 'inherit',
46
+ shell: false,
47
+ });
48
+ return r.status || 0;
49
+ }
50
+
51
+ module.exports = sync;