slackhive 0.1.44 → 0.1.46
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 +43 -23
- package/cli/README.md +29 -23
- package/cli/dist/index.js +507 -318
- package/package.json +15 -4
- package/cli/dist/commands/init.d.ts +0 -16
- package/cli/dist/commands/init.js +0 -488
- package/cli/dist/commands/manage.d.ts +0 -30
- package/cli/dist/commands/manage.js +0 -121
- package/cli/dist/index.d.ts +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slackhive",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Open-source platform for managing teams of Claude Code Slack agents with a boss agent orchestrator",
|
|
6
6
|
"workspaces": [
|
|
@@ -26,11 +26,22 @@
|
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@anthropic-ai/claude-agent-sdk": "^0.2.89",
|
|
29
|
-
"@anthropic-ai/claude-code": "^2.1.89"
|
|
29
|
+
"@anthropic-ai/claude-code": "^2.1.89",
|
|
30
|
+
"@elastic/elasticsearch": "^9.3.4",
|
|
31
|
+
"@mozilla/readability": "^0.6.0",
|
|
32
|
+
"linkedom": "^0.18.12",
|
|
33
|
+
"pg": "^8.20.0",
|
|
34
|
+
"turndown": "^7.2.4"
|
|
30
35
|
},
|
|
31
36
|
"overrides": {
|
|
32
|
-
"dompurify": "^3.
|
|
37
|
+
"dompurify": "^3.4.0",
|
|
33
38
|
"vite": "^6.4.2",
|
|
34
|
-
"axios": "^1.15.0"
|
|
39
|
+
"axios": "^1.15.0",
|
|
40
|
+
"hono": "^4.12.14",
|
|
41
|
+
"@hono/node-server": "^1.19.13",
|
|
42
|
+
"follow-redirects": "^1.15.12"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/turndown": "^5.0.6"
|
|
35
46
|
}
|
|
36
47
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview `slackhive init` — clone, configure, and start SlackHive.
|
|
3
|
-
*
|
|
4
|
-
* @module cli/commands/init
|
|
5
|
-
*/
|
|
6
|
-
interface InitOptions {
|
|
7
|
-
dir: string;
|
|
8
|
-
skipStart?: boolean;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Runs `slackhive init` — interactive setup wizard.
|
|
12
|
-
*
|
|
13
|
-
* @param {InitOptions} opts - CLI options.
|
|
14
|
-
*/
|
|
15
|
-
export declare function init(opts: InitOptions): Promise<void>;
|
|
16
|
-
export {};
|
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview `slackhive init` — clone, configure, and start SlackHive.
|
|
4
|
-
*
|
|
5
|
-
* @module cli/commands/init
|
|
6
|
-
*/
|
|
7
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
-
};
|
|
10
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.init = init;
|
|
12
|
-
const child_process_1 = require("child_process");
|
|
13
|
-
const fs_1 = require("fs");
|
|
14
|
-
const path_1 = require("path");
|
|
15
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
16
|
-
const ora_1 = __importDefault(require("ora"));
|
|
17
|
-
const prompts_1 = __importDefault(require("prompts"));
|
|
18
|
-
const REPO_URL = 'https://github.com/pelago-labs/slackhive.git';
|
|
19
|
-
function detectClaudeBin() {
|
|
20
|
-
let claudeBin;
|
|
21
|
-
try {
|
|
22
|
-
claudeBin = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
throw new Error('Claude Code not found. Please install Claude Code first.');
|
|
26
|
-
}
|
|
27
|
-
if (!claudeBin)
|
|
28
|
-
throw new Error('Claude Code not found. Please install Claude Code first.');
|
|
29
|
-
return claudeBin;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Parses a JSON credential blob and extracts OAuth tokens.
|
|
33
|
-
*/
|
|
34
|
-
function parseOAuthFromJson(json) {
|
|
35
|
-
try {
|
|
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 };
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch { /* invalid json */ }
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
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.
|
|
49
|
-
*/
|
|
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;
|
|
60
|
-
}
|
|
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;
|
|
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;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Runs `slackhive init` — interactive setup wizard.
|
|
88
|
-
*
|
|
89
|
-
* @param {InitOptions} opts - CLI options.
|
|
90
|
-
*/
|
|
91
|
-
async function init(opts) {
|
|
92
|
-
const dir = (0, path_1.resolve)(opts.dir);
|
|
93
|
-
const O = chalk_1.default.hex('#D97757').bold;
|
|
94
|
-
const W = chalk_1.default.hex('#EBE6E0').bold;
|
|
95
|
-
console.log('');
|
|
96
|
-
console.log(' ' + W('│ │'));
|
|
97
|
-
console.log(' ' + W('───┼───┼───'));
|
|
98
|
-
console.log(' ' + O('>') + W(' ──┼──') + O('█') + W('┼──'));
|
|
99
|
-
console.log(' ' + W('│ │'));
|
|
100
|
-
console.log('');
|
|
101
|
-
console.log(chalk_1.default.bold(' SlackHive') + chalk_1.default.gray(' — AI agent teams on Slack'));
|
|
102
|
-
console.log('');
|
|
103
|
-
// ── Step 1: Check prerequisites ───────────────────────────────────────────
|
|
104
|
-
console.log(chalk_1.default.bold.hex('#D97757')(' [1/4]') + chalk_1.default.bold(' Checking prerequisites'));
|
|
105
|
-
console.log('');
|
|
106
|
-
const checks = [
|
|
107
|
-
{ name: 'Docker daemon', cmd: 'docker info', errMsg: 'Docker is not running. Please start Docker Desktop and try again.' },
|
|
108
|
-
{ name: 'Docker Compose', cmd: 'docker compose version', errMsg: 'Docker Compose not found. Please install Docker Desktop.' },
|
|
109
|
-
{ name: 'Git', cmd: 'git --version', errMsg: 'Git not found. Please install Git first.' },
|
|
110
|
-
];
|
|
111
|
-
for (const check of checks) {
|
|
112
|
-
const spinner = (0, ora_1.default)(` Checking ${check.name}...`).start();
|
|
113
|
-
try {
|
|
114
|
-
(0, child_process_1.execSync)(check.cmd, { stdio: 'ignore' });
|
|
115
|
-
spinner.succeed(chalk_1.default.green(`${check.name} ready`));
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
spinner.fail(chalk_1.default.red(`${check.name}: ${check.errMsg}`));
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
console.log('');
|
|
123
|
-
// ── Step 2: Clone ─────────────────────────────────────────────────────────
|
|
124
|
-
console.log(chalk_1.default.bold.hex('#D97757')(' [2/4]') + chalk_1.default.bold(' Getting SlackHive'));
|
|
125
|
-
console.log('');
|
|
126
|
-
if ((0, fs_1.existsSync)(dir)) {
|
|
127
|
-
console.log(chalk_1.default.yellow(` note: Directory ${opts.dir} already exists — using existing`));
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
const spinner = (0, ora_1.default)(' Cloning repository...').start();
|
|
131
|
-
try {
|
|
132
|
-
(0, child_process_1.execSync)(`git clone --depth 1 ${REPO_URL} "${dir}"`, { stdio: 'ignore' });
|
|
133
|
-
spinner.succeed('Repository cloned');
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
spinner.fail('Failed to clone repository');
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
console.log('');
|
|
141
|
-
// ── Step 3: Configure .env ────────────────────────────────────────────────
|
|
142
|
-
const envPath = (0, path_1.join)(dir, '.env');
|
|
143
|
-
if (!(0, fs_1.existsSync)(envPath)) {
|
|
144
|
-
console.log(chalk_1.default.bold.hex('#D97757')(' [3/4]') + chalk_1.default.bold(' Configure environment'));
|
|
145
|
-
console.log('');
|
|
146
|
-
const authMode = await (0, prompts_1.default)({
|
|
147
|
-
type: 'select',
|
|
148
|
-
name: 'mode',
|
|
149
|
-
message: 'Claude authentication',
|
|
150
|
-
choices: [
|
|
151
|
-
{ title: 'API Key — pay-per-use via Anthropic API', value: 'apikey' },
|
|
152
|
-
{ title: 'Subscription — run `claude login` first', value: 'subscription' },
|
|
153
|
-
],
|
|
154
|
-
});
|
|
155
|
-
if (!authMode.mode) {
|
|
156
|
-
console.log(chalk_1.default.red('\n Setup cancelled.'));
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
const questions = [];
|
|
160
|
-
let oauthCreds = null;
|
|
161
|
-
if (authMode.mode === 'apikey') {
|
|
162
|
-
questions.push({
|
|
163
|
-
type: 'text',
|
|
164
|
-
name: 'anthropicKey',
|
|
165
|
-
message: 'Anthropic API key',
|
|
166
|
-
validate: (v) => v.startsWith('sk-') ? true : 'Must start with sk-',
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
// Claude subscription mode — extract OAuth token
|
|
171
|
-
const claudeDir = (0, path_1.join)(process.env.HOME || '~', '.claude');
|
|
172
|
-
if (!(0, fs_1.existsSync)(claudeDir)) {
|
|
173
|
-
console.log(chalk_1.default.yellow('\n warning: ~/.claude not found. Run `claude login` first, then re-run `slackhive init`.'));
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
console.log(chalk_1.default.green(' ✓') + ' Found ~/.claude credentials');
|
|
177
|
-
const spinner = (0, ora_1.default)(' Extracting OAuth credentials...').start();
|
|
178
|
-
oauthCreds = extractOAuthCredentials();
|
|
179
|
-
if (oauthCreds) {
|
|
180
|
-
spinner.succeed('OAuth credentials extracted');
|
|
181
|
-
}
|
|
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);
|
|
195
|
-
}
|
|
196
|
-
oauthCreds = { accessToken: tokenResponse.accessToken, refreshToken: tokenResponse.refreshToken };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
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) });
|
|
200
|
-
const response = await (0, prompts_1.default)(questions);
|
|
201
|
-
if (!response.adminPassword) {
|
|
202
|
-
console.log(chalk_1.default.red('\n Setup cancelled.'));
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
let envContent = '# Generated by slackhive init\n\n';
|
|
206
|
-
if (authMode.mode === 'apikey') {
|
|
207
|
-
envContent += `ANTHROPIC_API_KEY=${response.anthropicKey}\n`;
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
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`;
|
|
213
|
-
}
|
|
214
|
-
envContent += `\nPOSTGRES_DB=slackhive\n`;
|
|
215
|
-
envContent += `POSTGRES_USER=slackhive\n`;
|
|
216
|
-
envContent += `POSTGRES_PASSWORD=${response.postgresPassword}\n`;
|
|
217
|
-
envContent += `\nREDIS_PASSWORD=${response.redisPassword}\n`;
|
|
218
|
-
envContent += `\nADMIN_USERNAME=${response.adminUsername}\n`;
|
|
219
|
-
envContent += `ADMIN_PASSWORD=${response.adminPassword}\n`;
|
|
220
|
-
envContent += `AUTH_SECRET=${randomSecret()}\n`;
|
|
221
|
-
envContent += `ENV_SECRET_KEY=${randomSecret()}\n`;
|
|
222
|
-
envContent += `\nNODE_ENV=production\n`;
|
|
223
|
-
(0, fs_1.writeFileSync)(envPath, envContent);
|
|
224
|
-
console.log('');
|
|
225
|
-
console.log(chalk_1.default.green(' ✓') + ' .env file created');
|
|
226
|
-
console.log('');
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
console.log(chalk_1.default.bold.hex('#D97757')(' [3/4]') + chalk_1.default.bold(' Configure environment'));
|
|
230
|
-
console.log('');
|
|
231
|
-
// Check if existing .env is missing required keys
|
|
232
|
-
const envContents = (0, fs_1.existsSync)(envPath) ? require('fs').readFileSync(envPath, 'utf-8') : '';
|
|
233
|
-
const missingKeys = [];
|
|
234
|
-
if (!envContents.includes('REDIS_PASSWORD='))
|
|
235
|
-
missingKeys.push('REDIS_PASSWORD');
|
|
236
|
-
if (!envContents.includes('AUTH_SECRET='))
|
|
237
|
-
missingKeys.push('AUTH_SECRET');
|
|
238
|
-
if (!envContents.includes('ENV_SECRET_KEY='))
|
|
239
|
-
missingKeys.push('ENV_SECRET_KEY');
|
|
240
|
-
if (missingKeys.length > 0) {
|
|
241
|
-
console.log(chalk_1.default.yellow(` warning: .env is missing: ${missingKeys.join(', ')} — patching...`));
|
|
242
|
-
let patch = '';
|
|
243
|
-
if (!envContents.includes('REDIS_PASSWORD='))
|
|
244
|
-
patch += `\nREDIS_PASSWORD=${randomSecret().slice(0, 16)}\n`;
|
|
245
|
-
if (!envContents.includes('AUTH_SECRET='))
|
|
246
|
-
patch += `AUTH_SECRET=${randomSecret()}\n`;
|
|
247
|
-
if (!envContents.includes('ENV_SECRET_KEY='))
|
|
248
|
-
patch += `ENV_SECRET_KEY=${randomSecret()}\n`;
|
|
249
|
-
require('fs').appendFileSync(envPath, patch);
|
|
250
|
-
console.log(chalk_1.default.green(' ✓') + ' .env patched');
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
console.log(chalk_1.default.yellow(' note: .env already exists — skipping configuration'));
|
|
254
|
-
}
|
|
255
|
-
console.log('');
|
|
256
|
-
}
|
|
257
|
-
// ── Step 4: Build & start ─────────────────────────────────────────────────
|
|
258
|
-
let webReady = true;
|
|
259
|
-
if (!opts.skipStart) {
|
|
260
|
-
console.log(chalk_1.default.bold.hex('#D97757')(' [4/4]') + chalk_1.default.bold(' Building & starting services'));
|
|
261
|
-
console.log(chalk_1.default.gray(' This takes 3–5 minutes on first run while Docker builds images.'));
|
|
262
|
-
console.log('');
|
|
263
|
-
// Pre-flight: check Docker has enough disk space (need ~3GB)
|
|
264
|
-
try {
|
|
265
|
-
const dfOut = (0, child_process_1.execSync)('docker system df --format "{{.Size}}"', { encoding: 'utf-8' });
|
|
266
|
-
void dfOut; // just checking it runs without error
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
console.log(chalk_1.default.yellow(' note: Could not check Docker disk usage — continuing anyway'));
|
|
270
|
-
}
|
|
271
|
-
// Pre-flight: warn if low disk space on host
|
|
272
|
-
try {
|
|
273
|
-
const df = (0, child_process_1.execSync)('df -k . | tail -1', { encoding: 'utf-8' }).trim();
|
|
274
|
-
const available = parseInt(df.split(/\s+/)[3]);
|
|
275
|
-
if (!isNaN(available) && available < 3 * 1024 * 1024) {
|
|
276
|
-
console.log(chalk_1.default.yellow(` warning: less than 3GB disk space available. Build may fail.`));
|
|
277
|
-
console.log('');
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
catch { /* non-fatal */ }
|
|
281
|
-
// Remove stale Postgres volume if it exists — prevents password mismatch
|
|
282
|
-
// when re-running init with new credentials
|
|
283
|
-
try {
|
|
284
|
-
(0, child_process_1.execSync)('docker compose down -v', { cwd: dir, stdio: 'ignore' });
|
|
285
|
-
}
|
|
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
|
|
290
|
-
try {
|
|
291
|
-
(0, child_process_1.execSync)('docker compose up -d', { cwd: dir, stdio: 'ignore' });
|
|
292
|
-
}
|
|
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: ' ' });
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
webReady = false;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
// ── Done ──────────────────────────────────────────────────────────────────
|
|
320
|
-
console.log('');
|
|
321
|
-
if (webReady) {
|
|
322
|
-
console.log(' ' + chalk_1.default.bgHex('#D97757').black.bold(' SlackHive is ready! '));
|
|
323
|
-
console.log('');
|
|
324
|
-
console.log(` ${chalk_1.default.bold('Open:')} ${chalk_1.default.cyan('http://localhost:3001')}`);
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
console.log(' ' + chalk_1.default.bold('Setup complete!'));
|
|
328
|
-
console.log('');
|
|
329
|
-
console.log(chalk_1.default.gray(' Services are still starting. Once ready:'));
|
|
330
|
-
console.log(` ${chalk_1.default.bold('Run:')} ${chalk_1.default.cyan('slackhive start')}`);
|
|
331
|
-
console.log(` ${chalk_1.default.bold('Open:')} ${chalk_1.default.cyan('http://localhost:3001')}`);
|
|
332
|
-
}
|
|
333
|
-
console.log(` ${chalk_1.default.bold('Dir:')} ${chalk_1.default.gray(dir)}`);
|
|
334
|
-
console.log('');
|
|
335
|
-
console.log(chalk_1.default.gray(' Useful commands:'));
|
|
336
|
-
console.log(chalk_1.default.gray(' slackhive start — Start services'));
|
|
337
|
-
console.log(chalk_1.default.gray(' slackhive stop — Stop services'));
|
|
338
|
-
console.log(chalk_1.default.gray(' slackhive status — Show container status'));
|
|
339
|
-
console.log(chalk_1.default.gray(' slackhive logs — Tail runner logs'));
|
|
340
|
-
console.log(chalk_1.default.gray(' slackhive update — Pull latest & rebuild'));
|
|
341
|
-
console.log('');
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Runs `docker compose up -d --build`.
|
|
345
|
-
* Shows a single updating spinner line with the current build step.
|
|
346
|
-
* Docker's raw progress output is suppressed to keep the terminal clean.
|
|
347
|
-
*/
|
|
348
|
-
function runDockerBuild(cwd, displayDir) {
|
|
349
|
-
return new Promise((resolve) => {
|
|
350
|
-
const proc = (0, child_process_1.spawn)('docker', ['compose', '--progress', 'plain', 'up', '-d', '--build'], {
|
|
351
|
-
cwd,
|
|
352
|
-
env: { ...process.env },
|
|
353
|
-
});
|
|
354
|
-
const startTime = Date.now();
|
|
355
|
-
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
356
|
-
let frameIdx = 0;
|
|
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));
|
|
388
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
389
|
-
const phaseName = currentPhase < phases.length ? phases[currentPhase].name : 'Finishing';
|
|
390
|
-
const frame = frames[frameIdx++ % frames.length];
|
|
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()}`);
|
|
396
|
-
}, 80);
|
|
397
|
-
let buf = '';
|
|
398
|
-
const errorLines = [];
|
|
399
|
-
const onData = (chunk) => {
|
|
400
|
-
buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
401
|
-
const lines = buf.split('\n');
|
|
402
|
-
buf = lines.pop() ?? '';
|
|
403
|
-
for (const raw of lines) {
|
|
404
|
-
const line = raw.trim();
|
|
405
|
-
if (!line)
|
|
406
|
-
continue;
|
|
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
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (/error/i.test(line))
|
|
424
|
-
errorLines.push(line);
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
proc.stdout.on('data', onData);
|
|
428
|
-
proc.stderr.on('data', onData);
|
|
429
|
-
proc.on('close', (code) => {
|
|
430
|
-
clearInterval(spinnerInterval);
|
|
431
|
-
process.stdout.write('\r\x1b[K');
|
|
432
|
-
if (code === 0) {
|
|
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);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
console.log(' ' + chalk_1.default.red('✗') + ' Failed to start services');
|
|
447
|
-
console.log('');
|
|
448
|
-
const allErrors = errorLines.join('\n').toLowerCase();
|
|
449
|
-
if (allErrors.includes('no space left') || allErrors.includes('disk full')) {
|
|
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'));
|
|
452
|
-
}
|
|
453
|
-
else if (allErrors.includes('port is already allocated') || allErrors.includes('address already in use')) {
|
|
454
|
-
const portMatch = /bind for .+:(\d+)/.exec(allErrors);
|
|
455
|
-
const port = portMatch ? portMatch[1] : 'a required port';
|
|
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`));
|
|
458
|
-
}
|
|
459
|
-
else if (allErrors.includes('permission denied') || allErrors.includes('unauthorized')) {
|
|
460
|
-
console.log(chalk_1.default.yellow(' Cause: Docker permission denied — is Docker Desktop running?'));
|
|
461
|
-
}
|
|
462
|
-
else if (allErrors.includes('memory') || allErrors.includes('oom')) {
|
|
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.'));
|
|
468
|
-
}
|
|
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)));
|
|
472
|
-
}
|
|
473
|
-
console.log('');
|
|
474
|
-
console.log(chalk_1.default.gray(` To retry: cd ${displayDir} && docker compose up -d --build`));
|
|
475
|
-
resolve(false);
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
function randomSecret() {
|
|
480
|
-
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
481
|
-
let result = '';
|
|
482
|
-
for (let i = 0; i < 32; i++)
|
|
483
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
484
|
-
return result;
|
|
485
|
-
}
|
|
486
|
-
function sleep(ms) {
|
|
487
|
-
return new Promise(r => setTimeout(r, ms));
|
|
488
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Management commands — start, stop, status, logs, update.
|
|
3
|
-
*
|
|
4
|
-
* All commands look for docker-compose.yml in the current directory
|
|
5
|
-
* or the `slackhive` subdirectory.
|
|
6
|
-
*
|
|
7
|
-
* @module cli/commands/manage
|
|
8
|
-
*/
|
|
9
|
-
/**
|
|
10
|
-
* Start all SlackHive services.
|
|
11
|
-
*/
|
|
12
|
-
export declare function start(): Promise<void>;
|
|
13
|
-
/**
|
|
14
|
-
* Stop all SlackHive services.
|
|
15
|
-
*/
|
|
16
|
-
export declare function stop(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Show running SlackHive containers.
|
|
19
|
-
*/
|
|
20
|
-
export declare function status(): Promise<void>;
|
|
21
|
-
/**
|
|
22
|
-
* Tail runner service logs.
|
|
23
|
-
*/
|
|
24
|
-
export declare function logs(opts: {
|
|
25
|
-
follow?: boolean;
|
|
26
|
-
}): Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
* Pull latest changes and rebuild.
|
|
29
|
-
*/
|
|
30
|
-
export declare function update(): Promise<void>;
|