draftgo-cli 1.0.0
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 +119 -0
- package/bin/draftgo.js +3 -0
- package/package.json +45 -0
- package/resources/skill/CLAUDE.md +239 -0
- package/resources/skill/SKILL.md +75 -0
- package/resources/skill/bug/SKILL.md +72 -0
- package/resources/skill/init/SKILL.md +63 -0
- package/resources/skill/rules/debugging-syntax.md +279 -0
- package/resources/skill/rules/frontend.md +462 -0
- package/resources/skill/scripts/draftgo_init.py +192 -0
- package/resources/skill/scripts/draftgo_sync.py +150 -0
- package/resources/skill/sync/SKILL.md +72 -0
- package/src/cli.js +43 -0
- package/src/commands/doctor.js +39 -0
- package/src/commands/help.js +42 -0
- package/src/commands/init.js +71 -0
- package/src/commands/listTargets.js +13 -0
- package/src/commands/status.js +23 -0
- package/src/commands/uninstall.js +52 -0
- package/src/commands/update.js +54 -0
- package/src/detect.js +29 -0
- package/src/fsx.js +62 -0
- package/src/index.js +53 -0
- package/src/installers/_entryContent.js +45 -0
- package/src/installers/antigravity.js +28 -0
- package/src/installers/base.js +54 -0
- package/src/installers/claudecode.js +29 -0
- package/src/installers/codex.js +31 -0
- package/src/installers/copilot.js +28 -0
- package/src/installers/cursor.js +28 -0
- package/src/installers/gemini.js +31 -0
- package/src/installers/index.js +33 -0
- package/src/installers/kiro.js +33 -0
- package/src/installers/windsurf.js +28 -0
- package/src/logger.js +30 -0
- package/src/paths.js +35 -0
- package/src/python.js +27 -0
- package/src/skill.js +83 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DraftGo Sync Script
|
|
4
|
+
推送本地页面 / 导航栏 / DB Meta 修改到云端。
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
python draftgo_sync.py pages [page_id ...]
|
|
8
|
+
python draftgo_sync.py nav [nav_id ...]
|
|
9
|
+
python draftgo_sync.py db_meta [db_meta_id ...]
|
|
10
|
+
"""
|
|
11
|
+
import json, sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import urllib.request, urllib.error
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_project_root(start: Path) -> Path:
|
|
19
|
+
"""向上查找包含 `.draftgo/` 的目录作为项目根。"""
|
|
20
|
+
cur = start
|
|
21
|
+
for _ in range(10):
|
|
22
|
+
if (cur / ".draftgo").is_dir():
|
|
23
|
+
return cur
|
|
24
|
+
if cur.parent == cur:
|
|
25
|
+
break
|
|
26
|
+
cur = cur.parent
|
|
27
|
+
return start.parents[3] if len(start.parents) >= 4 else start
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DEFAULT_ROOT = find_project_root(SCRIPT_DIR)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_config():
|
|
34
|
+
cfg_path = DEFAULT_ROOT / ".draftgo/config.json"
|
|
35
|
+
if not cfg_path.exists():
|
|
36
|
+
print("ERR: .draftgo/config.json not found, run /draftgo init first", file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
|
|
39
|
+
return cfg["server"].rstrip("/"), cfg["token"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def api_call(method, server, token, path, body):
|
|
43
|
+
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
44
|
+
req = urllib.request.Request(
|
|
45
|
+
f"{server}{path}",
|
|
46
|
+
data=data,
|
|
47
|
+
method=method,
|
|
48
|
+
headers={
|
|
49
|
+
"Authorization": f"Bearer {token}",
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
55
|
+
return True, r.status
|
|
56
|
+
except urllib.error.HTTPError as e:
|
|
57
|
+
return False, f"HTTP {e.code}: {e.read().decode()}"
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return False, str(e)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_index(rel_index):
|
|
63
|
+
"""读取 .draftgo/ 下的 index.json;支持旧 (pages/index.json) 与新 (.draftgo/pages/index.json) 两种布局。"""
|
|
64
|
+
new_path = DEFAULT_ROOT / ".draftgo" / rel_index
|
|
65
|
+
legacy_path = DEFAULT_ROOT / rel_index
|
|
66
|
+
path = new_path if new_path.exists() else legacy_path
|
|
67
|
+
if not path.exists():
|
|
68
|
+
print(f"ERR: {rel_index} not found under .draftgo/, run /draftgo init first", file=sys.stderr)
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def sync_pages(server, token, ids=None):
|
|
74
|
+
pages = _load_index("pages/index.json")
|
|
75
|
+
if ids:
|
|
76
|
+
pages = [p for p in pages if str(p.get("id")) in ids]
|
|
77
|
+
for page in pages:
|
|
78
|
+
pid = page.get("id")
|
|
79
|
+
title = page.get("title", pid)
|
|
80
|
+
html_file = DEFAULT_ROOT / page.get("html_file", "")
|
|
81
|
+
if not html_file.exists():
|
|
82
|
+
print(f" SKIP [{title}] html_file not found: {page.get('html_file')}")
|
|
83
|
+
continue
|
|
84
|
+
html = html_file.read_text(encoding="utf-8")
|
|
85
|
+
payload = {
|
|
86
|
+
"title": page.get("title", ""),
|
|
87
|
+
"route": page.get("route", ""),
|
|
88
|
+
"tag": page.get("tag"),
|
|
89
|
+
"menu": page.get("menu"),
|
|
90
|
+
"status": page.get("status", "active"),
|
|
91
|
+
"permission": page.get("permission", {}),
|
|
92
|
+
"value": {"html": html},
|
|
93
|
+
}
|
|
94
|
+
ok, info = api_call("PUT", server, token, f"/api/pages/{pid}", payload)
|
|
95
|
+
print(f" {'OK' if ok else 'ERR'} [{title}] page_id={pid}{'' if ok else ' -> ' + str(info)}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def sync_db_meta(server, token, ids=None):
|
|
99
|
+
metas = _load_index("db_meta/index.json")
|
|
100
|
+
if ids:
|
|
101
|
+
metas = [m for m in metas if str(m.get("id")) in ids]
|
|
102
|
+
for meta in metas:
|
|
103
|
+
mid = meta.get("id")
|
|
104
|
+
label = meta.get("label", meta.get("type", mid))
|
|
105
|
+
payload = {
|
|
106
|
+
"type": meta.get("type"),
|
|
107
|
+
"label": meta.get("label"),
|
|
108
|
+
"describe": meta.get("describe"),
|
|
109
|
+
"schema": meta.get("schema"),
|
|
110
|
+
"permission": meta.get("permission"),
|
|
111
|
+
"schema_validation": meta.get("schema_validation", 0),
|
|
112
|
+
"extra": meta.get("extra"),
|
|
113
|
+
}
|
|
114
|
+
ok, info = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload)
|
|
115
|
+
print(f" {'OK' if ok else 'ERR'} [{label}] db_meta_id={mid}{'' if ok else ' -> ' + str(info)}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def sync_nav(server, token, ids=None):
|
|
119
|
+
navs = _load_index("navigations/index.json")
|
|
120
|
+
if ids:
|
|
121
|
+
navs = [n for n in navs if str(n.get("id")) in ids]
|
|
122
|
+
for nav in navs:
|
|
123
|
+
nid = nav.get("id")
|
|
124
|
+
name = nav.get("name", nid)
|
|
125
|
+
html_file = DEFAULT_ROOT / nav.get("html_file", "")
|
|
126
|
+
if not html_file.exists():
|
|
127
|
+
print(f" SKIP [{name}] html_file not found: {nav.get('html_file')}")
|
|
128
|
+
continue
|
|
129
|
+
html = html_file.read_text(encoding="utf-8")
|
|
130
|
+
ok, info = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html})
|
|
131
|
+
print(f" {'OK' if ok else 'ERR'} [{name}] nav_id={nid}{'' if ok else ' -> ' + str(info)}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
if len(sys.argv) < 2 or sys.argv[1] not in ("pages", "nav", "db_meta"):
|
|
136
|
+
print("usage: draftgo_sync.py pages|nav|db_meta [id ...]")
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
mode = sys.argv[1]
|
|
139
|
+
ids = sys.argv[2:] or None
|
|
140
|
+
server, token = load_config()
|
|
141
|
+
if mode == "pages":
|
|
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)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
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 page/navigation/db_meta changes to the DraftGo server.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
allowed-tools: Bash(python:*), Read, Glob
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# DraftGo 同步
|
|
9
|
+
|
|
10
|
+
> **STOP — 禁止手动调用 API、禁止用 curl、禁止自己写 Python 上传逻辑。**
|
|
11
|
+
> 唯一正确方式:运行下方 Python 脚本。脚本已处理所有字段结构、token 读取、错误处理。
|
|
12
|
+
|
|
13
|
+
脚本固定位于 `.draftgo/skill/scripts/draftgo_sync.py`。
|
|
14
|
+
|
|
15
|
+
## 同步页面("同步页面" / "sync pages")
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
!python .draftgo/skill/scripts/draftgo_sync.py pages
|
|
19
|
+
!python .draftgo/skill/scripts/draftgo_sync.py pages <page_id>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 同步数据库元数据("同步数据库" / "sync db_meta")
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
!python .draftgo/skill/scripts/draftgo_sync.py db_meta
|
|
26
|
+
!python .draftgo/skill/scripts/draftgo_sync.py db_meta <db_meta_id>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 同步导航栏("同步导航" / "sync nav")
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
!python .draftgo/skill/scripts/draftgo_sync.py nav
|
|
33
|
+
!python .draftgo/skill/scripts/draftgo_sync.py nav <nav_id>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
脚本会自动读取 `.draftgo/config.json` 和 `.draftgo/token`。
|
|
37
|
+
|
|
38
|
+
## 同步方式
|
|
39
|
+
|
|
40
|
+
**必须用 Python 脚本同步,禁止用 curl。**
|
|
41
|
+
|
|
42
|
+
curl 在 Windows/Git Bash 环境下传输大 HTML 时会报 `Argument list too long`(exit 126)。
|
|
43
|
+
统一使用 `draftgo_sync.py` 或临时 Python 脚本(`urllib.request`)进行同步。
|
|
44
|
+
|
|
45
|
+
## 接口说明
|
|
46
|
+
|
|
47
|
+
**页面同步**:`PUT /api/pages/{id}`
|
|
48
|
+
|
|
49
|
+
payload 必须包含完整元数据(从 `pages/index.json` 读取)+ HTML:
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"title": "...",
|
|
53
|
+
"route": "...",
|
|
54
|
+
"tag": null,
|
|
55
|
+
"menu": "...",
|
|
56
|
+
"status": "active",
|
|
57
|
+
"permission": { "default": "login", "roles": [...] },
|
|
58
|
+
"value": { "html": "完整HTML字符串" }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
缺少任何字段会返回 422。`value` 是 `{"html": "..."}` 的 dict,不是字符串。
|
|
62
|
+
|
|
63
|
+
**导航栏同步**:`PUT /api/navigations/{id}`,payload 为 `{"html": "..."}` 字符串。
|
|
64
|
+
|
|
65
|
+
## 失败处理
|
|
66
|
+
|
|
67
|
+
如果报错 `未找到 .draftgo/config.json`,提示用户先运行 `/draftgo init`。
|
|
68
|
+
|
|
69
|
+
如果报错 HTTP 401,提示:
|
|
70
|
+
```
|
|
71
|
+
⚠️ Token 无效,请重新运行 /draftgo init 更新 token。
|
|
72
|
+
```
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tiny, dependency-free argv parser tailored for draftgo-cli.
|
|
4
|
+
// Supports: subcommand, positional args, --flag, --key=value.
|
|
5
|
+
|
|
6
|
+
function parse(argv) {
|
|
7
|
+
const args = argv.slice();
|
|
8
|
+
const out = { command: null, positional: [], flags: {} };
|
|
9
|
+
while (args.length) {
|
|
10
|
+
const a = args.shift();
|
|
11
|
+
if (a === '--') { out.positional.push(...args); break; }
|
|
12
|
+
if (a.startsWith('--')) {
|
|
13
|
+
const raw = a.slice(2);
|
|
14
|
+
const eq = raw.indexOf('=');
|
|
15
|
+
if (eq >= 0) {
|
|
16
|
+
out.flags[raw.slice(0, eq)] = raw.slice(eq + 1);
|
|
17
|
+
} else {
|
|
18
|
+
const next = args[0];
|
|
19
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
20
|
+
// Only consume next token as value for a known set of flags.
|
|
21
|
+
if (['project', 'target'].includes(raw)) {
|
|
22
|
+
out.flags[raw] = next;
|
|
23
|
+
args.shift();
|
|
24
|
+
} else {
|
|
25
|
+
out.flags[raw] = true;
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
out.flags[raw] = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} else if (a.startsWith('-') && a.length > 1) {
|
|
32
|
+
const key = a.slice(1);
|
|
33
|
+
out.flags[key] = true;
|
|
34
|
+
} else if (!out.command) {
|
|
35
|
+
out.command = a;
|
|
36
|
+
} else {
|
|
37
|
+
out.positional.push(a);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { parse };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { all } = require('../installers');
|
|
5
|
+
const { detectTargets } = require('../detect');
|
|
6
|
+
const { findPython } = require('../python');
|
|
7
|
+
const { readInstalledVersion, getPackageVersion } = require('../skill');
|
|
8
|
+
|
|
9
|
+
function doctor(projectDir) {
|
|
10
|
+
log.title('draftgo doctor');
|
|
11
|
+
|
|
12
|
+
log.info(`Node 版本:${process.version}`);
|
|
13
|
+
log.info(`平台:${process.platform}`);
|
|
14
|
+
log.info(`CLI 版本:${getPackageVersion()}`);
|
|
15
|
+
log.info(`项目目录:${projectDir}`);
|
|
16
|
+
|
|
17
|
+
console.log('');
|
|
18
|
+
const py = findPython();
|
|
19
|
+
if (py) log.ok(`Python:${py.bin} (${py.version})`);
|
|
20
|
+
else log.err('Python 未检测到(DraftGo init/sync 脚本需要 Python 3.9+)。');
|
|
21
|
+
|
|
22
|
+
console.log('');
|
|
23
|
+
log.info('项目中检测到的 AI 工具信号:');
|
|
24
|
+
const hits = detectTargets(projectDir);
|
|
25
|
+
if (hits.length === 0) log.dim(' (无)');
|
|
26
|
+
else hits.forEach((n) => log.dim(` • ${n}`));
|
|
27
|
+
|
|
28
|
+
console.log('');
|
|
29
|
+
log.info(`已装 skill 版本:${readInstalledVersion(projectDir) || '未安装'}`);
|
|
30
|
+
log.info('各 AI 工具入口状态:');
|
|
31
|
+
for (const t of all) {
|
|
32
|
+
const st = t.status(projectDir);
|
|
33
|
+
const mark = st.installed ? log.c.green('✓') : log.c.gray('·');
|
|
34
|
+
console.log(` ${mark} ${t.name.padEnd(12)} ${t.displayName}`);
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = doctor;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { all } = require('../installers');
|
|
4
|
+
const { getPackageVersion } = require('../skill');
|
|
5
|
+
|
|
6
|
+
function help() {
|
|
7
|
+
const targets = all.map((i) => ` ${i.name.padEnd(12)} ${i.displayName}`).join('\n');
|
|
8
|
+
console.log(`draftgo v${getPackageVersion()} — manage the DraftGo skill across AI coding agents
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
draftgo init [<target>...] Install skill. No target = auto-detect.
|
|
12
|
+
Use "all" to install every target.
|
|
13
|
+
draftgo update [<target>...] Update skill body + entry files (preserves
|
|
14
|
+
config/iteration/bugs/local data).
|
|
15
|
+
draftgo uninstall [<target>...] Remove entry files for target(s).
|
|
16
|
+
--purge also removes .draftgo/ (runtime data).
|
|
17
|
+
draftgo status Show installed targets and skill version.
|
|
18
|
+
draftgo doctor Diagnose environment (python, targets).
|
|
19
|
+
draftgo list-targets List supported AI tools.
|
|
20
|
+
draftgo -v | --version Print CLI version.
|
|
21
|
+
draftgo -h | --help Show this help.
|
|
22
|
+
|
|
23
|
+
Flags:
|
|
24
|
+
--project <dir> Operate on <dir> instead of the current working directory.
|
|
25
|
+
--force Overwrite existing skill body during install/update.
|
|
26
|
+
--purge (uninstall) Also remove the entire .draftgo/ directory.
|
|
27
|
+
--yes Assume "yes" for interactive prompts.
|
|
28
|
+
|
|
29
|
+
Targets:
|
|
30
|
+
${targets}
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
draftgo init # auto-detect AI tools in this project
|
|
34
|
+
draftgo init claudecode # install for Claude Code only
|
|
35
|
+
draftgo init claudecode kiro # install for both
|
|
36
|
+
draftgo init all # install for every supported target
|
|
37
|
+
draftgo update # refresh skill content to latest
|
|
38
|
+
draftgo uninstall --purge # full removal incl. runtime data
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = help;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { detectTargets } = require('../detect');
|
|
5
|
+
const { resolveTargets, all } = require('../installers');
|
|
6
|
+
const { installSkillBody } = require('../skill');
|
|
7
|
+
const { findPython } = require('../python');
|
|
8
|
+
|
|
9
|
+
function warnIfNoPython() {
|
|
10
|
+
const py = findPython();
|
|
11
|
+
if (!py) {
|
|
12
|
+
log.warn('Python 未检测到。DraftGo 的 init/sync 脚本需要 Python 3.9+。');
|
|
13
|
+
log.dim(' 安装后请运行:python .draftgo/skill/scripts/draftgo_init.py --server ... --token ...');
|
|
14
|
+
} else {
|
|
15
|
+
log.dim(` detected ${py.bin} (${py.version})`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function init(projectDir, positional, flags) {
|
|
20
|
+
log.title('draftgo init');
|
|
21
|
+
|
|
22
|
+
// 1) Figure out which targets to install.
|
|
23
|
+
let resolved = [];
|
|
24
|
+
let unknown = [];
|
|
25
|
+
|
|
26
|
+
if (positional.length === 0) {
|
|
27
|
+
// Auto-detect
|
|
28
|
+
const hits = detectTargets(projectDir);
|
|
29
|
+
if (hits.length === 0) {
|
|
30
|
+
log.warn('未在当前项目检测到任何支持的 AI 工具目录。');
|
|
31
|
+
log.dim(' 使用 `draftgo init all` 或 `draftgo init <target>` 手动指定。');
|
|
32
|
+
log.dim(` 支持列表:${all.map((i) => i.name).join(', ')}`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
({ resolved, unknown } = resolveTargets(hits));
|
|
36
|
+
log.info(`自动识别到:${resolved.map((r) => r.displayName).join(', ')}`);
|
|
37
|
+
} else {
|
|
38
|
+
({ resolved, unknown } = resolveTargets(positional));
|
|
39
|
+
if (unknown.length) {
|
|
40
|
+
log.err(`未知 target:${unknown.join(', ')}`);
|
|
41
|
+
log.dim(` 支持列表:${all.map((i) => i.name).join(', ')}`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2) Install the shared skill body once.
|
|
47
|
+
log.step('写入 .draftgo/skill/(共享技能本体)');
|
|
48
|
+
installSkillBody(projectDir, { force: !!flags.force });
|
|
49
|
+
|
|
50
|
+
// 3) Install entry files for each target.
|
|
51
|
+
log.step('写入各 AI 工具入口文件');
|
|
52
|
+
for (const t of resolved) {
|
|
53
|
+
try {
|
|
54
|
+
const r = t.install(projectDir);
|
|
55
|
+
log.ok(`${t.displayName.padEnd(16)} → ${r.path}`);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
log.err(`${t.displayName} 安装失败:${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4) Environment check (advisory).
|
|
62
|
+
log.step('环境检查');
|
|
63
|
+
warnIfNoPython();
|
|
64
|
+
|
|
65
|
+
log.title('完成');
|
|
66
|
+
log.plain('下一步:在你的 AI 工具里说「初始化 draftgo 项目」,它会引导你配置服务器和 token。');
|
|
67
|
+
log.dim('或直接运行:python .draftgo/skill/scripts/draftgo_init.py --server <url> --token <token>');
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = init;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { all } = require('../installers');
|
|
4
|
+
|
|
5
|
+
function listTargets() {
|
|
6
|
+
console.log('draftgo 支持的 AI 工具(target):');
|
|
7
|
+
for (const t of all) {
|
|
8
|
+
console.log(` ${t.name.padEnd(12)} ${t.displayName}`);
|
|
9
|
+
}
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = listTargets;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { all } = require('../installers');
|
|
5
|
+
const { readInstalledVersion, getPackageVersion } = require('../skill');
|
|
6
|
+
|
|
7
|
+
function status(projectDir) {
|
|
8
|
+
log.title('draftgo status');
|
|
9
|
+
log.info(`项目目录:${projectDir}`);
|
|
10
|
+
log.info(`CLI 版本:${getPackageVersion()}`);
|
|
11
|
+
log.info(`已装 skill 版本:${readInstalledVersion(projectDir) || '(未安装)'}`);
|
|
12
|
+
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log('AI 工具入口:');
|
|
15
|
+
for (const t of all) {
|
|
16
|
+
const st = t.status(projectDir);
|
|
17
|
+
const mark = st.installed ? log.c.green('✓') : log.c.gray('·');
|
|
18
|
+
console.log(` ${mark} ${t.name.padEnd(12)} ${t.displayName}`);
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = status;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const paths = require('../paths');
|
|
5
|
+
const { all, resolveTargets } = require('../installers');
|
|
6
|
+
const { uninstallSkillBody } = require('../skill');
|
|
7
|
+
const { removePath, exists } = require('../fsx');
|
|
8
|
+
|
|
9
|
+
async function uninstall(projectDir, positional, flags) {
|
|
10
|
+
log.title('draftgo uninstall');
|
|
11
|
+
|
|
12
|
+
let resolved = [];
|
|
13
|
+
let unknown = [];
|
|
14
|
+
if (positional.length === 0 || positional.includes('all')) {
|
|
15
|
+
resolved = all.slice();
|
|
16
|
+
} else {
|
|
17
|
+
({ resolved, unknown } = resolveTargets(positional));
|
|
18
|
+
if (unknown.length) {
|
|
19
|
+
log.err(`未知 target:${unknown.join(', ')}`);
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
log.step('移除入口文件');
|
|
25
|
+
for (const t of resolved) {
|
|
26
|
+
try {
|
|
27
|
+
const removed = t.uninstall(projectDir);
|
|
28
|
+
if (removed) log.ok(`${t.displayName} 入口已移除`);
|
|
29
|
+
else log.dim(`${t.displayName} 入口不存在,跳过`);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
log.err(`${t.displayName} 卸载失败:${e.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Only wipe the shared skill body if removing every target, or user asked.
|
|
36
|
+
const removedAll = resolved.length === all.length;
|
|
37
|
+
if (removedAll || flags.purge) {
|
|
38
|
+
log.step('移除 .draftgo/skill/');
|
|
39
|
+
uninstallSkillBody(projectDir, { purge: !!flags.purge });
|
|
40
|
+
} else {
|
|
41
|
+
log.dim('保留 .draftgo/skill/(仍有其他 AI 工具入口可能在用)。');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (flags.purge) {
|
|
45
|
+
log.warn('已使用 --purge:.draftgo/(含 config/iteration/bugs 等运行时数据)已删除。');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log.title('完成');
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = uninstall;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('../logger');
|
|
4
|
+
const { all, resolveTargets } = require('../installers');
|
|
5
|
+
const { updateSkillBody, readInstalledVersion, getPackageVersion } = require('../skill');
|
|
6
|
+
|
|
7
|
+
async function update(projectDir, positional, flags) {
|
|
8
|
+
log.title('draftgo update');
|
|
9
|
+
|
|
10
|
+
const prev = readInstalledVersion(projectDir);
|
|
11
|
+
log.info(`当前已装:${prev || '未安装'},CLI 版本:${getPackageVersion()}`);
|
|
12
|
+
|
|
13
|
+
// Determine which entry files to refresh:
|
|
14
|
+
// - no positional → refresh only targets that are currently installed
|
|
15
|
+
// - "all" → refresh every known target
|
|
16
|
+
// - names → refresh the specified targets
|
|
17
|
+
let resolved = [];
|
|
18
|
+
let unknown = [];
|
|
19
|
+
|
|
20
|
+
if (positional.length === 0) {
|
|
21
|
+
resolved = all.filter((t) => t.status(projectDir).installed);
|
|
22
|
+
if (resolved.length === 0) {
|
|
23
|
+
log.warn('未检测到已安装的入口文件;仅刷新 .draftgo/skill/。');
|
|
24
|
+
} else {
|
|
25
|
+
log.info(`刷新已安装入口:${resolved.map((r) => r.displayName).join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
({ resolved, unknown } = resolveTargets(positional));
|
|
29
|
+
if (unknown.length) {
|
|
30
|
+
log.err(`未知 target:${unknown.join(', ')}`);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
log.step('覆盖 .draftgo/skill/ 到 CLI 内置版本');
|
|
36
|
+
updateSkillBody(projectDir);
|
|
37
|
+
|
|
38
|
+
if (resolved.length) {
|
|
39
|
+
log.step('刷新入口文件');
|
|
40
|
+
for (const t of resolved) {
|
|
41
|
+
try {
|
|
42
|
+
const r = t.install(projectDir);
|
|
43
|
+
log.ok(`${t.displayName.padEnd(16)} → ${r.path}`);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
log.err(`${t.displayName} 更新失败:${e.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
log.title('完成');
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = update;
|
package/src/detect.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { exists } = require('./fsx');
|
|
5
|
+
|
|
6
|
+
// Each target declares one or more signals (files/dirs). If ANY signal matches,
|
|
7
|
+
// the target is considered "in use" in the current project.
|
|
8
|
+
const SIGNALS = {
|
|
9
|
+
claudecode: ['.claude', 'CLAUDE.md'],
|
|
10
|
+
kiro: ['.kiro'],
|
|
11
|
+
cursor: ['.cursor'],
|
|
12
|
+
windsurf: ['.windsurf'],
|
|
13
|
+
antigravity:['.agent'],
|
|
14
|
+
copilot: ['.github/prompts', '.github/copilot-instructions.md'],
|
|
15
|
+
codex: ['.codex', 'AGENTS.md'],
|
|
16
|
+
gemini: ['.gemini', 'GEMINI.md'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function detectTargets(projectDir) {
|
|
20
|
+
const hits = [];
|
|
21
|
+
for (const [name, signals] of Object.entries(SIGNALS)) {
|
|
22
|
+
if (signals.some((s) => exists(path.join(projectDir, s)))) {
|
|
23
|
+
hits.push(name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return hits;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { SIGNALS, detectTargets };
|
package/src/fsx.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function exists(p) {
|
|
7
|
+
try { fs.accessSync(p); return true; } catch { return false; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ensureDir(dir) {
|
|
11
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readText(p) {
|
|
15
|
+
return fs.readFileSync(p, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeText(p, content) {
|
|
19
|
+
ensureDir(path.dirname(p));
|
|
20
|
+
fs.writeFileSync(p, content, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function copyFile(src, dest) {
|
|
24
|
+
ensureDir(path.dirname(dest));
|
|
25
|
+
fs.copyFileSync(src, dest);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function copyDir(src, dest) {
|
|
29
|
+
if (!exists(src)) return;
|
|
30
|
+
ensureDir(dest);
|
|
31
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
32
|
+
const s = path.join(src, entry.name);
|
|
33
|
+
const d = path.join(dest, entry.name);
|
|
34
|
+
if (entry.isDirectory()) copyDir(s, d);
|
|
35
|
+
else if (entry.isFile()) copyFile(s, d);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function removePath(p) {
|
|
40
|
+
if (!exists(p)) return false;
|
|
41
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function appendGitignoreLine(projectDir, line) {
|
|
46
|
+
const gi = path.join(projectDir, '.gitignore');
|
|
47
|
+
const content = exists(gi) ? readText(gi) : '';
|
|
48
|
+
if (content.split(/\r?\n/).some((l) => l.trim() === line)) return;
|
|
49
|
+
const sep = content && !content.endsWith('\n') ? '\n' : '';
|
|
50
|
+
fs.appendFileSync(gi, `${sep}${line}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
exists,
|
|
55
|
+
ensureDir,
|
|
56
|
+
readText,
|
|
57
|
+
writeText,
|
|
58
|
+
copyFile,
|
|
59
|
+
copyDir,
|
|
60
|
+
removePath,
|
|
61
|
+
appendGitignoreLine,
|
|
62
|
+
};
|