draftgo-cli 4.0.24 → 4.0.26
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 +23 -37
- package/package.json +3 -5
- package/resources/skill/SKILL.md +9 -5
- package/resources/skill/manifest.json +2 -5
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +2 -50
- package/resources/skill/references/architecture.md +1 -1
- package/resources/skill/references/chat-sdk.md +29 -37
- package/resources/skill/references/checkout.md +4 -4
- package/resources/skill/references/data.md +0 -46
- package/resources/skill/references/delivery.md +3 -3
- package/resources/skill/references/diagnostics.md +10 -11
- package/resources/skill/references/frontend.md +23 -20
- package/resources/skill/references/mcp.md +4 -14
- package/resources/skill/references/methods.md +15 -68
- package/resources/skill/references/modules.md +23 -44
- package/resources/skill/references/runtime.md +3 -20
- package/resources/skill/story/SKILL.md +2 -2
- package/src/apiContractCache.js +14 -6
- package/src/cli.js +0 -7
- package/src/commandRegistry.js +0 -6
- package/src/commands/api.js +87 -17
- package/src/commands/apiKey.js +2 -6
- package/src/commands/autoPush.js +15 -51
- package/src/commands/capabilities.js +22 -15
- package/src/commands/check.js +19 -53
- package/src/commands/checkout.js +1 -4
- package/src/commands/clean.js +1 -1
- package/src/commands/commit.js +1 -4
- package/src/commands/components.js +12 -8
- package/src/commands/conflict.js +4 -6
- package/src/commands/conflicts.js +1 -2
- package/src/commands/connect.js +0 -8
- package/src/commands/delete.js +15 -11
- package/src/commands/deploy.js +64 -26
- package/src/commands/diff.js +1 -4
- package/src/commands/group.js +2 -3
- package/src/commands/help.js +22 -43
- package/src/commands/init.js +13 -6
- package/src/commands/local.js +4 -1
- package/src/commands/map.js +138 -23
- package/src/commands/reconcile.js +1 -15
- package/src/commands/role.js +1 -2
- package/src/commands/status.js +12 -40
- package/src/commands/update.js +18 -24
- package/src/commands/verify.js +8 -7
- package/src/commands/worklog.js +11 -5
- package/src/contractCompatibility.js +10 -2
- package/src/localRuntime/compose.js +41 -27
- package/src/localRuntime/detect.js +6 -6
- package/src/localRuntime/index.js +47 -47
- package/src/localRuntime/services.js +2 -39
- package/src/mcp/client.js +99 -134
- package/src/mcp/parallel.js +25 -2
- package/src/mcp/protocol.js +38 -9
- package/src/mcp/tools.js +10 -19
- package/src/projectConfig.js +1 -4
- package/src/{workspaceHealth.js → projectHealth.js} +5 -5
- package/src/projectMap.js +1 -1
- package/src/runtimeFiles.js +2 -1
- package/src/worklog.js +3 -2
- package/src/worktree/backend.js +127 -15
- package/src/worktree/index.js +64 -22
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +18 -4
- package/src/worktree/status.js +4 -2
- package/resources/custom-service-sdk/ai.go +0 -520
- package/resources/custom-service-sdk/ai_test.go +0 -156
- package/resources/custom-service-sdk/auth_test.go +0 -56
- package/resources/custom-service-sdk/billing.go +0 -596
- package/resources/custom-service-sdk/billing_test.go +0 -150
- package/resources/custom-service-sdk/go.mod +0 -3
- package/resources/custom-service-sdk/manifest.json +0 -77
- package/resources/custom-service-sdk/platform.go +0 -352
- package/resources/custom-service-sdk/platform_logger_test.go +0 -24
- package/resources/custom-service-sdk/registration_test.go +0 -39
- package/resources/custom-service-sdk/resources.go +0 -247
- package/resources/custom-service-sdk/resources_billing_test.go +0 -115
- package/resources/custom-service-sdk/resources_files_test.go +0 -57
- package/resources/custom-service-sdk/resources_scope_test.go +0 -92
- package/resources/custom-service-sdk/sdk.go +0 -209
- package/resources/skill/references/aihub.md +0 -116
- package/resources/skill/references/custom-services.md +0 -201
- package/src/commands/customService.js +0 -95
- package/src/commands/dataRange.js +0 -33
- package/src/commands/grant.js +0 -29
- package/src/commands/space.js +0 -41
- package/src/customServices.js +0 -484
|
@@ -23,6 +23,7 @@ function readEnv(projectDir) {
|
|
|
23
23
|
|
|
24
24
|
function writeEnv(projectDir, env) {
|
|
25
25
|
writeText(envPath(projectDir), `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join('\n')}\n`);
|
|
26
|
+
fs.chmodSync(envPath(projectDir), 0o600);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function sanitizeProjectName(raw, fallback = 'draftgo') {
|
|
@@ -30,76 +31,89 @@ function sanitizeProjectName(raw, fallback = 'draftgo') {
|
|
|
30
31
|
return value || fallback;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function defaultRedisKeyPrefix(projectName, mysql) {
|
|
34
|
-
return (mysql && mysql.database) || projectName;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
function dockerHost(host) {
|
|
38
35
|
return host === 'localhost' || host === '127.0.0.1' ? 'host.docker.internal' : host;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
function generate(projectDir, opts) {
|
|
42
39
|
const root = stackDir(projectDir);
|
|
40
|
+
if (exists(composePath(projectDir)) && readText(composePath(projectDir)).includes('./data/storage:/app/data/storage')) {
|
|
41
|
+
throw new Error('Existing runtime uses legacy storage. Back up and migrate its data to the final DraftGo /app/data layout before regenerating Compose; existing files were preserved.');
|
|
42
|
+
}
|
|
43
43
|
ensureDir(root);
|
|
44
44
|
const env = readEnv(projectDir);
|
|
45
45
|
const projectName = sanitizeProjectName(opts.projectName, 'draftgo');
|
|
46
46
|
const database = opts.mysql.database;
|
|
47
47
|
const qdrant = opts.qdrant || { host: '127.0.0.1', port: 26333, apiKey: '' };
|
|
48
48
|
|
|
49
|
-
env.APP_PORT = String(opts.appPort ||
|
|
49
|
+
env.APP_PORT = String(opts.appPort || 7777);
|
|
50
50
|
env.SECRET_KEY = env.SECRET_KEY || randomHex(32);
|
|
51
|
+
env.SYSTEM_CONFIG_MASTER_KEY = env.SYSTEM_CONFIG_MASTER_KEY || randomHex(32);
|
|
51
52
|
env.DATABASE_URL = `mysql://${encodeURIComponent(opts.mysql.user)}:${encodeURIComponent(opts.mysql.password)}@${dockerHost(opts.mysql.host)}:${opts.mysql.port}/${encodeURIComponent(database)}`;
|
|
52
|
-
env.
|
|
53
|
-
|
|
54
|
-
env.
|
|
55
|
-
env.
|
|
56
|
-
env.QDRANT_URL = `http://${dockerHost(qdrant.host)}:${qdrant.port}`;
|
|
57
|
-
env.QDRANT_API_KEY = qdrant.apiKey || '';
|
|
53
|
+
env.REDIS_URL = 'redis://redis:6379/0';
|
|
54
|
+
for (const key of ['REDIS_HOST', 'REDIS_PORT', 'REDIS_PASSWORD', 'REDIS_KEY_PREFIX']) delete env[key];
|
|
55
|
+
env.QDRANT_URL = qdrant ? `http://${dockerHost(qdrant.host)}:${qdrant.port}` : '';
|
|
56
|
+
env.QDRANT_API_KEY = qdrant ? (qdrant.apiKey || '') : '';
|
|
58
57
|
env.QDRANT_COLLECTION_PREFIX = database;
|
|
59
58
|
|
|
60
59
|
const yaml = [
|
|
61
|
-
'# Generated by draftgo-cli.
|
|
60
|
+
'# Generated by draftgo-cli. MySQL and Qdrant are shared; Redis is project-isolated.',
|
|
62
61
|
`name: ${projectName}`,
|
|
63
62
|
'services:',
|
|
64
63
|
' app:',
|
|
65
|
-
|
|
64
|
+
` image: ${opts.image || 'cabinai/draftgo:latest'}`,
|
|
66
65
|
` container_name: ${projectName}`,
|
|
67
66
|
' restart: unless-stopped',
|
|
68
67
|
' ports:',
|
|
69
68
|
' - "${APP_PORT}:3000"',
|
|
70
69
|
' environment:',
|
|
70
|
+
' APP_ENV: production',
|
|
71
71
|
' APP_HOST: 0.0.0.0',
|
|
72
72
|
' APP_PORT: "3000"',
|
|
73
73
|
' PUBLIC_BASE_URL: http://localhost:${APP_PORT}',
|
|
74
74
|
' DATABASE_URL: ${DATABASE_URL}',
|
|
75
75
|
' SECRET_KEY: ${SECRET_KEY}',
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
' REDIS_PASSWORD: ${REDIS_PASSWORD}',
|
|
79
|
-
' REDIS_DB: "0"',
|
|
80
|
-
' REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX}',
|
|
76
|
+
' SYSTEM_CONFIG_MASTER_KEY: ${SYSTEM_CONFIG_MASTER_KEY}',
|
|
77
|
+
' REDIS_URL: ${REDIS_URL}',
|
|
81
78
|
' QDRANT_URL: ${QDRANT_URL}',
|
|
82
79
|
' QDRANT_API_KEY: ${QDRANT_API_KEY}',
|
|
83
80
|
' QDRANT_COLLECTION_PREFIX: ${QDRANT_COLLECTION_PREFIX}',
|
|
84
|
-
'
|
|
85
|
-
' KB_STORAGE_DIR: /app/data/storage/kb',
|
|
86
|
-
' DRAFTGO_GO_SERVICE_CACHE_DIR: /app/data/storage/custom-services/cache',
|
|
87
|
-
' MCP_TEMP_DIR: /app/data/storage/mcp/tmp',
|
|
81
|
+
' FILE_STORAGE_ROOT: /app/data/files',
|
|
88
82
|
' volumes:',
|
|
89
|
-
' -
|
|
83
|
+
' - app_data:/app/data',
|
|
84
|
+
' healthcheck:',
|
|
85
|
+
' test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:3000/api/system/health"]',
|
|
86
|
+
' interval: 10s',
|
|
87
|
+
' timeout: 5s',
|
|
88
|
+
' start_period: 10s',
|
|
89
|
+
' retries: 6',
|
|
90
|
+
' depends_on:',
|
|
91
|
+
' redis:',
|
|
92
|
+
' condition: service_healthy',
|
|
90
93
|
' extra_hosts:',
|
|
91
94
|
' - "host.docker.internal:host-gateway"',
|
|
95
|
+
' redis:',
|
|
96
|
+
' image: redis:7.4-alpine',
|
|
97
|
+
' restart: unless-stopped',
|
|
98
|
+
' command: ["redis-server", "--appendonly", "yes"]',
|
|
99
|
+
' volumes:',
|
|
100
|
+
' - redis_data:/data',
|
|
101
|
+
' healthcheck:',
|
|
102
|
+
' test: ["CMD", "redis-cli", "ping"]',
|
|
103
|
+
' interval: 5s',
|
|
104
|
+
' timeout: 5s',
|
|
105
|
+
' retries: 20',
|
|
106
|
+
'volumes:',
|
|
107
|
+
' app_data:',
|
|
108
|
+
' redis_data:',
|
|
92
109
|
'',
|
|
93
110
|
].join('\n');
|
|
94
111
|
writeText(composePath(projectDir), yaml);
|
|
95
112
|
writeEnv(projectDir, env);
|
|
96
|
-
for (const subdir of ['data/storage', 'data/storage/uploads', 'data/storage/kb', 'data/storage/mcp/tmp', 'data/storage/custom-services/cache']) {
|
|
97
|
-
try { fs.mkdirSync(path.join(root, subdir), { recursive: true }); } catch {}
|
|
98
|
-
}
|
|
99
113
|
return { dir: root, composeFile: composePath(projectDir), envFile: envPath(projectDir), projectName, env };
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
module.exports = {
|
|
103
117
|
stackDir, composePath, envPath,
|
|
104
|
-
generate, sanitizeProjectName,
|
|
118
|
+
generate, sanitizeProjectName, randomHex,
|
|
105
119
|
};
|
|
@@ -31,9 +31,8 @@ function probeHttpOnce(url, timeoutMs = 2000) {
|
|
|
31
31
|
return new Promise((resolve) => {
|
|
32
32
|
try {
|
|
33
33
|
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
34
|
-
|
|
35
|
-
res.
|
|
36
|
-
resolve(true);
|
|
34
|
+
res.resume();
|
|
35
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
37
36
|
});
|
|
38
37
|
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
39
38
|
req.on('error', () => resolve(false));
|
|
@@ -61,9 +60,10 @@ function tryCmd(cmd, args) {
|
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
function detectDocker() {
|
|
65
|
-
const hasDocker = tryCmd('docker', ['version'
|
|
66
|
-
if (!hasDocker) return { ok: false, reason: 'docker-missing' };
|
|
63
|
+
function detectDocker() {
|
|
64
|
+
const hasDocker = tryCmd('docker', ['--version']);
|
|
65
|
+
if (!hasDocker) return { ok: false, reason: 'docker-missing' };
|
|
66
|
+
if (!tryCmd('docker', ['info', '--format', '{{.ServerVersion}}'])) return { ok: false, reason: 'docker-not-running' };
|
|
67
67
|
// Prefer `docker compose` (plugin); fall back to legacy `docker-compose`.
|
|
68
68
|
if (tryCmd('docker', ['compose', 'version'])) {
|
|
69
69
|
return { ok: true, composeCmd: 'docker', composeArgs: ['compose'] };
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
4
5
|
const log = require('../logger');
|
|
5
6
|
const { askRequired, askPassword, confirm } = require('../prompt');
|
|
6
7
|
const { probePort, probeHttp, detectDocker } = require('./detect');
|
|
7
8
|
const compose = require('./compose');
|
|
8
9
|
const { ensureDatabase, testConnection, describeClient } = require('./mysqlClient');
|
|
9
|
-
const { defaults, portOpen,
|
|
10
|
+
const { defaults, portOpen, probeQdrant, startService } = require('./services');
|
|
10
11
|
const { writeProjectConfig } = require('../projectConfig');
|
|
11
|
-
const { appendGitignoreLine } = require('../fsx');
|
|
12
|
+
const { appendGitignoreLine, exists, readText } = require('../fsx');
|
|
12
13
|
|
|
13
|
-
const DEFAULT_APP_PORT =
|
|
14
|
+
const DEFAULT_APP_PORT = 7777;
|
|
14
15
|
|
|
15
16
|
async function promptMysqlConnection() {
|
|
16
17
|
while (true) {
|
|
@@ -25,10 +26,11 @@ async function promptMysqlConnection() {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
async function ensureProjectDatabase(conn, database) {
|
|
29
|
+
async function ensureProjectDatabase(conn, database, interactive = true) {
|
|
29
30
|
while (true) {
|
|
30
31
|
const result = await ensureDatabase({ ...conn, database }, {
|
|
31
32
|
rootPromptFn: async () => {
|
|
33
|
+
if (!interactive) return null;
|
|
32
34
|
log.warn(` ${conn.user} cannot create database ${database}.`);
|
|
33
35
|
if (!await confirm(' Create it with another MySQL account?', { default: true })) return null;
|
|
34
36
|
return {
|
|
@@ -39,13 +41,14 @@ async function ensureProjectDatabase(conn, database) {
|
|
|
39
41
|
});
|
|
40
42
|
if (result.ok) return { ...conn, database };
|
|
41
43
|
log.err(` Database setup failed: ${(result.detail || result.reason || 'unknown error').split('\n')[0]}`);
|
|
44
|
+
if (!interactive) throw new Error('Database setup needs credentials. Run `draftgo local setup` in a terminal to continue.');
|
|
42
45
|
const retry = await confirm(' Re-enter MySQL connection settings?', { default: true });
|
|
43
46
|
if (!retry) throw new Error('MySQL database was not prepared.');
|
|
44
47
|
conn = await promptMysqlConnection();
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
async function planMysql(docker, defaultDatabase) {
|
|
51
|
+
async function planMysql(docker, defaultDatabase, interactive = true) {
|
|
49
52
|
log.step('Checking MySQL');
|
|
50
53
|
let conn = { ...defaults.mysql };
|
|
51
54
|
if (!await portOpen(conn.host, conn.port)) {
|
|
@@ -54,40 +57,15 @@ async function planMysql(docker, defaultDatabase) {
|
|
|
54
57
|
}
|
|
55
58
|
if (!testConnection(conn).ok) {
|
|
56
59
|
log.warn(' Default MySQL credentials were not accepted.');
|
|
60
|
+
if (!interactive) throw new Error('MySQL needs credentials. Run `draftgo local setup` in a terminal to continue.');
|
|
57
61
|
conn = await promptMysqlConnection();
|
|
58
62
|
}
|
|
59
|
-
const database = await askRequired(' Project database name', { default: defaultDatabase });
|
|
60
|
-
const out = await ensureProjectDatabase(conn, database);
|
|
63
|
+
const database = interactive ? await askRequired(' Project database name', { default: defaultDatabase }) : defaultDatabase;
|
|
64
|
+
const out = await ensureProjectDatabase(conn, database, interactive);
|
|
61
65
|
log.ok(` MySQL ready: ${out.host}:${out.port}/${out.database} (${describeClient()})`);
|
|
62
66
|
return out;
|
|
63
67
|
}
|
|
64
68
|
|
|
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.');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
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.');
|
|
82
|
-
}
|
|
83
|
-
if (!await probeRedis(conn)) {
|
|
84
|
-
log.warn(' Default passwordless Redis PING failed.');
|
|
85
|
-
conn = await promptRedisConnection();
|
|
86
|
-
}
|
|
87
|
-
log.ok(` Redis ready: ${conn.host}:${conn.port}`);
|
|
88
|
-
return conn;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
69
|
async function promptQdrantConnection() {
|
|
92
70
|
while (true) {
|
|
93
71
|
const host = await askRequired(' Qdrant host', { default: '127.0.0.1' });
|
|
@@ -99,7 +77,7 @@ async function promptQdrantConnection() {
|
|
|
99
77
|
}
|
|
100
78
|
}
|
|
101
79
|
|
|
102
|
-
async function planQdrant(docker) {
|
|
80
|
+
async function planQdrant(docker, interactive = true) {
|
|
103
81
|
log.step('Checking Qdrant');
|
|
104
82
|
let conn = { ...defaults.qdrant };
|
|
105
83
|
if (!await portOpen(conn.host, conn.port)) {
|
|
@@ -108,15 +86,22 @@ async function planQdrant(docker) {
|
|
|
108
86
|
}
|
|
109
87
|
if (!await probeQdrant(conn)) {
|
|
110
88
|
log.warn(' Default unauthenticated Qdrant REST health check failed.');
|
|
89
|
+
if (!interactive) throw new Error('Qdrant needs credentials. Run `draftgo local setup` in a terminal to continue.');
|
|
111
90
|
conn = await promptQdrantConnection();
|
|
112
91
|
}
|
|
113
92
|
log.ok(` Qdrant ready: ${conn.host}:${conn.port}`);
|
|
114
93
|
return conn;
|
|
115
94
|
}
|
|
116
95
|
|
|
117
|
-
async function pickAppPort() {
|
|
96
|
+
async function pickAppPort(interactive = true) {
|
|
97
|
+
if (!interactive) {
|
|
98
|
+
for (let port = DEFAULT_APP_PORT; port < DEFAULT_APP_PORT + 100; port += 1) {
|
|
99
|
+
if (!await probePort('127.0.0.1', port)) return port;
|
|
100
|
+
}
|
|
101
|
+
throw new Error('No free local port found. Run `draftgo local setup` in a terminal to select a port.');
|
|
102
|
+
}
|
|
118
103
|
const taken = await probePort('127.0.0.1', DEFAULT_APP_PORT);
|
|
119
|
-
const suggested = taken ?
|
|
104
|
+
const suggested = String(taken ? DEFAULT_APP_PORT + 1 : DEFAULT_APP_PORT);
|
|
120
105
|
while (true) {
|
|
121
106
|
const port = Number(await askRequired(' DraftGo host port', { default: suggested }));
|
|
122
107
|
if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
|
|
@@ -141,7 +126,7 @@ function composeUp(docker, dir) {
|
|
|
141
126
|
}
|
|
142
127
|
|
|
143
128
|
async function waitForApp(appPort) {
|
|
144
|
-
const ok = await probeHttp(`http://127.0.0.1:${appPort}
|
|
129
|
+
const ok = await probeHttp(`http://127.0.0.1:${appPort}/api/system/health`, { totalMs: 120000, intervalMs: 2000 });
|
|
145
130
|
if (!ok) log.err(' Timed out waiting for DraftGo. Run `draftgo local logs` to inspect the app.');
|
|
146
131
|
return ok;
|
|
147
132
|
}
|
|
@@ -174,35 +159,50 @@ async function finishMcpSetup(projectDir, mcpCommands) {
|
|
|
174
159
|
return setupOk && testOk;
|
|
175
160
|
}
|
|
176
161
|
|
|
177
|
-
async function runWizard(projectDir, { yes = false, mcpCommands } = {}) {
|
|
162
|
+
async function runWizard(projectDir, { yes = false, mcpCommands, interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !yes, params = {} } = {}) {
|
|
178
163
|
log.title('draftgo local setup');
|
|
164
|
+
if (!yes && !interactive) throw new Error('Run `draftgo local setup --yes` for unattended setup, or use a terminal for guided setup.');
|
|
179
165
|
if (!yes && !await confirm('Continue with local DraftGo setup?', { default: true })) return 0;
|
|
180
166
|
|
|
181
167
|
const docker = detectDocker();
|
|
182
168
|
if (!docker.ok) {
|
|
183
169
|
throw new Error(docker.reason === 'compose-missing'
|
|
184
|
-
? 'Docker
|
|
185
|
-
:
|
|
170
|
+
? 'Install Docker Compose: https://docs.docker.com/compose/install/ then retry `draftgo local setup`.'
|
|
171
|
+
: docker.reason === 'docker-not-running'
|
|
172
|
+
? 'Start Docker Desktop or the Docker daemon, then retry `draftgo local setup`. Installed Skills are preserved.'
|
|
173
|
+
: 'Install Docker: https://docs.docker.com/get-started/get-docker/ then start it and retry `draftgo local setup`. Installed Skills are preserved.');
|
|
186
174
|
}
|
|
187
175
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
176
|
+
if (exists(compose.composePath(projectDir))) {
|
|
177
|
+
if (readText(compose.composePath(projectDir)).includes('./data/storage:/app/data/storage')) {
|
|
178
|
+
throw new Error('Existing runtime uses legacy storage. Back up and migrate it to final DraftGo before setup; Compose and data were preserved.');
|
|
179
|
+
}
|
|
180
|
+
log.dim(' Existing project runtime preserved. Use `draftgo local start` to resume it, then `draftgo connect` to bind your API Key.');
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const appPort = params.appPort || await pickAppPort(interactive);
|
|
184
|
+
const projectName = params.projectName || (interactive ? await pickProjectName() : compose.sanitizeProjectName(path.basename(projectDir)));
|
|
185
|
+
const mysql = params.mysql || await planMysql(docker, projectName, interactive);
|
|
186
|
+
const qdrant = params.qdrant === false ? null : (params.qdrant || await planQdrant(docker, interactive));
|
|
193
187
|
|
|
194
|
-
const out = compose.generate(projectDir, { projectName, appPort, mysql,
|
|
188
|
+
const out = compose.generate(projectDir, { projectName, appPort, mysql, qdrant, image: params.image });
|
|
195
189
|
appendGitignoreLine(projectDir, '.draftgo/docker/');
|
|
196
190
|
if (!composeUp(docker, out.dir) || !await waitForApp(appPort)) return 1;
|
|
197
191
|
|
|
198
192
|
const url = `http://localhost:${appPort}`;
|
|
193
|
+
log.ok(`DraftGo runtime is ready at ${url}`);
|
|
194
|
+
log.dim(' Open the site, register or sign in, and obtain your personal API Key.');
|
|
195
|
+
if (!interactive) {
|
|
196
|
+
log.dim(` Connection pending. Run \`draftgo connect --server ${url}\` in a terminal to enter your API Key.`);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
199
|
let token = '';
|
|
200
200
|
while (token.length < 10) {
|
|
201
201
|
token = String(await askPassword('Paste a DraftGo user API Key')).trim();
|
|
202
202
|
if (token.length < 10) log.err(' API Key appears too short. Please try again.');
|
|
203
203
|
}
|
|
204
204
|
const cfgPath = writeProjectConfig(projectDir, url, token);
|
|
205
|
-
await finishMcpSetup(projectDir, mcpCommands);
|
|
205
|
+
if (!await finishMcpSetup(projectDir, mcpCommands)) return 1;
|
|
206
206
|
log.ok(`DraftGo is ready at ${url}`);
|
|
207
207
|
log.dim(`Project config: ${cfgPath}`);
|
|
208
208
|
return 0;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// Shared local runtime dependencies live outside individual projects. Project Compose
|
|
4
|
-
// files
|
|
4
|
+
// files run DraftGo and isolated Redis, then connect back to MySQL and Qdrant.
|
|
5
5
|
const net = require('net');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
@@ -16,17 +16,15 @@ const MYSQL_INIT_FILE = path.join(SERVICES_DIR, 'mysql-init.sql');
|
|
|
16
16
|
|
|
17
17
|
const defaults = {
|
|
18
18
|
mysql: { host: '127.0.0.1', port: 3306, user: 'draftgo', password: 'draftgo' },
|
|
19
|
-
redis: { host: '127.0.0.1', port: 6379, password: '' },
|
|
20
19
|
qdrant: { host: '127.0.0.1', port: 26333, apiKey: '' },
|
|
21
20
|
};
|
|
22
21
|
|
|
23
22
|
function writeServiceFiles() {
|
|
24
23
|
ensureDir(SERVICES_DIR);
|
|
25
24
|
ensureDir(path.join(SERVICES_DIR, 'data', 'mysql'));
|
|
26
|
-
ensureDir(path.join(SERVICES_DIR, 'data', 'redis'));
|
|
27
25
|
ensureDir(path.join(SERVICES_DIR, 'data', 'qdrant'));
|
|
28
26
|
writeText(MYSQL_INIT_FILE, [
|
|
29
|
-
"GRANT ALL PRIVILEGES ON
|
|
27
|
+
"GRANT ALL PRIVILEGES ON `draftgo\\_%`.* TO 'draftgo'@'%';",
|
|
30
28
|
'FLUSH PRIVILEGES;',
|
|
31
29
|
'',
|
|
32
30
|
].join('\n'));
|
|
@@ -54,20 +52,6 @@ function writeServiceFiles() {
|
|
|
54
52
|
' timeout: 5s',
|
|
55
53
|
' retries: 24',
|
|
56
54
|
' start_period: 20s',
|
|
57
|
-
' redis:',
|
|
58
|
-
' image: redis:7.4-alpine',
|
|
59
|
-
' container_name: draftgo-local-redis',
|
|
60
|
-
' restart: unless-stopped',
|
|
61
|
-
' command: ["redis-server", "--appendonly", "yes"]',
|
|
62
|
-
' ports:',
|
|
63
|
-
' - "127.0.0.1:6379:6379"',
|
|
64
|
-
' volumes:',
|
|
65
|
-
' - ./data/redis:/data',
|
|
66
|
-
' healthcheck:',
|
|
67
|
-
' test: ["CMD", "redis-cli", "ping"]',
|
|
68
|
-
' interval: 5s',
|
|
69
|
-
' timeout: 5s',
|
|
70
|
-
' retries: 20',
|
|
71
55
|
' qdrant:',
|
|
72
56
|
' image: qdrant/qdrant:v1.19.0',
|
|
73
57
|
' container_name: draftgo-local-qdrant',
|
|
@@ -97,26 +81,6 @@ function portOpen(host, port, timeoutMs = 800) {
|
|
|
97
81
|
});
|
|
98
82
|
}
|
|
99
83
|
|
|
100
|
-
function probeRedis({ host, port, password = '' }) {
|
|
101
|
-
return new Promise((resolve) => {
|
|
102
|
-
const socket = net.createConnection({ host, port });
|
|
103
|
-
let response = '';
|
|
104
|
-
const finish = (ok) => { socket.destroy(); resolve(ok); };
|
|
105
|
-
socket.setTimeout(2000);
|
|
106
|
-
socket.once('connect', () => {
|
|
107
|
-
const auth = password ? `*2\r\n$4\r\nAUTH\r\n$${Buffer.byteLength(password)}\r\n${password}\r\n` : '';
|
|
108
|
-
socket.write(`${auth}*1\r\n$4\r\nPING\r\n`);
|
|
109
|
-
});
|
|
110
|
-
socket.on('data', (chunk) => {
|
|
111
|
-
response += chunk.toString('utf8');
|
|
112
|
-
if (response.includes('+PONG')) finish(true);
|
|
113
|
-
if (response.includes('-NOAUTH') || response.includes('-WRONGPASS') || response.includes('-ERR')) finish(false);
|
|
114
|
-
});
|
|
115
|
-
socket.once('timeout', () => finish(false));
|
|
116
|
-
socket.once('error', () => finish(false));
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
84
|
function probeQdrant({ host, port, apiKey = '' }) {
|
|
121
85
|
return new Promise((resolve) => {
|
|
122
86
|
const client = String(host || '').startsWith('https://') ? https : http;
|
|
@@ -148,7 +112,6 @@ module.exports = {
|
|
|
148
112
|
SERVICES_DIR,
|
|
149
113
|
defaults,
|
|
150
114
|
portOpen,
|
|
151
|
-
probeRedis,
|
|
152
115
|
probeQdrant,
|
|
153
116
|
startService,
|
|
154
117
|
};
|