draftgo-cli 1.0.0 → 1.1.1
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 +18 -2
- package/bin/draftgo.js +1 -1
- package/package.json +1 -1
- package/resources/skill/SKILL.md +385 -8
- package/resources/skill/init/SKILL.md +7 -4
- package/resources/skill/rules/frontend.md +19 -0
- package/resources/skill/scripts/draftgo_init.py +12 -10
- package/resources/skill/scripts/draftgo_sync.py +112 -12
- package/resources/skill/sync/SKILL.md +83 -14
- package/src/commands/doctor.js +16 -1
- package/src/commands/help.js +14 -8
- package/src/commands/update.js +68 -1
- package/src/index.js +1 -1
- package/src/updateCheck.js +58 -0
- package/resources/skill/CLAUDE.md +0 -239
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
DraftGo Sync Script
|
|
4
|
-
|
|
4
|
+
推送本地修改到云端。覆盖所有 init 拉取的类型:
|
|
5
|
+
pages / nav / db_meta / aihub / external_apis / system_config / roles / users
|
|
5
6
|
|
|
6
7
|
用法:
|
|
7
|
-
python draftgo_sync.py pages
|
|
8
|
-
python draftgo_sync.py nav
|
|
9
|
-
python draftgo_sync.py db_meta
|
|
8
|
+
python draftgo_sync.py pages [page_id ...]
|
|
9
|
+
python draftgo_sync.py nav [nav_id ...]
|
|
10
|
+
python draftgo_sync.py db_meta [db_meta_id ...]
|
|
11
|
+
python draftgo_sync.py aihub [aihub_id ...]
|
|
12
|
+
python draftgo_sync.py external_apis [api_id ...]
|
|
13
|
+
python draftgo_sync.py system_config [config_key ...]
|
|
14
|
+
python draftgo_sync.py roles [role_id ...] ⚠️ 涉及权限,调用前应人工确认
|
|
15
|
+
python draftgo_sync.py users [user_id ...] ⚠️ 涉及账号,调用前应人工确认
|
|
10
16
|
"""
|
|
11
17
|
import json, sys
|
|
12
18
|
from pathlib import Path
|
|
@@ -131,19 +137,113 @@ def sync_nav(server, token, ids=None):
|
|
|
131
137
|
print(f" {'OK' if ok else 'ERR'} [{name}] nav_id={nid}{'' if ok else ' -> ' + str(info)}")
|
|
132
138
|
|
|
133
139
|
|
|
140
|
+
def sync_aihub(server, token, ids=None):
|
|
141
|
+
items = _load_index("aihub/index.json")
|
|
142
|
+
if ids:
|
|
143
|
+
items = [it for it in items if str(it.get("id")) in ids]
|
|
144
|
+
for it in items:
|
|
145
|
+
iid = it.get("id")
|
|
146
|
+
name = it.get("name", iid)
|
|
147
|
+
# 服务端 AIHubUpdate 接受的字段子集
|
|
148
|
+
payload = {k: it.get(k) for k in (
|
|
149
|
+
"type", "name", "data", "priority", "version",
|
|
150
|
+
"tags", "describe", "permission", "status",
|
|
151
|
+
) if it.get(k) is not None}
|
|
152
|
+
ok, info = api_call("PUT", server, token, f"/api/aihub/{iid}", payload)
|
|
153
|
+
print(f" {'OK' if ok else 'ERR'} [{name}] aihub_id={iid}{'' if ok else ' -> ' + str(info)}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def sync_external_apis(server, token, ids=None):
|
|
157
|
+
items = _load_index("external_apis/index.json")
|
|
158
|
+
if ids:
|
|
159
|
+
items = [it for it in items if str(it.get("id")) in ids]
|
|
160
|
+
for it in items:
|
|
161
|
+
iid = it.get("id")
|
|
162
|
+
code = it.get("code", iid)
|
|
163
|
+
# 服务端 ExternalAPIUpdate 接受字段
|
|
164
|
+
payload = {k: it.get(k) for k in (
|
|
165
|
+
"name", "base_url", "method", "path", "headers",
|
|
166
|
+
"auth_type", "auth_config", "timeout_ms", "permission",
|
|
167
|
+
"param_schema", "tags", "description", "status",
|
|
168
|
+
) if it.get(k) is not None}
|
|
169
|
+
ok, info = api_call("PUT", server, token, f"/api/external-apis/{iid}", payload)
|
|
170
|
+
print(f" {'OK' if ok else 'ERR'} [{code}] api_id={iid}{'' if ok else ' -> ' + str(info)}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def sync_system_config(server, token, keys=None):
|
|
174
|
+
items = _load_index("system_config/index.json")
|
|
175
|
+
if keys:
|
|
176
|
+
items = [it for it in items if str(it.get("config_key")) in keys]
|
|
177
|
+
for it in items:
|
|
178
|
+
ck = it.get("config_key")
|
|
179
|
+
if not ck:
|
|
180
|
+
continue
|
|
181
|
+
# 优先使用 parsed_value(init 时 GET 返回的解析值),其次 config_value
|
|
182
|
+
value = it.get("parsed_value") if "parsed_value" in it else it.get("config_value")
|
|
183
|
+
payload = {
|
|
184
|
+
"config_value": value,
|
|
185
|
+
"value_type": it.get("value_type"),
|
|
186
|
+
"category": it.get("category"),
|
|
187
|
+
"description": it.get("description"),
|
|
188
|
+
"is_sensitive": it.get("is_sensitive"),
|
|
189
|
+
"status": it.get("status"),
|
|
190
|
+
}
|
|
191
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
192
|
+
ok, info = api_call("PUT", server, token, f"/api/system/{ck}", payload)
|
|
193
|
+
print(f" {'OK' if ok else 'ERR'} [{ck}]{'' if ok else ' -> ' + str(info)}")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def sync_roles(server, token, ids=None):
|
|
197
|
+
items = _load_index("roles/index.json")
|
|
198
|
+
if ids:
|
|
199
|
+
items = [it for it in items if str(it.get("id")) in ids]
|
|
200
|
+
for it in items:
|
|
201
|
+
rid = it.get("id")
|
|
202
|
+
code = it.get("code", rid)
|
|
203
|
+
# RoleUpdateRequest 字段
|
|
204
|
+
payload = {k: it.get(k) for k in (
|
|
205
|
+
"name", "description", "status", "sort_order", "user_visible",
|
|
206
|
+
) if it.get(k) is not None}
|
|
207
|
+
ok, info = api_call("PUT", server, token, f"/api/roles/{rid}", payload)
|
|
208
|
+
print(f" {'OK' if ok else 'ERR'} [{code}] role_id={rid}{'' if ok else ' -> ' + str(info)}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def sync_users(server, token, ids=None):
|
|
212
|
+
items = _load_index("users/index.json")
|
|
213
|
+
if ids:
|
|
214
|
+
items = [it for it in items if str(it.get("id")) in ids]
|
|
215
|
+
for it in items:
|
|
216
|
+
uid = it.get("id")
|
|
217
|
+
uname = it.get("username", uid)
|
|
218
|
+
# UserUpdateRequest 字段子集(不下发 password / role_ids 之类敏感修改)
|
|
219
|
+
payload = {k: it.get(k) for k in (
|
|
220
|
+
"username", "email", "phone_number", "nickname",
|
|
221
|
+
"avatar", "status", "notes",
|
|
222
|
+
) if it.get(k) is not None}
|
|
223
|
+
ok, info = api_call("PUT", server, token, f"/api/users/{uid}", payload)
|
|
224
|
+
print(f" {'OK' if ok else 'ERR'} [{uname}] user_id={uid}{'' if ok else ' -> ' + str(info)}")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
HANDLERS = {
|
|
228
|
+
"pages": sync_pages,
|
|
229
|
+
"nav": sync_nav,
|
|
230
|
+
"db_meta": sync_db_meta,
|
|
231
|
+
"aihub": sync_aihub,
|
|
232
|
+
"external_apis": sync_external_apis,
|
|
233
|
+
"system_config": sync_system_config,
|
|
234
|
+
"roles": sync_roles,
|
|
235
|
+
"users": sync_users,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
134
239
|
def main():
|
|
135
|
-
if len(sys.argv) < 2 or sys.argv[1] not in
|
|
136
|
-
print("usage: draftgo_sync.py
|
|
240
|
+
if len(sys.argv) < 2 or sys.argv[1] not in HANDLERS:
|
|
241
|
+
print(f"usage: draftgo_sync.py {{{'|'.join(HANDLERS)}}} [id ...]")
|
|
137
242
|
sys.exit(1)
|
|
138
243
|
mode = sys.argv[1]
|
|
139
244
|
ids = sys.argv[2:] or None
|
|
140
245
|
server, token = load_config()
|
|
141
|
-
|
|
142
|
-
sync_pages(server, token, ids)
|
|
143
|
-
elif mode == "db_meta":
|
|
144
|
-
sync_db_meta(server, token, ids)
|
|
145
|
-
else:
|
|
146
|
-
sync_nav(server, token, ids)
|
|
246
|
+
HANDLERS[mode](server, token, ids)
|
|
147
247
|
|
|
148
248
|
|
|
149
249
|
if __name__ == "__main__":
|
|
@@ -1,45 +1,102 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: draftgo-sync
|
|
3
|
-
description: Use this skill when the user says "同步页面", "同步导航", "同步数据库", "sync pages", "sync nav", "sync db_meta", "/draftgo sync", or wants to push local
|
|
4
|
-
version: 1.
|
|
3
|
+
description: Use this skill when the user says "同步页面", "同步导航", "同步数据库", "同步AI资产", "同步外部API", "同步系统配置", "同步角色", "同步用户", "sync pages", "sync nav", "sync db_meta", "sync aihub", "sync external_apis", "sync system_config", "sync roles", "sync users", "/draftgo sync", or wants to push local changes to the DraftGo server.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
allowed-tools: Bash(python:*), Read, Glob
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# DraftGo 同步
|
|
9
9
|
|
|
10
|
-
> **STOP —
|
|
11
|
-
> 唯一正确方式:运行下方 Python
|
|
10
|
+
> **STOP — 禁止用 curl、禁止自己写 Python 上传逻辑。**
|
|
11
|
+
> 唯一正确方式:运行下方 Python 脚本。脚本覆盖 init 拉取的全部 8 种类型,已处理字段结构、token 读取、错误处理。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
脚本路径取决于安装方式:
|
|
14
|
+
- CLI 安装:`.draftgo/skill/scripts/draftgo_sync.py`
|
|
15
|
+
- Claude Code 安装:`.claude/skills/draftgo/scripts/draftgo_sync.py`
|
|
14
16
|
|
|
15
17
|
## 同步页面("同步页面" / "sync pages")
|
|
16
18
|
|
|
17
19
|
```
|
|
18
|
-
!python
|
|
19
|
-
!python
|
|
20
|
+
!python <skill_scripts>/draftgo_sync.py pages
|
|
21
|
+
!python <skill_scripts>/draftgo_sync.py pages <page_id>
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
## 同步数据库元数据("同步数据库" / "sync db_meta")
|
|
23
25
|
|
|
24
26
|
```
|
|
25
|
-
!python
|
|
26
|
-
!python
|
|
27
|
+
!python <skill_scripts>/draftgo_sync.py db_meta
|
|
28
|
+
!python <skill_scripts>/draftgo_sync.py db_meta <db_meta_id>
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
## 同步导航栏("同步导航" / "sync nav")
|
|
30
32
|
|
|
31
33
|
```
|
|
32
|
-
!python
|
|
33
|
-
!python
|
|
34
|
+
!python <skill_scripts>/draftgo_sync.py nav
|
|
35
|
+
!python <skill_scripts>/draftgo_sync.py nav <nav_id>
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
脚本会自动读取 `.draftgo/config.json
|
|
38
|
+
脚本会自动读取 `.draftgo/config.json`。
|
|
39
|
+
|
|
40
|
+
## 同步 AI 资产("同步AI资产" / "sync aihub")
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
!python <skill_scripts>/draftgo_sync.py aihub
|
|
44
|
+
!python <skill_scripts>/draftgo_sync.py aihub <aihub_id>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
读取 `.draftgo/aihub/index.json`,按 `AIHubUpdate` schema 推送。
|
|
48
|
+
|
|
49
|
+
## 同步外部 API("同步外部API" / "sync external_apis")
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
!python <skill_scripts>/draftgo_sync.py external_apis
|
|
53
|
+
!python <skill_scripts>/draftgo_sync.py external_apis <api_id>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
读取 `.draftgo/external_apis/index.json`(已包含 init 时合并的 detail 字段),按 `ExternalAPIUpdate` schema 推送。
|
|
57
|
+
|
|
58
|
+
## 同步系统配置("同步系统配置" / "sync system_config")
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
!python <skill_scripts>/draftgo_sync.py system_config
|
|
62
|
+
!python <skill_scripts>/draftgo_sync.py system_config <config_key>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
读取 `.draftgo/system_config/index.json`,按 `config_key` 调用 `PUT /api/system/{config_key}`。脚本优先使用 `parsed_value`。
|
|
66
|
+
|
|
67
|
+
## 同步角色("同步角色" / "sync roles")⚠️ 需二次确认
|
|
68
|
+
|
|
69
|
+
roles 涉及权限安全,**强制要求人工确认**:
|
|
70
|
+
|
|
71
|
+
1. 读取 `.draftgo/roles/index.json`
|
|
72
|
+
2. **向用户展示即将同步的变更内容**
|
|
73
|
+
3. **等待用户明确确认**
|
|
74
|
+
4. 确认后运行:
|
|
75
|
+
```
|
|
76
|
+
!python <skill_scripts>/draftgo_sync.py roles
|
|
77
|
+
!python <skill_scripts>/draftgo_sync.py roles <role_id>
|
|
78
|
+
```
|
|
79
|
+
5. 用户拒绝则不同步
|
|
80
|
+
|
|
81
|
+
## 同步用户("同步用户" / "sync users")⚠️ 需二次确认
|
|
82
|
+
|
|
83
|
+
users 涉及账号安全,**强制要求人工确认**:
|
|
84
|
+
|
|
85
|
+
1. 读取 `.draftgo/users/index.json`
|
|
86
|
+
2. **向用户展示即将同步的变更内容**
|
|
87
|
+
3. **等待用户明确确认**
|
|
88
|
+
4. 确认后运行:
|
|
89
|
+
```
|
|
90
|
+
!python <skill_scripts>/draftgo_sync.py users
|
|
91
|
+
!python <skill_scripts>/draftgo_sync.py users <user_id>
|
|
92
|
+
```
|
|
93
|
+
5. 脚本不会下发 password / role_ids;如需修改请走专用接口
|
|
37
94
|
|
|
38
95
|
## 同步方式
|
|
39
96
|
|
|
40
|
-
|
|
97
|
+
**所有类型必须用 Python 脚本同步,禁止用 curl。**
|
|
41
98
|
|
|
42
|
-
curl 在 Windows/Git Bash 环境下传输大 HTML 时会报 `Argument list too long`(exit 126)。
|
|
99
|
+
curl 在 Windows/Git Bash 环境下传输大 HTML / JSON 时会报 `Argument list too long`(exit 126)。
|
|
43
100
|
统一使用 `draftgo_sync.py` 或临时 Python 脚本(`urllib.request`)进行同步。
|
|
44
101
|
|
|
45
102
|
## 接口说明
|
|
@@ -62,6 +119,18 @@ payload 必须包含完整元数据(从 `pages/index.json` 读取)+ HTML:
|
|
|
62
119
|
|
|
63
120
|
**导航栏同步**:`PUT /api/navigations/{id}`,payload 为 `{"html": "..."}` 字符串。
|
|
64
121
|
|
|
122
|
+
**DB Meta 同步**:`PUT /api/db-meta/{id}`,payload 含 `type`, `label`, `describe`, `schema`, `permission`, `schema_validation`, `extra`。
|
|
123
|
+
|
|
124
|
+
**AI 资产同步**:`PUT /api/aihub/{id}`,payload 子集:`type`, `name`, `data`, `priority`, `version`, `tags`, `describe`, `permission`, `status`。
|
|
125
|
+
|
|
126
|
+
**外部 API 同步**:`PUT /api/external-apis/{id}`,payload 子集:`name`, `base_url`, `method`, `path`, `headers`, `auth_type`, `auth_config`, `timeout_ms`, `permission`, `param_schema`, `tags`, `description`, `status`。
|
|
127
|
+
|
|
128
|
+
**系统配置同步**:`PUT /api/system/{config_key}`,payload:`config_value`(parsed), `value_type`, `category`, `description`, `is_sensitive`, `status`。
|
|
129
|
+
|
|
130
|
+
**角色同步**:`PUT /api/roles/{id}`,payload 含 `name`, `description`, `status`, `sort_order`, `user_visible`。
|
|
131
|
+
|
|
132
|
+
**用户同步**:`PUT /api/users/{id}`,payload 子集:`username`, `email`, `phone_number`, `nickname`, `avatar`, `status`, `notes`(不含 password / role_ids)。
|
|
133
|
+
|
|
65
134
|
## 失败处理
|
|
66
135
|
|
|
67
136
|
如果报错 `未找到 .draftgo/config.json`,提示用户先运行 `/draftgo init`。
|
package/src/commands/doctor.js
CHANGED
|
@@ -5,8 +5,9 @@ const { all } = require('../installers');
|
|
|
5
5
|
const { detectTargets } = require('../detect');
|
|
6
6
|
const { findPython } = require('../python');
|
|
7
7
|
const { readInstalledVersion, getPackageVersion } = require('../skill');
|
|
8
|
+
const { fetchLatestVersion, cmpSemver } = require('../updateCheck');
|
|
8
9
|
|
|
9
|
-
function doctor(projectDir) {
|
|
10
|
+
async function doctor(projectDir, flags = {}) {
|
|
10
11
|
log.title('draftgo doctor');
|
|
11
12
|
|
|
12
13
|
log.info(`Node 版本:${process.version}`);
|
|
@@ -19,6 +20,20 @@ function doctor(projectDir) {
|
|
|
19
20
|
if (py) log.ok(`Python:${py.bin} (${py.version})`);
|
|
20
21
|
else log.err('Python 未检测到(DraftGo init/sync 脚本需要 Python 3.9+)。');
|
|
21
22
|
|
|
23
|
+
console.log('');
|
|
24
|
+
if (!flags['skip-update-check'] && process.env.DRAFTGO_NO_UPDATE_CHECK !== '1') {
|
|
25
|
+
const current = getPackageVersion();
|
|
26
|
+
const latest = await fetchLatestVersion();
|
|
27
|
+
if (!latest) {
|
|
28
|
+
log.dim('CLI 最新版查询失败(可能离线或 registry 不可达)');
|
|
29
|
+
} else if (cmpSemver(latest, current) > 0) {
|
|
30
|
+
log.warn(`CLI 有新版:${current} → ${latest}`);
|
|
31
|
+
log.dim(' 下次运行 `draftgo update` 时会自动升级。');
|
|
32
|
+
} else {
|
|
33
|
+
log.ok(`CLI 已是最新:${current}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
console.log('');
|
|
23
38
|
log.info('项目中检测到的 AI 工具信号:');
|
|
24
39
|
const hits = detectTargets(projectDir);
|
package/src/commands/help.js
CHANGED
|
@@ -10,21 +10,27 @@ function help() {
|
|
|
10
10
|
Usage:
|
|
11
11
|
draftgo init [<target>...] Install skill. No target = auto-detect.
|
|
12
12
|
Use "all" to install every target.
|
|
13
|
-
draftgo update [<target>...] Update skill
|
|
14
|
-
|
|
13
|
+
draftgo update [<target>...] Update skill + entry files. If a newer CLI
|
|
14
|
+
version exists on npm, upgrades the CLI
|
|
15
|
+
automatically then reruns itself.
|
|
15
16
|
draftgo uninstall [<target>...] Remove entry files for target(s).
|
|
16
17
|
--purge also removes .draftgo/ (runtime data).
|
|
17
18
|
draftgo status Show installed targets and skill version.
|
|
18
|
-
draftgo doctor Diagnose environment (python, targets
|
|
19
|
+
draftgo doctor Diagnose environment (python, targets,
|
|
20
|
+
CLI freshness).
|
|
19
21
|
draftgo list-targets List supported AI tools.
|
|
20
22
|
draftgo -v | --version Print CLI version.
|
|
21
23
|
draftgo -h | --help Show this help.
|
|
22
24
|
|
|
23
25
|
Flags:
|
|
24
|
-
--project <dir>
|
|
25
|
-
--force
|
|
26
|
-
--purge
|
|
27
|
-
--
|
|
26
|
+
--project <dir> Operate on <dir> instead of the current directory.
|
|
27
|
+
--force Overwrite existing skill body during install/update.
|
|
28
|
+
--purge (uninstall) Also remove the entire .draftgo/ directory.
|
|
29
|
+
--skip-update-check Do not contact npm to check for a newer CLI.
|
|
30
|
+
--yes Assume "yes" for interactive prompts.
|
|
31
|
+
|
|
32
|
+
Environment:
|
|
33
|
+
DRAFTGO_NO_UPDATE_CHECK=1 Disable the automatic CLI-freshness check.
|
|
28
34
|
|
|
29
35
|
Targets:
|
|
30
36
|
${targets}
|
|
@@ -34,7 +40,7 @@ Examples:
|
|
|
34
40
|
draftgo init claudecode # install for Claude Code only
|
|
35
41
|
draftgo init claudecode kiro # install for both
|
|
36
42
|
draftgo init all # install for every supported target
|
|
37
|
-
draftgo update #
|
|
43
|
+
draftgo update # upgrade CLI if needed, then refresh skill
|
|
38
44
|
draftgo uninstall --purge # full removal incl. runtime data
|
|
39
45
|
`);
|
|
40
46
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -1,14 +1,81 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
3
4
|
const log = require('../logger');
|
|
4
5
|
const { all, resolveTargets } = require('../installers');
|
|
5
6
|
const { updateSkillBody, readInstalledVersion, getPackageVersion } = require('../skill');
|
|
7
|
+
const { fetchLatestVersion, cmpSemver } = require('../updateCheck');
|
|
8
|
+
|
|
9
|
+
// Env flag we set before re-exec to avoid infinite upgrade loops.
|
|
10
|
+
const REENTRY_FLAG = 'DRAFTGO_UPDATE_REENTERED';
|
|
11
|
+
|
|
12
|
+
function runNpmInstallLatest() {
|
|
13
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
14
|
+
log.step('执行:npm install -g draftgo-cli@latest');
|
|
15
|
+
const r = spawnSync(npm, ['install', '-g', 'draftgo-cli@latest'], { stdio: 'inherit' });
|
|
16
|
+
if (r.error) { log.err(`升级失败:${r.error.message}`); return false; }
|
|
17
|
+
if (r.status !== 0) {
|
|
18
|
+
log.err(`npm install 返回码 ${r.status}`);
|
|
19
|
+
log.dim(' 常见原因:权限不足(macOS/Linux 用 sudo,或用 nvm 管理 Node)');
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function reExecSameCommand(projectDir) {
|
|
26
|
+
// Re-run `draftgo update` from the freshly installed global CLI, so the user
|
|
27
|
+
// gets the newest skill content written into .draftgo/skill in the same call.
|
|
28
|
+
const cmd = process.platform === 'win32' ? 'draftgo.cmd' : 'draftgo';
|
|
29
|
+
const args = ['update', '--project', projectDir, '--skip-update-check'];
|
|
30
|
+
log.step('以新版 CLI 重新执行:draftgo update');
|
|
31
|
+
const r = spawnSync(cmd, args, {
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
env: { ...process.env, [REENTRY_FLAG]: '1' },
|
|
34
|
+
});
|
|
35
|
+
if (r.error) {
|
|
36
|
+
log.err(`重新执行失败:${r.error.message}`);
|
|
37
|
+
log.dim(' 请手动在项目目录再跑一次:draftgo update');
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
return r.status || 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function maybeAutoUpgrade(projectDir, flags) {
|
|
44
|
+
if (flags['skip-update-check']) return { upgraded: false };
|
|
45
|
+
if (process.env[REENTRY_FLAG] === '1') return { upgraded: false };
|
|
46
|
+
if (process.env.DRAFTGO_NO_UPDATE_CHECK === '1') return { upgraded: false };
|
|
47
|
+
|
|
48
|
+
const current = getPackageVersion();
|
|
49
|
+
const latest = await fetchLatestVersion();
|
|
50
|
+
if (!latest) {
|
|
51
|
+
log.dim(' (查询最新版失败,按当前 CLI 继续)');
|
|
52
|
+
return { upgraded: false };
|
|
53
|
+
}
|
|
54
|
+
if (cmpSemver(latest, current) <= 0) {
|
|
55
|
+
log.dim(` CLI 已是最新(${current})`);
|
|
56
|
+
return { upgraded: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
log.warn(`draftgo-cli 有新版:${current} → ${latest},自动升级中……`);
|
|
60
|
+
const ok = runNpmInstallLatest();
|
|
61
|
+
if (!ok) {
|
|
62
|
+
log.warn('自动升级失败,回退到当前 CLI 继续执行。');
|
|
63
|
+
return { upgraded: false };
|
|
64
|
+
}
|
|
65
|
+
log.ok(`CLI 已升级到 ${latest}。`);
|
|
66
|
+
const code = reExecSameCommand(projectDir);
|
|
67
|
+
return { upgraded: true, exitCode: code };
|
|
68
|
+
}
|
|
6
69
|
|
|
7
70
|
async function update(projectDir, positional, flags) {
|
|
8
71
|
log.title('draftgo update');
|
|
9
72
|
|
|
10
73
|
const prev = readInstalledVersion(projectDir);
|
|
11
|
-
log.info(
|
|
74
|
+
log.info(`当前已装 skill:${prev || '未安装'},CLI 版本:${getPackageVersion()}`);
|
|
75
|
+
|
|
76
|
+
// Auto-upgrade-and-rerun path.
|
|
77
|
+
const { upgraded, exitCode } = await maybeAutoUpgrade(projectDir, flags);
|
|
78
|
+
if (upgraded) return exitCode;
|
|
12
79
|
|
|
13
80
|
// Determine which entry files to refresh:
|
|
14
81
|
// - no positional → refresh only targets that are currently installed
|
package/src/index.js
CHANGED
|
@@ -34,7 +34,7 @@ async function run(argv) {
|
|
|
34
34
|
case 'status':
|
|
35
35
|
return require('./commands/status')(projectDir);
|
|
36
36
|
case 'doctor':
|
|
37
|
-
return require('./commands/doctor')(projectDir);
|
|
37
|
+
return await require('./commands/doctor')(projectDir, flags);
|
|
38
38
|
case 'list-targets':
|
|
39
39
|
case 'targets':
|
|
40
40
|
return require('./commands/listTargets')();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Fetch the latest draftgo-cli version from the npm registry.
|
|
4
|
+
// No cache — every call hits the network. Returns null on any failure so
|
|
5
|
+
// callers can gracefully fall back.
|
|
6
|
+
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const { getPackageVersion } = require('./skill');
|
|
9
|
+
|
|
10
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/draftgo-cli/latest';
|
|
11
|
+
|
|
12
|
+
function fetchLatestVersion(timeoutMs = 3000) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
let req;
|
|
15
|
+
try {
|
|
16
|
+
req = https.get(
|
|
17
|
+
REGISTRY_URL,
|
|
18
|
+
{ timeout: timeoutMs, headers: { accept: 'application/json' } },
|
|
19
|
+
(res) => {
|
|
20
|
+
if (res.statusCode !== 200) { res.resume(); return resolve(null); }
|
|
21
|
+
let body = '';
|
|
22
|
+
res.setEncoding('utf8');
|
|
23
|
+
res.on('data', (c) => { body += c; });
|
|
24
|
+
res.on('end', () => {
|
|
25
|
+
try { resolve(JSON.parse(body).version || null); }
|
|
26
|
+
catch { resolve(null); }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
return resolve(null);
|
|
32
|
+
}
|
|
33
|
+
req.on('error', () => resolve(null));
|
|
34
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Compare 'a.b.c' ignoring prerelease tags. Returns 1 / 0 / -1.
|
|
39
|
+
function cmpSemver(a, b) {
|
|
40
|
+
const pa = String(a).split('-')[0].split('.').map((x) => Number(x) || 0);
|
|
41
|
+
const pb = String(b).split('-')[0].split('.').map((x) => Number(x) || 0);
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
44
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Report-only helper used by doctor. Never throws.
|
|
50
|
+
async function getFreshness() {
|
|
51
|
+
if (process.env.DRAFTGO_NO_UPDATE_CHECK === '1') return null;
|
|
52
|
+
const current = getPackageVersion();
|
|
53
|
+
const latest = await fetchLatestVersion();
|
|
54
|
+
if (!latest) return null;
|
|
55
|
+
return { current, latest, outdated: cmpSemver(latest, current) > 0 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { fetchLatestVersion, cmpSemver, getFreshness };
|