draftgo-cli 3.0.1 → 3.0.33

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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -124
  3. package/package.json +21 -8
  4. package/resources/skill/SKILL.md +62 -89
  5. package/resources/skill/init/SKILL.md +18 -67
  6. package/resources/skill/manifest.json +27 -0
  7. package/resources/skill/pull/SKILL.md +18 -44
  8. package/resources/skill/push/SKILL.md +30 -247
  9. package/resources/skill/references/aihub.md +86 -0
  10. package/resources/skill/references/api-endpoints.md +178 -0
  11. package/resources/skill/references/api.json +20248 -0
  12. package/resources/skill/{quickref → references}/app-api.md +44 -14
  13. package/resources/skill/{core → references}/architecture.md +6 -26
  14. package/resources/skill/references/chat-sdk.md +201 -0
  15. package/resources/skill/references/custom-services.md +308 -0
  16. package/resources/skill/references/data.md +298 -0
  17. package/resources/skill/references/db-relations.md +227 -0
  18. package/resources/skill/references/frontend.md +788 -0
  19. package/resources/skill/references/modules.md +66 -0
  20. package/resources/skill/references/parallel.md +48 -0
  21. package/resources/skill/{specs → references}/runtime.md +31 -1
  22. package/resources/skill/{specs → references}/security.md +3 -3
  23. package/resources/skill/references/ui-protocol.md +99 -0
  24. package/resources/skill/scripts/draftgo_delete.py +0 -2
  25. package/resources/skill/scripts/draftgo_init.py +15 -3
  26. package/resources/skill/scripts/draftgo_pull.py +154 -87
  27. package/resources/skill/scripts/draftgo_push.py +440 -183
  28. package/resources/skill/story/SKILL.md +13 -23
  29. package/src/cli.js +22 -7
  30. package/src/commandRegistry.js +34 -0
  31. package/src/commands/api.js +204 -0
  32. package/src/commands/autoPush.js +41 -0
  33. package/src/commands/check.js +27 -17
  34. package/src/commands/delete.js +6 -4
  35. package/src/commands/deploy.js +31 -0
  36. package/src/commands/help.js +41 -28
  37. package/src/commands/init.js +34 -20
  38. package/src/commands/local.js +9 -3
  39. package/src/commands/map.js +18 -7
  40. package/src/commands/sync.js +11 -4
  41. package/src/commands/update.js +39 -52
  42. package/src/commands/verifyUi.js +199 -0
  43. package/src/index.js +13 -46
  44. package/src/localdev/compose.js +48 -197
  45. package/src/localdev/index.js +116 -216
  46. package/src/localdev/mysqlClient.js +12 -9
  47. package/src/localdev/services.js +163 -0
  48. package/src/platforms.js +3 -3
  49. package/src/projectConfig.js +12 -2
  50. package/src/projectMap.js +240 -68
  51. package/src/skill.js +113 -29
  52. package/src/updateCheck.js +37 -15
  53. package/resources/skill/core/modules.md +0 -54
  54. package/resources/skill/practices/anti-patterns.md +0 -70
  55. package/resources/skill/practices/best-practices.md +0 -41
  56. package/resources/skill/practices/dev-declaration.md +0 -94
  57. package/resources/skill/quickref/api-endpoints.md +0 -130
  58. package/resources/skill/quickref/api.json +0 -17675
  59. package/resources/skill/quickref/dg-components.md +0 -198
  60. package/resources/skill/rules/dev-workflow.md +0 -652
  61. package/resources/skill/rules/frontend.md +0 -210
  62. package/resources/skill/rules/parallel.md +0 -263
  63. package/resources/skill/specs/data.md +0 -108
  64. package/resources/skill/specs/ui-protocol.md +0 -68
  65. package/src/commands/doctor.js +0 -54
  66. package/src/commands/new.js +0 -183
  67. package/src/commands/projectScript.js +0 -37
  68. /package/resources/skill/{rules → references}/debugging-syntax.md +0 -0
@@ -8,7 +8,6 @@ DraftGo Pull Script
8
8
  python draftgo_pull.py nav [nav_id ...]
9
9
  python draftgo_pull.py db_meta [db_meta_id ...]
10
10
  python draftgo_pull.py aihub [aihub_id ...]
11
- python draftgo_pull.py external_apis [api_id ...]
12
11
  python draftgo_pull.py system_config [config_key ...]
13
12
  python draftgo_pull.py docs [article_id ...]
14
13
  python draftgo_pull.py doc_categories [category_id ...]
