draftgo-cli 3.0.29 → 3.0.35
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/LICENSE +21 -0
- package/README.md +41 -139
- package/package.json +10 -2
- package/resources/skill/SKILL.md +61 -184
- package/resources/skill/init/SKILL.md +18 -66
- package/resources/skill/manifest.json +34 -0
- package/resources/skill/pull/SKILL.md +18 -52
- package/resources/skill/push/SKILL.md +30 -282
- package/resources/skill/references/aihub.md +86 -0
- package/resources/skill/{quickref → references}/api-endpoints.md +39 -13
- package/resources/skill/references/api.json +20248 -0
- package/resources/skill/{quickref → references}/app-api.md +40 -0
- package/resources/skill/{core → references}/architecture.md +2 -2
- package/resources/skill/references/chat-sdk.md +201 -0
- package/resources/skill/references/custom-services.md +308 -0
- package/resources/skill/{specs → references}/data.md +5 -5
- package/resources/skill/{rules → references}/frontend.md +138 -32
- package/resources/skill/{core → references}/modules.md +7 -5
- package/resources/skill/references/parallel.md +48 -0
- package/resources/skill/{specs → references}/runtime.md +1 -1
- package/resources/skill/scripts/draftgo_push.py +80 -12
- package/resources/skill/story/SKILL.md +11 -16
- package/src/cli.js +13 -7
- package/src/commandRegistry.js +34 -0
- package/src/commands/api.js +153 -8
- package/src/commands/help.js +24 -29
- package/src/commands/init.js +17 -18
- package/src/commands/local.js +9 -3
- package/src/commands/sync.js +1 -1
- package/src/commands/update.js +40 -12
- package/src/index.js +13 -57
- package/src/localdev/compose.js +44 -200
- package/src/localdev/index.js +116 -216
- package/src/localdev/mysqlClient.js +12 -9
- package/src/localdev/services.js +163 -0
- package/src/projectConfig.js +1 -1
- package/src/projectMap.js +17 -80
- package/src/skill.js +1 -1
- package/src/updateCheck.js +2 -12
- package/resources/skill/practices/anti-patterns.md +0 -80
- package/resources/skill/practices/best-practices.md +0 -60
- package/resources/skill/practices/dev-declaration.md +0 -114
- package/resources/skill/quickref/api.json +0 -17784
- package/resources/skill/rules/dev-workflow.md +0 -749
- package/resources/skill/rules/parallel.md +0 -263
- package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
- package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
- package/resources/skill/specs/custom-services.md +0 -199
- package/src/commands/doctor.js +0 -54
- package/src/commands/new.js +0 -186
- package/src/commands/projectScript.js +0 -37
- package/src/commands/upgrade.js +0 -52
- /package/resources/skill/{specs → references}/db-relations.md +0 -0
- /package/resources/skill/{rules → references}/debugging-syntax.md +0 -0
- /package/resources/skill/{specs → references}/security.md +0 -0
- /package/resources/skill/{specs → references}/ui-protocol.md +0 -0
package/src/localdev/index.js
CHANGED
|
@@ -1,281 +1,181 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Local-dev wizard: spin up a fresh DraftGo stack on the user's machine
|
|
4
|
-
// using Docker, then guide them through finishing setup + generating a
|
|
5
|
-
// Server Access Token (SAT) that powers `.draftgo/config.json`.
|
|
6
|
-
//
|
|
7
|
-
// High-level flow:
|
|
8
|
-
// 1. Verify Docker + compose plugin are available.
|
|
9
|
-
// 2. Decide what to do with MySQL / Redis (existing host service vs container).
|
|
10
|
-
// For host MySQL: validate creds and auto-create the database if missing.
|
|
11
|
-
// 3. Ask for the app port and the compose project name (container prefix).
|
|
12
|
-
// 4. Generate <project>/.draftgo/docker/{docker-compose.yaml, .env} with
|
|
13
|
-
// random secrets, gitignore the directory, and `docker compose up -d`.
|
|
14
|
-
// 5. Wait until http://localhost:<port> responds.
|
|
15
|
-
// 6. Print step-by-step browser instructions; prompt user to paste a SAT.
|
|
16
|
-
// 7. Write .draftgo/config.json in the project dir and (optionally) run
|
|
17
|
-
// draftgo_init.py to pull initial project context.
|
|
18
|
-
|
|
19
3
|
const { spawnSync } = require('child_process');
|
|
20
4
|
const log = require('../logger');
|
|
21
5
|
const { ask, askRequired, askPassword, confirm } = require('../prompt');
|
|
22
6
|
const { probePort, probeHttp, detectDocker } = require('./detect');
|
|
23
7
|
const compose = require('./compose');
|
|
24
|
-
const { ensureDatabase, describeClient } = require('./mysqlClient');
|
|
8
|
+
const { ensureDatabase, testConnection, describeClient } = require('./mysqlClient');
|
|
9
|
+
const { defaults, portOpen, probeRedis, probeMilvus, startService } = require('./services');
|
|
25
10
|
const { writeProjectConfig, maybeRunInit } = require('../projectConfig');
|
|
26
11
|
const { appendGitignoreLine } = require('../fsx');
|
|
27
12
|
|
|
28
13
|
const DEFAULT_APP_PORT = 3000;
|
|
29
14
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
15
|
+
async function promptMysqlConnection() {
|
|
16
|
+
while (true) {
|
|
17
|
+
const host = await askRequired(' MySQL host', { default: '127.0.0.1' });
|
|
18
|
+
const port = Number(await askRequired(' MySQL port', { default: '3306' }));
|
|
19
|
+
const user = await askRequired(' MySQL user', { default: 'draftgo' });
|
|
20
|
+
const password = await askPassword(' MySQL password', { default: 'draftgo' });
|
|
21
|
+
const conn = { host, port, user, password };
|
|
22
|
+
const result = testConnection(conn);
|
|
23
|
+
if (result.ok) return conn;
|
|
24
|
+
log.err(` MySQL connection failed: ${(result.detail || result.reason || 'unknown error').split('\n')[0]}`);
|
|
25
|
+
}
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const password = await askPassword(' Password');
|
|
56
|
-
const conn = { host, port: Number(port), user, password, database };
|
|
57
|
-
|
|
58
|
-
const verified = await verifyAndEnsureDatabase(conn);
|
|
59
|
-
if (verified) return { useContainer: false, ...conn };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
log.dim(' 未检测到本地 MySQL,将通过 Docker 容器提供');
|
|
28
|
+
async function ensureProjectDatabase(conn, database) {
|
|
29
|
+
while (true) {
|
|
30
|
+
const result = await ensureDatabase({ ...conn, database }, {
|
|
31
|
+
rootPromptFn: async () => {
|
|
32
|
+
log.warn(` ${conn.user} cannot create database ${database}.`);
|
|
33
|
+
if (!await confirm(' Create it with another MySQL account?', { default: true })) return null;
|
|
34
|
+
return {
|
|
35
|
+
user: await askRequired(' MySQL admin user', { default: 'root' }),
|
|
36
|
+
password: await askPassword(' MySQL admin password'),
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
if (result.ok) return { ...conn, database };
|
|
41
|
+
log.err(` Database setup failed: ${(result.detail || result.reason || 'unknown error').split('\n')[0]}`);
|
|
42
|
+
const retry = await confirm(' Re-enter MySQL connection settings?', { default: true });
|
|
43
|
+
if (!retry) throw new Error('MySQL database was not prepared.');
|
|
44
|
+
conn = await promptMysqlConnection();
|
|
64
45
|
}
|
|
65
|
-
return { useContainer: true };
|
|
66
46
|
}
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
48
|
+
async function planMysql(docker, defaultDatabase) {
|
|
49
|
+
log.step('Checking MySQL');
|
|
50
|
+
let conn = { ...defaults.mysql };
|
|
51
|
+
if (!await portOpen(conn.host, conn.port)) {
|
|
52
|
+
log.dim(' MySQL is not running locally; starting the shared local service.');
|
|
53
|
+
if (!startService(docker, 'mysql')) throw new Error('Unable to start shared MySQL.');
|
|
54
|
+
}
|
|
55
|
+
if (!testConnection(conn).ok) {
|
|
56
|
+
log.warn(' Default MySQL credentials were not accepted.');
|
|
57
|
+
conn = await promptMysqlConnection();
|
|
58
|
+
}
|
|
59
|
+
const database = await askRequired(' Project database name', { default: defaultDatabase });
|
|
60
|
+
const out = await ensureProjectDatabase(conn, database);
|
|
61
|
+
log.ok(` MySQL ready: ${out.host}:${out.port}/${out.database} (${describeClient()})`);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
84
64
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
65
|
+
async function promptRedisConnection() {
|
|
66
|
+
while (true) {
|
|
67
|
+
const host = await askRequired(' Redis host', { default: '127.0.0.1' });
|
|
68
|
+
const port = Number(await askRequired(' Redis port', { default: '6379' }));
|
|
69
|
+
const password = await askPassword(' Redis password (empty for none)', { default: '' });
|
|
70
|
+
const conn = { host, port, password };
|
|
71
|
+
if (await probeRedis(conn)) return conn;
|
|
72
|
+
log.err(' Redis PING failed. Check the address and password.');
|
|
90
73
|
}
|
|
74
|
+
}
|
|
91
75
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
log.err(' 认证失败:用户名或密码不正确。');
|
|
99
|
-
break;
|
|
100
|
-
case 'unreachable':
|
|
101
|
-
log.err(' 无法连接到 MySQL:' + (res.detail || '').split('\n')[0]);
|
|
102
|
-
break;
|
|
103
|
-
case 'create-failed':
|
|
104
|
-
log.err(' 建库失败:' + (res.detail || '').split('\n')[0]);
|
|
105
|
-
break;
|
|
106
|
-
default:
|
|
107
|
-
log.err(' 校验失败:' + (res.detail || res.reason));
|
|
76
|
+
async function planRedis(docker) {
|
|
77
|
+
log.step('Checking Redis');
|
|
78
|
+
let conn = { ...defaults.redis };
|
|
79
|
+
if (!await portOpen(conn.host, conn.port)) {
|
|
80
|
+
log.dim(' Redis is not running locally; starting the shared local service.');
|
|
81
|
+
if (!startService(docker, 'redis')) throw new Error('Unable to start shared Redis.');
|
|
108
82
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return true;
|
|
83
|
+
if (!await probeRedis(conn)) {
|
|
84
|
+
log.warn(' Default passwordless Redis PING failed.');
|
|
85
|
+
conn = await promptRedisConnection();
|
|
113
86
|
}
|
|
114
|
-
|
|
87
|
+
log.ok(` Redis ready: ${conn.host}:${conn.port}`);
|
|
88
|
+
return conn;
|
|
115
89
|
}
|
|
116
90
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const host = await ask(' Host', { default: '127.0.0.1' });
|
|
127
|
-
const port = await ask(' Port', { default: '6379' });
|
|
128
|
-
const password = await askPassword(' Password(无密码直接回车)', { default: '' });
|
|
129
|
-
return { useContainer: false, host, port: Number(port), password };
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
log.dim(' 未检测到本地 Redis,将通过 Docker 容器提供');
|
|
91
|
+
async function promptMilvusConnection() {
|
|
92
|
+
while (true) {
|
|
93
|
+
const host = await askRequired(' Milvus host', { default: '127.0.0.1' });
|
|
94
|
+
const port = Number(await askRequired(' Milvus port', { default: '19530' }));
|
|
95
|
+
const username = await ask(' Milvus username (empty for none)', { default: '' });
|
|
96
|
+
const password = await askPassword(' Milvus password (empty for none)', { default: '' });
|
|
97
|
+
const conn = { host, port, username, password };
|
|
98
|
+
if (await probeMilvus(conn)) return conn;
|
|
99
|
+
log.err(' Milvus health check failed. Check the address and credentials.');
|
|
133
100
|
}
|
|
134
|
-
return { useContainer: true };
|
|
135
101
|
}
|
|
136
102
|
|
|
137
|
-
|
|
103
|
+
async function planMilvus(docker) {
|
|
104
|
+
log.step('Checking Milvus');
|
|
105
|
+
let conn = { ...defaults.milvus };
|
|
106
|
+
if (!await portOpen(conn.host, conn.port)) {
|
|
107
|
+
log.dim(' Milvus is not running locally; starting the shared local service.');
|
|
108
|
+
if (!startService(docker, 'milvus')) throw new Error('Unable to start shared Milvus.');
|
|
109
|
+
}
|
|
110
|
+
if (!await probeMilvus(conn)) {
|
|
111
|
+
log.warn(' Default unauthenticated Milvus health check failed.');
|
|
112
|
+
conn = await promptMilvusConnection();
|
|
113
|
+
}
|
|
114
|
+
log.ok(` Milvus ready: ${conn.host}:${conn.port}`);
|
|
115
|
+
return conn;
|
|
116
|
+
}
|
|
138
117
|
|
|
139
118
|
async function pickAppPort() {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const taken3000 = await probePort('127.0.0.1', DEFAULT_APP_PORT);
|
|
143
|
-
let suggested = String(DEFAULT_APP_PORT);
|
|
144
|
-
if (taken3000) {
|
|
145
|
-
log.warn(` ${DEFAULT_APP_PORT} 已被占用,默认改用 3001`);
|
|
146
|
-
suggested = '3001';
|
|
147
|
-
}
|
|
119
|
+
const taken = await probePort('127.0.0.1', DEFAULT_APP_PORT);
|
|
120
|
+
const suggested = taken ? '3001' : String(DEFAULT_APP_PORT);
|
|
148
121
|
while (true) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
if (!
|
|
152
|
-
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (await probePort('127.0.0.1', n)) {
|
|
156
|
-
const reuse = await confirm(` ${n} 当前被占用,仍然使用?`, { default: false });
|
|
157
|
-
if (!reuse) continue;
|
|
158
|
-
}
|
|
159
|
-
return n;
|
|
122
|
+
const port = Number(await askRequired(' DraftGo host port', { default: suggested }));
|
|
123
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
|
|
124
|
+
if (!await probePort('127.0.0.1', port)) return port;
|
|
125
|
+
if (await confirm(` Port ${port} is in use. Use it anyway?`, { default: false })) return port;
|
|
160
126
|
}
|
|
161
127
|
}
|
|
162
128
|
|
|
163
129
|
async function pickProjectName() {
|
|
164
|
-
// The compose `name:` field becomes the container prefix (e.g. draftgo-app-1).
|
|
165
|
-
// Default `draftgo`; users with multiple stacks can pick anything matching
|
|
166
|
-
// docker-compose's identifier rules.
|
|
167
|
-
log.step('选择容器名前缀');
|
|
168
130
|
while (true) {
|
|
169
|
-
const raw = await askRequired('
|
|
131
|
+
const raw = await askRequired(' Project container prefix', { default: 'draftgo' });
|
|
170
132
|
const clean = compose.sanitizeProjectName(raw, '');
|
|
171
|
-
if (
|
|
172
|
-
log.dim(' 名称需以字母/数字开头,可包含字母数字、下划线、连字符');
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (clean !== raw.toLowerCase()) {
|
|
176
|
-
log.dim(` 已规范化为:${clean}`);
|
|
177
|
-
}
|
|
178
|
-
return clean;
|
|
133
|
+
if (clean) return clean;
|
|
179
134
|
}
|
|
180
135
|
}
|
|
181
136
|
|
|
182
|
-
// ── Compose lifecycle ──────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
137
|
function composeUp(docker, dir) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const r = spawnSync(docker.composeCmd, args, {
|
|
188
|
-
cwd: dir,
|
|
189
|
-
stdio: 'inherit',
|
|
190
|
-
shell: false,
|
|
138
|
+
const result = spawnSync(docker.composeCmd, [...docker.composeArgs, '-f', 'docker-compose.yaml', 'up', '-d'], {
|
|
139
|
+
cwd: dir, stdio: 'inherit', shell: false,
|
|
191
140
|
});
|
|
192
|
-
return
|
|
141
|
+
return result.status === 0;
|
|
193
142
|
}
|
|
194
143
|
|
|
195
144
|
async function waitForApp(appPort) {
|
|
196
|
-
log.step(`等待 DraftGo 启动 (http://localhost:${appPort}) …`);
|
|
197
145
|
const ok = await probeHttp(`http://127.0.0.1:${appPort}/`, { totalMs: 120000, intervalMs: 2000 });
|
|
198
|
-
if (!ok)
|
|
199
|
-
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
log.ok(' DraftGo 已就绪');
|
|
203
|
-
return true;
|
|
146
|
+
if (!ok) log.err(' Timed out waiting for DraftGo. Run `draftgo local logs` to inspect the app.');
|
|
147
|
+
return ok;
|
|
204
148
|
}
|
|
205
149
|
|
|
206
|
-
// ── Main wizard ────────────────────────────────────────────────────────────
|
|
207
|
-
|
|
208
150
|
async function runWizard(projectDir, { yes = false } = {}) {
|
|
209
|
-
log.title('draftgo local
|
|
210
|
-
|
|
211
|
-
log.dim(' 1) 检测 / 询问 MySQL、Redis 配置(不存在的库会自动创建)');
|
|
212
|
-
log.dim(' 2) 在 <项目>/.draftgo/docker/ 生成 docker-compose.yaml(密钥随机)');
|
|
213
|
-
log.dim(' 3) docker compose up -d 启动 DraftGo');
|
|
214
|
-
log.dim(' 4) 浏览器完成初始化 → 系统设置生成访问令牌 → 回到这里粘贴');
|
|
151
|
+
log.title('draftgo local setup');
|
|
152
|
+
if (!yes && !await confirm('Continue with local DraftGo setup?', { default: true })) return 0;
|
|
215
153
|
|
|
216
|
-
if (!yes) {
|
|
217
|
-
const cont = await confirm('\n是否继续?', { default: true });
|
|
218
|
-
if (!cont) { log.dim('已取消。'); return 0; }
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 1) Docker availability
|
|
222
154
|
const docker = detectDocker();
|
|
223
155
|
if (!docker.ok) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
printDockerInstallHelp();
|
|
228
|
-
}
|
|
229
|
-
return 1;
|
|
156
|
+
throw new Error(docker.reason === 'compose-missing'
|
|
157
|
+
? 'Docker is installed but Docker Compose is unavailable.'
|
|
158
|
+
: 'Docker is required to start missing local dependencies.');
|
|
230
159
|
}
|
|
231
|
-
log.ok(`Docker 可用:${docker.composeCmd} ${docker.composeArgs.join(' ')}`.trim());
|
|
232
160
|
|
|
233
|
-
// 2) Plan services
|
|
234
|
-
const mysql = await planMysql();
|
|
235
|
-
const redis = await planRedis();
|
|
236
161
|
const appPort = await pickAppPort();
|
|
237
162
|
const projectName = await pickProjectName();
|
|
163
|
+
const mysql = await planMysql(docker, projectName);
|
|
164
|
+
const redis = await planRedis(docker);
|
|
165
|
+
const milvus = await planMilvus(docker);
|
|
238
166
|
|
|
239
|
-
|
|
240
|
-
log.step('生成 docker-compose.yaml');
|
|
241
|
-
const out = compose.generate(projectDir, { projectName, appPort, mysql, redis });
|
|
242
|
-
// Keep the stack (secrets + bind-mount data) out of git.
|
|
167
|
+
const out = compose.generate(projectDir, { projectName, appPort, mysql, redis, milvus });
|
|
243
168
|
appendGitignoreLine(projectDir, '.draftgo/docker/');
|
|
244
|
-
|
|
245
|
-
log.dim(` 密钥保存在 ${out.envFile}(已随机生成,请勿外传,已加入 .gitignore)`);
|
|
246
|
-
log.dim(` 容器名:${out.projectName} / ${out.projectName}-mysql / ${out.projectName}-redis`);
|
|
247
|
-
|
|
248
|
-
// 4) Bring stack up + wait for the app
|
|
249
|
-
if (!composeUp(docker, out.dir)) {
|
|
250
|
-
log.err('docker compose 启动失败。请检查上方输出,确认 Docker 已运行。');
|
|
251
|
-
return 1;
|
|
252
|
-
}
|
|
253
|
-
if (!await waitForApp(appPort)) return 1;
|
|
169
|
+
if (!composeUp(docker, out.dir) || !await waitForApp(appPort)) return 1;
|
|
254
170
|
|
|
255
|
-
// 5) Guide user to finish initial setup and mint a SAT
|
|
256
171
|
const url = `http://localhost:${appPort}`;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
log.plain(' 2) 按页面提示创建管理员账号 / 完成首次设置');
|
|
260
|
-
log.plain(' 3) 进入「系统设置 → 访问令牌 (SAT)」,生成一个新令牌并复制');
|
|
261
|
-
log.plain(' 4) 回到这里把令牌粘贴下来\n');
|
|
262
|
-
|
|
263
|
-
const token = await askRequired('请粘贴访问令牌 (SAT)', {
|
|
264
|
-
validate: (v) => v.length < 10 ? 'token 看起来太短了,请检查后重试' : '',
|
|
172
|
+
const token = await askRequired('Paste a DraftGo system access token (SAT)', {
|
|
173
|
+
validate: (value) => value.length < 10 ? 'Token appears too short.' : '',
|
|
265
174
|
});
|
|
266
|
-
|
|
267
|
-
// 6) Persist project config and (optionally) bootstrap local context
|
|
268
175
|
const cfgPath = writeProjectConfig(projectDir, url, token);
|
|
269
|
-
log.ok(`已写入 ${cfgPath}`);
|
|
270
176
|
maybeRunInit(projectDir, url, token);
|
|
271
|
-
|
|
272
|
-
log.
|
|
273
|
-
log.plain(` - DraftGo: ${url}`);
|
|
274
|
-
log.plain(` - 配置文件: ${cfgPath}`);
|
|
275
|
-
log.plain(` - Compose 项目: ${out.dir}`);
|
|
276
|
-
log.plain(` - 容器名前缀: ${out.projectName}`);
|
|
277
|
-
log.dim(` 停止:cd "${out.dir}" && docker compose down`);
|
|
278
|
-
log.dim(` 升级:cd "${out.dir}" && docker compose pull && docker compose up -d`);
|
|
177
|
+
log.ok(`DraftGo is ready at ${url}`);
|
|
178
|
+
log.dim(`Project config: ${cfgPath}`);
|
|
279
179
|
return 0;
|
|
280
180
|
}
|
|
281
181
|
|
|
@@ -75,6 +75,15 @@ function runSQL(client, conn, sql) {
|
|
|
75
75
|
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '', code: r.status };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function testConnection(conn) {
|
|
79
|
+
const client = pickClient();
|
|
80
|
+
if (!client) return { ok: false, reason: 'no-client' };
|
|
81
|
+
const probe = runSQL(client, conn, 'SELECT 1');
|
|
82
|
+
if (probe.ok) return { ok: true };
|
|
83
|
+
const why = classifyError(probe.stderr);
|
|
84
|
+
return { ok: false, reason: why === 'auth' ? 'auth' : 'unreachable', detail: probe.stderr.trim() };
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
function classifyError(stderr) {
|
|
79
88
|
const s = (stderr || '').toLowerCase();
|
|
80
89
|
if (s.includes('access denied')) return 'auth';
|
|
@@ -98,14 +107,8 @@ function escIdent(name) {
|
|
|
98
107
|
// privilege; it should resolve to `{ user, password }` (or null to abort).
|
|
99
108
|
async function ensureDatabase(conn, { rootPromptFn } = {}) {
|
|
100
109
|
const client = pickClient();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// 1) Connectivity + auth probe.
|
|
104
|
-
const probe = runSQL(client, conn, 'SELECT 1');
|
|
105
|
-
if (!probe.ok) {
|
|
106
|
-
const why = classifyError(probe.stderr);
|
|
107
|
-
return { ok: false, reason: why === 'auth' ? 'auth' : 'unreachable', detail: probe.stderr.trim() };
|
|
108
|
-
}
|
|
110
|
+
const connected = testConnection(conn);
|
|
111
|
+
if (!client || !connected.ok) return connected;
|
|
109
112
|
|
|
110
113
|
// 2) Does the database already exist?
|
|
111
114
|
const showSQL = `SHOW DATABASES LIKE '${String(conn.database).replace(/'/g, "''")}'`;
|
|
@@ -149,4 +152,4 @@ function describeClient() {
|
|
|
149
152
|
return c.kind === 'native' ? 'native mysql client' : 'docker mysql:8.0';
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
module.exports = { ensureDatabase, describeClient };
|
|
155
|
+
module.exports = { ensureDatabase, testConnection, describeClient };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared local dependencies live outside individual projects. Project Compose
|
|
4
|
+
// files only start DraftGo itself and connect back to these loopback services.
|
|
5
|
+
const net = require('net');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
const { ensureDir, writeText } = require('../fsx');
|
|
10
|
+
|
|
11
|
+
const SERVICES_DIR = path.join(os.homedir(), '.draftgo', 'local-services');
|
|
12
|
+
const COMPOSE_FILE = path.join(SERVICES_DIR, 'docker-compose.yaml');
|
|
13
|
+
const MYSQL_INIT_FILE = path.join(SERVICES_DIR, 'mysql-init.sql');
|
|
14
|
+
|
|
15
|
+
const defaults = {
|
|
16
|
+
mysql: { host: '127.0.0.1', port: 3306, user: 'draftgo', password: 'draftgo' },
|
|
17
|
+
redis: { host: '127.0.0.1', port: 6379, password: '' },
|
|
18
|
+
milvus: { host: '127.0.0.1', port: 19530, username: '', password: '' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function writeServiceFiles() {
|
|
22
|
+
ensureDir(SERVICES_DIR);
|
|
23
|
+
ensureDir(path.join(SERVICES_DIR, 'data', 'mysql'));
|
|
24
|
+
ensureDir(path.join(SERVICES_DIR, 'data', 'redis'));
|
|
25
|
+
ensureDir(path.join(SERVICES_DIR, 'data', 'milvus'));
|
|
26
|
+
writeText(MYSQL_INIT_FILE, [
|
|
27
|
+
"GRANT ALL PRIVILEGES ON *.* TO 'draftgo'@'%' WITH GRANT OPTION;",
|
|
28
|
+
'FLUSH PRIVILEGES;',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n'));
|
|
31
|
+
writeText(COMPOSE_FILE, [
|
|
32
|
+
'name: draftgo-local-services',
|
|
33
|
+
'services:',
|
|
34
|
+
' mysql:',
|
|
35
|
+
' image: mysql:8.0',
|
|
36
|
+
' container_name: draftgo-local-mysql',
|
|
37
|
+
' restart: unless-stopped',
|
|
38
|
+
' command: ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]',
|
|
39
|
+
' environment:',
|
|
40
|
+
' MYSQL_ROOT_PASSWORD: draftgo',
|
|
41
|
+
' MYSQL_DATABASE: draftgo_bootstrap',
|
|
42
|
+
' MYSQL_USER: draftgo',
|
|
43
|
+
' MYSQL_PASSWORD: draftgo',
|
|
44
|
+
' ports:',
|
|
45
|
+
' - "127.0.0.1:3306:3306"',
|
|
46
|
+
' volumes:',
|
|
47
|
+
' - ./data/mysql:/var/lib/mysql',
|
|
48
|
+
' - ./mysql-init.sql:/docker-entrypoint-initdb.d/01-grants.sql:ro',
|
|
49
|
+
' healthcheck:',
|
|
50
|
+
' test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pdraftgo"]',
|
|
51
|
+
' interval: 5s',
|
|
52
|
+
' timeout: 5s',
|
|
53
|
+
' retries: 24',
|
|
54
|
+
' start_period: 20s',
|
|
55
|
+
' redis:',
|
|
56
|
+
' image: redis:7.4-alpine',
|
|
57
|
+
' container_name: draftgo-local-redis',
|
|
58
|
+
' restart: unless-stopped',
|
|
59
|
+
' command: ["redis-server", "--appendonly", "yes"]',
|
|
60
|
+
' ports:',
|
|
61
|
+
' - "127.0.0.1:6379:6379"',
|
|
62
|
+
' volumes:',
|
|
63
|
+
' - ./data/redis:/data',
|
|
64
|
+
' healthcheck:',
|
|
65
|
+
' test: ["CMD", "redis-cli", "ping"]',
|
|
66
|
+
' interval: 5s',
|
|
67
|
+
' timeout: 5s',
|
|
68
|
+
' retries: 20',
|
|
69
|
+
' milvus:',
|
|
70
|
+
' image: milvusdb/milvus:v2.6.0',
|
|
71
|
+
' container_name: draftgo-local-milvus',
|
|
72
|
+
' restart: unless-stopped',
|
|
73
|
+
' command: ["milvus", "run", "standalone"]',
|
|
74
|
+
' security_opt:',
|
|
75
|
+
' - seccomp:unconfined',
|
|
76
|
+
' environment:',
|
|
77
|
+
' DEPLOY_MODE: STANDALONE',
|
|
78
|
+
' ETCD_USE_EMBED: "true"',
|
|
79
|
+
' ETCD_DATA_DIR: /var/lib/milvus/etcd',
|
|
80
|
+
' COMMON_STORAGETYPE: local',
|
|
81
|
+
' ports:',
|
|
82
|
+
' - "127.0.0.1:19530:19530"',
|
|
83
|
+
' volumes:',
|
|
84
|
+
' - ./data/milvus:/var/lib/milvus',
|
|
85
|
+
' healthcheck:',
|
|
86
|
+
' test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]',
|
|
87
|
+
' interval: 10s',
|
|
88
|
+
' timeout: 5s',
|
|
89
|
+
' retries: 18',
|
|
90
|
+
' start_period: 30s',
|
|
91
|
+
'',
|
|
92
|
+
].join('\n'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function portOpen(host, port, timeoutMs = 800) {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const socket = net.createConnection({ host, port });
|
|
98
|
+
const finish = (ok) => { socket.destroy(); resolve(ok); };
|
|
99
|
+
socket.setTimeout(timeoutMs);
|
|
100
|
+
socket.once('connect', () => finish(true));
|
|
101
|
+
socket.once('timeout', () => finish(false));
|
|
102
|
+
socket.once('error', () => finish(false));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function probeRedis({ host, port, password = '' }) {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
const socket = net.createConnection({ host, port });
|
|
109
|
+
let response = '';
|
|
110
|
+
const finish = (ok) => { socket.destroy(); resolve(ok); };
|
|
111
|
+
socket.setTimeout(2000);
|
|
112
|
+
socket.once('connect', () => {
|
|
113
|
+
const auth = password ? `*2\r\n$4\r\nAUTH\r\n$${Buffer.byteLength(password)}\r\n${password}\r\n` : '';
|
|
114
|
+
socket.write(`${auth}*1\r\n$4\r\nPING\r\n`);
|
|
115
|
+
});
|
|
116
|
+
socket.on('data', (chunk) => {
|
|
117
|
+
response += chunk.toString('utf8');
|
|
118
|
+
if (response.includes('+PONG')) finish(true);
|
|
119
|
+
if (response.includes('-NOAUTH') || response.includes('-WRONGPASS') || response.includes('-ERR')) finish(false);
|
|
120
|
+
});
|
|
121
|
+
socket.once('timeout', () => finish(false));
|
|
122
|
+
socket.once('error', () => finish(false));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function probeMilvus({ host, port, username = '', password = '' }) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const grpc = require('@grpc/grpc-js');
|
|
129
|
+
const client = new grpc.Client(`${host}:${port}`, grpc.credentials.createInsecure());
|
|
130
|
+
const metadata = new grpc.Metadata();
|
|
131
|
+
if (username || password) metadata.add('authorization', `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`);
|
|
132
|
+
client.makeUnaryRequest(
|
|
133
|
+
'/milvus.proto.milvus.MilvusService/CheckHealth',
|
|
134
|
+
(value) => value,
|
|
135
|
+
(value) => value,
|
|
136
|
+
Buffer.alloc(0),
|
|
137
|
+
metadata,
|
|
138
|
+
{ deadline: Date.now() + 4000 },
|
|
139
|
+
(err, response) => {
|
|
140
|
+
client.close();
|
|
141
|
+
// CheckHealthResponse starts with field 1 (is_healthy) when the server is healthy.
|
|
142
|
+
resolve(!err && Buffer.isBuffer(response) && response.includes(0x01));
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function startService(docker, service) {
|
|
149
|
+
writeServiceFiles();
|
|
150
|
+
const result = spawnSync(docker.composeCmd, [
|
|
151
|
+
...docker.composeArgs, '-f', COMPOSE_FILE, 'up', '-d', '--wait', service,
|
|
152
|
+
], { cwd: SERVICES_DIR, stdio: 'inherit', shell: false });
|
|
153
|
+
return result.status === 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
SERVICES_DIR,
|
|
158
|
+
defaults,
|
|
159
|
+
portOpen,
|
|
160
|
+
probeRedis,
|
|
161
|
+
probeMilvus,
|
|
162
|
+
startService,
|
|
163
|
+
};
|
package/src/projectConfig.js
CHANGED
|
@@ -40,7 +40,7 @@ function maybeRunInit(projectDir, server, token) {
|
|
|
40
40
|
const py = findPython();
|
|
41
41
|
if (!py) {
|
|
42
42
|
log.dim(' 未检测到 Python,跳过自动初始化项目上下文。');
|
|
43
|
-
log.dim(' 安装 Python 3.9+
|
|
43
|
+
log.dim(' 安装 Python 3.9+ 后运行 `draftgo pull` 获取项目资源。');
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
46
|
const script = findInitScript(projectDir);
|