@justin0713/opspilot 1.0.7 → 1.0.9

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 +120 -1
  2. package/package.json +1 -1
package/bin/opspilot.js CHANGED
@@ -340,6 +340,121 @@ function cmdConfig() {
340
340
  console.log(JSON.stringify(safe, null, 2));
341
341
  }
342
342
 
343
+ function cmdUninstall(flags) {
344
+ const keepData = flags['keep-data'] || false;
345
+ const cfg = loadConfig();
346
+ const data = cfg.dataDir || 'opspilot-data';
347
+
348
+ console.log('');
349
+ log('Uninstalling OpsPilot Local...');
350
+
351
+ // 1. Stop + remove container
352
+ if (tryRun('docker --version')) {
353
+ const status = tryRun(`docker inspect -f "{{.State.Status}}" ${CONTAINER} 2>/dev/null`);
354
+ if (status) {
355
+ log('Stopping container...');
356
+ try { run(`docker stop ${CONTAINER}`, { silent: true }); } catch (_) {}
357
+ try { run(`docker rm -f ${CONTAINER}`, { silent: true }); } catch (_) {}
358
+ ok('Container removed.');
359
+ }
360
+
361
+ // 2. Remove data volume (unless --keep-data)
362
+ if (!keepData) {
363
+ const volExists = tryRun(`docker volume inspect ${data} 2>/dev/null`);
364
+ if (volExists) {
365
+ log(`Removing data volume '${data}'...`);
366
+ try { run(`docker volume rm ${data}`, { silent: true }); ok('Data volume removed.'); }
367
+ catch (_) { warn(`Could not remove volume '${data}' — remove manually: docker volume rm ${data}`); }
368
+ }
369
+ } else {
370
+ warn(`Data volume '${data}' kept (--keep-data). Remove manually later: docker volume rm ${data}`);
371
+ }
372
+
373
+ // 3. Remove Docker image
374
+ const imgExists = tryRun(`docker image inspect ${IMAGE} 2>/dev/null`);
375
+ if (imgExists) {
376
+ log(`Removing image ${IMAGE}...`);
377
+ try { run(`docker rmi ${IMAGE}`, { silent: true }); ok('Image removed.'); } catch (_) {}
378
+ }
379
+ } else {
380
+ warn('Docker not available — skipping container/volume cleanup.');
381
+ }
382
+
383
+ // 4. Remove local CLI config + credentials
384
+ const toDelete = [CONFIG_FILE, CREDENTIALS_FILE, SETUP_TOKEN_FILE];
385
+ toDelete.forEach(f => {
386
+ try { if (fs.existsSync(f)) { fs.unlinkSync(f); } } catch (_) {}
387
+ });
388
+ // Remove config dir if empty
389
+ try {
390
+ const remaining = fs.readdirSync(CONFIG_DIR);
391
+ if (remaining.length === 0) fs.rmdirSync(CONFIG_DIR);
392
+ } catch (_) {}
393
+
394
+ ok('OpsPilot Local uninstalled.');
395
+ console.log('');
396
+ console.log(`To reinstall: ${CYAN}opspilot start --token <license-token>${RESET}`);
397
+ console.log(`To remove CLI: ${CYAN}npm uninstall -g @justin0713/opspilot${RESET}`);
398
+ console.log('');
399
+ }
400
+
401
+ function cmdResetAdmin() {
402
+ checkDocker();
403
+
404
+ const status = tryRun(`docker inspect -f "{{.State.Status}}" ${CONTAINER} 2>/dev/null`);
405
+ if (status !== 'running') {
406
+ die(
407
+ `Container '${CONTAINER}' is not running.\n\n` +
408
+ ` Start it first: opspilot start --token <token>\n` +
409
+ ` Then reset: opspilot reset-admin`
410
+ );
411
+ }
412
+
413
+ const newPassword = generatePassword();
414
+
415
+ // Python runs INSIDE the container — reads password from stdin (never exposed in ps/docker inspect)
416
+ const script = [
417
+ 'import sys, os',
418
+ 'sys.path.insert(0, "/app")',
419
+ 'from user_store import hash_password, load_users, save_users',
420
+ 'data_dir = os.environ.get("OPSPILOT_DATA_DIR", "/app/data")',
421
+ 'uf = data_dir + "/users.json"',
422
+ 'pw = sys.stdin.readline().strip()',
423
+ 'ud = load_users(uf)',
424
+ 'ul = ud.get("users", [])',
425
+ 'admin = next((u for u in ul if u.get("role") == "admin"), None)',
426
+ 'assert admin, "ERROR: no admin user found"',
427
+ 'salt, hashed = hash_password(pw)',
428
+ 'admin["password_salt"] = salt',
429
+ 'admin["password_hash"] = hashed',
430
+ 'save_users(uf, ud)',
431
+ 'print("OK:" + admin["username"])',
432
+ ].join('\n');
433
+
434
+ log('Resetting admin password inside container...');
435
+ const result = spawnSync(
436
+ 'docker', ['exec', '-i', CONTAINER, 'python3', '-c', script],
437
+ { input: newPassword, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
438
+ );
439
+
440
+ if (result.status !== 0 || (result.stdout || '').startsWith('ERROR') || result.error) {
441
+ const detail = (result.stderr || result.stdout || String(result.error)).trim();
442
+ die(`Reset failed: ${detail}\n\nCheck logs: opspilot logs`);
443
+ }
444
+
445
+ // Persist to credentials file (chmod 600) for `opspilot password`
446
+ writeCredentials(newPassword);
447
+
448
+ console.log('');
449
+ ok('Admin password reset successfully.');
450
+ console.log(`${BOLD} Username :${RESET} admin`);
451
+ console.log(`${BOLD} Password :${RESET} ${BOLD}${newPassword}${RESET}`);
452
+ console.log('');
453
+ console.log(`${YELLOW}Save this password — or retrieve it later with:${RESET}`);
454
+ console.log(` ${CYAN}opspilot password${RESET}`);
455
+ console.log('');
456
+ }
457
+
343
458
  function cmdInstallGuide() {
344
459
  const platform = os.platform();
345
460
  console.log(`
@@ -399,6 +514,8 @@ ${BOLD}Commands:${RESET}
399
514
  ${CYAN}status${RESET} Show container status
400
515
  ${CYAN}config${RESET} Show saved local config
401
516
  ${CYAN}password${RESET} Show first-run admin password (--clear to delete after reading)
517
+ ${CYAN}reset-admin${RESET} Reset admin password (official recovery if password is lost)
518
+ ${CYAN}uninstall${RESET} Remove container, image, data volume and local config
402
519
  ${CYAN}install-guide${RESET} Show Docker installation instructions for your platform
403
520
 
404
521
  ${BOLD}Start options:${RESET}
@@ -440,7 +557,9 @@ switch (cmd) {
440
557
  break;
441
558
  case 'status': cmdStatus(); break;
442
559
  case 'config': cmdConfig(); break;
443
- case 'install-guide': cmdInstallGuide(); break;
560
+ case 'reset-admin': cmdResetAdmin(); break;
561
+ case 'uninstall': cmdUninstall(parsed.flags); break;
562
+ case 'install-guide': cmdInstallGuide(); break;
444
563
  case 'password':
445
564
  if (parsed.flags.clear) {
446
565
  try { fs.unlinkSync(CREDENTIALS_FILE); ok('Credentials file cleared.'); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justin0713/opspilot",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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",