draftgo-cli 3.0.29 → 3.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -139
  3. package/package.json +10 -2
  4. package/resources/skill/SKILL.md +61 -184
  5. package/resources/skill/init/SKILL.md +18 -66
  6. package/resources/skill/manifest.json +27 -0
  7. package/resources/skill/pull/SKILL.md +18 -52
  8. package/resources/skill/push/SKILL.md +30 -282
  9. package/resources/skill/references/aihub.md +86 -0
  10. package/resources/skill/{quickref → references}/api-endpoints.md +39 -13
  11. package/resources/skill/references/api.json +20248 -0
  12. package/resources/skill/{quickref → references}/app-api.md +40 -0
  13. package/resources/skill/{core → references}/architecture.md +2 -2
  14. package/resources/skill/references/chat-sdk.md +201 -0
  15. package/resources/skill/references/custom-services.md +308 -0
  16. package/resources/skill/{specs → references}/data.md +5 -5
  17. package/resources/skill/{rules → references}/frontend.md +41 -11
  18. package/resources/skill/{core → references}/modules.md +7 -5
  19. package/resources/skill/references/parallel.md +48 -0
  20. package/resources/skill/{specs → references}/runtime.md +1 -1
  21. package/resources/skill/scripts/draftgo_push.py +80 -12
  22. package/resources/skill/story/SKILL.md +11 -16
  23. package/src/cli.js +13 -7
  24. package/src/commandRegistry.js +34 -0
  25. package/src/commands/api.js +153 -8
  26. package/src/commands/help.js +24 -29
  27. package/src/commands/init.js +17 -18
  28. package/src/commands/local.js +9 -3
  29. package/src/commands/sync.js +1 -1
  30. package/src/commands/update.js +40 -12
  31. package/src/index.js +13 -57
  32. package/src/localdev/compose.js +44 -200
  33. package/src/localdev/index.js +116 -216
  34. package/src/localdev/mysqlClient.js +12 -9
  35. package/src/localdev/services.js +163 -0
  36. package/src/projectConfig.js +1 -1
  37. package/src/projectMap.js +17 -80
  38. package/src/skill.js +1 -1
  39. package/src/updateCheck.js +2 -12
  40. package/resources/skill/practices/anti-patterns.md +0 -80
  41. package/resources/skill/practices/best-practices.md +0 -60
  42. package/resources/skill/practices/dev-declaration.md +0 -114
  43. package/resources/skill/quickref/api.json +0 -17784
  44. package/resources/skill/rules/dev-workflow.md +0 -749
  45. package/resources/skill/rules/parallel.md +0 -263
  46. package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
  47. package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
  48. package/resources/skill/specs/custom-services.md +0 -199
  49. package/src/commands/doctor.js +0 -54
  50. package/src/commands/new.js +0 -186
  51. package/src/commands/projectScript.js +0 -37
  52. package/src/commands/upgrade.js +0 -52
  53. /package/resources/skill/{specs → references}/db-relations.md +0 -0
  54. /package/resources/skill/{rules → references}/debugging-syntax.md +0 -0
  55. /package/resources/skill/{specs → references}/security.md +0 -0
  56. /package/resources/skill/{specs → references}/ui-protocol.md +0 -0
