@justin0713/opspilot 1.0.4 → 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 +87 -20
  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,6 +101,17 @@ 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
 
@@ -115,7 +129,7 @@ function cmdStart(flags) {
115
129
  );
116
130
  }
117
131
 
118
- // Persist for next run
132
+ // Persist token for next run (never persists password)
119
133
  saveConfig({ token, cloudUrl: url || 'https://teams.codetop.net', port, dataDir: data });
120
134
 
121
135
  // Stop existing container if running
@@ -132,8 +146,27 @@ function cmdStart(flags) {
132
146
  log(`Pulling ${IMAGE} ...`);
133
147
  run(`docker pull ${IMAGE}`);
134
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
+
135
169
  // Volume arg: named volume or host path
136
- const isAbsolute = path.isAbsolute(data);
137
170
  const volArg = `${data}:/app/data`;
138
171
 
139
172
  log(`Starting OpsPilot Local on port ${port} ...`);
@@ -144,19 +177,44 @@ function cmdStart(flags) {
144
177
  `-e OPSPILOT_CLOUD_URL=${url || 'https://teams.codetop.net'} ` +
145
178
  `-e OPSPILOT_CLOUD_TOKEN=${token} ` +
146
179
  `-e OPSPILOT_PORT=5000 ` +
180
+ setupMount +
147
181
  `-v ${volArg} ` +
148
182
  `--restart unless-stopped ` +
149
183
  `${IMAGE}`
150
184
  );
151
185
 
186
+ // 4. Delete the temp setup token from host immediately after container starts
187
+ try { fs.unlinkSync(SETUP_TOKEN_FILE); } catch (_) {}
188
+
152
189
  console.log('');
153
190
  ok(`OpsPilot Local is running!`);
154
- console.log(`${BOLD} URL :${RESET} http://localhost:${port}`);
155
- console.log(`${BOLD} Data:${RESET} ${isAbsolute ? data : `docker volume '${data}'`}`);
191
+ console.log(`${BOLD} URL :${RESET} http://localhost:${port}`);
192
+ console.log(`${BOLD} Username :${RESET} admin`);
193
+ if (isFirstRun) {
194
+ console.log(`${BOLD} Password :${RESET} run ${CYAN}opspilot password${RESET} to reveal`);
195
+ }
196
+ console.log(`${BOLD} Data :${RESET} ${path.isAbsolute(data) ? data : `docker volume '${data}'`}`);
156
197
  console.log('');
157
- console.log(` ${CYAN}opspilot logs -f${RESET} — tail live logs`);
158
- console.log(` ${CYAN}opspilot stop${RESET} stop the server`);
159
- 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}`);
160
218
  }
161
219
 
162
220
  function cmdStop() {
@@ -226,18 +284,19 @@ ${BOLD}Usage:${RESET}
226
284
  opspilot <command> [options]
227
285
 
228
286
  ${BOLD}Commands:${RESET}
229
- ${CYAN}start${RESET} Pull image and start the container
230
- ${CYAN}stop${RESET} Stop and remove the container
231
- ${CYAN}logs${RESET} Show container logs (-f to follow)
232
- ${CYAN}update${RESET} Pull latest image and restart
233
- ${CYAN}status${RESET} Show container status
234
- ${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)
235
294
 
236
295
  ${BOLD}Start options:${RESET}
237
- --token <token> License token from OpsPilot Cloud ${YELLOW}(required)${RESET}
238
- --cloud-url <url> Cloud server URL [default: https://teams.codetop.net]
239
- --port <port> Local port [default: 5000]
240
- --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]
241
300
 
242
301
  ${BOLD}Examples:${RESET}
243
302
  npx @justin0713/opspilot start --token eyJ...
@@ -270,8 +329,16 @@ switch (cmd) {
270
329
  cmdStart(Object.assign({}, cfg, parsed.flags));
271
330
  })();
272
331
  break;
273
- case 'status': cmdStatus(); break;
274
- 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;
275
342
  case undefined:
276
343
  case 'help':
277
344
  case '--help':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justin0713/opspilot",
3
- "version": "1.0.4",
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",