@justin0713/opspilot 1.0.5 → 1.0.6
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 +89 -34
- 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';
|
|
@@ -98,15 +101,25 @@ function parseArgs(argv) {
|
|
|
98
101
|
|
|
99
102
|
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
100
103
|
|
|
104
|
+
function generatePassword() {
|
|
105
|
+
// 18 random bytes → 24-char base64url (no shell-special chars)
|
|
106
|
+
return crypto.randomBytes(18).toString('base64url');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeCredentials(password) {
|
|
110
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
111
|
+
fs.writeFileSync(CREDENTIALS_FILE, `username: admin\npassword: ${password}\n`);
|
|
112
|
+
try { fs.chmodSync(CREDENTIALS_FILE, 0o600); } catch (_) {} // best-effort on Windows
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
function cmdStart(flags) {
|
|
102
116
|
checkDocker();
|
|
103
117
|
|
|
104
|
-
const cfg
|
|
105
|
-
const token
|
|
106
|
-
const url
|
|
107
|
-
const port
|
|
108
|
-
const data
|
|
109
|
-
const adminPass = flags['admin-password'] || '';
|
|
118
|
+
const cfg = loadConfig();
|
|
119
|
+
const token = flags.token || cfg.token || '';
|
|
120
|
+
const url = flags['cloud-url'] || cfg.cloudUrl || '';
|
|
121
|
+
const port = flags.port || cfg.port || '5000';
|
|
122
|
+
const data = flags['data-dir'] || cfg.dataDir || 'opspilot-data';
|
|
110
123
|
|
|
111
124
|
if (!token) {
|
|
112
125
|
die(
|
|
@@ -116,7 +129,7 @@ function cmdStart(flags) {
|
|
|
116
129
|
);
|
|
117
130
|
}
|
|
118
131
|
|
|
119
|
-
// Persist for next run
|
|
132
|
+
// Persist token for next run (never persists password)
|
|
120
133
|
saveConfig({ token, cloudUrl: url || 'https://teams.codetop.net', port, dataDir: data });
|
|
121
134
|
|
|
122
135
|
// Stop existing container if running
|
|
@@ -133,13 +146,29 @@ function cmdStart(flags) {
|
|
|
133
146
|
log(`Pulling ${IMAGE} ...`);
|
|
134
147
|
run(`docker pull ${IMAGE}`);
|
|
135
148
|
|
|
149
|
+
// ── Secure first-run password delivery ────────────────────────────────────
|
|
150
|
+
// Detect first run: credentials file doesn't exist yet
|
|
151
|
+
const isFirstRun = !fs.existsSync(CREDENTIALS_FILE);
|
|
152
|
+
let setupMount = '';
|
|
153
|
+
|
|
154
|
+
if (isFirstRun) {
|
|
155
|
+
const password = generatePassword();
|
|
156
|
+
|
|
157
|
+
// 1. Write to host temp file (chmod 600) — never passed as CLI arg or env var
|
|
158
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
159
|
+
fs.writeFileSync(SETUP_TOKEN_FILE, password);
|
|
160
|
+
try { fs.chmodSync(SETUP_TOKEN_FILE, 0o600); } catch (_) {}
|
|
161
|
+
|
|
162
|
+
// 2. Save to credentials file (chmod 600) for `opspilot password` command
|
|
163
|
+
writeCredentials(password);
|
|
164
|
+
|
|
165
|
+
// 3. Mount into container read-only — entrypoint reads & deletes it
|
|
166
|
+
setupMount = `-v "${SETUP_TOKEN_FILE}:/app/.setup_token:ro" `;
|
|
167
|
+
}
|
|
168
|
+
|
|
136
169
|
// Volume arg: named volume or host path
|
|
137
|
-
const isAbsolute = path.isAbsolute(data);
|
|
138
170
|
const volArg = `${data}:/app/data`;
|
|
139
171
|
|
|
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
172
|
log(`Starting OpsPilot Local on port ${port} ...`);
|
|
144
173
|
run(
|
|
145
174
|
`docker run -d ` +
|
|
@@ -148,26 +177,44 @@ function cmdStart(flags) {
|
|
|
148
177
|
`-e OPSPILOT_CLOUD_URL=${url || 'https://teams.codetop.net'} ` +
|
|
149
178
|
`-e OPSPILOT_CLOUD_TOKEN=${token} ` +
|
|
150
179
|
`-e OPSPILOT_PORT=5000 ` +
|
|
151
|
-
|
|
180
|
+
setupMount +
|
|
152
181
|
`-v ${volArg} ` +
|
|
153
182
|
`--restart unless-stopped ` +
|
|
154
183
|
`${IMAGE}`
|
|
155
184
|
);
|
|
156
185
|
|
|
186
|
+
// 4. Delete the temp setup token from host immediately after container starts
|
|
187
|
+
try { fs.unlinkSync(SETUP_TOKEN_FILE); } catch (_) {}
|
|
188
|
+
|
|
157
189
|
console.log('');
|
|
158
190
|
ok(`OpsPilot Local is running!`);
|
|
159
191
|
console.log(`${BOLD} URL :${RESET} http://localhost:${port}`);
|
|
160
192
|
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}`);
|
|
193
|
+
if (isFirstRun) {
|
|
194
|
+
console.log(`${BOLD} Password :${RESET} run ${CYAN}opspilot password${RESET} to reveal`);
|
|
165
195
|
}
|
|
166
|
-
console.log(`${BOLD} Data :${RESET} ${isAbsolute ? data : `docker volume '${data}'`}`);
|
|
196
|
+
console.log(`${BOLD} Data :${RESET} ${path.isAbsolute(data) ? data : `docker volume '${data}'`}`);
|
|
167
197
|
console.log('');
|
|
168
|
-
console.log(` ${CYAN}opspilot
|
|
169
|
-
console.log(` ${CYAN}opspilot
|
|
170
|
-
console.log(` ${CYAN}opspilot
|
|
198
|
+
console.log(` ${CYAN}opspilot password${RESET} — show first-run admin password`);
|
|
199
|
+
console.log(` ${CYAN}opspilot logs -f${RESET} — tail live logs`);
|
|
200
|
+
console.log(` ${CYAN}opspilot stop${RESET} — stop the server`);
|
|
201
|
+
console.log(` ${CYAN}opspilot update${RESET} — upgrade to latest`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function cmdPassword() {
|
|
205
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
206
|
+
warn('No credentials file found. Already cleared, or this is not a first-run install.');
|
|
207
|
+
warn(`File would be at: ${CREDENTIALS_FILE}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const content = fs.readFileSync(CREDENTIALS_FILE, 'utf8').trim();
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(`${BOLD}First-run admin credentials${RESET} (${CREDENTIALS_FILE}):`);
|
|
213
|
+
console.log('');
|
|
214
|
+
content.split('\n').forEach(line => console.log(` ${line}`));
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(`${YELLOW}Change your password after first login, then run:${RESET}`);
|
|
217
|
+
console.log(` ${CYAN}opspilot password --clear${RESET}`);
|
|
171
218
|
}
|
|
172
219
|
|
|
173
220
|
function cmdStop() {
|
|
@@ -237,19 +284,19 @@ ${BOLD}Usage:${RESET}
|
|
|
237
284
|
opspilot <command> [options]
|
|
238
285
|
|
|
239
286
|
${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}
|
|
287
|
+
${CYAN}start${RESET} Pull image and start the container
|
|
288
|
+
${CYAN}stop${RESET} Stop and remove the container
|
|
289
|
+
${CYAN}logs${RESET} Show container logs (-f to follow)
|
|
290
|
+
${CYAN}update${RESET} Pull latest image and restart
|
|
291
|
+
${CYAN}status${RESET} Show container status
|
|
292
|
+
${CYAN}config${RESET} Show saved local config
|
|
293
|
+
${CYAN}password${RESET} Show first-run admin password (--clear to delete after reading)
|
|
246
294
|
|
|
247
295
|
${BOLD}Start options:${RESET}
|
|
248
|
-
--token
|
|
249
|
-
--
|
|
250
|
-
--
|
|
251
|
-
--
|
|
252
|
-
--data-dir <path> Host data path or Docker volume name [default: opspilot-data]
|
|
296
|
+
--token <token> License token from OpsPilot Cloud ${YELLOW}(required)${RESET}
|
|
297
|
+
--cloud-url <url> Cloud server URL [default: https://teams.codetop.net]
|
|
298
|
+
--port <port> Local port [default: 5000]
|
|
299
|
+
--data-dir <path> Host data path or Docker volume name [default: opspilot-data]
|
|
253
300
|
|
|
254
301
|
${BOLD}Examples:${RESET}
|
|
255
302
|
npx @justin0713/opspilot start --token eyJ...
|
|
@@ -282,8 +329,16 @@ switch (cmd) {
|
|
|
282
329
|
cmdStart(Object.assign({}, cfg, parsed.flags));
|
|
283
330
|
})();
|
|
284
331
|
break;
|
|
285
|
-
case 'status':
|
|
286
|
-
case 'config':
|
|
332
|
+
case 'status': cmdStatus(); break;
|
|
333
|
+
case 'config': cmdConfig(); break;
|
|
334
|
+
case 'password':
|
|
335
|
+
if (parsed.flags.clear) {
|
|
336
|
+
try { fs.unlinkSync(CREDENTIALS_FILE); ok('Credentials file cleared.'); }
|
|
337
|
+
catch (_) { warn('No credentials file to clear.'); }
|
|
338
|
+
} else {
|
|
339
|
+
cmdPassword();
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
287
342
|
case undefined:
|
|
288
343
|
case 'help':
|
|
289
344
|
case '--help':
|
package/package.json
CHANGED