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.
- package/README.md +100 -27
- package/package.json +2 -2
- package/resources/skill/SKILL.md +257 -74
- package/resources/skill/push/SKILL.md +7 -4
- package/resources/skill/rules/dev-workflow.md +123 -96
- package/resources/skill/rules/frontend.md +130 -160
- package/resources/skill/scripts/draftgo_pull.py +15 -2
- package/resources/skill/scripts/draftgo_push.py +110 -20
- package/resources/skill/story/SKILL.md +2 -29
- package/resources/skill/story/story.example.yaml +0 -21
- package/src/commands/check.js +53 -0
- package/src/commands/help.js +27 -1
- package/src/commands/local.js +57 -0
- package/src/commands/map.js +94 -0
- package/src/commands/projectScript.js +37 -0
- package/src/commands/sync.js +51 -0
- package/src/index.js +12 -0
- package/src/localdev/compose.js +19 -0
- package/src/projectMap.js +368 -0
- package/resources/skill/rules/data-table.md +0 -320
|
@@ -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 = {
|
|
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
|
-
|
|
297
|
+
all_items = _load_index("aihub/index.json")
|
|
298
|
+
items = all_items
|
|
291
299
|
if ids:
|
|
292
|
-
items = [it for it in
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
335
|
+
all_items = _load_index("external_apis/index.json")
|
|
336
|
+
items = all_items
|
|
307
337
|
if ids:
|
|
308
|
-
items = [it for it in
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
482
|
+
all_items = _load_index("doc_categories/index.json")
|
|
483
|
+
items = all_items
|
|
425
484
|
if ids:
|
|
426
|
-
items = [it for it in
|
|
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
|
-
|
|
435
|
-
|
|
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", "
|
|
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
|
|
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":
|
|
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;
|
package/src/commands/help.js
CHANGED
|
@@ -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()} —
|
|
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;
|