slackhive 0.1.33 → 0.1.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/dist/commands/init.js +183 -165
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -12,16 +12,11 @@ exports.init = init;
|
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
13
|
const fs_1 = require("fs");
|
|
14
14
|
const path_1 = require("path");
|
|
15
|
-
const os_1 = require("os");
|
|
16
15
|
const chalk_1 = __importDefault(require("chalk"));
|
|
17
16
|
const ora_1 = __importDefault(require("ora"));
|
|
18
17
|
const prompts_1 = __importDefault(require("prompts"));
|
|
19
18
|
const REPO_URL = 'https://github.com/pelago-labs/slackhive.git';
|
|
20
|
-
|
|
21
|
-
* Simple Claude installation detection for Linux.
|
|
22
|
-
* Uses the binary directly without module path detection.
|
|
23
|
-
*/
|
|
24
|
-
function detectClaudeInstallationLinux() {
|
|
19
|
+
function detectClaudeBin() {
|
|
25
20
|
let claudeBin;
|
|
26
21
|
try {
|
|
27
22
|
claudeBin = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
|
|
@@ -29,82 +24,64 @@ function detectClaudeInstallationLinux() {
|
|
|
29
24
|
catch {
|
|
30
25
|
throw new Error('Claude Code not found. Please install Claude Code first.');
|
|
31
26
|
}
|
|
32
|
-
if (!claudeBin)
|
|
27
|
+
if (!claudeBin)
|
|
33
28
|
throw new Error('Claude Code not found. Please install Claude Code first.');
|
|
34
|
-
|
|
35
|
-
// For Linux, we don't need to detect the module path - just use the binary directly
|
|
36
|
-
// The Docker container will use the mounted binary
|
|
37
|
-
return { claudeBin, claudeModulePath: '' };
|
|
29
|
+
return claudeBin;
|
|
38
30
|
}
|
|
39
31
|
/**
|
|
40
|
-
*
|
|
41
|
-
* Finds both the binary and module directory automatically with symlink resolution.
|
|
32
|
+
* Parses a JSON credential blob and extracts OAuth tokens.
|
|
42
33
|
*/
|
|
43
|
-
function
|
|
44
|
-
// Find Claude binary
|
|
45
|
-
let claudeBin;
|
|
34
|
+
function parseOAuthFromJson(json) {
|
|
46
35
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (!claudeBin) {
|
|
53
|
-
throw new Error('Claude Code not found. Please install Claude Code first.');
|
|
54
|
-
}
|
|
55
|
-
let claudeModulePath;
|
|
56
|
-
// Check if it's a symlink and resolve it
|
|
57
|
-
if ((0, fs_1.existsSync)(claudeBin)) {
|
|
58
|
-
try {
|
|
59
|
-
const target = (0, fs_1.readlinkSync)(claudeBin);
|
|
60
|
-
// Handle relative paths
|
|
61
|
-
const resolvedTarget = target.startsWith('/') ? target : (0, path_1.join)((0, path_1.dirname)(claudeBin), target);
|
|
62
|
-
// Extract module directory from cli.js path
|
|
63
|
-
claudeModulePath = (0, path_1.dirname)(resolvedTarget);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// Not a symlink, try common installation paths
|
|
67
|
-
const possiblePaths = [
|
|
68
|
-
'/usr/local/lib/node_modules/@anthropic-ai/claude-code',
|
|
69
|
-
'/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code',
|
|
70
|
-
(0, path_1.join)(process.env.HOME || '~', '.local/lib/node_modules/@anthropic-ai/claude-code'),
|
|
71
|
-
'/usr/lib/node_modules/@anthropic-ai/claude-code',
|
|
72
|
-
];
|
|
73
|
-
claudeModulePath = '';
|
|
74
|
-
for (const path of possiblePaths) {
|
|
75
|
-
if ((0, fs_1.existsSync)(path) && (0, fs_1.existsSync)((0, path_1.join)(path, 'cli.js'))) {
|
|
76
|
-
claudeModulePath = path;
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
36
|
+
const parsed = JSON.parse(json);
|
|
37
|
+
const oauth = parsed?.claudeAiOauth;
|
|
38
|
+
if (oauth?.accessToken && oauth?.refreshToken) {
|
|
39
|
+
return { accessToken: oauth.accessToken, refreshToken: oauth.refreshToken };
|
|
80
40
|
}
|
|
81
41
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (!claudeModulePath || !(0, fs_1.existsSync)((0, path_1.join)(claudeModulePath, 'cli.js'))) {
|
|
86
|
-
throw new Error('Could not find Claude Code module directory.');
|
|
87
|
-
}
|
|
88
|
-
// Verify required files exist
|
|
89
|
-
if (!(0, fs_1.existsSync)((0, path_1.join)(claudeModulePath, 'yoga.wasm'))) {
|
|
90
|
-
throw new Error(`Warning: yoga.wasm not found in ${claudeModulePath}`);
|
|
91
|
-
}
|
|
92
|
-
return { claudeBin, claudeModulePath };
|
|
42
|
+
catch { /* invalid json */ }
|
|
43
|
+
return null;
|
|
93
44
|
}
|
|
94
45
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
46
|
+
* Extracts the OAuth credentials from the OS credential store.
|
|
47
|
+
* Tries macOS Keychain, then Linux secret-tool (GNOME Keyring).
|
|
48
|
+
* Returns access + refresh tokens, or null if not found.
|
|
97
49
|
*/
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
50
|
+
function extractOAuthCredentials() {
|
|
51
|
+
// macOS: read from Keychain
|
|
52
|
+
try {
|
|
53
|
+
const creds = (0, child_process_1.execSync)('security find-generic-password -s "Claude Code-credentials" -w', {
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
56
|
+
}).trim();
|
|
57
|
+
const result = parseOAuthFromJson(creds);
|
|
58
|
+
if (result)
|
|
59
|
+
return result;
|
|
103
60
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
61
|
+
catch { /* not macOS or not found */ }
|
|
62
|
+
// Linux: try secret-tool (GNOME Keyring)
|
|
63
|
+
try {
|
|
64
|
+
const creds = (0, child_process_1.execSync)('secret-tool lookup service "Claude Code-credentials"', {
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
67
|
+
}).trim();
|
|
68
|
+
const result = parseOAuthFromJson(creds);
|
|
69
|
+
if (result)
|
|
70
|
+
return result;
|
|
107
71
|
}
|
|
72
|
+
catch { /* not available */ }
|
|
73
|
+
// Fallback: read credentials file directly (headless Linux / no keyring)
|
|
74
|
+
try {
|
|
75
|
+
const credPath = (0, path_1.join)(process.env.HOME || '~', '.claude', '.credentials.json');
|
|
76
|
+
if ((0, fs_1.existsSync)(credPath)) {
|
|
77
|
+
const creds = (0, fs_1.readFileSync)(credPath, 'utf-8').trim();
|
|
78
|
+
const result = parseOAuthFromJson(creds);
|
|
79
|
+
if (result)
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { /* file not readable or invalid */ }
|
|
84
|
+
return null;
|
|
108
85
|
}
|
|
109
86
|
/**
|
|
110
87
|
* Runs `slackhive init` — interactive setup wizard.
|
|
@@ -116,10 +93,10 @@ async function init(opts) {
|
|
|
116
93
|
const O = chalk_1.default.hex('#D97757').bold;
|
|
117
94
|
const W = chalk_1.default.hex('#EBE6E0').bold;
|
|
118
95
|
console.log('');
|
|
119
|
-
console.log('
|
|
120
|
-
console.log('
|
|
121
|
-
console.log(' ' + O('>') + W('
|
|
122
|
-
console.log('
|
|
96
|
+
console.log(' ' + W('│ │'));
|
|
97
|
+
console.log(' ' + W('───┼───┼───'));
|
|
98
|
+
console.log(' ' + O('>') + W(' ──┼──') + O('█') + W('┼──'));
|
|
99
|
+
console.log(' ' + W('│ │'));
|
|
123
100
|
console.log('');
|
|
124
101
|
console.log(chalk_1.default.bold(' SlackHive') + chalk_1.default.gray(' — AI agent teams on Slack'));
|
|
125
102
|
console.log('');
|
|
@@ -180,6 +157,7 @@ async function init(opts) {
|
|
|
180
157
|
process.exit(1);
|
|
181
158
|
}
|
|
182
159
|
const questions = [];
|
|
160
|
+
let oauthCreds = null;
|
|
183
161
|
if (authMode.mode === 'apikey') {
|
|
184
162
|
questions.push({
|
|
185
163
|
type: 'text',
|
|
@@ -189,61 +167,33 @@ async function init(opts) {
|
|
|
189
167
|
});
|
|
190
168
|
}
|
|
191
169
|
else {
|
|
192
|
-
// Claude subscription mode —
|
|
170
|
+
// Claude subscription mode — extract OAuth token
|
|
193
171
|
const claudeDir = (0, path_1.join)(process.env.HOME || '~', '.claude');
|
|
194
172
|
if (!(0, fs_1.existsSync)(claudeDir)) {
|
|
195
173
|
console.log(chalk_1.default.yellow('\n warning: ~/.claude not found. Run `claude login` first, then re-run `slackhive init`.'));
|
|
196
174
|
process.exit(1);
|
|
197
175
|
}
|
|
198
176
|
console.log(chalk_1.default.green(' ✓') + ' Found ~/.claude credentials');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
spinner.succeed(`Found Claude at ${claudeBin}`);
|
|
204
|
-
// Set these for the .env file
|
|
205
|
-
questions.push({
|
|
206
|
-
type: 'text',
|
|
207
|
-
name: 'claudeBin',
|
|
208
|
-
message: 'Path to claude binary',
|
|
209
|
-
initial: claudeBin,
|
|
210
|
-
});
|
|
211
|
-
// Only ask for module path on macOS (Linux uses binary directly)
|
|
212
|
-
if (claudeModulePath) {
|
|
213
|
-
questions.push({
|
|
214
|
-
type: 'text',
|
|
215
|
-
name: 'claudeModulePath',
|
|
216
|
-
message: 'Path to claude module',
|
|
217
|
-
initial: claudeModulePath,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
177
|
+
const spinner = (0, ora_1.default)(' Extracting OAuth credentials...').start();
|
|
178
|
+
oauthCreds = extractOAuthCredentials();
|
|
179
|
+
if (oauthCreds) {
|
|
180
|
+
spinner.succeed('OAuth credentials extracted');
|
|
220
181
|
}
|
|
221
|
-
|
|
222
|
-
spinner.
|
|
223
|
-
console.log(chalk_1.default.
|
|
224
|
-
console.log(chalk_1.default.gray('
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
name: 'claudeBin',
|
|
235
|
-
message: 'Path to claude binary',
|
|
236
|
-
initial: claudeBinDefault,
|
|
237
|
-
});
|
|
238
|
-
// Only ask for module path on macOS
|
|
239
|
-
if ((0, os_1.platform)() === 'darwin') {
|
|
240
|
-
questions.push({
|
|
241
|
-
type: 'text',
|
|
242
|
-
name: 'claudeModulePath',
|
|
243
|
-
message: 'Path to claude module directory',
|
|
244
|
-
initial: '/usr/local/lib/node_modules/@anthropic-ai/claude-code',
|
|
245
|
-
});
|
|
182
|
+
else {
|
|
183
|
+
spinner.warn('Could not auto-extract credentials from keychain');
|
|
184
|
+
console.log(chalk_1.default.gray(' On Linux/headless servers, paste your OAuth token manually.'));
|
|
185
|
+
console.log(chalk_1.default.gray(' Get it from a machine where you ran `claude login`:'));
|
|
186
|
+
console.log(chalk_1.default.gray(' security find-generic-password -s "Claude Code-credentials" -w'));
|
|
187
|
+
console.log('');
|
|
188
|
+
const tokenResponse = await (0, prompts_1.default)([
|
|
189
|
+
{ type: 'password', name: 'accessToken', message: 'OAuth access token (sk-ant-oat01-...)', validate: (v) => v.startsWith('sk-ant-oat') ? true : 'Must start with sk-ant-oat' },
|
|
190
|
+
{ type: 'password', name: 'refreshToken', message: 'OAuth refresh token (sk-ant-ort01-...)', validate: (v) => v.startsWith('sk-ant-ort') ? true : 'Must start with sk-ant-ort' },
|
|
191
|
+
]);
|
|
192
|
+
if (!tokenResponse.accessToken || !tokenResponse.refreshToken) {
|
|
193
|
+
console.log(chalk_1.default.red('\n Setup cancelled. Use API Key mode instead on headless servers.'));
|
|
194
|
+
process.exit(1);
|
|
246
195
|
}
|
|
196
|
+
oauthCreds = { accessToken: tokenResponse.accessToken, refreshToken: tokenResponse.refreshToken };
|
|
247
197
|
}
|
|
248
198
|
}
|
|
249
199
|
questions.push({ type: 'text', name: 'adminUsername', message: 'Admin username', initial: 'admin' }, { type: 'password', name: 'adminPassword', message: 'Admin password', validate: (v) => v.length >= 6 ? true : 'At least 6 characters' }, { type: 'text', name: 'postgresPassword', message: 'Postgres password', initial: randomSecret().slice(0, 16) }, { type: 'text', name: 'redisPassword', message: 'Redis password', initial: randomSecret().slice(0, 16) });
|
|
@@ -257,11 +207,9 @@ async function init(opts) {
|
|
|
257
207
|
envContent += `ANTHROPIC_API_KEY=${response.anthropicKey}\n`;
|
|
258
208
|
}
|
|
259
209
|
else {
|
|
260
|
-
envContent += `# Claude Code subscription — credentials from
|
|
261
|
-
envContent += `
|
|
262
|
-
|
|
263
|
-
envContent += `CLAUDE_MODULE_PATH=${response.claudeModulePath}\n`;
|
|
264
|
-
}
|
|
210
|
+
envContent += `# Claude Code subscription — OAuth credentials from keychain\n`;
|
|
211
|
+
envContent += `CLAUDE_CODE_OAUTH_TOKEN=${oauthCreds.accessToken}\n`;
|
|
212
|
+
envContent += `CLAUDE_CODE_OAUTH_REFRESH_TOKEN=${oauthCreds.refreshToken}\n`;
|
|
265
213
|
}
|
|
266
214
|
envContent += `\nPOSTGRES_DB=slackhive\n`;
|
|
267
215
|
envContent += `POSTGRES_USER=slackhive\n`;
|
|
@@ -330,31 +278,42 @@ async function init(opts) {
|
|
|
330
278
|
}
|
|
331
279
|
}
|
|
332
280
|
catch { /* non-fatal */ }
|
|
333
|
-
|
|
334
|
-
//
|
|
281
|
+
// Remove stale Postgres volume if it exists — prevents password mismatch
|
|
282
|
+
// when re-running init with new credentials
|
|
335
283
|
try {
|
|
336
|
-
(0, child_process_1.execSync)('docker compose
|
|
284
|
+
(0, child_process_1.execSync)('docker compose down -v', { cwd: dir, stdio: 'ignore' });
|
|
337
285
|
}
|
|
338
|
-
catch { /* non-fatal */ }
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
for (let i = 0; i < 60; i++) {
|
|
286
|
+
catch { /* non-fatal — may not exist yet */ }
|
|
287
|
+
const buildOk = await runDockerBuild(dir, opts.dir);
|
|
288
|
+
if (buildOk) {
|
|
289
|
+
// If containers didn't come up during build, retry once silently
|
|
343
290
|
try {
|
|
344
|
-
(0, child_process_1.execSync)('
|
|
345
|
-
ready = true;
|
|
346
|
-
break;
|
|
291
|
+
(0, child_process_1.execSync)('docker compose up -d', { cwd: dir, stdio: 'ignore' });
|
|
347
292
|
}
|
|
348
|
-
catch {
|
|
349
|
-
|
|
293
|
+
catch { /* non-fatal */ }
|
|
294
|
+
// Wait for web UI — up to 3 minutes
|
|
295
|
+
const webSpinner = (0, ora_1.default)(' Waiting for web UI to be ready...').start();
|
|
296
|
+
let ready = false;
|
|
297
|
+
for (let i = 0; i < 60; i++) {
|
|
298
|
+
try {
|
|
299
|
+
(0, child_process_1.execSync)('curl -sf http://localhost:3001/login', { stdio: 'ignore' });
|
|
300
|
+
ready = true;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
await sleep(3000);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (ready) {
|
|
308
|
+
webSpinner.succeed('Web UI is ready');
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
webReady = false;
|
|
312
|
+
webSpinner.stopAndPersist({ symbol: ' ' });
|
|
350
313
|
}
|
|
351
|
-
}
|
|
352
|
-
if (ready) {
|
|
353
|
-
webSpinner.succeed('Web UI is ready');
|
|
354
314
|
}
|
|
355
315
|
else {
|
|
356
316
|
webReady = false;
|
|
357
|
-
webSpinner.stopAndPersist({ symbol: ' ' });
|
|
358
317
|
}
|
|
359
318
|
}
|
|
360
319
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
@@ -395,28 +354,71 @@ function runDockerBuild(cwd, displayDir) {
|
|
|
395
354
|
const startTime = Date.now();
|
|
396
355
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
397
356
|
let frameIdx = 0;
|
|
398
|
-
|
|
399
|
-
const
|
|
357
|
+
// Phased progress tracking
|
|
358
|
+
const phases = [
|
|
359
|
+
{ name: 'Installing system packages', weight: 10, pattern: /apk add|fetch.*APKINDEX/i },
|
|
360
|
+
{ name: 'Installing npm dependencies', weight: 30, pattern: /npm ci|npm install|added \d+ packages/i },
|
|
361
|
+
{ name: 'Compiling TypeScript', weight: 10, pattern: /tsc|--skipLibCheck/i },
|
|
362
|
+
{ name: 'Building web app', weight: 30, pattern: /next build|next\.config/i },
|
|
363
|
+
{ name: 'Creating containers', weight: 10, pattern: /exporting to image|naming to|exporting layers/i },
|
|
364
|
+
{ name: 'Starting services', weight: 10, pattern: /Container .*(Starting|Started|Healthy|Created)/i },
|
|
365
|
+
];
|
|
366
|
+
let currentPhase = 0;
|
|
367
|
+
let phaseStartTime = Date.now();
|
|
368
|
+
function getProgress() {
|
|
369
|
+
let pct = 0;
|
|
370
|
+
for (let i = 0; i < currentPhase; i++)
|
|
371
|
+
pct += phases[i].weight;
|
|
372
|
+
// Add partial progress within current phase
|
|
373
|
+
if (currentPhase < phases.length) {
|
|
374
|
+
const elapsed = (Date.now() - phaseStartTime) / 1000;
|
|
375
|
+
const estimatedDuration = currentPhase === 1 ? 90 : currentPhase === 3 ? 100 : 30;
|
|
376
|
+
const partial = Math.min(0.9, elapsed / estimatedDuration);
|
|
377
|
+
pct += phases[currentPhase].weight * partial;
|
|
378
|
+
}
|
|
379
|
+
return Math.min(99, Math.round(pct));
|
|
380
|
+
}
|
|
381
|
+
function renderBar() {
|
|
382
|
+
const pct = getProgress();
|
|
383
|
+
const cols = process.stdout.columns || 80;
|
|
384
|
+
const barWidth = Math.min(20, Math.max(10, cols - 55));
|
|
385
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
386
|
+
const empty = barWidth - filled;
|
|
387
|
+
const bar = chalk_1.default.hex('#D97757')('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty));
|
|
400
388
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
389
|
+
const phaseName = currentPhase < phases.length ? phases[currentPhase].name : 'Finishing';
|
|
401
390
|
const frame = frames[frameIdx++ % frames.length];
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
391
|
+
const pctStr = String(pct).padStart(2);
|
|
392
|
+
return ` ${chalk_1.default.hex('#D97757')(frame)} ${bar} ${chalk_1.default.bold(pctStr + '%')} ${phaseName} ${chalk_1.default.gray('(' + elapsed + 's)')}`;
|
|
393
|
+
}
|
|
394
|
+
const spinnerInterval = setInterval(() => {
|
|
395
|
+
process.stdout.write(`\r\x1b[K${renderBar()}`);
|
|
405
396
|
}, 80);
|
|
406
397
|
let buf = '';
|
|
407
398
|
const errorLines = [];
|
|
408
399
|
const onData = (chunk) => {
|
|
409
|
-
buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
400
|
+
buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
410
401
|
const lines = buf.split('\n');
|
|
411
402
|
buf = lines.pop() ?? '';
|
|
412
403
|
for (const raw of lines) {
|
|
413
404
|
const line = raw.trim();
|
|
414
405
|
if (!line)
|
|
415
406
|
continue;
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
407
|
+
// Check if we've entered a new phase
|
|
408
|
+
for (let i = currentPhase + 1; i < phases.length; i++) {
|
|
409
|
+
if (phases[i].pattern.test(line)) {
|
|
410
|
+
// Print completed phases
|
|
411
|
+
const elapsed = Math.floor((Date.now() - phaseStartTime) / 1000);
|
|
412
|
+
process.stdout.write('\r\x1b[K');
|
|
413
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[currentPhase].name + chalk_1.default.gray(` (${elapsed}s)`));
|
|
414
|
+
// Skip intermediate phases
|
|
415
|
+
for (let j = currentPhase + 1; j < i; j++) {
|
|
416
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[j].name + chalk_1.default.gray(' (cached)'));
|
|
417
|
+
}
|
|
418
|
+
currentPhase = i;
|
|
419
|
+
phaseStartTime = Date.now();
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
420
422
|
}
|
|
421
423
|
if (/error/i.test(line))
|
|
422
424
|
errorLines.push(line);
|
|
@@ -428,33 +430,49 @@ function runDockerBuild(cwd, displayDir) {
|
|
|
428
430
|
clearInterval(spinnerInterval);
|
|
429
431
|
process.stdout.write('\r\x1b[K');
|
|
430
432
|
if (code === 0) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
+
// Print any remaining phases as done
|
|
434
|
+
const elapsed = Math.floor((Date.now() - phaseStartTime) / 1000);
|
|
435
|
+
if (currentPhase < phases.length) {
|
|
436
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[currentPhase].name + chalk_1.default.gray(` (${elapsed}s)`));
|
|
437
|
+
for (let j = currentPhase + 1; j < phases.length; j++) {
|
|
438
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[j].name);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
console.log('');
|
|
442
|
+
console.log(' ' + chalk_1.default.green('✓') + chalk_1.default.bold(' All services started'));
|
|
443
|
+
resolve(true);
|
|
433
444
|
return;
|
|
434
445
|
}
|
|
446
|
+
console.log(' ' + chalk_1.default.red('✗') + ' Failed to start services');
|
|
447
|
+
console.log('');
|
|
435
448
|
const allErrors = errorLines.join('\n').toLowerCase();
|
|
436
449
|
if (allErrors.includes('no space left') || allErrors.includes('disk full')) {
|
|
437
|
-
console.log(chalk_1.default.yellow(' Docker is out of disk space.'));
|
|
438
|
-
console.log(chalk_1.default.gray(' Fix:
|
|
450
|
+
console.log(chalk_1.default.yellow(' Cause: Docker is out of disk space.'));
|
|
451
|
+
console.log(chalk_1.default.gray(' Fix: docker system prune -a'));
|
|
439
452
|
}
|
|
440
453
|
else if (allErrors.includes('port is already allocated') || allErrors.includes('address already in use')) {
|
|
441
454
|
const portMatch = /bind for .+:(\d+)/.exec(allErrors);
|
|
442
455
|
const port = portMatch ? portMatch[1] : 'a required port';
|
|
443
|
-
console.log(chalk_1.default.yellow(` Port ${port} is already in use.`));
|
|
444
|
-
console.log(chalk_1.default.gray(` Fix:
|
|
456
|
+
console.log(chalk_1.default.yellow(` Cause: Port ${port} is already in use.`));
|
|
457
|
+
console.log(chalk_1.default.gray(` Fix: stop the process on port ${port} and retry`));
|
|
445
458
|
}
|
|
446
459
|
else if (allErrors.includes('permission denied') || allErrors.includes('unauthorized')) {
|
|
447
|
-
console.log(chalk_1.default.yellow(' Docker permission denied — is Docker Desktop running?'));
|
|
460
|
+
console.log(chalk_1.default.yellow(' Cause: Docker permission denied — is Docker Desktop running?'));
|
|
448
461
|
}
|
|
449
462
|
else if (allErrors.includes('memory') || allErrors.includes('oom')) {
|
|
450
|
-
console.log(chalk_1.default.yellow(' Docker ran out of memory.'));
|
|
451
|
-
console.log(chalk_1.default.gray(' Fix:
|
|
463
|
+
console.log(chalk_1.default.yellow(' Cause: Docker ran out of memory.'));
|
|
464
|
+
console.log(chalk_1.default.gray(' Fix: increase Docker Desktop memory to 4GB+ in Settings → Resources'));
|
|
465
|
+
}
|
|
466
|
+
else if (allErrors.includes('network') || allErrors.includes('timeout') || allErrors.includes('pull') || allErrors.includes('tls') || allErrors.includes('certificate')) {
|
|
467
|
+
console.log(chalk_1.default.yellow(' Cause: Network/TLS error — try restarting Docker Desktop.'));
|
|
452
468
|
}
|
|
453
|
-
else if (
|
|
454
|
-
console.log(chalk_1.default.
|
|
469
|
+
else if (errorLines.length > 0) {
|
|
470
|
+
console.log(chalk_1.default.gray(' Error details:'));
|
|
471
|
+
errorLines.slice(-5).forEach(l => console.log(chalk_1.default.red(' ' + l)));
|
|
455
472
|
}
|
|
473
|
+
console.log('');
|
|
456
474
|
console.log(chalk_1.default.gray(` To retry: cd ${displayDir} && docker compose up -d --build`));
|
|
457
|
-
resolve();
|
|
475
|
+
resolve(false);
|
|
458
476
|
});
|
|
459
477
|
});
|
|
460
478
|
}
|