@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.
Files changed (2) hide show
  1. package/bin/opspilot.js +89 -34
  2. 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 = path.join(CONFIG_DIR, 'config.json');
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 = loadConfig();
105
- const token = flags.token || cfg.token || '';
106
- const url = flags['cloud-url'] || cfg.cloudUrl || '';
107
- const port = flags.port || cfg.port || '5000';
108
- const data = flags['data-dir'] || cfg.dataDir || 'opspilot-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
- adminEnv +
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 (adminPass) {
162
- console.log(`${BOLD} Password :${RESET} ${adminPass} ${YELLOW} change this after first login${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 logs -f${RESET} — tail live logs`);
169
- console.log(` ${CYAN}opspilot stop${RESET} stop the server`);
170
- console.log(` ${CYAN}opspilot update${RESET} upgrade to latest`);
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} Pull image and start the container
241
- ${CYAN}stop${RESET} Stop and remove the container
242
- ${CYAN}logs${RESET} Show container logs (-f to follow)
243
- ${CYAN}update${RESET} Pull latest image and restart
244
- ${CYAN}status${RESET} Show container status
245
- ${CYAN}config${RESET} Show saved local config
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 <token> License token from OpsPilot Cloud ${YELLOW}(required)${RESET}
249
- --admin-password <password> Set admin password on first run ${YELLOW}(recommended)${RESET}
250
- --cloud-url <url> Cloud server URL [default: https://teams.codetop.net]
251
- --port <port> Local port [default: 5000]
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': cmdStatus(); break;
286
- case 'config': cmdConfig(); break;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justin0713/opspilot",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "CLI installer for OpsPilot Local — self-hosted SSH operations platform",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/Albert0977/ShellShare#readme",