@@ -19,7 +18,7 @@ DraftGo Pull Script
19
18
  """
20
19
  import json, sys
21
20
  from pathlib import Path
22
- import urllib.request, urllib.error
21
+ import urllib.request, urllib.error, urllib.parse
23
22
 
24
23
  SCRIPT_DIR = Path(__file__).resolve().parent
25
24
 
@@ -43,28 +42,18 @@ DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
43
42
 
44
43
  # ── 端点映射 ──
45
44
  ENDPOINTS = {
46
- "pages": "/api/pages/?page=1&page_size=500",
45
+ "pages": "/api/pages/",
47
46
  "navigations": "/api/navigations",
48
47
  "roles": "/api/roles",
49
- "users": "/api/users?page=1&page_size=500",
48
+ "users": "/api/users",
50
49
  "db_meta": "/api/db-meta",
51
- "aihub": "/api/aihub?page=1&page_size=500",
52
- "external_apis": "/api/external-apis?page=1&page_size=500",
50
+ "aihub": "/api/aihub",
53
51
  "system_config": "/api/system/config",
54
52
  "doc_categories": "/api/docs/categories?flat=true",
55
- "docs": "/api/docs/admin/articles?page=1&page_size=500",
56
- "custom_scripts": "/api/scripts/?page=1&page_size=500",
53
+ "docs": "/api/docs/admin/articles",
54
+ "custom_scripts": "/api/scripts/",
57
55
  }
58
56
 
59
- _LANG_EXT = {
60
- "python": "py", "py": "py",
61
- "javascript": "js", "js": "js",
62
- "typescript": "ts", "ts": "ts",
63
- "shell": "sh", "bash": "sh",
64
- "go": "go",
65
- }
66
-
67
-
68
57
  # ── 通用工具函数 ──
69
58
 
70
59
  def load_config(root=None):
@@ -129,7 +118,31 @@ def safe_slug(s):
129
118
  return str(s).strip("/").replace("/", "_") or "root"
130
119
 
131
120
 
132
- def write_index(folder, items):
121
+ def write_index(folder, items, preserve_existing=False, id_key="id"):
122
+ if preserve_existing:
123
+ index_path = folder / "index.json"
124
+ try:
125
+ existing = json.loads(index_path.read_text(encoding="utf-8"))
126
+ if not isinstance(existing, list):
127
+ existing = []
128
+ except (OSError, json.JSONDecodeError):
129
+ existing = []
130
+ replacements = {
131
+ str(item.get(id_key)): item for item in items if item.get(id_key) is not None
132
+ }
133
+ retained = []
134
+ seen = set()
135
+ for item in existing:
136
+ key = str(item.get(id_key)) if item.get(id_key) is not None else None
137
+ if key in replacements:
138
+ retained.append(replacements[key])
139
+ seen.add(key)
140
+ else:
141
+ retained.append(item)
142
+ items = retained + [
143
+ item for item in items
144
+ if item.get(id_key) is None or str(item.get(id_key)) not in seen
145
+ ]
133
146
  folder.mkdir(parents=True, exist_ok=True)
134
147
  (folder / "index.json").write_text(
135
148
  json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8"
@@ -138,7 +151,7 @@ def write_index(folder, items):
138
151
 
139
152
  # ── 各类型的 explode 逻辑 ──
140
153
 
141
- def explode_pages(out_dir, items):
154
+ def explode_pages(out_dir, items, preserve_existing=False):
142
155
  index = []
143
156
  for p in items:
144
157
  pid = p.get("id", "x")
@@ -148,66 +161,95 @@ def explode_pages(out_dir, items):
148
161
  meta = {k: v for k, v in p.items() if k != "value"}
149
162
  meta["html_file"] = f".draftgo/pages/{fname}"
150
163
  index.append(meta)
151
- write_index(out_dir, index)
164
+ write_index(out_dir, index, preserve_existing)
152
165
  return len(index)
153
166
 
154
167
 
155
- def explode_navigations(out_dir, items):
168
+ def explode_navigations(out_dir, items, preserve_existing=False):
156
169
  index = []
157
170
  for item in items:
158
171
  iid = item.get("id", "x")
159
172
  name_slug = safe_slug(item.get("code") or item.get("route") or iid)
160
173
  fname = f"nav_{iid}_{name_slug}.html"
161
- (out_dir / fname).write_text(item.get("html", ""), encoding="utf-8")
174
+ # 列表接口只返回导航元数据。详情拉取失败时不能以空内容覆盖本地导航。
175
+ if isinstance(item.get("html"), str):
176
+ (out_dir / fname).write_text(item["html"], encoding="utf-8")
177
+ else:
178
+ print(
179
+ f" WARN nav_id={iid} 未取得 HTML,保留本地文件: {fname}",
180
+ file=sys.stderr,
181
+ )
162
182
  meta = {k: v for k, v in item.items() if k != "html"}
163
183
  meta["html_file"] = f".draftgo/navigations/{fname}"
164
184
  index.append(meta)
165
- write_index(out_dir, index)
185
+ write_index(out_dir, index, preserve_existing)
166
186
  return len(index)
167
187
 
168
188
 
169
- def explode_docs(out_dir, items):
189
+ def explode_docs(out_dir, items, preserve_existing=False):
170
190
  index = []
171
191
  for art in items:
172
192
  aid = art.get("id", "x")
173
193
  slug = safe_slug(art.get("slug") or art.get("title") or aid)
174
- fname = f"article_{aid}_{slug}.md"
194
+ fname = f"article_{aid}_{slug}.html"
175
195
  content = art.get("content") or ""
176
196
  (out_dir / fname).write_text(content, encoding="utf-8")
177
197
  meta = {k: v for k, v in art.items() if k != "content"}
178
198
  meta["content_file"] = f".draftgo/docs/articles/{fname}"
179
199
  index.append(meta)
180
- write_index(out_dir, index)
200
+ write_index(out_dir, index, preserve_existing)
181
201
  return len(index)
182
202
 
183
203
 
184
- def explode_scripts(out_dir, items):
204
+ def explode_scripts(out_dir, items, preserve_existing=False):
185
205
  index = []
186
206
  for sc in items:
187
207
  sid = sc.get("id", "x")
188
208
  slug = safe_slug(sc.get("slug") or sc.get("name") or sid)
189
- ext = _LANG_EXT.get(str(sc.get("language") or "").lower(), "txt")
190
- fname = f"script_{sid}_{slug}.{ext}"
191
- (out_dir / fname).write_text(sc.get("code") or "", encoding="utf-8")
192
- meta = {k: v for k, v in sc.items() if k != "code"}
209
+ fname = f"script_{sid}_{slug}.go"
210
+ # 列表接口不含 code;详情拉取失败时绝不能用空内容覆盖受管源码。
211
+ if "code" in sc:
212
+ (out_dir / fname).write_text(sc["code"], encoding="utf-8")
213
+ else:
214
+ print(f" WARN script_id={sid} 未取得代码,保留本地文件: {fname}", file=sys.stderr)
215
+ meta = {k: v for k, v in sc.items() if k not in ("code", "language")}
193
216
  meta["code_file"] = f".draftgo/custom_scripts/{fname}"
194
217
  index.append(meta)
195
- write_index(out_dir, index)
218
+ write_index(out_dir, index, preserve_existing)
196
219
  return len(index)
197
220
 
198
221
 
199
222
  # ── 按类型拉取 ──
200
223
 
224
+ def select_items(items, ids, id_key, type_name):
225
+ """筛选指定目标,并拒绝用空结果覆盖现有索引。"""
226
+ if not ids:
227
+ return items
228
+ wanted = [str(value) for value in ids]
229
+ found = {str(item.get(id_key)) for item in items if item.get(id_key) is not None}
230
+ missing = [value for value in wanted if value not in found]
231
+ if missing:
232
+ print(
233
+ f" ERR {type_name} 云端未返回目标 {id_key}: {', '.join(missing)}",
234
+ file=sys.stderr,
235
+ )
236
+ return None
237
+ return [item for item in items if str(item.get(id_key)) in wanted]
238
+
201
239
  def pull_pages(server, token, root, ids=None):
202
240
  raw = fetch(server, token, ENDPOINTS["pages"])
203
241
  if raw is None:
204
- return 0
242
+ return None
205
243
  items = extract_items(raw)
206
- if ids:
207
- items = [p for p in items if str(p.get("id")) in ids]
244
+ items = select_items(items, ids, "id", "pages")
245
+ if items is None:
246
+ return None
247
+ if any(not isinstance(p.get("value"), dict) or not isinstance(p["value"].get("html"), str) for p in items):
248
+ print(" ERR pages 响应缺少 HTML,未写入本地缓存", file=sys.stderr)
249
+ return None
208
250
  out = root / ".draftgo/pages"
209
251
  out.mkdir(parents=True, exist_ok=True)
210
- count = explode_pages(out, items)
252
+ count = explode_pages(out, items, preserve_existing=bool(ids))
211
253
  print(f" OK pages ({count} items)")
212
254
  return count
213
255
 
@@ -215,13 +257,33 @@ def pull_pages(server, token, root, ids=None):
215
257
  def pull_navigations(server, token, root, ids=None):
216
258
  raw = fetch(server, token, ENDPOINTS["navigations"])
217
259
  if raw is None:
218
- return 0
260
+ return None
219
261
  items = extract_items(raw)
220
- if ids:
221
- items = [n for n in items if str(n.get("id")) in ids]
262
+ items = select_items(items, ids, "id", "navigations")
263
+ if items is None:
264
+ return None
265
+ hydrated = []
266
+ for item in items:
267
+ code = item.get("code")
268
+ if not code:
269
+ print(
270
+ f" ERR nav_id={item.get('id', '?')} 缺少 code,未写入本地缓存",
271
+ file=sys.stderr,
272
+ )
273
+ return None
274
+ detail = unwrap(fetch(
275
+ server, token, f"/api/navigations/{urllib.parse.quote(str(code), safe='')}"
276
+ ))
277
+ if not isinstance(detail, dict) or not isinstance(detail.get("html"), str):
278
+ print(
279
+ f" ERR nav_id={item.get('id', '?')} 未取得 HTML,未写入本地缓存",
280
+ file=sys.stderr,
281
+ )
282
+ return None
283
+ hydrated.append({**item, **detail})
222
284
  out = root / ".draftgo/navigations"
223
285
  out.mkdir(parents=True, exist_ok=True)
224
- count = explode_navigations(out, items)
286
+ count = explode_navigations(out, hydrated, preserve_existing=bool(ids))
225
287
  print(f" OK navigations ({count} items)")
226
288
  return count
227
289
 
@@ -230,50 +292,40 @@ def pull_simple(server, token, root, type_name, ids=None):
230
292
  """拉取仅需 index.json 的简单类型(db_meta, aihub, roles, users, system_config)。"""
231
293
  raw = fetch(server, token, ENDPOINTS[type_name])
232
294
  if raw is None:
233
- return 0
295
+ return None
234
296
  items = extract_items(raw)
235
- if ids:
236
- id_key = "config_key" if type_name == "system_config" else "id"
237
- items = [it for it in items if str(it.get(id_key)) in ids]
297
+ id_key = "config_key" if type_name == "system_config" else "id"
298
+ items = select_items(items, ids, id_key, type_name)
299
+ if items is None:
300
+ return None
238
301
  out = root / f".draftgo/{type_name}"
239
- write_index(out, items)
302
+ write_index(out, items, preserve_existing=bool(ids), id_key=id_key)
240
303
  print(f" OK {type_name} ({len(items)} items)")
241
304
  return len(items)
242
305
 
243
306
 
244
- def pull_external_apis(server, token, root, ids=None):
245
- raw = fetch(server, token, ENDPOINTS["external_apis"])
246
- if raw is None:
247
- return 0
248
- items = extract_items(raw)
249
- if ids:
250
- items = [it for it in items if str(it.get("id")) in ids]
251
- hydrated = []
252
- for it in items:
253
- api_id = it.get("id")
254
- detail = unwrap(fetch(server, token, f"/api/external-apis/{api_id}")) if api_id else None
255
- hydrated.append({**it, **detail} if isinstance(detail, dict) else it)
256
- out = root / ".draftgo/external_apis"
257
- write_index(out, hydrated)
258
- print(f" OK external_apis ({len(hydrated)} items)")
259
- return len(hydrated)
260
-
261
-
262
307
  def pull_docs(server, token, root, ids=None):
263
308
  raw = fetch(server, token, ENDPOINTS["docs"])
264
309
  if raw is None:
265
- return 0
310
+ return None
266
311
  items = extract_items(raw)
267
- if ids:
268
- items = [it for it in items if str(it.get("id")) in ids]
312
+ items = select_items(items, ids, "id", "docs")
313
+ if items is None:
314
+ return None
269
315
  hydrated = []
270
316
  for it in items:
271
317
  aid = it.get("id")
272
318
  detail = unwrap(fetch(server, token, f"/api/docs/articles/{aid}")) if aid else None
273
- hydrated.append({**it, **detail} if isinstance(detail, dict) else it)
319
+ if not isinstance(detail, dict) or not isinstance(detail.get("content"), str):
320
+ print(
321
+ f" ERR article_id={aid or '?'} 未取得正文,未写入本地缓存",
322
+ file=sys.stderr,
323
+ )
324
+ return None
325
+ hydrated.append({**it, **detail})
274
326
  out = root / ".draftgo/docs/articles"
275
327
  out.mkdir(parents=True, exist_ok=True)
276
- count = explode_docs(out, hydrated)
328
+ count = explode_docs(out, hydrated, preserve_existing=bool(ids))
277
329
  print(f" OK docs ({count} articles)")
278
330
  return count
279
331
 
@@ -281,12 +333,13 @@ def pull_docs(server, token, root, ids=None):
281
333
  def pull_doc_categories(server, token, root, ids=None):
282
334
  raw = fetch(server, token, ENDPOINTS["doc_categories"])
283
335
  if raw is None:
284
- return 0
336
+ return None
285
337
  items = extract_items(raw)
286
- if ids:
287
- items = [it for it in items if str(it.get("id")) in ids]
338
+ items = select_items(items, ids, "id", "doc_categories")
339
+ if items is None:
340
+ return None
288
341
  out = root / ".draftgo/doc_categories"
289
- write_index(out, items)
342
+ write_index(out, items, preserve_existing=bool(ids))
290
343
  print(f" OK doc_categories ({len(items)} items)")
291
344
  return len(items)
292
345
 
@@ -294,14 +347,26 @@ def pull_doc_categories(server, token, root, ids=None):
294
347
  def pull_custom_scripts(server, token, root, ids=None):
295
348
  raw = fetch(server, token, ENDPOINTS["custom_scripts"])
296
349
  if raw is None:
297
- return 0
350
+ return None
298
351
  items = extract_items(raw)
299
- if ids:
300
- items = [it for it in items if str(it.get("id")) in ids]
352
+ items = select_items(items, ids, "id", "custom_scripts")
353
+ if items is None:
354
+ return None
355
+ hydrated = []
356
+ for it in items:
357
+ script_id = it.get("id")
358
+ detail = unwrap(fetch(server, token, f"/api/scripts/{script_id}")) if script_id else None
359
+ if not isinstance(detail, dict) or not isinstance(detail.get("code"), str):
360
+ print(
361
+ f" ERR script_id={script_id or '?'} 未取得代码,未写入本地缓存",
362
+ file=sys.stderr,
363
+ )
364
+ return None
365
+ hydrated.append({**it, **detail})
301
366
  out = root / ".draftgo/custom_scripts"
302
367
  out.mkdir(parents=True, exist_ok=True)
303
- count = explode_scripts(out, items)
304
- print(f" OK custom_scripts ({count} scripts)")
368
+ count = explode_scripts(out, hydrated, preserve_existing=bool(ids))
369
+ print(f" OK custom_scripts ({count} scripts, code from details)")
305
370
  return count
306
371
 
307
372
 
@@ -316,7 +381,6 @@ PULL_HANDLERS = {
316
381
  "roles": lambda s, t, r, ids=None: pull_simple(s, t, r, "roles", ids),
317
382
  "users": lambda s, t, r, ids=None: pull_simple(s, t, r, "users", ids),
318
383
  "system_config": lambda s, t, r, ids=None: pull_simple(s, t, r, "system_config", ids),
319
- "external_apis": pull_external_apis,
320
384
  "docs": pull_docs,
321
385
  "doc_categories": pull_doc_categories,
322
386
  "custom_scripts": pull_custom_scripts,
@@ -325,14 +389,15 @@ PULL_HANDLERS = {
325
389
 
326
390
  def pull_all(server, token, root):
327
391
  """拉取全部类型,供 init 复用。"""
328
- pull_pages(server, token, root)
329
- pull_navigations(server, token, root)
392
+ results = [pull_pages(server, token, root), pull_navigations(server, token, root)]
330
393
  for t in ("db_meta", "aihub", "roles", "users", "system_config"):
331
- pull_simple(server, token, root, t)
332
- pull_external_apis(server, token, root)
333
- pull_doc_categories(server, token, root)
334
- pull_docs(server, token, root)
335
- pull_custom_scripts(server, token, root)
394
+ results.append(pull_simple(server, token, root, t))
395
+ results.extend([
396
+ pull_doc_categories(server, token, root),
397
+ pull_docs(server, token, root),
398
+ pull_custom_scripts(server, token, root),
399
+ ])
400
+ return None if any(result is None for result in results) else results
336
401
 
337
402
 
338
403
  def main():
@@ -345,7 +410,8 @@ def main():
345
410
  root = DEFAULT_ROOT
346
411
 
347
412
  if mode == "--all":
348
- pull_all(server, token, root)
413
+ if pull_all(server, token, root) is None:
414
+ sys.exit(1)
349
415
  return
350
416
 
351
417
  if mode not in PULL_HANDLERS:
@@ -353,7 +419,8 @@ def main():
353
419
  sys.exit(1)
354
420
 
355
421
  ids = sys.argv[2:] or None
356
- PULL_HANDLERS[mode](server, token, root, ids)
422
+ if PULL_HANDLERS[mode](server, token, root, ids) is None:
423
+ sys.exit(1)
357
424
 
358
425
 
359
426
  if __name__ == "__main__":