slackhive 0.1.33 → 0.1.34
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 +172 -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,53 @@ 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);
|
|
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 };
|
|
64
40
|
}
|
|
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
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
throw new Error(`Claude binary not found at: ${claudeBin}`);
|
|
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
41
|
}
|
|
92
|
-
|
|
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
|
+
return null;
|
|
108
74
|
}
|
|
109
75
|
/**
|
|
110
76
|
* Runs `slackhive init` — interactive setup wizard.
|
|
@@ -116,10 +82,10 @@ async function init(opts) {
|
|
|
116
82
|
const O = chalk_1.default.hex('#D97757').bold;
|
|
117
83
|
const W = chalk_1.default.hex('#EBE6E0').bold;
|
|
118
84
|
console.log('');
|
|
119
|
-
console.log('
|
|
120
|
-
console.log('
|
|
121
|
-
console.log(' ' + O('>') + W('
|
|
122
|
-
console.log('
|
|
85
|
+
console.log(' ' + W('│ │'));
|
|
86
|
+
console.log(' ' + W('───┼───┼───'));
|
|
87
|
+
console.log(' ' + O('>') + W(' ──┼──') + O('█') + W('┼──'));
|
|
88
|
+
console.log(' ' + W('│ │'));
|
|
123
89
|
console.log('');
|
|
124
90
|
console.log(chalk_1.default.bold(' SlackHive') + chalk_1.default.gray(' — AI agent teams on Slack'));
|
|
125
91
|
console.log('');
|
|
@@ -180,6 +146,7 @@ async function init(opts) {
|
|
|
180
146
|
process.exit(1);
|
|
181
147
|
}
|
|
182
148
|
const questions = [];
|
|
149
|
+
let oauthCreds = null;
|
|
183
150
|
if (authMode.mode === 'apikey') {
|
|
184
151
|
questions.push({
|
|
185
152
|
type: 'text',
|
|
@@ -189,61 +156,33 @@ async function init(opts) {
|
|
|
189
156
|
});
|
|
190
157
|
}
|
|
191
158
|
else {
|
|
192
|
-
// Claude subscription mode —
|
|
159
|
+
// Claude subscription mode — extract OAuth token
|
|
193
160
|
const claudeDir = (0, path_1.join)(process.env.HOME || '~', '.claude');
|
|
194
161
|
if (!(0, fs_1.existsSync)(claudeDir)) {
|
|
195
162
|
console.log(chalk_1.default.yellow('\n warning: ~/.claude not found. Run `claude login` first, then re-run `slackhive init`.'));
|
|
196
163
|
process.exit(1);
|
|
197
164
|
}
|
|
198
165
|
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
|
-
}
|
|
166
|
+
const spinner = (0, ora_1.default)(' Extracting OAuth credentials...').start();
|
|
167
|
+
oauthCreds = extractOAuthCredentials();
|
|
168
|
+
if (oauthCreds) {
|
|
169
|
+
spinner.succeed('OAuth credentials extracted');
|
|
220
170
|
}
|
|
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
|
-
});
|
|
171
|
+
else {
|
|
172
|
+
spinner.warn('Could not auto-extract credentials from keychain');
|
|
173
|
+
console.log(chalk_1.default.gray(' On Linux/headless servers, paste your OAuth token manually.'));
|
|
174
|
+
console.log(chalk_1.default.gray(' Get it from a machine where you ran `claude login`:'));
|
|
175
|
+
console.log(chalk_1.default.gray(' security find-generic-password -s "Claude Code-credentials" -w'));
|
|
176
|
+
console.log('');
|
|
177
|
+
const tokenResponse = await (0, prompts_1.default)([
|
|
178
|
+
{ 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' },
|
|
179
|
+
{ 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' },
|
|
180
|
+
]);
|
|
181
|
+
if (!tokenResponse.accessToken || !tokenResponse.refreshToken) {
|
|
182
|
+
console.log(chalk_1.default.red('\n Setup cancelled. Use API Key mode instead on headless servers.'));
|
|
183
|
+
process.exit(1);
|
|
246
184
|
}
|
|
185
|
+
oauthCreds = { accessToken: tokenResponse.accessToken, refreshToken: tokenResponse.refreshToken };
|
|
247
186
|
}
|
|
248
187
|
}
|
|
249
188
|
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 +196,9 @@ async function init(opts) {
|
|
|
257
196
|
envContent += `ANTHROPIC_API_KEY=${response.anthropicKey}\n`;
|
|
258
197
|
}
|
|
259
198
|
else {
|
|
260
|
-
envContent += `# Claude Code subscription — credentials from
|
|
261
|
-
envContent += `
|
|
262
|
-
|
|
263
|
-
envContent += `CLAUDE_MODULE_PATH=${response.claudeModulePath}\n`;
|
|
264
|
-
}
|
|
199
|
+
envContent += `# Claude Code subscription — OAuth credentials from keychain\n`;
|
|
200
|
+
envContent += `CLAUDE_CODE_OAUTH_TOKEN=${oauthCreds.accessToken}\n`;
|
|
201
|
+
envContent += `CLAUDE_CODE_OAUTH_REFRESH_TOKEN=${oauthCreds.refreshToken}\n`;
|
|
265
202
|
}
|
|
266
203
|
envContent += `\nPOSTGRES_DB=slackhive\n`;
|
|
267
204
|
envContent += `POSTGRES_USER=slackhive\n`;
|
|
@@ -330,31 +267,42 @@ async function init(opts) {
|
|
|
330
267
|
}
|
|
331
268
|
}
|
|
332
269
|
catch { /* non-fatal */ }
|
|
333
|
-
|
|
334
|
-
//
|
|
270
|
+
// Remove stale Postgres volume if it exists — prevents password mismatch
|
|
271
|
+
// when re-running init with new credentials
|
|
335
272
|
try {
|
|
336
|
-
(0, child_process_1.execSync)('docker compose
|
|
273
|
+
(0, child_process_1.execSync)('docker compose down -v', { cwd: dir, stdio: 'ignore' });
|
|
337
274
|
}
|
|
338
|
-
catch { /* non-fatal */ }
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
for (let i = 0; i < 60; i++) {
|
|
275
|
+
catch { /* non-fatal — may not exist yet */ }
|
|
276
|
+
const buildOk = await runDockerBuild(dir, opts.dir);
|
|
277
|
+
if (buildOk) {
|
|
278
|
+
// If containers didn't come up during build, retry once silently
|
|
343
279
|
try {
|
|
344
|
-
(0, child_process_1.execSync)('
|
|
345
|
-
ready = true;
|
|
346
|
-
break;
|
|
280
|
+
(0, child_process_1.execSync)('docker compose up -d', { cwd: dir, stdio: 'ignore' });
|
|
347
281
|
}
|
|
348
|
-
catch {
|
|
349
|
-
|
|
282
|
+
catch { /* non-fatal */ }
|
|
283
|
+
// Wait for web UI — up to 3 minutes
|
|
284
|
+
const webSpinner = (0, ora_1.default)(' Waiting for web UI to be ready...').start();
|
|
285
|
+
let ready = false;
|
|
286
|
+
for (let i = 0; i < 60; i++) {
|
|
287
|
+
try {
|
|
288
|
+
(0, child_process_1.execSync)('curl -sf http://localhost:3001/login', { stdio: 'ignore' });
|
|
289
|
+
ready = true;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
await sleep(3000);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (ready) {
|
|
297
|
+
webSpinner.succeed('Web UI is ready');
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
webReady = false;
|
|
301
|
+
webSpinner.stopAndPersist({ symbol: ' ' });
|
|
350
302
|
}
|
|
351
|
-
}
|
|
352
|
-
if (ready) {
|
|
353
|
-
webSpinner.succeed('Web UI is ready');
|
|
354
303
|
}
|
|
355
304
|
else {
|
|
356
305
|
webReady = false;
|
|
357
|
-
webSpinner.stopAndPersist({ symbol: ' ' });
|
|
358
306
|
}
|
|
359
307
|
}
|
|
360
308
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
@@ -395,28 +343,71 @@ function runDockerBuild(cwd, displayDir) {
|
|
|
395
343
|
const startTime = Date.now();
|
|
396
344
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
397
345
|
let frameIdx = 0;
|
|
398
|
-
|
|
399
|
-
const
|
|
346
|
+
// Phased progress tracking
|
|
347
|
+
const phases = [
|
|
348
|
+
{ name: 'Installing system packages', weight: 10, pattern: /apk add|fetch.*APKINDEX/i },
|
|
349
|
+
{ name: 'Installing npm dependencies', weight: 30, pattern: /npm ci|npm install|added \d+ packages/i },
|
|
350
|
+
{ name: 'Compiling TypeScript', weight: 10, pattern: /tsc|--skipLibCheck/i },
|
|
351
|
+
{ name: 'Building web app', weight: 30, pattern: /next build|next\.config/i },
|
|
352
|
+
{ name: 'Creating containers', weight: 10, pattern: /exporting to image|naming to|exporting layers/i },
|
|
353
|
+
{ name: 'Starting services', weight: 10, pattern: /Container .*(Starting|Started|Healthy|Created)/i },
|
|
354
|
+
];
|
|
355
|
+
let currentPhase = 0;
|
|
356
|
+
let phaseStartTime = Date.now();
|
|
357
|
+
function getProgress() {
|
|
358
|
+
let pct = 0;
|
|
359
|
+
for (let i = 0; i < currentPhase; i++)
|
|
360
|
+
pct += phases[i].weight;
|
|
361
|
+
// Add partial progress within current phase
|
|
362
|
+
if (currentPhase < phases.length) {
|
|
363
|
+
const elapsed = (Date.now() - phaseStartTime) / 1000;
|
|
364
|
+
const estimatedDuration = currentPhase === 1 ? 90 : currentPhase === 3 ? 100 : 30;
|
|
365
|
+
const partial = Math.min(0.9, elapsed / estimatedDuration);
|
|
366
|
+
pct += phases[currentPhase].weight * partial;
|
|
367
|
+
}
|
|
368
|
+
return Math.min(99, Math.round(pct));
|
|
369
|
+
}
|
|
370
|
+
function renderBar() {
|
|
371
|
+
const pct = getProgress();
|
|
372
|
+
const cols = process.stdout.columns || 80;
|
|
373
|
+
const barWidth = Math.min(20, Math.max(10, cols - 55));
|
|
374
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
375
|
+
const empty = barWidth - filled;
|
|
376
|
+
const bar = chalk_1.default.hex('#D97757')('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty));
|
|
400
377
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
378
|
+
const phaseName = currentPhase < phases.length ? phases[currentPhase].name : 'Finishing';
|
|
401
379
|
const frame = frames[frameIdx++ % frames.length];
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
380
|
+
const pctStr = String(pct).padStart(2);
|
|
381
|
+
return ` ${chalk_1.default.hex('#D97757')(frame)} ${bar} ${chalk_1.default.bold(pctStr + '%')} ${phaseName} ${chalk_1.default.gray('(' + elapsed + 's)')}`;
|
|
382
|
+
}
|
|
383
|
+
const spinnerInterval = setInterval(() => {
|
|
384
|
+
process.stdout.write(`\r\x1b[K${renderBar()}`);
|
|
405
385
|
}, 80);
|
|
406
386
|
let buf = '';
|
|
407
387
|
const errorLines = [];
|
|
408
388
|
const onData = (chunk) => {
|
|
409
|
-
buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
389
|
+
buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
410
390
|
const lines = buf.split('\n');
|
|
411
391
|
buf = lines.pop() ?? '';
|
|
412
392
|
for (const raw of lines) {
|
|
413
393
|
const line = raw.trim();
|
|
414
394
|
if (!line)
|
|
415
395
|
continue;
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
396
|
+
// Check if we've entered a new phase
|
|
397
|
+
for (let i = currentPhase + 1; i < phases.length; i++) {
|
|
398
|
+
if (phases[i].pattern.test(line)) {
|
|
399
|
+
// Print completed phases
|
|
400
|
+
const elapsed = Math.floor((Date.now() - phaseStartTime) / 1000);
|
|
401
|
+
process.stdout.write('\r\x1b[K');
|
|
402
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[currentPhase].name + chalk_1.default.gray(` (${elapsed}s)`));
|
|
403
|
+
// Skip intermediate phases
|
|
404
|
+
for (let j = currentPhase + 1; j < i; j++) {
|
|
405
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[j].name + chalk_1.default.gray(' (cached)'));
|
|
406
|
+
}
|
|
407
|
+
currentPhase = i;
|
|
408
|
+
phaseStartTime = Date.now();
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
420
411
|
}
|
|
421
412
|
if (/error/i.test(line))
|
|
422
413
|
errorLines.push(line);
|
|
@@ -428,33 +419,49 @@ function runDockerBuild(cwd, displayDir) {
|
|
|
428
419
|
clearInterval(spinnerInterval);
|
|
429
420
|
process.stdout.write('\r\x1b[K');
|
|
430
421
|
if (code === 0) {
|
|
431
|
-
|
|
432
|
-
|
|
422
|
+
// Print any remaining phases as done
|
|
423
|
+
const elapsed = Math.floor((Date.now() - phaseStartTime) / 1000);
|
|
424
|
+
if (currentPhase < phases.length) {
|
|
425
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[currentPhase].name + chalk_1.default.gray(` (${elapsed}s)`));
|
|
426
|
+
for (let j = currentPhase + 1; j < phases.length; j++) {
|
|
427
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[j].name);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
console.log('');
|
|
431
|
+
console.log(' ' + chalk_1.default.green('✓') + chalk_1.default.bold(' All services started'));
|
|
432
|
+
resolve(true);
|
|
433
433
|
return;
|
|
434
434
|
}
|
|
435
|
+
console.log(' ' + chalk_1.default.red('✗') + ' Failed to start services');
|
|
436
|
+
console.log('');
|
|
435
437
|
const allErrors = errorLines.join('\n').toLowerCase();
|
|
436
438
|
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:
|
|
439
|
+
console.log(chalk_1.default.yellow(' Cause: Docker is out of disk space.'));
|
|
440
|
+
console.log(chalk_1.default.gray(' Fix: docker system prune -a'));
|
|
439
441
|
}
|
|
440
442
|
else if (allErrors.includes('port is already allocated') || allErrors.includes('address already in use')) {
|
|
441
443
|
const portMatch = /bind for .+:(\d+)/.exec(allErrors);
|
|
442
444
|
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:
|
|
445
|
+
console.log(chalk_1.default.yellow(` Cause: Port ${port} is already in use.`));
|
|
446
|
+
console.log(chalk_1.default.gray(` Fix: stop the process on port ${port} and retry`));
|
|
445
447
|
}
|
|
446
448
|
else if (allErrors.includes('permission denied') || allErrors.includes('unauthorized')) {
|
|
447
|
-
console.log(chalk_1.default.yellow(' Docker permission denied — is Docker Desktop running?'));
|
|
449
|
+
console.log(chalk_1.default.yellow(' Cause: Docker permission denied — is Docker Desktop running?'));
|
|
448
450
|
}
|
|
449
451
|
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:
|
|
452
|
+
console.log(chalk_1.default.yellow(' Cause: Docker ran out of memory.'));
|
|
453
|
+
console.log(chalk_1.default.gray(' Fix: increase Docker Desktop memory to 4GB+ in Settings → Resources'));
|
|
454
|
+
}
|
|
455
|
+
else if (allErrors.includes('network') || allErrors.includes('timeout') || allErrors.includes('pull') || allErrors.includes('tls') || allErrors.includes('certificate')) {
|
|
456
|
+
console.log(chalk_1.default.yellow(' Cause: Network/TLS error — try restarting Docker Desktop.'));
|
|
452
457
|
}
|
|
453
|
-
else if (
|
|
454
|
-
console.log(chalk_1.default.
|
|
458
|
+
else if (errorLines.length > 0) {
|
|
459
|
+
console.log(chalk_1.default.gray(' Error details:'));
|
|
460
|
+
errorLines.slice(-5).forEach(l => console.log(chalk_1.default.red(' ' + l)));
|
|
455
461
|
}
|
|
462
|
+
console.log('');
|
|
456
463
|
console.log(chalk_1.default.gray(` To retry: cd ${displayDir} && docker compose up -d --build`));
|
|
457
|
-
resolve();
|
|
464
|
+
resolve(false);
|
|
458
465
|
});
|
|
459
466
|
});
|
|
460
467
|
}
|