draftgo-cli 3.0.35 → 3.0.39

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 (64) hide show
  1. package/README.md +220 -272
  2. package/package.json +6 -2
  3. package/resources/skill/SKILL.md +114 -55
  4. package/resources/skill/init/SKILL.md +29 -15
  5. package/resources/skill/manifest.json +5 -4
  6. package/resources/skill/push/SKILL.md +41 -29
  7. package/resources/skill/references/aihub.md +8 -5
  8. package/resources/skill/references/api-endpoints.md +5 -3
  9. package/resources/skill/references/architecture.md +1 -1
  10. package/resources/skill/references/checkout.md +116 -0
  11. package/resources/skill/references/custom-services.md +9 -10
  12. package/resources/skill/references/data.md +4 -2
  13. package/resources/skill/references/frontend.md +1 -1
  14. package/resources/skill/references/mcp.md +101 -0
  15. package/resources/skill/references/modules.md +8 -8
  16. package/resources/skill/references/parallel.md +6 -3
  17. package/resources/skill/references/runtime.md +7 -10
  18. package/resources/skill/scripts/README.md +8 -0
  19. package/resources/skill/story/SKILL.md +8 -8
  20. package/src/cli.js +5 -0
  21. package/src/commandRegistry.js +7 -1
  22. package/src/commands/api.js +24 -187
  23. package/src/commands/autoPush.js +48 -17
  24. package/src/commands/check.js +17 -47
  25. package/src/commands/checkout.js +18 -0
  26. package/src/commands/commit.js +21 -0
  27. package/src/commands/conflict.js +30 -0
  28. package/src/commands/conflicts.js +16 -0
  29. package/src/commands/connect.js +60 -48
  30. package/src/commands/delete.js +79 -64
  31. package/src/commands/deploy.js +18 -10
  32. package/src/commands/diff.js +23 -0
  33. package/src/commands/help.js +99 -75
  34. package/src/commands/init.js +4 -10
  35. package/src/commands/local.js +23 -6
  36. package/src/commands/map.js +89 -89
  37. package/src/commands/mcp.js +126 -0
  38. package/src/commands/sync.js +28 -43
  39. package/src/commands/verifyUi.js +3 -2
  40. package/src/localdev/index.js +37 -7
  41. package/src/localdev/mysqlClient.js +1 -1
  42. package/src/mcp/client.js +275 -0
  43. package/src/mcp/hosts.js +520 -0
  44. package/src/mcp/protocol.js +173 -0
  45. package/src/mcp/stdio.js +300 -0
  46. package/src/mcp/tools.js +47 -0
  47. package/src/platforms.js +3 -4
  48. package/src/projectConfig.js +91 -49
  49. package/src/projectMap.js +123 -460
  50. package/src/skill.js +6 -28
  51. package/src/worktree/backend.js +326 -0
  52. package/src/worktree/errors.js +28 -0
  53. package/src/worktree/index.js +461 -0
  54. package/src/worktree/manifest.js +75 -0
  55. package/src/worktree/streams.js +200 -0
  56. package/src/worktree/types.js +103 -0
  57. package/src/worktree/validate.js +37 -0
  58. package/resources/skill/pull/SKILL.md +0 -33
  59. package/resources/skill/references/api.json +0 -20248
  60. package/resources/skill/scripts/draftgo_delete.py +0 -149
  61. package/resources/skill/scripts/draftgo_init.py +0 -80
  62. package/resources/skill/scripts/draftgo_pull.py +0 -427
  63. package/resources/skill/scripts/draftgo_push.py +0 -1022
  64. package/src/python.js +0 -27
