draftgo-cli 4.0.22 → 4.0.24
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 +2 -2
- package/bin/draftgo.js +8 -8
- package/package.json +72 -72
- package/resources/custom-service-sdk/auth_test.go +56 -0
- package/resources/custom-service-sdk/manifest.json +14 -9
- package/resources/custom-service-sdk/platform.go +19 -27
- package/resources/custom-service-sdk/resources.go +1 -0
- package/resources/custom-service-sdk/resources_scope_test.go +10 -5
- package/resources/custom-service-sdk/sdk.go +6 -5
- package/resources/skill/SKILL.md +1 -1
- package/resources/skill/manifest.json +1 -1
- package/resources/skill/references/aihub.md +74 -74
- package/resources/skill/references/app-api.md +78 -78
- package/resources/skill/references/architecture.md +40 -40
- package/resources/skill/references/checkout.md +105 -105
- package/resources/skill/references/custom-services.md +6 -6
- package/resources/skill/references/data.md +168 -168
- package/resources/skill/references/methods.md +3 -0
- package/resources/skill/references/modules.md +48 -48
- package/resources/skill/references/runtime.md +95 -96
- package/resources/skill/story/SKILL.md +264 -264
- package/src/commands/help.js +72 -72
- package/src/commands/listTargets.js +12 -12
- package/src/commands/status.js +2 -2
- package/src/commands/uninstall.js +45 -45
- package/src/commands/update.js +20 -20
- package/src/customServices.js +5 -4
- package/src/detect.js +14 -14
- package/src/fsx.js +67 -67
- package/src/index.js +25 -25
- package/src/localRuntime/detect.js +76 -76
- package/src/localRuntime/mysqlClient.js +138 -138
- package/src/logger.js +37 -37
- package/src/mcp/client.js +586 -595
- package/src/mcp/hosts.js +520 -520
- package/src/mcp/protocol.js +184 -164
- package/src/prompt.js +94 -94
- package/src/updateCheck.js +16 -16
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { parse } = require('./cli');
|
|
5
5
|
const { resolveCommand } = require('./commandRegistry');
|
|
@@ -15,32 +15,32 @@ async function run(argv) {
|
|
|
15
15
|
require('./commands/help')();
|
|
16
16
|
return 1;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
// --version / --help shortcuts
|
|
20
|
-
if (flags.version || flags.v || command === 'version') {
|
|
21
|
-
console.log(require('./skill').getPackageVersion());
|
|
22
|
-
return 0;
|
|
23
|
-
}
|
|
24
|
-
if (!command || flags.help || flags.h || command === 'help') {
|
|
25
|
-
require('./commands/help')();
|
|
26
|
-
return 0;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const projectDir = flags.project
|
|
30
|
-
? path.resolve(String(flags.project))
|
|
31
|
-
: process.cwd();
|
|
32
|
-
|
|
18
|
+
|
|
19
|
+
// --version / --help shortcuts
|
|
20
|
+
if (flags.version || flags.v || command === 'version') {
|
|
21
|
+
console.log(require('./skill').getPackageVersion());
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
if (!command || flags.help || flags.h || command === 'help') {
|
|
25
|
+
require('./commands/help')();
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const projectDir = flags.project
|
|
30
|
+
? path.resolve(String(flags.project))
|
|
31
|
+
: process.cwd();
|
|
32
|
+
|
|
33
33
|
try {
|
|
34
34
|
const definition = resolveCommand(command);
|
|
35
35
|
if (definition) return await definition.run(projectDir, positional, flags);
|
|
36
36
|
log.err(`未知命令:${command}`);
|
|
37
37
|
require('./commands/help')();
|
|
38
38
|
return 1;
|
|
39
|
-
} catch (e) {
|
|
40
|
-
log.err(e.message || String(e));
|
|
41
|
-
if (process.env.DEBUG) console.error(e.stack);
|
|
42
|
-
return 1;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
module.exports = { run };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
log.err(e.message || String(e));
|
|
41
|
+
if (process.env.DEBUG) console.error(e.stack);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { run };
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
3
|
// Environment detection helpers for the local stack setup wizard.
|
|
4
|
-
// - probePort: TCP probe (used to find existing MySQL/Redis)
|
|
5
|
-
// - probeHttp: HTTP GET probe with retry (used to wait for the app)
|
|
6
|
-
// - detectDocker: Find a working `docker` + `docker compose` (or `docker-compose`)
|
|
7
|
-
|
|
8
|
-
const net = require('net');
|
|
9
|
-
const http = require('http');
|
|
10
|
-
const { spawnSync } = require('child_process');
|
|
11
|
-
|
|
12
|
-
function probePort(host, port, timeoutMs = 800) {
|
|
13
|
-
return new Promise((resolve) => {
|
|
14
|
-
const socket = new net.Socket();
|
|
15
|
-
let done = false;
|
|
16
|
-
const finish = (ok) => {
|
|
17
|
-
if (done) return;
|
|
18
|
-
done = true;
|
|
19
|
-
try { socket.destroy(); } catch {}
|
|
20
|
-
resolve(ok);
|
|
21
|
-
};
|
|
22
|
-
socket.setTimeout(timeoutMs);
|
|
23
|
-
socket.once('connect', () => finish(true));
|
|
24
|
-
socket.once('timeout', () => finish(false));
|
|
25
|
-
socket.once('error', () => finish(false));
|
|
26
|
-
try { socket.connect(port, host); } catch { finish(false); }
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function probeHttpOnce(url, timeoutMs = 2000) {
|
|
31
|
-
return new Promise((resolve) => {
|
|
32
|
-
try {
|
|
33
|
-
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
34
|
-
// Any HTTP response (including 4xx/5xx) means the app is listening.
|
|
35
|
-
res.resume();
|
|
36
|
-
resolve(true);
|
|
37
|
-
});
|
|
38
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
39
|
-
req.on('error', () => resolve(false));
|
|
40
|
-
} catch {
|
|
41
|
-
resolve(false);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function probeHttp(url, { totalMs = 90000, intervalMs = 1500 } = {}) {
|
|
47
|
-
const start = Date.now();
|
|
48
|
-
while (Date.now() - start < totalMs) {
|
|
49
|
-
if (await probeHttpOnce(url)) return true;
|
|
50
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
51
|
-
}
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function tryCmd(cmd, args) {
|
|
56
|
-
try {
|
|
57
|
-
const r = spawnSync(cmd, args, { encoding: 'utf8', shell: false });
|
|
58
|
-
return r.status === 0;
|
|
59
|
-
} catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function detectDocker() {
|
|
65
|
-
const hasDocker = tryCmd('docker', ['version', '--format', '{{.Client.Version}}']);
|
|
66
|
-
if (!hasDocker) return { ok: false, reason: 'docker-missing' };
|
|
67
|
-
// Prefer `docker compose` (plugin); fall back to legacy `docker-compose`.
|
|
68
|
-
if (tryCmd('docker', ['compose', 'version'])) {
|
|
69
|
-
return { ok: true, composeCmd: 'docker', composeArgs: ['compose'] };
|
|
70
|
-
}
|
|
71
|
-
if (tryCmd('docker-compose', ['version'])) {
|
|
72
|
-
return { ok: true, composeCmd: 'docker-compose', composeArgs: [] };
|
|
73
|
-
}
|
|
74
|
-
return { ok: false, reason: 'compose-missing' };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
module.exports = { probePort, probeHttp, probeHttpOnce, detectDocker };
|
|
4
|
+
// - probePort: TCP probe (used to find existing MySQL/Redis)
|
|
5
|
+
// - probeHttp: HTTP GET probe with retry (used to wait for the app)
|
|
6
|
+
// - detectDocker: Find a working `docker` + `docker compose` (or `docker-compose`)
|
|
7
|
+
|
|
8
|
+
const net = require('net');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
function probePort(host, port, timeoutMs = 800) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const socket = new net.Socket();
|
|
15
|
+
let done = false;
|
|
16
|
+
const finish = (ok) => {
|
|
17
|
+
if (done) return;
|
|
18
|
+
done = true;
|
|
19
|
+
try { socket.destroy(); } catch {}
|
|
20
|
+
resolve(ok);
|
|
21
|
+
};
|
|
22
|
+
socket.setTimeout(timeoutMs);
|
|
23
|
+
socket.once('connect', () => finish(true));
|
|
24
|
+
socket.once('timeout', () => finish(false));
|
|
25
|
+
socket.once('error', () => finish(false));
|
|
26
|
+
try { socket.connect(port, host); } catch { finish(false); }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function probeHttpOnce(url, timeoutMs = 2000) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
try {
|
|
33
|
+
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
34
|
+
// Any HTTP response (including 4xx/5xx) means the app is listening.
|
|
35
|
+
res.resume();
|
|
36
|
+
resolve(true);
|
|
37
|
+
});
|
|
38
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
39
|
+
req.on('error', () => resolve(false));
|
|
40
|
+
} catch {
|
|
41
|
+
resolve(false);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function probeHttp(url, { totalMs = 90000, intervalMs = 1500 } = {}) {
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
while (Date.now() - start < totalMs) {
|
|
49
|
+
if (await probeHttpOnce(url)) return true;
|
|
50
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tryCmd(cmd, args) {
|
|
56
|
+
try {
|
|
57
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', shell: false });
|
|
58
|
+
return r.status === 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectDocker() {
|
|
65
|
+
const hasDocker = tryCmd('docker', ['version', '--format', '{{.Client.Version}}']);
|
|
66
|
+
if (!hasDocker) return { ok: false, reason: 'docker-missing' };
|
|
67
|
+
// Prefer `docker compose` (plugin); fall back to legacy `docker-compose`.
|
|
68
|
+
if (tryCmd('docker', ['compose', 'version'])) {
|
|
69
|
+
return { ok: true, composeCmd: 'docker', composeArgs: ['compose'] };
|
|
70
|
+
}
|
|
71
|
+
if (tryCmd('docker-compose', ['version'])) {
|
|
72
|
+
return { ok: true, composeCmd: 'docker-compose', composeArgs: [] };
|
|
73
|
+
}
|
|
74
|
+
return { ok: false, reason: 'compose-missing' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { probePort, probeHttp, probeHttpOnce, detectDocker };
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
3
|
// Lightweight MySQL helper used by the local stack wizard to validate
|
|
4
|
-
// credentials and auto-create the project database when missing.
|
|
5
|
-
//
|
|
6
|
-
// We avoid taking a JS MySQL driver as a dependency. Instead we shell out
|
|
7
|
-
// to one of:
|
|
8
|
-
// - a native `mysql` client on PATH (preferred — fast, no pull)
|
|
9
|
-
// - a dockerised `mysql:8.0` client (always available because the wizard
|
|
10
|
-
// already requires Docker; first call may have to pull the image)
|
|
11
|
-
//
|
|
12
|
-
// Passwords are passed via the `MYSQL_PWD` env var so they don't show up
|
|
13
|
-
// in process listings.
|
|
14
|
-
|
|
15
|
-
const { spawnSync } = require('child_process');
|
|
16
|
-
const log = require('../logger');
|
|
17
|
-
|
|
18
|
-
function hasNativeMysql() {
|
|
19
|
-
try {
|
|
20
|
-
const r = spawnSync('mysql', ['--version'], { encoding: 'utf8', shell: false });
|
|
21
|
-
return r.status === 0;
|
|
22
|
-
} catch { return false; }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function hasDocker() {
|
|
26
|
-
try {
|
|
27
|
-
const r = spawnSync('docker', ['version', '--format', '{{.Client.Version}}'], { encoding: 'utf8', shell: false });
|
|
28
|
-
return r.status === 0;
|
|
29
|
-
} catch { return false; }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function pickClient() {
|
|
33
|
-
if (hasNativeMysql()) return { kind: 'native' };
|
|
34
|
-
if (hasDocker()) return { kind: 'docker' };
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Rewrite localhost/127.0.0.1 to host.docker.internal when running inside
|
|
39
|
-
// a container — the host MySQL is on the host network, not the bridge.
|
|
40
|
-
function dockerHost(host) {
|
|
41
|
-
return (host === 'localhost' || host === '127.0.0.1') ? 'host.docker.internal' : host;
|
|
42
|
-
}
|
|
43
|
-
|
|
4
|
+
// credentials and auto-create the project database when missing.
|
|
5
|
+
//
|
|
6
|
+
// We avoid taking a JS MySQL driver as a dependency. Instead we shell out
|
|
7
|
+
// to one of:
|
|
8
|
+
// - a native `mysql` client on PATH (preferred — fast, no pull)
|
|
9
|
+
// - a dockerised `mysql:8.0` client (always available because the wizard
|
|
10
|
+
// already requires Docker; first call may have to pull the image)
|
|
11
|
+
//
|
|
12
|
+
// Passwords are passed via the `MYSQL_PWD` env var so they don't show up
|
|
13
|
+
// in process listings.
|
|
14
|
+
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
16
|
+
const log = require('../logger');
|
|
17
|
+
|
|
18
|
+
function hasNativeMysql() {
|
|
19
|
+
try {
|
|
20
|
+
const r = spawnSync('mysql', ['--version'], { encoding: 'utf8', shell: false });
|
|
21
|
+
return r.status === 0;
|
|
22
|
+
} catch { return false; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasDocker() {
|
|
26
|
+
try {
|
|
27
|
+
const r = spawnSync('docker', ['version', '--format', '{{.Client.Version}}'], { encoding: 'utf8', shell: false });
|
|
28
|
+
return r.status === 0;
|
|
29
|
+
} catch { return false; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pickClient() {
|
|
33
|
+
if (hasNativeMysql()) return { kind: 'native' };
|
|
34
|
+
if (hasDocker()) return { kind: 'docker' };
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Rewrite localhost/127.0.0.1 to host.docker.internal when running inside
|
|
39
|
+
// a container — the host MySQL is on the host network, not the bridge.
|
|
40
|
+
function dockerHost(host) {
|
|
41
|
+
return (host === 'localhost' || host === '127.0.0.1') ? 'host.docker.internal' : host;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
44
|
function runSQL(client, conn, sql) {
|
|
45
|
-
const env = { ...process.env, MYSQL_PWD: conn.password || '' };
|
|
46
|
-
if (client.kind === 'native') {
|
|
47
|
-
const args = [
|
|
48
|
-
'-h', conn.host,
|
|
49
|
-
'-P', String(conn.port),
|
|
50
|
-
'-u', conn.user,
|
|
51
|
-
'--protocol=TCP',
|
|
52
|
-
'--connect-timeout=5',
|
|
53
|
-
'-N', '-B',
|
|
54
|
-
'-e', sql,
|
|
55
|
-
];
|
|
56
|
-
const r = spawnSync('mysql', args, { encoding: 'utf8', env, shell: false });
|
|
57
|
-
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '', code: r.status };
|
|
58
|
-
}
|
|
59
|
-
// docker variant
|
|
60
|
-
const args = [
|
|
61
|
-
'run', '--rm', '-i',
|
|
62
|
-
'--add-host', 'host.docker.internal:host-gateway',
|
|
45
|
+
const env = { ...process.env, MYSQL_PWD: conn.password || '' };
|
|
46
|
+
if (client.kind === 'native') {
|
|
47
|
+
const args = [
|
|
48
|
+
'-h', conn.host,
|
|
49
|
+
'-P', String(conn.port),
|
|
50
|
+
'-u', conn.user,
|
|
51
|
+
'--protocol=TCP',
|
|
52
|
+
'--connect-timeout=5',
|
|
53
|
+
'-N', '-B',
|
|
54
|
+
'-e', sql,
|
|
55
|
+
];
|
|
56
|
+
const r = spawnSync('mysql', args, { encoding: 'utf8', env, shell: false });
|
|
57
|
+
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '', code: r.status };
|
|
58
|
+
}
|
|
59
|
+
// docker variant
|
|
60
|
+
const args = [
|
|
61
|
+
'run', '--rm', '-i',
|
|
62
|
+
'--add-host', 'host.docker.internal:host-gateway',
|
|
63
63
|
'-e', 'MYSQL_PWD',
|
|
64
|
-
'mysql:8.0',
|
|
65
|
-
'mysql',
|
|
66
|
-
'-h', dockerHost(conn.host),
|
|
67
|
-
'-P', String(conn.port),
|
|
68
|
-
'-u', conn.user,
|
|
69
|
-
'--protocol=TCP',
|
|
70
|
-
'--connect-timeout=5',
|
|
71
|
-
'-N', '-B',
|
|
72
|
-
'-e', sql,
|
|
73
|
-
];
|
|
74
|
-
const r = spawnSync('docker', args, { encoding: 'utf8', env, shell: false });
|
|
75
|
-
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '', code: r.status };
|
|
64
|
+
'mysql:8.0',
|
|
65
|
+
'mysql',
|
|
66
|
+
'-h', dockerHost(conn.host),
|
|
67
|
+
'-P', String(conn.port),
|
|
68
|
+
'-u', conn.user,
|
|
69
|
+
'--protocol=TCP',
|
|
70
|
+
'--connect-timeout=5',
|
|
71
|
+
'-N', '-B',
|
|
72
|
+
'-e', sql,
|
|
73
|
+
];
|
|
74
|
+
const r = spawnSync('docker', args, { encoding: 'utf8', env, shell: false });
|
|
75
|
+
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '', code: r.status };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function testConnection(conn) {
|
|
@@ -83,73 +83,73 @@ function testConnection(conn) {
|
|
|
83
83
|
const why = classifyError(probe.stderr);
|
|
84
84
|
return { ok: false, reason: why === 'auth' ? 'auth' : 'unreachable', detail: probe.stderr.trim() };
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
function classifyError(stderr) {
|
|
88
|
-
const s = (stderr || '').toLowerCase();
|
|
89
|
-
if (s.includes('access denied')) return 'auth';
|
|
90
|
-
if (s.includes("can't connect") || s.includes('connection refused') || s.includes('unknown server host') || s.includes('timed out')) return 'unreachable';
|
|
91
|
-
if (s.includes('access denied for user') && s.includes("to database")) return 'no-create-privilege';
|
|
92
|
-
return 'other';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Escape a db identifier so it can be embedded in a backticked identifier.
|
|
96
|
-
function escIdent(name) {
|
|
97
|
-
return String(name).replace(/`/g, '``');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Public API ----------------------------------------------------------------
|
|
101
|
-
//
|
|
102
|
-
// ensureDatabase(conn, { rootPromptFn }) returns one of:
|
|
103
|
-
// { ok: true, created: bool, mode: 'existed' | 'created' | 'created-as-root' }
|
|
104
|
-
// { ok: false, reason: 'no-client' | 'auth' | 'unreachable' | 'create-failed', detail }
|
|
105
|
-
//
|
|
106
|
-
// `rootPromptFn` (optional) is called when the user's account lacks CREATE
|
|
107
|
-
// privilege; it should resolve to `{ user, password }` (or null to abort).
|
|
108
|
-
async function ensureDatabase(conn, { rootPromptFn } = {}) {
|
|
86
|
+
|
|
87
|
+
function classifyError(stderr) {
|
|
88
|
+
const s = (stderr || '').toLowerCase();
|
|
89
|
+
if (s.includes('access denied')) return 'auth';
|
|
90
|
+
if (s.includes("can't connect") || s.includes('connection refused') || s.includes('unknown server host') || s.includes('timed out')) return 'unreachable';
|
|
91
|
+
if (s.includes('access denied for user') && s.includes("to database")) return 'no-create-privilege';
|
|
92
|
+
return 'other';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Escape a db identifier so it can be embedded in a backticked identifier.
|
|
96
|
+
function escIdent(name) {
|
|
97
|
+
return String(name).replace(/`/g, '``');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Public API ----------------------------------------------------------------
|
|
101
|
+
//
|
|
102
|
+
// ensureDatabase(conn, { rootPromptFn }) returns one of:
|
|
103
|
+
// { ok: true, created: bool, mode: 'existed' | 'created' | 'created-as-root' }
|
|
104
|
+
// { ok: false, reason: 'no-client' | 'auth' | 'unreachable' | 'create-failed', detail }
|
|
105
|
+
//
|
|
106
|
+
// `rootPromptFn` (optional) is called when the user's account lacks CREATE
|
|
107
|
+
// privilege; it should resolve to `{ user, password }` (or null to abort).
|
|
108
|
+
async function ensureDatabase(conn, { rootPromptFn } = {}) {
|
|
109
109
|
const client = pickClient();
|
|
110
110
|
const connected = testConnection(conn);
|
|
111
111
|
if (!client || !connected.ok) return connected;
|
|
112
|
-
|
|
113
|
-
// 2) Does the database already exist?
|
|
114
|
-
const showSQL = `SHOW DATABASES LIKE '${String(conn.database).replace(/'/g, "''")}'`;
|
|
115
|
-
const show = runSQL(client, conn, showSQL);
|
|
116
|
-
if (show.ok && show.stdout.trim()) {
|
|
117
|
-
return { ok: true, created: false, mode: 'existed' };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 3) Try to create as the supplied user.
|
|
121
|
-
const createSQL = `CREATE DATABASE IF NOT EXISTS \`${escIdent(conn.database)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`;
|
|
122
|
-
const create = runSQL(client, conn, createSQL);
|
|
123
|
-
if (create.ok) return { ok: true, created: true, mode: 'created' };
|
|
124
|
-
|
|
125
|
-
// 4) Fall back to root if available (privilege issue).
|
|
126
|
-
if (typeof rootPromptFn === 'function' && /denied/i.test(create.stderr || '')) {
|
|
127
|
-
const root = await rootPromptFn();
|
|
128
|
-
if (root && root.user && root.password !== undefined) {
|
|
129
|
-
const rootConn = { ...conn, user: root.user, password: root.password };
|
|
130
|
-
const probeRoot = runSQL(client, rootConn, 'SELECT 1');
|
|
131
|
-
if (!probeRoot.ok) {
|
|
132
|
-
return { ok: false, reason: 'auth', detail: probeRoot.stderr.trim() };
|
|
133
|
-
}
|
|
134
|
-
const createRoot = runSQL(client, rootConn, createSQL);
|
|
135
|
-
if (!createRoot.ok) {
|
|
136
|
-
return { ok: false, reason: 'create-failed', detail: createRoot.stderr.trim() };
|
|
137
|
-
}
|
|
138
|
-
// Grant privileges on the new DB to the project user so the app can use it.
|
|
139
|
-
const grantSQL =
|
|
140
|
-
`GRANT ALL PRIVILEGES ON \`${escIdent(conn.database)}\`.* TO '${conn.user.replace(/'/g, "''")}'@'%'; FLUSH PRIVILEGES;`;
|
|
141
|
-
runSQL(client, rootConn, grantSQL); // best-effort
|
|
142
|
-
return { ok: true, created: true, mode: 'created-as-root' };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return { ok: false, reason: 'create-failed', detail: create.stderr.trim() };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function describeClient() {
|
|
150
|
-
const c = pickClient();
|
|
151
|
-
if (!c) return 'none';
|
|
152
|
-
return c.kind === 'native' ? 'native mysql client' : 'docker mysql:8.0';
|
|
153
|
-
}
|
|
154
|
-
|
|
112
|
+
|
|
113
|
+
// 2) Does the database already exist?
|
|
114
|
+
const showSQL = `SHOW DATABASES LIKE '${String(conn.database).replace(/'/g, "''")}'`;
|
|
115
|
+
const show = runSQL(client, conn, showSQL);
|
|
116
|
+
if (show.ok && show.stdout.trim()) {
|
|
117
|
+
return { ok: true, created: false, mode: 'existed' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3) Try to create as the supplied user.
|
|
121
|
+
const createSQL = `CREATE DATABASE IF NOT EXISTS \`${escIdent(conn.database)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`;
|
|
122
|
+
const create = runSQL(client, conn, createSQL);
|
|
123
|
+
if (create.ok) return { ok: true, created: true, mode: 'created' };
|
|
124
|
+
|
|
125
|
+
// 4) Fall back to root if available (privilege issue).
|
|
126
|
+
if (typeof rootPromptFn === 'function' && /denied/i.test(create.stderr || '')) {
|
|
127
|
+
const root = await rootPromptFn();
|
|
128
|
+
if (root && root.user && root.password !== undefined) {
|
|
129
|
+
const rootConn = { ...conn, user: root.user, password: root.password };
|
|
130
|
+
const probeRoot = runSQL(client, rootConn, 'SELECT 1');
|
|
131
|
+
if (!probeRoot.ok) {
|
|
132
|
+
return { ok: false, reason: 'auth', detail: probeRoot.stderr.trim() };
|
|
133
|
+
}
|
|
134
|
+
const createRoot = runSQL(client, rootConn, createSQL);
|
|
135
|
+
if (!createRoot.ok) {
|
|
136
|
+
return { ok: false, reason: 'create-failed', detail: createRoot.stderr.trim() };
|
|
137
|
+
}
|
|
138
|
+
// Grant privileges on the new DB to the project user so the app can use it.
|
|
139
|
+
const grantSQL =
|
|
140
|
+
`GRANT ALL PRIVILEGES ON \`${escIdent(conn.database)}\`.* TO '${conn.user.replace(/'/g, "''")}'@'%'; FLUSH PRIVILEGES;`;
|
|
141
|
+
runSQL(client, rootConn, grantSQL); // best-effort
|
|
142
|
+
return { ok: true, created: true, mode: 'created-as-root' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { ok: false, reason: 'create-failed', detail: create.stderr.trim() };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function describeClient() {
|
|
150
|
+
const c = pickClient();
|
|
151
|
+
if (!c) return 'none';
|
|
152
|
+
return c.kind === 'native' ? 'native mysql client' : 'docker mysql:8.0';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
155
|
module.exports = { ensureDatabase, testConnection, describeClient };
|
package/src/logger.js
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Minimal ANSI color helpers - no external deps.
|
|
4
|
-
const isTTY = process.stdout && process.stdout.isTTY;
|
|
5
|
-
const supportsColor = isTTY && !process.env.NO_COLOR;
|
|
6
|
-
const isWindows = process.platform === 'win32';
|
|
7
|
-
const wantsUnicode = process.env.DRAFTGO_UNICODE === '1';
|
|
8
|
-
const useUnicode = wantsUnicode || !isWindows || process.env.TERM_PROGRAM === 'vscode' || process.env.WT_SESSION;
|
|
9
|
-
const sym = useUnicode
|
|
10
|
-
? { info: '\u2139', ok: '\u2713', err: '\u2717', step: '\u2192' }
|
|
11
|
-
: { info: 'i', ok: 'OK', err: 'x', step: '>' };
|
|
12
|
-
|
|
13
|
-
const wrap = (code) => (s) => supportsColor ? `\x1b[${code}m${s}\x1b[0m` : String(s);
|
|
14
|
-
|
|
15
|
-
const c = {
|
|
16
|
-
bold: wrap(1),
|
|
17
|
-
dim: wrap(2),
|
|
18
|
-
red: wrap(31),
|
|
19
|
-
green: wrap(32),
|
|
20
|
-
yellow: wrap(33),
|
|
21
|
-
blue: wrap(34),
|
|
22
|
-
magenta: wrap(35),
|
|
23
|
-
cyan: wrap(36),
|
|
24
|
-
gray: wrap(90),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function info(msg) { console.log(`${c.cyan(sym.info)} ${msg}`); }
|
|
28
|
-
function ok(msg) { console.log(`${c.green(sym.ok)} ${msg}`); }
|
|
29
|
-
function warn(msg) { console.warn(`${c.yellow('!')} ${msg}`); }
|
|
30
|
-
function err(msg) { console.error(`${c.red(sym.err)} ${msg}`); }
|
|
31
|
-
function step(msg) { console.log(`${c.blue(sym.step)} ${msg}`); }
|
|
32
|
-
function title(msg) { console.log(`
|
|
33
|
-
${c.bold(c.magenta(msg))}`); }
|
|
34
|
-
function plain(msg) { console.log(msg); }
|
|
35
|
-
function dim(msg) { console.log(c.dim(msg)); }
|
|
36
|
-
|
|
37
|
-
module.exports = { c, info, ok, warn, err, step, title, plain, dim };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Minimal ANSI color helpers - no external deps.
|
|
4
|
+
const isTTY = process.stdout && process.stdout.isTTY;
|
|
5
|
+
const supportsColor = isTTY && !process.env.NO_COLOR;
|
|
6
|
+
const isWindows = process.platform === 'win32';
|
|
7
|
+
const wantsUnicode = process.env.DRAFTGO_UNICODE === '1';
|
|
8
|
+
const useUnicode = wantsUnicode || !isWindows || process.env.TERM_PROGRAM === 'vscode' || process.env.WT_SESSION;
|
|
9
|
+
const sym = useUnicode
|
|
10
|
+
? { info: '\u2139', ok: '\u2713', err: '\u2717', step: '\u2192' }
|
|
11
|
+
: { info: 'i', ok: 'OK', err: 'x', step: '>' };
|
|
12
|
+
|
|
13
|
+
const wrap = (code) => (s) => supportsColor ? `\x1b[${code}m${s}\x1b[0m` : String(s);
|
|
14
|
+
|
|
15
|
+
const c = {
|
|
16
|
+
bold: wrap(1),
|
|
17
|
+
dim: wrap(2),
|
|
18
|
+
red: wrap(31),
|
|
19
|
+
green: wrap(32),
|
|
20
|
+
yellow: wrap(33),
|
|
21
|
+
blue: wrap(34),
|
|
22
|
+
magenta: wrap(35),
|
|
23
|
+
cyan: wrap(36),
|
|
24
|
+
gray: wrap(90),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function info(msg) { console.log(`${c.cyan(sym.info)} ${msg}`); }
|
|
28
|
+
function ok(msg) { console.log(`${c.green(sym.ok)} ${msg}`); }
|
|
29
|
+
function warn(msg) { console.warn(`${c.yellow('!')} ${msg}`); }
|
|
30
|
+
function err(msg) { console.error(`${c.red(sym.err)} ${msg}`); }
|
|
31
|
+
function step(msg) { console.log(`${c.blue(sym.step)} ${msg}`); }
|
|
32
|
+
function title(msg) { console.log(`
|
|
33
|
+
${c.bold(c.magenta(msg))}`); }
|
|
34
|
+
function plain(msg) { console.log(msg); }
|
|
35
|
+
function dim(msg) { console.log(c.dim(msg)); }
|
|
36
|
+
|
|
37
|
+
module.exports = { c, info, ok, warn, err, step, title, plain, dim };
|