@@ -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 printDockerInstallHelp() {
31
- log.err('未检测到可用的 Docker。');
32
- log.dim(' 请先安装 Docker Desktop / Docker Engine:');
33
- log.dim(' Windows / macOS: https://www.docker.com/products/docker-desktop/');
34
- log.dim(' Linux: https://docs.docker.com/engine/install/');
35
- log.dim(' 安装并启动 Docker 后,重新运行:draftgo local-dev');
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
- // ── MySQL ──────────────────────────────────────────────────────────────────
39
-
40
- async function planMysql() {
41
- // Returns: { useContainer, host, port, user, password, database }
42
- log.step('检查 MySQL');
43
- const open = await probePort('127.0.0.1', 3306);
44
- if (open) {
45
- log.ok(' 127.0.0.1:3306 发现监听中的服务');
46
- const reuse = await confirm(' 使用已有的 MySQL?', { default: true });
47
- if (reuse) {
48
- // Collect credentials and validate them in a loop so the user can
49
- // recover from typos / missing database without restarting the wizard.
50
- while (true) {
51
- const host = await ask(' Host', { default: '127.0.0.1' });
52
- const port = await ask(' Port', { default: '3306' });
53
- const database = await askRequired(' Database(不存在会尝试自动创建)', { default: 'draftgo' });
54
- const user = await askRequired(' User', { default: 'draftgo' });
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
- // Validate user-supplied MySQL credentials, auto-creating the database if
69
- // missing. Returns true on success. On failure, prints a helpful message and
70
- // returns false so the caller can re-prompt.
71
- async function verifyAndEnsureDatabase(conn) {
72
- log.step(' 验证 MySQL 连接 / 数据库…');
73
- log.dim(` using ${describeClient()}`);
74
- const res = await ensureDatabase(conn, {
75
- rootPromptFn: async () => {
76
- log.warn(` 账号 ${conn.user} 没有创建数据库的权限。`);
77
- const ok = await confirm(' 用 root 账号自动建库并授权?', { default: true });
78
- if (!ok) return null;
79
- const rootUser = await ask(' root 用户名', { default: 'root' });
80
- const rootPass = await askPassword(' root 密码');
81
- return { user: rootUser, password: rootPass };
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
- if (res.ok) {
86
- if (res.mode === 'existed') log.ok(` 数据库 \`${conn.database}\` 已存在,连接正常`);
87
- else if (res.mode === 'created') log.ok(` 数据库 \`${conn.database}\` 已自动创建(utf8mb4)`);
88
- else log.ok(` 数据库 \`${conn.database}\` 已由 root 创建并授权 ${conn.user}`);
89
- return true;
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
- switch (res.reason) {
93
- case 'no-client':
94
- log.warn(' 未找到 mysql 客户端,也没有 Docker,跳过数据库自动创建。');
95
- log.dim(' 请确保数据库已存在,否则首次启动会失败。');
96
- return true;
97
- case 'auth':
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
- const retry = await confirm(' 是否重新输入?', { default: true });
110
- if (!retry) {
111
- log.warn(' 跳过校验,继续。后续启动若失败请检查 MySQL 配置。');
112
- return true;
83
+ if (!await probeRedis(conn)) {
84
+ log.warn(' Default passwordless Redis PING failed.');
85
+ conn = await promptRedisConnection();
113
86
  }
114
- return false;
87
+ log.ok(` Redis ready: ${conn.host}:${conn.port}`);
88
+ return conn;
115
89
  }
116
90
 
117
- // ── Redis ──────────────────────────────────────────────────────────────────
118
-
119
- async function planRedis() {
120
- log.step('检查 Redis');
121
- const open = await probePort('127.0.0.1', 6379);
122
- if (open) {
123
- log.ok(' 在 127.0.0.1:6379 发现监听中的服务');
124
- const reuse = await confirm(' 使用已有的 Redis?', { default: true });
125
- if (reuse) {
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
- // ── App port + project name ────────────────────────────────────────────────
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
- // Always let the user pick; default to 3000 unless taken (then suggest 3001).
141
- log.step('选择 DraftGo 暴露端口');
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 v = await askRequired(' DraftGo 宿主端口', { default: suggested });
150
- const n = Number(v);
151
- if (!Number.isInteger(n) || n <= 0 || n > 65535) {
152
- log.dim(' 端口需为 1-65535 的整数');
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(' 容器名前缀 (compose project name)', { default: 'draftgo' });
131
+ const raw = await askRequired(' Project container prefix', { default: 'draftgo' });
170
132
  const clean = compose.sanitizeProjectName(raw, '');
171
- if (!clean) {
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
- log.step('启动容器(docker compose up -d)…');
186
- const args = [...docker.composeArgs, '-f', 'docker-compose.yaml', 'up', '-d'];
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 r.status === 0;
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
- log.err(' 等待超时。可在 .draftgo/docker/ 下运行 `docker compose logs app` 排查。');
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-dev — 一键本地部署');
210
- log.plain('适用于尚未部署 DraftGo 基座的用户。流程:');
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
- if (docker.reason === 'compose-missing') {
225
- log.err('检测到 docker 但缺少 compose 插件。请安装 docker compose v2 或 docker-compose。');
226
- } else {
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
- // 3) Generate compose + .env (random secrets, persisted across re-runs)
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
- log.ok(` 写入 ${out.composeFile}`);
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
- log.title('下一步:在浏览器完成初始化');
258
- log.plain(` 1) 打开 ${log.c.cyan(url)}`);
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.title('完成');
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
- if (!client) return { ok: false, reason: 'no-client' };
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
+ };
@@ -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+ 后,可让 AI 工具调用其内部 draftgo_init.py 完成初始化。');
43
+ log.dim(' 安装 Python 3.9+ 后运行 `draftgo pull` 获取项目资源。');
44
44
  return false;
45
45
  }
46
46
  const script = findInitScript(projectDir);