@@ -1,1022 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- DraftGo Push Script
4
- 推送本地修改到云端。覆盖所有 init 拉取的类型:
5
- pages / nav / db_meta / aihub / system_config / roles / users
6
- docs / doc_categories / custom_scripts
7
-
8
- 用法:
9
- python draftgo_push.py pages [page_id ...]
10
- python draftgo_push.py nav [nav_id ...]
11
- python draftgo_push.py db_meta [db_meta_id ...]
12
- python draftgo_push.py aihub [aihub_id ...]
13
- python draftgo_push.py system_config [config_key ...]
14
- python draftgo_push.py docs [article_id ...]
15
- python draftgo_push.py doc_categories [category_id ...]
16
- python draftgo_push.py custom_scripts [script_id ...]
17
- python draftgo_push.py roles [role_id ...]
18
- python draftgo_push.py users [user_id ...]
19
- """
20
- import json, sys, re, datetime
21
- from pathlib import Path
22
- import urllib.request, urllib.error
23
-
24
- SCRIPT_DIR = Path(__file__).resolve().parent
25
- PROBE_ROUTES = False
26
- DRY_RUN = False
27
- RUN_FAILURES = 0
28
-
29
-
30
- def find_project_root(start: Path) -> Path:
31
- """向上查找包含 `.draftgo/` 的目录作为项目根。"""
32
- cur = start
33
- for _ in range(10):
34
- if (cur / ".draftgo").is_dir():
35
- return cur
36
- if cur.parent == cur:
37
- break
38
- cur = cur.parent
39
- # CWD 回退:脚本可能在中文路径下被调用,SCRIPT_DIR 解析失败时用 CWD
40
- cwd = Path.cwd()
41
- if (cwd / ".draftgo").is_dir():
42
- return cwd
43
- return start.parents[3] if len(start.parents) >= 4 else start
44
-
45
-
46
- DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
47
-
48
- def load_config():
49
- cfg_path = DEFAULT_ROOT / ".draftgo/config.json"
50
- if not cfg_path.exists():
51
- print("ERR: .draftgo/config.json not found, run /draftgo init first", file=sys.stderr)
52
- sys.exit(1)
53
- cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
54
- return cfg["server"].rstrip("/"), cfg["token"], cfg
55
-
56
-
57
- def _lessons_reminder(cfg):
58
- if DRY_RUN:
59
- return
60
- if not cfg.get("lessons_on_push", False):
61
- return
62
- print("")
63
- print("[Lessons 回顾] Push 完成。回顾本次是否需要写入 .draftgo/lessons/:")
64
- print(" - 踩了文档没写的坑")
65
- print(" - 尝试了 ≥2 次才找到正确方案")
66
- print(" - 发现了违反直觉的行为或约束")
67
- print(" - 产出了通用性强的设计模式")
68
- print(" 未命中则跳过。")
69
-
70
-
71
- def api_call(method, server, token, path, body=None, acceptable_errors=()):
72
- """发起 API 调用,并记录调用方不会恢复的失败。"""
73
- global RUN_FAILURES
74
- data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
75
- headers = {
76
- "Authorization": f"Bearer {token}",
77
- "User-Agent": (
78
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
79
- "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
80
- ),
81
- "Accept": "application/json",
82
- }
83
- if data is not None:
84
- headers["Content-Type"] = "application/json"
85
- req = urllib.request.Request(
86
- f"{server}{path}",
87
- data=data,
88
- method=method,
89
- headers=headers,
90
- )
91
- try:
92
- with urllib.request.urlopen(req, timeout=15) as r:
93
- return True, r.status, r.read().decode("utf-8", errors="replace")
94
- except urllib.error.HTTPError as e:
95
- body_text = e.read().decode(errors="replace")
96
- if e.code not in acceptable_errors:
97
- RUN_FAILURES += 1
98
- return False, e.code, body_text
99
- except Exception as e:
100
- RUN_FAILURES += 1
101
- return False, None, str(e)
102
-
103
-
104
- def _info(ok, status, body):
105
- """把 api_call 三元组压成简短描述,兼容旧打印格式。"""
106
- if ok:
107
- return status
108
- if status is None:
109
- return body or "error"
110
- return f"HTTP {status}: {body}"
111
-
112
-
113
- def _extract_created_id(body_text):
114
- """从 POST 成功响应中提取新建资源的 id。
115
- 信封格式: {code, data: {id:...}} 或 {code, data: [{id:...}]}
116
- """
117
- try:
118
- resp = json.loads(body_text)
119
- data = resp.get("data") if isinstance(resp, dict) else None
120
- if isinstance(data, list):
121
- data = data[0] if data else None
122
- if isinstance(data, dict):
123
- return data.get("id")
124
- except Exception:
125
- pass
126
- return None
127
-
128
-
129
- def _writeback_index(rel_index, items):
130
- """将更新后的 index 列表写回 .draftgo/ 下的 index.json。"""
131
- path = DEFAULT_ROOT / ".draftgo" / rel_index
132
- if not path.exists() and not (DEFAULT_ROOT / ".draftgo").exists():
133
- path = DEFAULT_ROOT / rel_index
134
- path.parent.mkdir(parents=True, exist_ok=True)
135
- path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")
136
-
137
-
138
- def _safe_slug(s):
139
- """与 pull 脚本一致的文件名 slug 规则。"""
140
- return str(s).strip("/").replace("/", "_") or "root"
141
-
142
-
143
- def _rename_to_canonical(sub_dir, old_rel, prefix, new_id, slug_src, ext):
144
- """新建成功后把本地文件重命名为 pull 约定的 {prefix}_{id}_{slug}.{ext},
145
- 与 /draftgo pull 落地命名保持一致。返回新的相对路径(.draftgo/...);
146
- 任何异常都回退为原相对路径,不阻断推送。"""
147
- new_name = f"{prefix}_{new_id}_{_safe_slug(slug_src)}.{ext}"
148
- new_rel = f".draftgo/{sub_dir}/{new_name}"
149
- try:
150
- old_path = DEFAULT_ROOT / old_rel
151
- new_path = DEFAULT_ROOT / new_rel
152
- if old_path.exists() and old_path.resolve() != new_path.resolve():
153
- old_path.rename(new_path)
154
- return new_rel
155
- except Exception:
156
- return old_rel
157
-
158
-
159
- def _load_index(rel_index, allow_missing=False):
160
- """读取 .draftgo/ 下的 index.json;支持旧 (pages/index.json) 与新 (.draftgo/pages/index.json) 两种布局。"""
161
- new_path = DEFAULT_ROOT / ".draftgo" / rel_index
162
- legacy_path = DEFAULT_ROOT / rel_index
163
- path = new_path if new_path.exists() else legacy_path
164
- if not path.exists():
165
- if allow_missing:
166
- print(f" WARN {rel_index} 不存在,将尝试从云端恢复指定目标的索引")
167
- return []
168
- print(f"ERR: {rel_index} not found under .draftgo/, run /draftgo init first", file=sys.stderr)
169
- sys.exit(1)
170
- return json.loads(path.read_text(encoding="utf-8"))
171
-
172
-
173
- def _filter_items_by_ids(items, ids, id_key, type_name, orphan=None):
174
- """按用户指定 id 过滤;指定 id 不存在时直接失败,避免 0 目标假成功。"""
175
- if not ids:
176
- return items
177
- wanted = [str(x) for x in ids]
178
- found = {str(it.get(id_key)) for it in items if it.get(id_key) is not None}
179
- missing = [x for x in wanted if x not in found]
180
- if missing:
181
- print(f"ERR: {type_name} index.json 未登记目标 {id_key}: {', '.join(missing)}", file=sys.stderr)
182
- if orphan:
183
- hints = orphan(missing)
184
- if hints:
185
- print(" 发现疑似未入索引的本地文件:", file=sys.stderr)
186
- for hint in hints[:8]:
187
- print(f" - {hint}", file=sys.stderr)
188
- print(" 请先恢复/追加对应 index 元数据,或重新运行 pull 后再 push。", file=sys.stderr)
189
- sys.exit(1)
190
- return [it for it in items if str(it.get(id_key)) in wanted]
191
-
192
-
193
- def _response_items(body):
194
- """解析列表接口的常见统一信封格式。"""
195
- try:
196
- raw = json.loads(body)
197
- except Exception:
198
- return []
199
- if isinstance(raw, dict) and "data" in raw:
200
- raw = raw["data"]
201
- if isinstance(raw, list):
202
- return raw
203
- if isinstance(raw, dict):
204
- for key in ("items", "results", "data"):
205
- if isinstance(raw.get(key), list):
206
- return raw[key]
207
- return [raw]
208
- return []
209
-
210
-
211
- def _response_item(body):
212
- """解析单资源响应的统一信封。"""
213
- try:
214
- raw = json.loads(body)
215
- except Exception:
216
- return None
217
- if isinstance(raw, dict) and "data" in raw:
218
- raw = raw["data"]
219
- return raw if isinstance(raw, dict) else None
220
-
221
-
222
- def _cloud_version_allows_push(server, token, path, local_item, label):
223
- """云端优先:本地 pull 基线落后于云端时禁止静默覆盖。
224
-
225
- 没有历史 updated_at 的旧缓存保持兼容;404 由调用方按默认重建策略处理。
226
- """
227
- global RUN_FAILURES
228
- baseline = local_item.get("updated_at")
229
- if not baseline:
230
- return True
231
- ok, status, body = api_call("GET", server, token, path, acceptable_errors=(404,))
232
- if not ok and status == 404:
233
- return True
234
- if not ok:
235
- return False
236
- remote = _response_item(body)
237
- remote_updated = remote.get("updated_at") if remote else None
238
- if remote_updated and str(remote_updated) != str(baseline):
239
- RUN_FAILURES += 1
240
- conflict_dir = DEFAULT_ROOT / ".draftgo" / "sync-conflicts"
241
- conflict_dir.mkdir(parents=True, exist_ok=True)
242
- identity = str(local_item.get("id") or label).replace("/", "_").replace("\\", "_")
243
- stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
244
- conflict_path = conflict_dir / f"{identity}-{stamp}.json"
245
- conflict_path.write_text(json.dumps({
246
- "kind": "remote-newer",
247
- "resource_path": path,
248
- "baseline_updated_at": baseline,
249
- "remote_updated_at": remote_updated,
250
- "local": local_item,
251
- "remote": remote,
252
- }, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
253
- print(
254
- f" ERR [{label}] 云端版本已变化(local={baseline}, remote={remote_updated});"
255
- f"云端优先,已保存冲突快照:{conflict_path.relative_to(DEFAULT_ROOT)}。",
256
- file=sys.stderr,
257
- )
258
- return False
259
- return True
260
-
261
-
262
- def _recover_missing_index_entries(
263
- server, token, items, ids, id_key, type_name, rel_index, endpoint,
264
- file_spec=None, drop_fields=(),
265
- ):
266
- """补齐缺失的 index 条目,且绝不覆盖本地受管内容。
267
-
268
- 只有显式指定的目标才可恢复,防止 `push --all` 把不明文件自动纳管。
269
- file_spec 为 (目录, index 文件字段, 文件名前缀, 扩展名列表)。
270
- """
271
- if not ids:
272
- return items
273
- wanted = [str(value) for value in ids]
274
- registered = {str(item.get(id_key)) for item in items if item.get(id_key) is not None}
275
- missing = [value for value in wanted if value not in registered]
276
- if not missing:
277
- return items
278
- if DRY_RUN:
279
- print(
280
- f"ERR: {type_name} index.json 未登记目标 {id_key}: {', '.join(missing)}。"
281
- "dry-run 不会回读云端或修改本地索引。",
282
- file=sys.stderr,
283
- )
284
- sys.exit(1)
285
-
286
- ok, status, body = api_call("GET", server, token, endpoint)
287
- if not ok:
288
- print(
289
- f"ERR: 无法恢复 {type_name} index.json:云端回读失败 -> {_info(ok, status, body)}",
290
- file=sys.stderr,
291
- )
292
- sys.exit(1)
293
- remote_by_id = {
294
- str(item.get(id_key)): item
295
- for item in _response_items(body)
296
- if isinstance(item, dict) and item.get(id_key) is not None
297
- }
298
- absent = [value for value in missing if value not in remote_by_id]
299
- if absent:
300
- print(
301
- f"ERR: {type_name} 云端不存在目标 {id_key}: {', '.join(absent)}。"
302
- "本地资源可能已成为孤儿文件,请确认后删除本地文件及其引用,"
303
- "或改为新建资源推送。",
304
- file=sys.stderr,
305
- )
306
- if file_spec:
307
- rel_dir, _, prefix, exts = file_spec
308
- hints = _orphan_hints(rel_dir, prefix, absent, exts)
309
- for hint in hints:
310
- print(f" 建议删除:{hint}", file=sys.stderr)
311
- sys.exit(1)
312
-
313
- recovered = []
314
- for value in missing:
315
- meta = dict(remote_by_id[value])
316
- if file_spec:
317
- rel_dir, file_field, prefix, exts = file_spec
318
- candidates = _orphan_hints(rel_dir, prefix, [value], exts)
319
- if len(candidates) != 1:
320
- detail = "未找到" if not candidates else f"找到 {len(candidates)} 个"
321
- print(
322
- f"ERR: {type_name} {id_key}={value} 云端存在,但本地孤儿文件{detail},"
323
- "无法安全补齐索引。请恢复唯一的本地文件或运行 pull 后重试。",
324
- file=sys.stderr,
325
- )
326
- sys.exit(1)
327
- meta[file_field] = candidates[0]
328
- for field in drop_fields:
329
- meta.pop(field, None)
330
- items.append(meta)
331
- recovered.append(value)
332
-
333
- _writeback_index(rel_index, items)
334
- print(f" OK {type_name}: 已从云端补齐 index.json 目标 {', '.join(recovered)},继续推送本地内容")
335
- return items
336
-
337
-
338
- def _orphan_hints(rel_dir, prefix, missing_ids, exts):
339
- base = DEFAULT_ROOT / ".draftgo" / rel_dir
340
- if not base.exists():
341
- return []
342
- hints = []
343
- for mid in missing_ids:
344
- for ext in exts:
345
- for p in base.glob(f"{prefix}_{mid}_*{ext}"):
346
- hints.append(f".draftgo/{rel_dir}/{p.name}")
347
- return hints
348
-
349
-
350
- def _warn_orphan_files(rel_dir, items, file_field, prefix, exts, type_name):
351
- base = DEFAULT_ROOT / ".draftgo" / rel_dir
352
- if not base.exists():
353
- return
354
- indexed = {str(it.get(file_field, "")).replace("\\", "/") for it in items}
355
- orphans = []
356
- for p in base.iterdir():
357
- if not p.is_file():
358
- continue
359
- if not p.name.startswith(f"{prefix}_") or p.suffix not in exts:
360
- continue
361
- rel = f".draftgo/{rel_dir}/{p.name}"
362
- if rel not in indexed:
363
- orphans.append(rel)
364
- if orphans:
365
- print(f" WARN {type_name}: 发现 {len(orphans)} 个未登记到 index.json 的本地文件,本次不会推送:")
366
- for rel in orphans[:8]:
367
- print(f" - {rel}")
368
-
369
-
370
- def _summary(type_name, targets):
371
- print(f" SUMMARY {type_name}: targets={targets}")
372
-
373
-
374
- def _preview(type_name, items, id_key="id"):
375
- """打印不会产生云端或本地副作用的推送预览。"""
376
- if not DRY_RUN:
377
- return False
378
- for item in items:
379
- identity = item.get(id_key)
380
- label = item.get("title") or item.get("name") or item.get("code") or identity or "(new)"
381
- action = "UPDATE" if identity else "CREATE"
382
- print(f" DRY-RUN {action} [{label}]")
383
- _summary(type_name, len(items))
384
- return True
385
-
386
-
387
- def sync_pages(server, token, ids=None):
388
- all_pages = _load_index("pages/index.json", allow_missing=bool(ids))
389
- all_pages = _recover_missing_index_entries(
390
- server, token, all_pages, ids, "id", "pages", "pages/index.json", "/api/pages/",
391
- file_spec=("pages", "html_file", "page", [".html"]), drop_fields=("value",),
392
- )
393
- _warn_orphan_files("pages", all_pages, "html_file", "page", [".html"], "pages")
394
- pages = _filter_items_by_ids(
395
- all_pages, ids, "id", "pages",
396
- orphan=lambda missing: _orphan_hints("pages", "page", missing, [".html"]),
397
- )
398
- if _preview("pages", pages):
399
- return
400
- dirty = False
401
- for page in pages:
402
- pid = page.get("id")
403
- title = page.get("title", pid or "(new)")
404
- html_file = DEFAULT_ROOT / page.get("html_file", "")
405
- if not html_file.exists():
406
- print(f" SKIP [{title}] html_file not found: {page.get('html_file')}")
407
- continue
408
- html = html_file.read_text(encoding="utf-8")
409
- _lint_page_html(html, title)
410
- payload = {
411
- "title": page.get("title", ""),
412
- "route": page.get("route", ""),
413
- "tag": page.get("tag"),
414
- "menu": page.get("menu"),
415
- "status": page.get("status", "active"),
416
- "permission": page.get("permission", {}),
417
- "value": {"html": html},
418
- }
419
- if pid:
420
- if not _cloud_version_allows_push(server, token, f"/api/pages/{pid}", page, title):
421
- continue
422
- ok, status, body = api_call("PUT", server, token, f"/api/pages/{pid}", payload, acceptable_errors=(404,))
423
- # PUT 404:本地 id 与云端不一致(删了重建 / 跨环境),按 route 自动创建
424
- if not ok and status == 404:
425
- print(f" WARN [{title}] page_id={pid} 不存在,尝试创建")
426
- pid = None
427
- else:
428
- print(f" {'OK' if ok else 'ERR'} [{title}] page_id={pid}{'' if ok else ' -> ' + _info(ok, status, body)}")
429
- if not pid:
430
- ok, status, body = api_call("POST", server, token, "/api/pages/", payload)
431
- new_id = _extract_created_id(body) if ok else None
432
- if ok and new_id:
433
- page["id"] = new_id
434
- new_rel = _rename_to_canonical(
435
- "pages", page.get("html_file", ""), "page", new_id,
436
- page.get("route", new_id), "html")
437
- page["html_file"] = new_rel
438
- dirty = True
439
- print(f" OK [{title}] 已创建 page_id={new_id}(已回写 index)")
440
- else:
441
- print(f" ERR [{title}] 创建失败 -> {_info(ok, status, body)}")
442
- if dirty:
443
- _writeback_index("pages/index.json", all_pages)
444
- _summary("pages", len(pages))
445
-
446
-
447
- def sync_db_meta(server, token, ids=None):
448
- all_metas = _load_index("db_meta/index.json", allow_missing=bool(ids))
449
- all_metas = _recover_missing_index_entries(
450
- server, token, all_metas, ids, "id", "db_meta", "db_meta/index.json", "/api/db-meta",
451
- )
452
- metas = _filter_items_by_ids(all_metas, ids, "id", "db_meta")
453
- if _preview("db_meta", metas):
454
- return
455
- dirty = False
456
- for meta in metas:
457
- mid = meta.get("id")
458
- label = meta.get("label", meta.get("type", mid or "(new)"))
459
- payload = {
460
- "type": meta.get("type"),
461
- "label": meta.get("label"),
462
- "describe": meta.get("describe"),
463
- "schema": meta.get("schema"),
464
- "permission": meta.get("permission"),
465
- "schema_validation": meta.get("schema_validation", 0),
466
- "extra": meta.get("extra"),
467
- }
468
- if mid:
469
- ok, status, body = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload, acceptable_errors=(404,))
470
- # 本地 index 的 id 与云端不一致(删了重建 / 跨环境同步)时 PUT 404,按 type 创建
471
- if not ok and status == 404:
472
- print(f" WARN [{label}] db_meta_id={mid} 不存在,尝试按 type 创建")
473
- mid = None
474
- else:
475
- print(f" {'OK' if ok else 'ERR'} [{label}] db_meta_id={mid}{'' if ok else ' -> ' + _info(ok, status, body)}")
476
- if not mid:
477
- ok, status, body = api_call("POST", server, token, "/api/db-meta", payload)
478
- new_id = _extract_created_id(body) if ok else None
479
- if ok:
480
- if new_id:
481
- meta["id"] = new_id
482
- dirty = True
483
- print(f" OK [{label}] 已创建 db_meta_id={new_id}{'(已回写 index)' if new_id else ''}")
484
- else:
485
- print(f" ERR [{label}] 创建失败 -> {_info(ok, status, body)}")
486
- if dirty:
487
- _writeback_index("db_meta/index.json", all_metas)
488
- _summary("db_meta", len(metas))
489
-
490
-
491
- def sync_nav(server, token, ids=None):
492
- all_navs = _load_index("navigations/index.json", allow_missing=bool(ids))
493
- all_navs = _recover_missing_index_entries(
494
- server, token, all_navs, ids, "id", "nav", "navigations/index.json", "/api/navigations",
495
- file_spec=("navigations", "html_file", "nav", [".html"]), drop_fields=("html",),
496
- )
497
- _warn_orphan_files("navigations", all_navs, "html_file", "nav", [".html"], "nav")
498
- navs = _filter_items_by_ids(
499
- all_navs, ids, "id", "nav",
500
- orphan=lambda missing: _orphan_hints("navigations", "nav", missing, [".html"]),
501
- )
502
- if _preview("nav", navs):
503
- return
504
- dirty = False
505
- for nav in navs:
506
- nid = nav.get("id")
507
- name = nav.get("name", nid or "(new)")
508
- html_file = DEFAULT_ROOT / nav.get("html_file", "")
509
- if not html_file.exists():
510
- print(f" SKIP [{name}] html_file not found: {nav.get('html_file')}")
511
- continue
512
- html = html_file.read_text(encoding="utf-8")
513
- if nid:
514
- ok, status, body = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html}, acceptable_errors=(404,))
515
- if not ok and status == 404:
516
- print(f" WARN [{name}] nav_id={nid} 不存在,尝试创建")
517
- nid = None
518
- else:
519
- print(f" {'OK' if ok else 'ERR'} [{name}] nav_id={nid}{'' if ok else ' -> ' + _info(ok, status, body)}")
520
- if not nid:
521
- # 创建需要完整字段(name/code 必填),PUT 不发的字段在此补齐
522
- payload = {
523
- "name": nav.get("name", ""),
524
- "code": nav.get("code", ""),
525
- "html": html,
526
- "tag": nav.get("tag"),
527
- "order": nav.get("order", 0),
528
- "status": nav.get("status", "active"),
529
- }
530
- ok, status, body = api_call("POST", server, token, "/api/navigations", payload)
531
- new_id = _extract_created_id(body) if ok else None
532
- if ok and new_id:
533
- nav["id"] = new_id
534
- new_rel = _rename_to_canonical(
535
- "navigations", nav.get("html_file", ""), "nav", new_id,
536
- nav.get("code") or nav.get("name") or new_id, "html")
537
- nav["html_file"] = new_rel
538
- dirty = True
539
- print(f" OK [{name}] 已创建 nav_id={new_id}(已回写 index)")
540
- else:
541
- print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
542
- if dirty:
543
- _writeback_index("navigations/index.json", all_navs)
544
- _summary("nav", len(navs))
545
-
546
-
547
- def sync_aihub(server, token, ids=None):
548
- all_items = _load_index("aihub/index.json", allow_missing=bool(ids))
549
- all_items = _recover_missing_index_entries(
550
- server, token, all_items, ids, "id", "aihub", "aihub/index.json", "/api/aihub",
551
- )
552
- items = _filter_items_by_ids(all_items, ids, "id", "aihub")
553
- if _preview("aihub", items):
554
- return
555
- dirty = False
556
- for it in items:
557
- iid = it.get("id")
558
- name = it.get("name", iid or "(new)")
559
- # 服务端 AIHubUpdate 接受的字段子集
560
- payload = {k: it.get(k) for k in (
561
- "type", "name", "data", "priority", "version",
562
- "tags", "describe", "permission", "status",
563
- ) if it.get(k) is not None}
564
- if iid:
565
- ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload, acceptable_errors=(404,))
566
- if not ok and status == 404:
567
- print(f" WARN [{name}] aihub_id={iid} 不存在,尝试创建")
568
- iid = None
569
- else:
570
- print(f" {'OK' if ok else 'ERR'} [{name}] aihub_id={iid}{'' if ok else ' -> ' + _info(ok, status, body)}")
571
- if not iid:
572
- create_payload = {k: payload.get(k) for k in (
573
- "type", "name", "data", "priority", "version",
574
- "tags", "describe", "permission",
575
- ) if payload.get(k) is not None}
576
- ok, status, body = api_call("POST", server, token, "/api/aihub", create_payload)
577
- new_id = _extract_created_id(body) if ok else None
578
- if ok and new_id:
579
- it["id"] = new_id
580
- dirty = True
581
- print(f" OK [{name}] 已创建 aihub_id={new_id}(已回写 index)")
582
- else:
583
- print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
584
- if dirty:
585
- _writeback_index("aihub/index.json", all_items)
586
- _summary("aihub", len(items))
587
-
588
-
589
- def sync_system_config(server, token, keys=None):
590
- items = _load_index("system_config/index.json", allow_missing=bool(keys))
591
- items = _recover_missing_index_entries(
592
- server, token, items, keys, "config_key", "system_config", "system_config/index.json", "/api/system/config",
593
- )
594
- items = _filter_items_by_ids(items, keys, "config_key", "system_config")
595
- if _preview("system_config", items, "config_key"):
596
- return
597
- for it in items:
598
- ck = it.get("config_key")
599
- if not ck:
600
- continue
601
- # 优先使用 parsed_value(init 时 GET 返回的解析值),其次 config_value
602
- value = it.get("parsed_value") if "parsed_value" in it else it.get("config_value")
603
- value_payload = {"config_value": value}
604
- meta_payload = {
605
- "config_value": value,
606
- "value_type": it.get("value_type"),
607
- "category": it.get("category"),
608
- "description": it.get("description"),
609
- "is_sensitive": it.get("is_sensitive"),
610
- "status": it.get("status"),
611
- }
612
- meta_payload = {k: v for k, v in meta_payload.items() if v is not None}
613
- # 前端全局层属于系统默认配置,默认字段的描述/分类/状态由基座维护。
614
- # 推送时只更新值,避免旧 index 中的元信息触发“系统默认字段不允许修改字段描述”。
615
- payload = value_payload if _is_frontend_global_config(it) else meta_payload
616
- ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", payload, acceptable_errors=(404,))
617
- if not ok and _is_protected_system_config_error(body) and payload != value_payload:
618
- print(f" WARN [{ck}] 系统默认字段元信息受保护,改为仅推送 config_value")
619
- ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", value_payload, acceptable_errors=(404,))
620
- if not ok and status == 404:
621
- print(f" WARN [{ck}] system_config 不存在,尝试创建")
622
- create_payload = _system_config_create_payload(ck, it, value)
623
- ok, status, body = api_call("POST", server, token, "/api/system/", create_payload)
624
- print(f" {'OK' if ok else 'ERR'} [{ck}] 已创建{'' if ok else ' -> ' + _info(ok, status, body)}")
625
- else:
626
- print(f" {'OK' if ok else 'ERR'} [{ck}]{'' if ok else ' -> ' + _info(ok, status, body)}")
627
- _summary("system_config", len(items))
628
-
629
-
630
- def _is_frontend_global_config(item):
631
- return (
632
- str(item.get("category") or "") == "frontend_global"
633
- or str(item.get("config_key") or "").startswith("frontend_global_")
634
- )
635
-
636
-
637
- def _is_protected_system_config_error(body):
638
- text = body if isinstance(body, str) else json.dumps(body, ensure_ascii=False)
639
- return "系统默认字段不允许修改字段" in text
640
-
641
-
642
- def _system_config_create_payload(config_key, item, value):
643
- return {
644
- "config_key": config_key,
645
- "config_value": value,
646
- "value_type": item.get("value_type") or _infer_system_config_value_type(value),
647
- "category": item.get("category") or "custom",
648
- "description": item.get("description") or "",
649
- "is_sensitive": bool(item.get("is_sensitive", False)),
650
- "status": item.get("status") or "active",
651
- }
652
-
653
-
654
- def _infer_system_config_value_type(value):
655
- if isinstance(value, bool):
656
- return "bool"
657
- if isinstance(value, int) and not isinstance(value, bool):
658
- return "int"
659
- if isinstance(value, (dict, list)):
660
- return "json"
661
- return "string"
662
-
663
-
664
- def sync_roles(server, token, ids=None):
665
- items = _load_index("roles/index.json", allow_missing=bool(ids))
666
- items = _recover_missing_index_entries(
667
- server, token, items, ids, "id", "roles", "roles/index.json", "/api/roles",
668
- )
669
- items = _filter_items_by_ids(items, ids, "id", "roles")
670
- if _preview("roles", items):
671
- return
672
- for it in items:
673
- rid = it.get("id")
674
- code = it.get("code", rid)
675
- # RoleUpdateRequest 字段
676
- payload = {k: it.get(k) for k in (
677
- "name", "description", "status", "sort_order", "user_visible",
678
- ) if it.get(k) is not None}
679
- ok, status, body = api_call("PUT", server, token, f"/api/roles/{rid}", payload)
680
- print(f" {'OK' if ok else 'ERR'} [{code}] role_id={rid}{'' if ok else ' -> ' + _info(ok, status, body)}")
681
- _summary("roles", len(items))
682
-
683
-
684
- def sync_users(server, token, ids=None):
685
- items = _load_index("users/index.json", allow_missing=bool(ids))
686
- items = _recover_missing_index_entries(
687
- server, token, items, ids, "id", "users", "users/index.json", "/api/users",
688
- )
689
- items = _filter_items_by_ids(items, ids, "id", "users")
690
- if _preview("users", items):
691
- return
692
- for it in items:
693
- uid = it.get("id")
694
- uname = it.get("username", uid)
695
- # UserUpdateRequest 字段子集(不下发 password / role_ids 之类敏感修改)
696
- payload = {k: it.get(k) for k in (
697
- "username", "email", "phone_number", "nickname",
698
- "avatar", "status", "notes",
699
- ) if it.get(k) is not None}
700
- ok, status, body = api_call("PUT", server, token, f"/api/users/{uid}", payload)
701
- print(f" {'OK' if ok else 'ERR'} [{uname}] user_id={uid}{'' if ok else ' -> ' + _info(ok, status, body)}")
702
- _summary("users", len(items))
703
-
704
-
705
- def sync_docs(server, token, ids=None):
706
- """文档文章:从 .draftgo/docs/articles/index.json + 同目录 .md 文件回填正文。"""
707
- all_items = _load_index("docs/articles/index.json", allow_missing=bool(ids))
708
- all_items = _recover_missing_index_entries(
709
- server, token, all_items, ids, "id", "docs", "docs/articles/index.json", "/api/docs/admin/articles",
710
- file_spec=("docs/articles", "content_file", "article", [".html", ".md"]), drop_fields=("content",),
711
- )
712
- _warn_orphan_files("docs/articles", all_items, "content_file", "article", [".html", ".md"], "docs")
713
- items = _filter_items_by_ids(
714
- all_items, ids, "id", "docs",
715
- orphan=lambda missing: _orphan_hints("docs/articles", "article", missing, [".html", ".md"]),
716
- )
717
- if _preview("docs", items):
718
- return
719
- dirty = False
720
- for it in items:
721
- aid = it.get("id")
722
- title = it.get("title", aid or "(new)")
723
- content_rel = it.get("content_file") or ""
724
- content_path = DEFAULT_ROOT / content_rel if content_rel else None
725
- if not content_path or not content_path.exists():
726
- print(f" SKIP [{title}] content_file not found: {content_rel}")
727
- continue
728
- content = content_path.read_text(encoding="utf-8")
729
- # ArticleUpdate / ArticleCreate 接受字段(仅 title 必填,其余有默认值)
730
- payload = {k: it.get(k) for k in (
731
- "title", "slug", "category_id", "summary", "cover", "tags",
732
- "status", "is_top", "sort_order", "seo_title", "seo_description",
733
- "permission",
734
- ) if it.get(k) is not None}
735
- payload["content"] = content
736
- payload["content_type"] = "html"
737
- if aid:
738
- ok, status, body = api_call("PUT", server, token, f"/api/docs/articles/{aid}", payload, acceptable_errors=(404,))
739
- if not ok and status == 404:
740
- print(f" WARN [{title}] article_id={aid} 不存在,尝试创建")
741
- aid = None
742
- else:
743
- print(f" {'OK' if ok else 'ERR'} [{title}] article_id={aid}{'' if ok else ' -> ' + _info(ok, status, body)}")
744
- if not aid:
745
- ok, status, body = api_call("POST", server, token, "/api/docs/articles", payload)
746
- new_id = _extract_created_id(body) if ok else None
747
- if ok and new_id:
748
- it["id"] = new_id
749
- new_rel = _rename_to_canonical(
750
- "docs/articles", it.get("content_file", ""), "article", new_id,
751
- it.get("slug") or it.get("title") or new_id, "html")
752
- it["content_file"] = new_rel
753
- dirty = True
754
- print(f" OK [{title}] 已创建 article_id={new_id}(已回写 index)")
755
- else:
756
- print(f" ERR [{title}] 创建失败 -> {_info(ok, status, body)}")
757
- if dirty:
758
- _writeback_index("docs/articles/index.json", all_items)
759
- _summary("docs", len(items))
760
-
761
-
762
- def sync_doc_categories(server, token, ids=None):
763
- all_items = _load_index("doc_categories/index.json", allow_missing=bool(ids))
764
- all_items = _recover_missing_index_entries(
765
- server, token, all_items, ids, "id", "doc_categories", "doc_categories/index.json", "/api/docs/categories?flat=true",
766
- )
767
- items = _filter_items_by_ids(all_items, ids, "id", "doc_categories")
768
- if _preview("doc_categories", items):
769
- return
770
- dirty = False
771
- for it in items:
772
- cid = it.get("id")
773
- name = it.get("name", cid or "(new)")
774
- # CategoryUpdate 字段
775
- payload = {k: it.get(k) for k in (
776
- "name", "slug", "description", "icon", "parent_id", "sort_order", "status",
777
- ) if it.get(k) is not None}
778
- if cid:
779
- ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload, acceptable_errors=(404,))
780
- if not ok and status == 404:
781
- print(f" WARN [{name}] category_id={cid} 不存在,尝试创建")
782
- cid = None
783
- else:
784
- print(f" {'OK' if ok else 'ERR'} [{name}] category_id={cid}{'' if ok else ' -> ' + _info(ok, status, body)}")
785
- if not cid:
786
- ok, status, body = api_call("POST", server, token, "/api/docs/categories", payload)
787
- new_id = _extract_created_id(body) if ok else None
788
- if ok and new_id:
789
- it["id"] = new_id
790
- dirty = True
791
- print(f" OK [{name}] 已创建 category_id={new_id}(已回写 index)")
792
- else:
793
- print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
794
- if dirty:
795
- _writeback_index("doc_categories/index.json", all_items)
796
- _summary("doc_categories", len(items))
797
-
798
-
799
- def _route_specs_from_code(code):
800
- specs = []
801
- for m in re.finditer(r'@route\(\s*["\']([A-Za-z]+)\s+([^"\']+)["\']\s*\)', code):
802
- specs.append((m.group(1).upper(), m.group(2)))
803
- for m in re.finditer(r'\.Route\(\s*["\']([A-Za-z]+)["\']\s*,\s*["\']([^"\']+)["\']', code):
804
- specs.append((m.group(1).upper(), m.group(2)))
805
- return specs
806
-
807
-
808
- def _probe_script_routes(server, token, script_meta, code):
809
- slug = script_meta.get("slug")
810
- if not slug:
811
- return
812
- for method, route_path in _route_specs_from_code(code):
813
- if method != "GET":
814
- print(f" SKIP probe [{slug}] {method} {route_path}(只自动探测 GET,避免副作用)")
815
- continue
816
- path = route_path if route_path.startswith("/") else f"/{route_path}"
817
- ok, status, body = api_call("GET", server, token, f"/api/x/{slug}{path}")
818
- print(f" {'OK' if ok else 'ERR'} probe GET /api/x/{slug}{path}{'' if ok else ' -> ' + _info(ok, status, body)}")
819
-
820
-
821
- def sync_custom_scripts(server, token, ids=None):
822
- """自定义脚本:从 .draftgo/custom_scripts/index.json + 同目录代码文件回填 code。"""
823
- all_items = _load_index("custom_scripts/index.json", allow_missing=bool(ids))
824
- all_items = _recover_missing_index_entries(
825
- server, token, all_items, ids, "id", "custom_scripts", "custom_scripts/index.json", "/api/scripts/",
826
- file_spec=("custom_scripts", "code_file", "script", [".py", ".js", ".ts", ".sh", ".go", ".txt"]), drop_fields=("code",),
827
- )
828
- _warn_orphan_files("custom_scripts", all_items, "code_file", "script", [".go"], "custom_scripts")
829
- items = _filter_items_by_ids(
830
- all_items, ids, "id", "custom_scripts",
831
- orphan=lambda missing: _orphan_hints("custom_scripts", "script", missing, [".go"]),
832
- )
833
- if _preview("custom_scripts", items):
834
- return
835
- dirty = False
836
- for it in items:
837
- sid = it.get("id")
838
- name = it.get("name") or it.get("slug") or sid or "(new)"
839
- code_rel = it.get("code_file") or ""
840
- code_path = DEFAULT_ROOT / code_rel if code_rel else None
841
- if not code_path or not code_path.exists():
842
- print(f" SKIP [{name}] code_file not found: {code_rel}")
843
- continue
844
- code = code_path.read_text(encoding="utf-8")
845
- if sid:
846
- if not _cloud_version_allows_push(server, token, f"/api/scripts/{sid}", it, name):
847
- continue
848
- # ScriptUpdate 接受字段(不含 slug/mode,避免误改启停/路由)
849
- payload = {k: it.get(k) for k in (
850
- "name", "description", "config", "permission", "go_mod", "go_sum",
851
- ) if it.get(k) is not None}
852
- payload["code"] = code
853
- ok, status, body = api_call("PUT", server, token, f"/api/scripts/{sid}", payload, acceptable_errors=(404,))
854
- if not ok and status == 404:
855
- print(f" WARN [{name}] script_id={sid} 不存在,尝试创建")
856
- sid = None
857
- else:
858
- print(f" {'OK' if ok else 'ERR'} [{name}] script_id={sid}{'' if ok else ' -> ' + _info(ok, status, body)}")
859
- if ok and PROBE_ROUTES and str(it.get("mode") or "").lower() in ("route", "mixed"):
860
- _probe_script_routes(server, token, it, code)
861
- if not sid:
862
- # ScriptCreate 必填 name/slug/code/mode;触发来源统一以代码装饰器为准
863
- payload = {
864
- "name": it.get("name", ""),
865
- "slug": it.get("slug", ""),
866
- "code": code,
867
- "mode": it.get("mode", "route"),
868
- "config": it.get("config"),
869
- "permission": it.get("permission"),
870
- "description": it.get("description"),
871
- "go_mod": it.get("go_mod"),
872
- "go_sum": it.get("go_sum"),
873
- }
874
- payload = {k: v for k, v in payload.items() if v is not None}
875
- ok, status, body = api_call("POST", server, token, "/api/scripts/", payload)
876
- new_id = _extract_created_id(body) if ok else None
877
- if ok and new_id:
878
- it["id"] = new_id
879
- ext = (code_rel.rsplit(".", 1)[-1] if "." in code_rel else "txt")
880
- new_rel = _rename_to_canonical(
881
- "custom_scripts", code_rel, "script", new_id,
882
- it.get("slug") or it.get("name") or new_id, ext)
883
- it["code_file"] = new_rel
884
- dirty = True
885
- print(f" OK [{name}] 已创建 script_id={new_id}(已回写 index)")
886
- if PROBE_ROUTES and str(it.get("mode") or "").lower() in ("route", "mixed"):
887
- _probe_script_routes(server, token, it, code)
888
- else:
889
- print(f" ERR [{name}] 创建失败 -> {_info(ok, status, body)}")
890
- if dirty:
891
- _writeback_index("custom_scripts/index.json", all_items)
892
- _summary("custom_scripts", len(items))
893
-
894
-
895
- def _lint_page_html(html, label):
896
- """Non-blocking lint for page HTML. Prints warnings, never raises."""
897
- warnings = []
898
- # Extract all <script> content
899
- script_blocks = re.findall(r'<script[^>]*>(.*?)</script>', html, re.DOTALL | re.IGNORECASE)
900
- for block_idx, block in enumerate(script_blocks):
901
- # Detect let/const duplicate declarations in same scope
902
- decls = {}
903
- for line_offset, line in enumerate(block.split('\n'), start=1):
904
- for m in re.finditer(r'\b(let|const)\s+([A-Za-z_$][\w$]*)', line):
905
- kind, name = m.group(1), m.group(2)
906
- if name in decls:
907
- prev_kind, prev_line = decls[name]
908
- warnings.append(
909
- f"变量 '{name}' 重复声明 ({prev_kind} @行~{prev_line}, {kind} @行~{line_offset})"
910
- )
911
- else:
912
- decls[name] = (kind, line_offset)
913
- # Detect destructured let/const: let {a, b} = ... or let [a, b] = ...
914
- for line_offset, line in enumerate(block.split('\n'), start=1):
915
- for m in re.finditer(r'\b(let|const)\s*[\[{]([^}\]]+)[}\]]', line):
916
- kind = m.group(1)
917
- names_str = m.group(2)
918
- for nm in re.findall(r'([A-Za-z_$][\w$]*)', names_str):
919
- if nm in decls and decls[nm][1] != line_offset:
920
- prev_kind, prev_line = decls[nm]
921
- warnings.append(
922
- f"变量 '{nm}' 重复声明 ({prev_kind} @行~{prev_line}, {kind} 解构 @行~{line_offset})"
923
- )
924
- # Detect browser native dialogs (alert/confirm/prompt)
925
- for line_offset, line in enumerate(block.split('\n'), start=1):
926
- # Match bare alert/confirm/prompt calls, exclude App.confirm
927
- for m in re.finditer(r'(?<!\w)(?<!App\.)(alert|confirm|prompt)\s*\(', line):
928
- fn = m.group(1)
929
- alt = {'alert': 'App.showModal()', 'confirm': 'await App.confirm()', 'prompt': 'custom input modal'}
930
- warnings.append(
931
- f"禁止使用浏览器原生 {fn}() (@行~{line_offset}), 请用 {alt[fn]}"
932
- )
933
- if warnings:
934
- print(f" [!] [lint] [{label}] found {len(warnings)} potential issue(s):")
935
- for w in warnings:
936
- print(f" {w}")
937
-
938
-
939
- HANDLERS = {
940
- "pages": sync_pages,
941
- "nav": sync_nav,
942
- "db_meta": sync_db_meta,
943
- "aihub": sync_aihub,
944
- "system_config": sync_system_config,
945
- "roles": sync_roles,
946
- "users": sync_users,
947
- "docs": sync_docs,
948
- "doc_categories": sync_doc_categories,
949
- "custom_scripts": sync_custom_scripts,
950
- }
951
-
952
-
953
- def run_batch(server, token, args):
954
- """批量推送:--batch pages 1,2,3 nav 4 custom_scripts 7,8
955
- 解析为多组 (mode, [ids]) 依次执行,共享同一连接配置。"""
956
- groups = []
957
- i = 0
958
- while i < len(args):
959
- mode = args[i]
960
- if mode not in HANDLERS:
961
- print(f"ERR: unknown mode '{mode}' in --batch", file=sys.stderr)
962
- sys.exit(1)
963
- i += 1
964
- ids = []
965
- if i < len(args) and args[i] not in HANDLERS and not args[i].startswith("-"):
966
- ids = args[i].split(",")
967
- i += 1
968
- groups.append((mode, ids or None))
969
- print(f"[batch] {len(groups)} group(s) to push")
970
- for mode, ids in groups:
971
- print(f"\n--- {mode} {'(all)' if not ids else ','.join(ids)} ---")
972
- HANDLERS[mode](server, token, ids)
973
- result = "previewed" if DRY_RUN else "pushed"
974
- print(f"\n[batch] done, {len(groups)} group(s) {result}")
975
-
976
-
977
- def main():
978
- global PROBE_ROUTES, DRY_RUN
979
- if len(sys.argv) < 2:
980
- print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]\n"
981
- f" draftgo_push.py --batch <mode> <ids> [<mode> <ids> ...]")
982
- sys.exit(1)
983
-
984
- args = sys.argv[1:]
985
- if "--probe-routes" in args:
986
- PROBE_ROUTES = True
987
- args = [a for a in args if a != "--probe-routes"]
988
- if "--dry-run" in args:
989
- DRY_RUN = True
990
- args = [a for a in args if a != "--dry-run"]
991
-
992
- if not args:
993
- print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]")
994
- sys.exit(1)
995
-
996
- if args[0] == "--all":
997
- args = ["--batch", *HANDLERS]
998
-
999
- if args[0] == "--batch":
1000
- server, token, cfg = load_config()
1001
- run_batch(server, token, args[1:])
1002
- _lessons_reminder(cfg)
1003
- if RUN_FAILURES:
1004
- print(f"ERR: push finished with {RUN_FAILURES} failed request(s)", file=sys.stderr)
1005
- sys.exit(1)
1006
- return
1007
-
1008
- if args[0] not in HANDLERS:
1009
- print(f"usage: draftgo_push.py {{{'|'.join(HANDLERS)}}} [id ...]")
1010
- sys.exit(1)
1011
- mode = args[0]
1012
- ids = args[1:] or None
1013
- server, token, cfg = load_config()
1014
- HANDLERS[mode](server, token, ids)
1015
- _lessons_reminder(cfg)
1016
- if RUN_FAILURES:
1017
- print(f"ERR: push finished with {RUN_FAILURES} failed request(s)", file=sys.stderr)
1018
- sys.exit(1)
1019
-
1020
-
1021
- if __name__ == "__main__":
1022
- main()