slackhive 0.1.6 → 0.1.8
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 +150 -85
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -33,55 +33,53 @@ async function init(opts) {
|
|
|
33
33
|
console.log('');
|
|
34
34
|
console.log(chalk_1.default.bold(' SlackHive') + chalk_1.default.gray(' — AI agent teams on Slack'));
|
|
35
35
|
console.log('');
|
|
36
|
-
// ── Check prerequisites
|
|
36
|
+
// ── Step 1: Check prerequisites ───────────────────────────────────────────
|
|
37
|
+
console.log(chalk_1.default.bold.hex('#D97757')(' [1/4]') + chalk_1.default.bold(' Checking prerequisites'));
|
|
38
|
+
console.log('');
|
|
37
39
|
const checks = [
|
|
38
|
-
{ name: 'Docker', cmd: 'docker info' },
|
|
39
|
-
{ name: 'Docker Compose', cmd: 'docker compose version' },
|
|
40
|
-
{ name: 'Git', cmd: 'git --version' },
|
|
40
|
+
{ name: 'Docker daemon', cmd: 'docker info', errMsg: 'Docker is not running. Please start Docker Desktop and try again.' },
|
|
41
|
+
{ name: 'Docker Compose', cmd: 'docker compose version', errMsg: 'Docker Compose not found. Please install Docker Desktop.' },
|
|
42
|
+
{ name: 'Git', cmd: 'git --version', errMsg: 'Git not found. Please install Git first.' },
|
|
41
43
|
];
|
|
42
44
|
for (const check of checks) {
|
|
45
|
+
const spinner = (0, ora_1.default)(` Checking ${check.name}...`).start();
|
|
43
46
|
try {
|
|
44
47
|
(0, child_process_1.execSync)(check.cmd, { stdio: 'ignore' });
|
|
45
|
-
|
|
48
|
+
spinner.succeed(chalk_1.default.green(`${check.name} ready`));
|
|
46
49
|
}
|
|
47
50
|
catch {
|
|
48
|
-
|
|
49
|
-
console.log(chalk_1.default.red(' ✗ Docker daemon is not running. Please start Docker Desktop and try again.'));
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
console.log(chalk_1.default.red(` ✗ ${check.name} not found. Please install it first.`));
|
|
53
|
-
}
|
|
51
|
+
spinner.fail(chalk_1.default.red(`${check.name}: ${check.errMsg}`));
|
|
54
52
|
process.exit(1);
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
55
|
console.log('');
|
|
58
|
-
// ── Clone
|
|
56
|
+
// ── Step 2: Clone ─────────────────────────────────────────────────────────
|
|
57
|
+
console.log(chalk_1.default.bold.hex('#D97757')(' [2/4]') + chalk_1.default.bold(' Getting SlackHive'));
|
|
58
|
+
console.log('');
|
|
59
59
|
if ((0, fs_1.existsSync)(dir)) {
|
|
60
|
-
console.log(chalk_1.default.yellow(` Directory ${opts.dir} already exists
|
|
60
|
+
console.log(chalk_1.default.yellow(` ⚡ Directory ${opts.dir} already exists — using existing`));
|
|
61
61
|
}
|
|
62
62
|
else {
|
|
63
|
-
const spinner = (0, ora_1.default)('Cloning
|
|
63
|
+
const spinner = (0, ora_1.default)(' Cloning repository...').start();
|
|
64
64
|
try {
|
|
65
65
|
(0, child_process_1.execSync)(`git clone ${REPO_URL} "${dir}"`, { stdio: 'ignore' });
|
|
66
66
|
spinner.succeed('Repository cloned');
|
|
67
67
|
}
|
|
68
|
-
catch
|
|
68
|
+
catch {
|
|
69
69
|
spinner.fail('Failed to clone repository');
|
|
70
70
|
process.exit(1);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
console.log('');
|
|
74
|
+
// ── Step 3: Configure .env ────────────────────────────────────────────────
|
|
74
75
|
const envPath = (0, path_1.join)(dir, '.env');
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.log('');
|
|
78
|
-
console.log(chalk_1.default.bold(' Configure environment:'));
|
|
76
|
+
if (!(0, fs_1.existsSync)(envPath)) {
|
|
77
|
+
console.log(chalk_1.default.bold.hex('#D97757')(' [3/4]') + chalk_1.default.bold(' Configure environment'));
|
|
79
78
|
console.log('');
|
|
80
|
-
// Auth mode selection
|
|
81
79
|
const authMode = await (0, prompts_1.default)({
|
|
82
80
|
type: 'select',
|
|
83
81
|
name: 'mode',
|
|
84
|
-
message: '
|
|
82
|
+
message: 'Claude authentication',
|
|
85
83
|
choices: [
|
|
86
84
|
{ title: 'API Key — pay-per-use via Anthropic API', value: 'apikey' },
|
|
87
85
|
{ title: 'Subscription — run `claude login` first', value: 'subscription' },
|
|
@@ -101,14 +99,12 @@ async function init(opts) {
|
|
|
101
99
|
});
|
|
102
100
|
}
|
|
103
101
|
else {
|
|
104
|
-
// Check if ~/.claude exists
|
|
105
102
|
const claudeDir = (0, path_1.join)(process.env.HOME || '~', '.claude');
|
|
106
103
|
if (!(0, fs_1.existsSync)(claudeDir)) {
|
|
107
104
|
console.log(chalk_1.default.yellow('\n ⚠ ~/.claude not found. Run `claude login` first, then re-run `slackhive init`.'));
|
|
108
105
|
process.exit(1);
|
|
109
106
|
}
|
|
110
107
|
console.log(chalk_1.default.green(' ✓') + ' Found ~/.claude credentials');
|
|
111
|
-
// Find claude binary
|
|
112
108
|
let claudeBinDefault = '/usr/local/bin/claude';
|
|
113
109
|
try {
|
|
114
110
|
const found = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
|
|
@@ -123,37 +119,12 @@ async function init(opts) {
|
|
|
123
119
|
initial: claudeBinDefault,
|
|
124
120
|
});
|
|
125
121
|
}
|
|
126
|
-
questions.push({
|
|
127
|
-
type: 'text',
|
|
128
|
-
name: 'adminUsername',
|
|
129
|
-
message: 'Admin username',
|
|
130
|
-
initial: 'admin',
|
|
131
|
-
}, {
|
|
132
|
-
type: 'password',
|
|
133
|
-
name: 'adminPassword',
|
|
134
|
-
message: 'Admin password',
|
|
135
|
-
validate: (v) => v.length >= 6 ? true : 'At least 6 characters',
|
|
136
|
-
}, {
|
|
137
|
-
type: 'text',
|
|
138
|
-
name: 'postgresPassword',
|
|
139
|
-
message: 'Postgres password',
|
|
140
|
-
initial: randomSecret().slice(0, 16),
|
|
141
|
-
}, {
|
|
142
|
-
type: 'text',
|
|
143
|
-
name: 'redisPassword',
|
|
144
|
-
message: 'Redis password',
|
|
145
|
-
initial: randomSecret().slice(0, 16),
|
|
146
|
-
});
|
|
122
|
+
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) });
|
|
147
123
|
const response = await (0, prompts_1.default)(questions);
|
|
148
|
-
if (authMode.mode === 'apikey' && !response.anthropicKey) {
|
|
149
|
-
console.log(chalk_1.default.red('\n Setup cancelled.'));
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
124
|
if (!response.adminPassword) {
|
|
153
125
|
console.log(chalk_1.default.red('\n Setup cancelled.'));
|
|
154
126
|
process.exit(1);
|
|
155
127
|
}
|
|
156
|
-
// Build .env from scratch so there are no placeholder values
|
|
157
128
|
let envContent = '# Generated by slackhive init\n\n';
|
|
158
129
|
if (authMode.mode === 'apikey') {
|
|
159
130
|
envContent += `ANTHROPIC_API_KEY=${response.anthropicKey}\n`;
|
|
@@ -171,68 +142,162 @@ async function init(opts) {
|
|
|
171
142
|
envContent += `AUTH_SECRET=${randomSecret()}\n`;
|
|
172
143
|
envContent += `\nNODE_ENV=production\n`;
|
|
173
144
|
(0, fs_1.writeFileSync)(envPath, envContent);
|
|
174
|
-
console.log(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
console.log(chalk_1.default.yellow(' .env already exists, skipping configuration'));
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(chalk_1.default.green(' ✓') + ' .env file created');
|
|
147
|
+
console.log('');
|
|
178
148
|
}
|
|
179
|
-
|
|
180
|
-
|
|
149
|
+
else {
|
|
150
|
+
console.log(chalk_1.default.bold.hex('#D97757')(' [3/4]') + chalk_1.default.bold(' Configure environment'));
|
|
181
151
|
console.log('');
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
152
|
+
// Check if existing .env is missing required keys
|
|
153
|
+
const envContents = (0, fs_1.existsSync)(envPath) ? require('fs').readFileSync(envPath, 'utf-8') : '';
|
|
154
|
+
const missingKeys = [];
|
|
155
|
+
if (!envContents.includes('REDIS_PASSWORD='))
|
|
156
|
+
missingKeys.push('REDIS_PASSWORD');
|
|
157
|
+
if (!envContents.includes('AUTH_SECRET='))
|
|
158
|
+
missingKeys.push('AUTH_SECRET');
|
|
159
|
+
if (missingKeys.length > 0) {
|
|
160
|
+
console.log(chalk_1.default.yellow(` ⚠ .env is missing: ${missingKeys.join(', ')} — patching...`));
|
|
161
|
+
let patch = '';
|
|
162
|
+
if (!envContents.includes('REDIS_PASSWORD='))
|
|
163
|
+
patch += `\nREDIS_PASSWORD=${randomSecret().slice(0, 16)}\n`;
|
|
164
|
+
if (!envContents.includes('AUTH_SECRET='))
|
|
165
|
+
patch += `AUTH_SECRET=${randomSecret()}\n`;
|
|
166
|
+
require('fs').appendFileSync(envPath, patch);
|
|
167
|
+
console.log(chalk_1.default.green(' ✓') + ' .env patched');
|
|
186
168
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
console.log(chalk_1.default.gray(' Try running manually: cd ' + opts.dir + ' && docker compose up -d --build'));
|
|
190
|
-
process.exit(1);
|
|
169
|
+
else {
|
|
170
|
+
console.log(chalk_1.default.yellow(' ⚡ .env already exists — skipping configuration'));
|
|
191
171
|
}
|
|
192
|
-
|
|
193
|
-
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
// ── Step 4: Build & start ─────────────────────────────────────────────────
|
|
175
|
+
if (!opts.skipStart) {
|
|
176
|
+
console.log(chalk_1.default.bold.hex('#D97757')(' [4/4]') + chalk_1.default.bold(' Building & starting services'));
|
|
177
|
+
console.log(chalk_1.default.gray(' This takes 3–5 minutes on first run while Docker builds images.'));
|
|
178
|
+
console.log('');
|
|
179
|
+
await runDockerBuild(dir, opts.dir);
|
|
180
|
+
// Wait for web UI
|
|
181
|
+
const webSpinner = (0, ora_1.default)(' Waiting for web UI to be ready...').start();
|
|
194
182
|
let ready = false;
|
|
195
|
-
for (let i = 0; i <
|
|
183
|
+
for (let i = 0; i < 40; i++) {
|
|
196
184
|
try {
|
|
197
|
-
(0, child_process_1.execSync)('curl -
|
|
185
|
+
(0, child_process_1.execSync)('curl -sf http://localhost:3001/login', { stdio: 'ignore' });
|
|
198
186
|
ready = true;
|
|
199
187
|
break;
|
|
200
188
|
}
|
|
201
189
|
catch {
|
|
202
|
-
await sleep(
|
|
190
|
+
await sleep(3000);
|
|
203
191
|
}
|
|
204
192
|
}
|
|
205
193
|
if (ready) {
|
|
206
|
-
webSpinner.succeed('Web UI ready');
|
|
194
|
+
webSpinner.succeed('Web UI is ready');
|
|
207
195
|
}
|
|
208
196
|
else {
|
|
209
|
-
webSpinner.warn('Web UI may still be starting
|
|
197
|
+
webSpinner.warn('Web UI may still be starting up');
|
|
210
198
|
}
|
|
211
199
|
}
|
|
212
|
-
// ── Done
|
|
200
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
213
201
|
console.log('');
|
|
214
|
-
console.log(chalk_1.default.
|
|
202
|
+
console.log(' ' + chalk_1.default.bgHex('#D97757').black.bold(' SlackHive is ready! '));
|
|
215
203
|
console.log('');
|
|
216
|
-
console.log(` ${chalk_1.default.bold('
|
|
217
|
-
console.log(` ${chalk_1.default.bold('
|
|
218
|
-
console.log(` ${chalk_1.default.bold('Project dir:')} ${dir}`);
|
|
204
|
+
console.log(` ${chalk_1.default.bold('→ Open:')} ${chalk_1.default.cyan('http://localhost:3001')}`);
|
|
205
|
+
console.log(` ${chalk_1.default.bold('→ Dir:')} ${chalk_1.default.gray(dir)}`);
|
|
219
206
|
console.log('');
|
|
220
|
-
console.log(chalk_1.default.gray('
|
|
221
|
-
console.log(chalk_1.default.gray(' slackhive start Start services'));
|
|
222
|
-
console.log(chalk_1.default.gray(' slackhive stop Stop services'));
|
|
223
|
-
console.log(chalk_1.default.gray(' slackhive status Show container status'));
|
|
224
|
-
console.log(chalk_1.default.gray(' slackhive logs Tail runner logs'));
|
|
225
|
-
console.log(chalk_1.default.gray(' slackhive update Pull latest & rebuild'));
|
|
207
|
+
console.log(chalk_1.default.gray(' Useful commands:'));
|
|
208
|
+
console.log(chalk_1.default.gray(' slackhive start — Start services'));
|
|
209
|
+
console.log(chalk_1.default.gray(' slackhive stop — Stop services'));
|
|
210
|
+
console.log(chalk_1.default.gray(' slackhive status — Show container status'));
|
|
211
|
+
console.log(chalk_1.default.gray(' slackhive logs — Tail runner logs'));
|
|
212
|
+
console.log(chalk_1.default.gray(' slackhive update — Pull latest & rebuild'));
|
|
226
213
|
console.log('');
|
|
227
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Runs `docker compose up -d --build` with live streaming progress output.
|
|
217
|
+
* Shows each build step as it happens instead of a silent spinner.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} cwd - The project directory.
|
|
220
|
+
* @param {string} displayDir - Display name for error message.
|
|
221
|
+
* @returns {Promise<void>}
|
|
222
|
+
*/
|
|
223
|
+
function runDockerBuild(cwd, displayDir) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const proc = (0, child_process_1.spawn)('docker', ['compose', 'up', '-d', '--build'], {
|
|
226
|
+
cwd,
|
|
227
|
+
env: { ...process.env },
|
|
228
|
+
});
|
|
229
|
+
const stepPattern = /^#\d+ \[([^\]]+)\] (.+)/;
|
|
230
|
+
const donePattern = /^\s*(✔|Container .+ (Started|Running|Healthy)|Image .+ Built)/i;
|
|
231
|
+
let lastStep = '';
|
|
232
|
+
const processLine = (line) => {
|
|
233
|
+
const trimmed = line.trim();
|
|
234
|
+
if (!trimmed || trimmed.startsWith('#') && trimmed.includes('CACHED'))
|
|
235
|
+
return;
|
|
236
|
+
const stepMatch = stepPattern.exec(trimmed);
|
|
237
|
+
if (stepMatch) {
|
|
238
|
+
const label = ` ${chalk_1.default.gray('▸')} ${chalk_1.default.dim(stepMatch[1])} ${stepMatch[2]}`;
|
|
239
|
+
if (label !== lastStep) {
|
|
240
|
+
process.stdout.write('\r\x1b[K' + label.slice(0, process.stdout.columns - 2));
|
|
241
|
+
lastStep = label;
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (donePattern.test(trimmed)) {
|
|
246
|
+
process.stdout.write('\r\x1b[K');
|
|
247
|
+
console.log(' ' + chalk_1.default.green('✓') + ' ' + trimmed.replace(/^✔\s*/, '').replace(/Container /, ''));
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
// Fallback animated spinner shown when no build lines are detected
|
|
251
|
+
const startTime = Date.now();
|
|
252
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
253
|
+
let frameIdx = 0;
|
|
254
|
+
let lastActivity = Date.now();
|
|
255
|
+
const fallbackInterval = setInterval(() => {
|
|
256
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
257
|
+
const idle = Date.now() - lastActivity > 3000;
|
|
258
|
+
if (idle) {
|
|
259
|
+
const frame = frames[frameIdx++ % frames.length];
|
|
260
|
+
process.stdout.write(`\r\x1b[K ${chalk_1.default.hex('#D97757')(frame)} Building... ${chalk_1.default.gray(elapsed + 's elapsed')}`);
|
|
261
|
+
}
|
|
262
|
+
}, 100);
|
|
263
|
+
let stdoutBuf = '';
|
|
264
|
+
let stderrBuf = '';
|
|
265
|
+
proc.stdout.on('data', (chunk) => {
|
|
266
|
+
lastActivity = Date.now();
|
|
267
|
+
stdoutBuf += chunk.toString();
|
|
268
|
+
const lines = stdoutBuf.split('\n');
|
|
269
|
+
stdoutBuf = lines.pop() ?? '';
|
|
270
|
+
lines.forEach(processLine);
|
|
271
|
+
});
|
|
272
|
+
proc.stderr.on('data', (chunk) => {
|
|
273
|
+
lastActivity = Date.now();
|
|
274
|
+
stderrBuf += chunk.toString();
|
|
275
|
+
const lines = stderrBuf.split('\n');
|
|
276
|
+
stderrBuf = lines.pop() ?? '';
|
|
277
|
+
lines.forEach(processLine);
|
|
278
|
+
});
|
|
279
|
+
proc.on('close', (code) => {
|
|
280
|
+
clearInterval(fallbackInterval);
|
|
281
|
+
process.stdout.write('\r\x1b[K');
|
|
282
|
+
if (code === 0) {
|
|
283
|
+
console.log(' ' + chalk_1.default.green('✓') + ' All services started');
|
|
284
|
+
resolve();
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
console.log(' ' + chalk_1.default.red('✗') + ' Failed to start services');
|
|
288
|
+
console.log(chalk_1.default.gray(` Try manually: cd ${displayDir} && docker compose up -d --build`));
|
|
289
|
+
reject(new Error(`docker compose exited with code ${code}`));
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
228
294
|
function randomSecret() {
|
|
229
295
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
230
296
|
let result = '';
|
|
231
|
-
for (let i = 0; i < 32; i++)
|
|
297
|
+
for (let i = 0; i < 32; i++)
|
|
232
298
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
233
|
-
}
|
|
234
299
|
return result;
|
|
235
300
|
}
|
|
236
301
|
function sleep(ms) {
|
|
237
|
-
return new Promise(
|
|
302
|
+
return new Promise(r => setTimeout(r, ms));
|
|
238
303
|
}
|