@justin0713/opspilot 1.0.5 → 1.0.7
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/bin/opspilot.js +202 -37
- package/package.json +1 -1
package/bin/opspilot.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const { execSync, spawnSync } = require('child_process');
|
|
19
|
+
const crypto = require('crypto');
|
|
19
20
|
const fs = require('fs');
|
|
20
21
|
const path = require('path');
|
|
21
22
|
const os = require('os');
|
|
@@ -27,7 +28,9 @@ const PKG_VERSION = require('../package.json').version;
|
|
|
27
28
|
const IMAGE = 'opspilot/local:latest';
|
|
28
29
|
const CONTAINER = 'opspilot';
|
|
29
30
|
const CONFIG_DIR = path.join(os.homedir(), '.opspilot');
|
|
30
|
-
const CONFIG_FILE
|
|
31
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
32
|
+
const CREDENTIALS_FILE = path.join(CONFIG_DIR, '.credentials'); // chmod 600
|
|
33
|
+
const SETUP_TOKEN_FILE = path.join(CONFIG_DIR, '.setup_token'); // temp, deleted after start
|
|
31
34
|
|
|
32
35
|
const CYAN = '\x1b[36m';
|
|
33
36
|
const GREEN = '\x1b[32m';
|
|
@@ -53,11 +56,75 @@ function tryRun(cmd) {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
function checkDocker() {
|
|
56
|
-
if (
|
|
59
|
+
if (tryRun('docker --version')) return; // already installed
|
|
60
|
+
|
|
61
|
+
const platform = os.platform();
|
|
62
|
+
warn('Docker is not installed. Attempting auto-install...');
|
|
63
|
+
|
|
64
|
+
if (platform === 'darwin') {
|
|
65
|
+
// macOS — prefer Colima (lightweight, no GUI, works on managed Macs)
|
|
66
|
+
if (tryRun('brew --version')) {
|
|
67
|
+
log('Installing Colima + Docker CLI via Homebrew (no Docker Desktop needed)...');
|
|
68
|
+
try {
|
|
69
|
+
run('brew install colima docker');
|
|
70
|
+
log('Starting Colima...');
|
|
71
|
+
run('colima start');
|
|
72
|
+
ok('Docker is ready via Colima.');
|
|
73
|
+
return;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
warn('Colima install failed. Trying Docker Desktop...');
|
|
76
|
+
try {
|
|
77
|
+
run('brew install --cask docker');
|
|
78
|
+
warn('Docker Desktop installed. Please open Docker Desktop from Applications,');
|
|
79
|
+
warn('wait for the whale icon in the menu bar, then run: opspilot start again.');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
} catch (_) {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// No Homebrew
|
|
57
85
|
die(
|
|
58
|
-
'Docker is
|
|
59
|
-
' Install
|
|
86
|
+
'Docker is required but could not be auto-installed.\n\n' +
|
|
87
|
+
' Option 1 — Install Homebrew first, then retry:\n' +
|
|
88
|
+
' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n\n' +
|
|
89
|
+
' Option 2 — Install OrbStack (lightweight Docker Desktop alternative):\n' +
|
|
90
|
+
' https://orbstack.dev\n\n' +
|
|
91
|
+
' Option 3 — Run without Docker (Python only):\n' +
|
|
92
|
+
' opspilot install-guide'
|
|
60
93
|
);
|
|
94
|
+
|
|
95
|
+
} else if (platform === 'linux') {
|
|
96
|
+
log('Installing Docker via get.docker.com...');
|
|
97
|
+
try {
|
|
98
|
+
run('curl -fsSL https://get.docker.com | sh');
|
|
99
|
+
const user = tryRun('whoami');
|
|
100
|
+
if (user && user !== 'root') {
|
|
101
|
+
try { run(`sudo usermod -aG docker ${user}`); } catch (_) {}
|
|
102
|
+
warn(`Added ${user} to docker group. You may need to log out and back in.`);
|
|
103
|
+
}
|
|
104
|
+
ok('Docker installed. Starting Docker service...');
|
|
105
|
+
try { run('sudo systemctl enable --now docker'); } catch (_) {}
|
|
106
|
+
return;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
die(
|
|
109
|
+
'Auto-install failed. Install Docker manually:\n' +
|
|
110
|
+
' https://docs.docker.com/engine/install/'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
} else {
|
|
115
|
+
// Windows or unknown
|
|
116
|
+
die(
|
|
117
|
+
'Docker is required.\n\n' +
|
|
118
|
+
' Download Docker Desktop for Windows:\n' +
|
|
119
|
+
' https://docs.docker.com/desktop/install/windows-install/\n\n' +
|
|
120
|
+
' After installing, open Docker Desktop and wait for it to start,\n' +
|
|
121
|
+
' then run: opspilot start'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Final check after auto-install
|
|
126
|
+
if (!tryRun('docker --version')) {
|
|
127
|
+
die('Docker still not available after install. Please reopen your terminal and try again.');
|
|
61
128
|
}
|
|
62
129
|
}
|
|
63
130
|
|
|
@@ -98,15 +165,25 @@ function parseArgs(argv) {
|
|
|
98
165
|
|
|
99
166
|
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
100
167
|
|
|
168
|
+
function generatePassword() {
|
|
169
|
+
// 18 random bytes → 24-char base64url (no shell-special chars)
|
|
170
|
+
return crypto.randomBytes(18).toString('base64url');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function writeCredentials(password) {
|
|
174
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
175
|
+
fs.writeFileSync(CREDENTIALS_FILE, `username: admin\npassword: ${password}\n`);
|
|
176
|
+
try { fs.chmodSync(CREDENTIALS_FILE, 0o600); } catch (_) {} // best-effort on Windows
|
|
177
|
+
}
|
|
178
|
+
|
|
101
179
|
function cmdStart(flags) {
|
|
102
180
|
checkDocker();
|
|
103
181
|
|
|
104
|
-
const cfg
|
|
105
|
-
const token
|
|
106
|
-
const url
|
|
107
|
-
const port
|
|
108
|
-
const data
|
|
109
|
-
const adminPass = flags['admin-password'] || '';
|
|
182
|
+
const cfg = loadConfig();
|
|
183
|
+
const token = flags.token || cfg.token || '';
|
|
184
|
+
const url = flags['cloud-url'] || cfg.cloudUrl || '';
|
|
185
|
+
const port = flags.port || cfg.port || '5000';
|
|
186
|
+
const data = flags['data-dir'] || cfg.dataDir || 'opspilot-data';
|
|
110
187
|
|
|
111
188
|
if (!token) {
|
|
112
189
|
die(
|
|
@@ -116,7 +193,7 @@ function cmdStart(flags) {
|
|
|
116
193
|
);
|
|
117
194
|
}
|
|
118
195
|
|
|
119
|
-
// Persist for next run
|
|
196
|
+
// Persist token for next run (never persists password)
|
|
120
197
|
saveConfig({ token, cloudUrl: url || 'https://teams.codetop.net', port, dataDir: data });
|
|
121
198
|
|
|
122
199
|
// Stop existing container if running
|
|
@@ -133,13 +210,29 @@ function cmdStart(flags) {
|
|
|
133
210
|
log(`Pulling ${IMAGE} ...`);
|
|
134
211
|
run(`docker pull ${IMAGE}`);
|
|
135
212
|
|
|
213
|
+
// ── Secure first-run password delivery ────────────────────────────────────
|
|
214
|
+
// Detect first run: credentials file doesn't exist yet
|
|
215
|
+
const isFirstRun = !fs.existsSync(CREDENTIALS_FILE);
|
|
216
|
+
let setupMount = '';
|
|
217
|
+
|
|
218
|
+
if (isFirstRun) {
|
|
219
|
+
const password = generatePassword();
|
|
220
|
+
|
|
221
|
+
// 1. Write to host temp file (chmod 600) — never passed as CLI arg or env var
|
|
222
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
223
|
+
fs.writeFileSync(SETUP_TOKEN_FILE, password);
|
|
224
|
+
try { fs.chmodSync(SETUP_TOKEN_FILE, 0o600); } catch (_) {}
|
|
225
|
+
|
|
226
|
+
// 2. Save to credentials file (chmod 600) for `opspilot password` command
|
|
227
|
+
writeCredentials(password);
|
|
228
|
+
|
|
229
|
+
// 3. Mount into container read-only — entrypoint reads & deletes it
|
|
230
|
+
setupMount = `-v "${SETUP_TOKEN_FILE}:/app/.setup_token:ro" `;
|
|
231
|
+
}
|
|
232
|
+
|
|
136
233
|
// Volume arg: named volume or host path
|
|
137
|
-
const isAbsolute = path.isAbsolute(data);
|
|
138
234
|
const volArg = `${data}:/app/data`;
|
|
139
235
|
|
|
140
|
-
// Admin password env (first-run only — ignored if users.json already exists in volume)
|
|
141
|
-
const adminEnv = adminPass ? `-e SHELLSHADOW_INITIAL_ADMIN_PASSWORD=${adminPass} ` : '';
|
|
142
|
-
|
|
143
236
|
log(`Starting OpsPilot Local on port ${port} ...`);
|
|
144
237
|
run(
|
|
145
238
|
`docker run -d ` +
|
|
@@ -148,26 +241,44 @@ function cmdStart(flags) {
|
|
|
148
241
|
`-e OPSPILOT_CLOUD_URL=${url || 'https://teams.codetop.net'} ` +
|
|
149
242
|
`-e OPSPILOT_CLOUD_TOKEN=${token} ` +
|
|
150
243
|
`-e OPSPILOT_PORT=5000 ` +
|
|
151
|
-
|
|
244
|
+
setupMount +
|
|
152
245
|
`-v ${volArg} ` +
|
|
153
246
|
`--restart unless-stopped ` +
|
|
154
247
|
`${IMAGE}`
|
|
155
248
|
);
|
|
156
249
|
|
|
250
|
+
// 4. Delete the temp setup token from host immediately after container starts
|
|
251
|
+
try { fs.unlinkSync(SETUP_TOKEN_FILE); } catch (_) {}
|
|
252
|
+
|
|
157
253
|
console.log('');
|
|
158
254
|
ok(`OpsPilot Local is running!`);
|
|
159
255
|
console.log(`${BOLD} URL :${RESET} http://localhost:${port}`);
|
|
160
256
|
console.log(`${BOLD} Username :${RESET} admin`);
|
|
161
|
-
if (
|
|
162
|
-
console.log(`${BOLD} Password :${RESET} ${
|
|
163
|
-
} else {
|
|
164
|
-
console.log(`${BOLD} Password :${RESET} ${YELLOW}check logs → opspilot logs | grep SECURITY${RESET}`);
|
|
257
|
+
if (isFirstRun) {
|
|
258
|
+
console.log(`${BOLD} Password :${RESET} run ${CYAN}opspilot password${RESET} to reveal`);
|
|
165
259
|
}
|
|
166
|
-
console.log(`${BOLD} Data :${RESET} ${isAbsolute ? data : `docker volume '${data}'`}`);
|
|
260
|
+
console.log(`${BOLD} Data :${RESET} ${path.isAbsolute(data) ? data : `docker volume '${data}'`}`);
|
|
167
261
|
console.log('');
|
|
168
|
-
console.log(` ${CYAN}opspilot
|
|
169
|
-
console.log(` ${CYAN}opspilot
|
|
170
|
-
console.log(` ${CYAN}opspilot
|
|
262
|
+
console.log(` ${CYAN}opspilot password${RESET} — show first-run admin password`);
|
|
263
|
+
console.log(` ${CYAN}opspilot logs -f${RESET} — tail live logs`);
|
|
264
|
+
console.log(` ${CYAN}opspilot stop${RESET} — stop the server`);
|
|
265
|
+
console.log(` ${CYAN}opspilot update${RESET} — upgrade to latest`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function cmdPassword() {
|
|
269
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
270
|
+
warn('No credentials file found. Already cleared, or this is not a first-run install.');
|
|
271
|
+
warn(`File would be at: ${CREDENTIALS_FILE}`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const content = fs.readFileSync(CREDENTIALS_FILE, 'utf8').trim();
|
|
275
|
+
console.log('');
|
|
276
|
+
console.log(`${BOLD}First-run admin credentials${RESET} (${CREDENTIALS_FILE}):`);
|
|
277
|
+
console.log('');
|
|
278
|
+
content.split('\n').forEach(line => console.log(` ${line}`));
|
|
279
|
+
console.log('');
|
|
280
|
+
console.log(`${YELLOW}Change your password after first login, then run:${RESET}`);
|
|
281
|
+
console.log(` ${CYAN}opspilot password --clear${RESET}`);
|
|
171
282
|
}
|
|
172
283
|
|
|
173
284
|
function cmdStop() {
|
|
@@ -229,6 +340,50 @@ function cmdConfig() {
|
|
|
229
340
|
console.log(JSON.stringify(safe, null, 2));
|
|
230
341
|
}
|
|
231
342
|
|
|
343
|
+
function cmdInstallGuide() {
|
|
344
|
+
const platform = os.platform();
|
|
345
|
+
console.log(`
|
|
346
|
+
${BOLD}${CYAN}OpsPilot Local — Docker Install Guide${RESET}
|
|
347
|
+
|
|
348
|
+
Docker is required to run OpsPilot Local.
|
|
349
|
+
The CLI will auto-install Docker when you run ${CYAN}opspilot start${RESET}.
|
|
350
|
+
|
|
351
|
+
${BOLD}Auto-install behaviour by platform:${RESET}
|
|
352
|
+
|
|
353
|
+
${BOLD}macOS${RESET}
|
|
354
|
+
• If Homebrew is installed → installs ${CYAN}Colima + Docker CLI${RESET} automatically
|
|
355
|
+
(lightweight, no Docker Desktop, works on managed/corporate Macs)
|
|
356
|
+
• If Homebrew is missing → installs Homebrew first:
|
|
357
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
358
|
+
Then retry: ${CYAN}opspilot start --token <token>${RESET}
|
|
359
|
+
|
|
360
|
+
${BOLD}Linux${RESET}
|
|
361
|
+
• Runs the official Docker install script automatically:
|
|
362
|
+
curl -fsSL https://get.docker.com | sh
|
|
363
|
+
|
|
364
|
+
${BOLD}Windows${RESET}
|
|
365
|
+
• Cannot auto-install. Download Docker Desktop:
|
|
366
|
+
https://docs.docker.com/desktop/install/windows-install/
|
|
367
|
+
|
|
368
|
+
${BOLD}Manual install options (macOS):${RESET}
|
|
369
|
+
|
|
370
|
+
1. ${CYAN}Colima${RESET} — lightweight, CLI-only, free (recommended for managed Macs)
|
|
371
|
+
brew install colima docker
|
|
372
|
+
colima start
|
|
373
|
+
|
|
374
|
+
2. ${CYAN}OrbStack${RESET} — lightweight Docker Desktop alternative
|
|
375
|
+
brew install orbstack
|
|
376
|
+
# or: https://orbstack.dev
|
|
377
|
+
|
|
378
|
+
3. ${CYAN}Docker Desktop${RESET} — official GUI app
|
|
379
|
+
brew install --cask docker
|
|
380
|
+
# or: https://www.docker.com/products/docker-desktop/
|
|
381
|
+
|
|
382
|
+
${BOLD}Current platform:${RESET} ${platform}
|
|
383
|
+
${BOLD}Docker status :${RESET} ${tryRun('docker --version') || RED + 'not found' + RESET}
|
|
384
|
+
`);
|
|
385
|
+
}
|
|
386
|
+
|
|
232
387
|
function printHelp() {
|
|
233
388
|
console.log(`
|
|
234
389
|
${BOLD}${CYAN}OpsPilot Local${RESET} — CLI v${PKG_VERSION}
|
|
@@ -237,19 +392,20 @@ ${BOLD}Usage:${RESET}
|
|
|
237
392
|
opspilot <command> [options]
|
|
238
393
|
|
|
239
394
|
${BOLD}Commands:${RESET}
|
|
240
|
-
${CYAN}start${RESET}
|
|
241
|
-
${CYAN}stop${RESET}
|
|
242
|
-
${CYAN}logs${RESET}
|
|
243
|
-
${CYAN}update${RESET}
|
|
244
|
-
${CYAN}status${RESET}
|
|
245
|
-
${CYAN}config${RESET}
|
|
395
|
+
${CYAN}start${RESET} Pull image and start the container ${YELLOW}(auto-installs Docker if missing)${RESET}
|
|
396
|
+
${CYAN}stop${RESET} Stop and remove the container
|
|
397
|
+
${CYAN}logs${RESET} Show container logs (-f to follow)
|
|
398
|
+
${CYAN}update${RESET} Pull latest image and restart
|
|
399
|
+
${CYAN}status${RESET} Show container status
|
|
400
|
+
${CYAN}config${RESET} Show saved local config
|
|
401
|
+
${CYAN}password${RESET} Show first-run admin password (--clear to delete after reading)
|
|
402
|
+
${CYAN}install-guide${RESET} Show Docker installation instructions for your platform
|
|
246
403
|
|
|
247
404
|
${BOLD}Start options:${RESET}
|
|
248
|
-
--token
|
|
249
|
-
--
|
|
250
|
-
--
|
|
251
|
-
--
|
|
252
|
-
--data-dir <path> Host data path or Docker volume name [default: opspilot-data]
|
|
405
|
+
--token <token> License token from OpsPilot Cloud ${YELLOW}(required)${RESET}
|
|
406
|
+
--cloud-url <url> Cloud server URL [default: https://teams.codetop.net]
|
|
407
|
+
--port <port> Local port [default: 5000]
|
|
408
|
+
--data-dir <path> Host data path or Docker volume name [default: opspilot-data]
|
|
253
409
|
|
|
254
410
|
${BOLD}Examples:${RESET}
|
|
255
411
|
npx @justin0713/opspilot start --token eyJ...
|
|
@@ -282,8 +438,17 @@ switch (cmd) {
|
|
|
282
438
|
cmdStart(Object.assign({}, cfg, parsed.flags));
|
|
283
439
|
})();
|
|
284
440
|
break;
|
|
285
|
-
case 'status':
|
|
286
|
-
case 'config':
|
|
441
|
+
case 'status': cmdStatus(); break;
|
|
442
|
+
case 'config': cmdConfig(); break;
|
|
443
|
+
case 'install-guide': cmdInstallGuide(); break;
|
|
444
|
+
case 'password':
|
|
445
|
+
if (parsed.flags.clear) {
|
|
446
|
+
try { fs.unlinkSync(CREDENTIALS_FILE); ok('Credentials file cleared.'); }
|
|
447
|
+
catch (_) { warn('No credentials file to clear.'); }
|
|
448
|
+
} else {
|
|
449
|
+
cmdPassword();
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
287
452
|
case undefined:
|
|
288
453
|
case 'help':
|
|
289
454
|
case '--help':
|
package/package.json
CHANGED