openship 0.1.10 → 0.2.0

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 (42) hide show
  1. package/dist/index.js +1132 -335
  2. package/dist/server/index.js +17089 -8373
  3. package/dist/server/lua/geo_country.lua +101 -0
  4. package/dist/server/lua/mgmt_api.lua +291 -0
  5. package/dist/server/lua/pipe_log.lua +71 -0
  6. package/dist/server/lua/pipe_stream.lua +90 -0
  7. package/dist/server/lua/rules_guard.lua +118 -0
  8. package/dist/server/lua/rules_lib.lua +176 -0
  9. package/dist/server/lua/site_logger.lua +182 -0
  10. package/dist/server/lua/webhook_handler.lua +126 -0
  11. package/dist/server/migrations/0035_thankful_morgan_stark.sql +1 -0
  12. package/dist/server/migrations/0036_gorgeous_scalphunter.sql +17 -0
  13. package/dist/server/migrations/0037_clever_makkari.sql +28 -0
  14. package/dist/server/migrations/0038_young_magneto.sql +1 -0
  15. package/dist/server/migrations/0039_secret_lady_ursula.sql +15 -0
  16. package/dist/server/migrations/0040_sharp_red_wolf.sql +16 -0
  17. package/dist/server/migrations/0041_round_spyke.sql +4 -0
  18. package/dist/server/migrations/0042_dapper_havok.sql +1 -0
  19. package/dist/server/migrations/0043_fast_sharon_ventura.sql +15 -0
  20. package/dist/server/migrations/0044_amused_shape.sql +8 -0
  21. package/dist/server/migrations/0045_amusing_korath.sql +1 -0
  22. package/dist/server/migrations/0046_white_harrier.sql +35 -0
  23. package/dist/server/migrations/0047_sloppy_strong_guy.sql +2 -0
  24. package/dist/server/migrations/0048_funny_emma_frost.sql +2 -0
  25. package/dist/server/migrations/0049_nappy_piledriver.sql +3 -0
  26. package/dist/server/migrations/meta/0035_snapshot.json +7610 -0
  27. package/dist/server/migrations/meta/0036_snapshot.json +7752 -0
  28. package/dist/server/migrations/meta/0037_snapshot.json +7978 -0
  29. package/dist/server/migrations/meta/0038_snapshot.json +7985 -0
  30. package/dist/server/migrations/meta/0039_snapshot.json +8090 -0
  31. package/dist/server/migrations/meta/0040_snapshot.json +8198 -0
  32. package/dist/server/migrations/meta/0041_snapshot.json +8211 -0
  33. package/dist/server/migrations/meta/0042_snapshot.json +8217 -0
  34. package/dist/server/migrations/meta/0043_snapshot.json +8316 -0
  35. package/dist/server/migrations/meta/0044_snapshot.json +8360 -0
  36. package/dist/server/migrations/meta/0045_snapshot.json +8367 -0
  37. package/dist/server/migrations/meta/0046_snapshot.json +8657 -0
  38. package/dist/server/migrations/meta/0047_snapshot.json +8669 -0
  39. package/dist/server/migrations/meta/0048_snapshot.json +8683 -0
  40. package/dist/server/migrations/meta/0049_snapshot.json +8702 -0
  41. package/dist/server/migrations/meta/_journal.json +105 -0
  42. package/package.json +4 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command24 } from "commander";
4
+ import { Command as Command25 } from "commander";
5
5
 
6
6
  // src/lib/output.ts
7
7
  import chalk from "chalk";
@@ -902,11 +902,13 @@ var PLANS = {
902
902
  free: {
903
903
  id: "free",
904
904
  name: "Free",
905
- description: "Get started with 500 credits per month",
905
+ description: "Get started for free",
906
906
  price: { monthly: 0, annual: 0 },
907
907
  stripePriceId: { monthly: null, annual: null },
908
- monthlyCredits: 5e5,
909
- // milli-credits
908
+ // Paid tiers below are `null` (price + credits) until Openship Cloud pricing
909
+ // is finalized — the UI renders "coming soon". Self-hosted is free and never
910
+ // surfaces any of these numbers.
911
+ monthlyCredits: null,
910
912
  oblienLimits: {
911
913
  max_workspaces: 1,
912
914
  max_vcpus: 2,
@@ -914,7 +916,6 @@ var PLANS = {
914
916
  max_disk_gb: 10
915
917
  },
916
918
  features: [
917
- "500 credits/mo",
918
919
  "1 workspace",
919
920
  "Community support"
920
921
  ],
@@ -925,13 +926,13 @@ var PLANS = {
925
926
  id: "pro",
926
927
  name: "Pro",
927
928
  description: "For solo builders shipping production workloads",
928
- price: { monthly: 2e3, annual: 19200 },
929
- // cents (20% annual discount)
929
+ price: { monthly: null, annual: null },
930
+ // coming soon
930
931
  stripePriceId: {
931
932
  monthly: process.env.STRIPE_PRICE_PRO_MONTHLY ?? "price_pro_monthly_placeholder",
932
933
  annual: process.env.STRIPE_PRICE_PRO_ANNUAL ?? "price_pro_annual_placeholder"
933
934
  },
934
- monthlyCredits: 5e6,
935
+ monthlyCredits: null,
935
936
  oblienLimits: {
936
937
  max_workspaces: 10,
937
938
  max_vcpus: 16,
@@ -939,7 +940,6 @@ var PLANS = {
939
940
  max_disk_gb: 100
940
941
  },
941
942
  features: [
942
- "5,000 credits/mo",
943
943
  "Up to 10 workspaces",
944
944
  "Email support"
945
945
  ],
@@ -950,12 +950,13 @@ var PLANS = {
950
950
  id: "team",
951
951
  name: "Team",
952
952
  description: "For teams collaborating on shared infra",
953
- price: { monthly: 5e3, annual: 48e3 },
953
+ price: { monthly: null, annual: null },
954
+ // coming soon
954
955
  stripePriceId: {
955
956
  monthly: process.env.STRIPE_PRICE_TEAM_MONTHLY ?? "price_team_monthly_placeholder",
956
957
  annual: process.env.STRIPE_PRICE_TEAM_ANNUAL ?? "price_team_annual_placeholder"
957
958
  },
958
- monthlyCredits: 25e6,
959
+ monthlyCredits: null,
959
960
  oblienLimits: {
960
961
  max_workspaces: 50,
961
962
  max_vcpus: 64,
@@ -963,7 +964,6 @@ var PLANS = {
963
964
  max_disk_gb: 500
964
965
  },
965
966
  features: [
966
- "25,000 credits/mo",
967
967
  "Up to 50 workspaces",
968
968
  "Team collaboration",
969
969
  "Priority email support"
@@ -1128,8 +1128,12 @@ var localhost = (port) => `http://localhost:${port}`;
1128
1128
  var LOCAL_WEB_URL = localhost(DEFAULT_PORT.web);
1129
1129
  var LOCAL_DASHBOARD_URL = localhost(DEFAULT_PORT.dashboard);
1130
1130
  var LOCAL_API_URL = localhost(DEFAULT_PORT.api);
1131
- var CLOUD_DASHBOARD_URL = "https://app.openship.io";
1132
- var CLOUD_API_URL = "https://api.openship.io";
1131
+ var envUrl = (key) => {
1132
+ const v = typeof process !== "undefined" ? process.env?.[key] : void 0;
1133
+ return v && v.trim() ? v.trim() : void 0;
1134
+ };
1135
+ var CLOUD_DASHBOARD_URL = envUrl("OPENSHIP_CLOUD_DASHBOARD_URL") ?? "https://app.openship.io";
1136
+ var CLOUD_API_URL = envUrl("OPENSHIP_CLOUD_API_URL") ?? "https://api.openship.io";
1133
1137
  var DASHBOARD_RUNTIME_TARGETS = {
1134
1138
  local: {
1135
1139
  dashboard: LOCAL_DASHBOARD_URL,
@@ -2230,8 +2234,8 @@ var loginCommand = new Command("login").description("Authenticate with a Persona
2230
2234
  chalk2.bold("\n Openship login\n") + chalk2.dim(" Create a Personal Access Token in Settings \u2192 Personal Access Tokens,\n") + chalk2.dim(" then paste it here.\n")
2231
2235
  );
2232
2236
  try {
2233
- const { default: open } = await import("open");
2234
- await open(settingsUrl);
2237
+ const { default: open2 } = await import("open");
2238
+ await open2(settingsUrl);
2235
2239
  } catch {
2236
2240
  }
2237
2241
  console.log(
@@ -2350,8 +2354,8 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
2350
2354
  }
2351
2355
  }
2352
2356
  try {
2353
- const { default: open } = await import("open");
2354
- await open(target);
2357
+ const { default: open2 } = await import("open");
2358
+ await open2(target);
2355
2359
  console.log(chalk4.dim(`
2356
2360
  Opening ${target}
2357
2361
  `));
@@ -2367,9 +2371,9 @@ import chalk5 from "chalk";
2367
2371
  import ora from "ora";
2368
2372
  import { spawn } from "child_process";
2369
2373
  import { randomBytes } from "crypto";
2370
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
2371
- import { homedir as homedir3 } from "os";
2372
- import { dirname as dirname2, join as join4 } from "path";
2374
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
2375
+ import { homedir as homedir4 } from "os";
2376
+ import { dirname as dirname2, join as join5 } from "path";
2373
2377
  import { fileURLToPath } from "url";
2374
2378
 
2375
2379
  // src/lib/dashboard.ts
@@ -2419,8 +2423,8 @@ async function downloadToFile(url, dest, onProgress) {
2419
2423
  } finally {
2420
2424
  file.end();
2421
2425
  }
2422
- await new Promise((resolve, reject2) => {
2423
- file.on("finish", () => resolve());
2426
+ await new Promise((resolve2, reject2) => {
2427
+ file.on("finish", () => resolve2());
2424
2428
  file.on("error", reject2);
2425
2429
  });
2426
2430
  return { sha256: hash.digest("hex"), size: received };
@@ -2474,6 +2478,17 @@ function assetName(tag) {
2474
2478
  return `openship-dashboard-${tag}.tar.gz`;
2475
2479
  }
2476
2480
  async function ensureDashboard(opts = {}) {
2481
+ const override = process.env.OPENSHIP_DASHBOARD_DIR?.trim();
2482
+ if (override) {
2483
+ const cwd2 = join3(override, "apps", "dashboard");
2484
+ const entry2 = join3(cwd2, "server.js");
2485
+ if (!existsSync2(entry2)) {
2486
+ throw new Error(
2487
+ `OPENSHIP_DASHBOARD_DIR=${override} but ${entry2} is missing \u2014 build the dashboard standalone first (see docs).`
2488
+ );
2489
+ }
2490
+ return { tag: "local", entry: entry2, cwd: cwd2 };
2491
+ }
2477
2492
  const tag = opts.tag ?? await resolveLatestTag();
2478
2493
  const dir = join3(DASHBOARD_CACHE, tag);
2479
2494
  const cwd = join3(dir, "apps", "dashboard");
@@ -2510,47 +2525,349 @@ async function ensureDashboard(opts = {}) {
2510
2525
  return { tag, entry, cwd };
2511
2526
  }
2512
2527
 
2528
+ // src/lib/service.ts
2529
+ import { spawnSync as spawnSync2 } from "child_process";
2530
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
2531
+ import { homedir as homedir3 } from "os";
2532
+ import { join as join4, resolve } from "path";
2533
+ var HOME = homedir3();
2534
+ var OS_DIR = join4(HOME, ".openship");
2535
+ var LOG_DIR = join4(OS_DIR, "logs");
2536
+ var MAC_LABEL = "io.openship.up";
2537
+ var MAC_PLIST = join4(HOME, "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
2538
+ var SYSTEMD_NAME = "openship";
2539
+ var WIN_TASK = "Openship";
2540
+ function selfInvocation() {
2541
+ const runtime = process.execPath;
2542
+ const entry = resolve(process.argv[1] ?? "");
2543
+ return { runtime, args: [entry] };
2544
+ }
2545
+ function upArgs(flags) {
2546
+ const a = ["up", "--foreground"];
2547
+ if (flags.port) a.push("--port", flags.port);
2548
+ if (flags.dataDir) a.push("--data-dir", flags.dataDir);
2549
+ if (flags.dashboardPort) a.push("--dashboard-port", flags.dashboardPort);
2550
+ if (flags.ui === false) a.push("--no-ui");
2551
+ if (flags.uiVersion) a.push("--ui-version", flags.uiVersion);
2552
+ if (flags.publicUrl) a.push("--public-url", flags.publicUrl);
2553
+ if (flags.trustProxy) a.push("--trust-proxy");
2554
+ if (flags.managedEdge) a.push("--managed-edge");
2555
+ if (flags.acmeEmail) a.push("--acme-email", flags.acmeEmail);
2556
+ return a;
2557
+ }
2558
+ function runArgv(flags) {
2559
+ const { runtime, args } = selfInvocation();
2560
+ return [runtime, ...args, ...upArgs(flags)];
2561
+ }
2562
+ function run(cmd, args) {
2563
+ const r = spawnSync2(cmd, args, { encoding: "utf8" });
2564
+ return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
2565
+ }
2566
+ function isRoot() {
2567
+ return typeof process.getuid === "function" && process.getuid() === 0;
2568
+ }
2569
+ function detectKind() {
2570
+ if (process.platform === "darwin") return "launchd";
2571
+ if (process.platform === "linux") {
2572
+ if (!hasSystemd()) return "unsupported";
2573
+ return isRoot() ? "systemd-system" : "systemd-user";
2574
+ }
2575
+ if (process.platform === "win32") return "schtasks";
2576
+ return "unsupported";
2577
+ }
2578
+ function hasSystemd() {
2579
+ return spawnSync2("sh", ["-c", "command -v systemctl"]).status === 0;
2580
+ }
2581
+ function xmlEscape(s) {
2582
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2583
+ }
2584
+ function serviceEnv() {
2585
+ const extra = {};
2586
+ const dashDir = process.env.OPENSHIP_DASHBOARD_DIR?.trim();
2587
+ if (dashDir) extra.OPENSHIP_DASHBOARD_DIR = dashDir;
2588
+ return extra;
2589
+ }
2590
+ function plist(flags) {
2591
+ const argv = runArgv(flags);
2592
+ const items = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
2593
+ const path2 = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", join4(HOME, ".bun/bin")].join(":");
2594
+ const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `<key>${xmlEscape(k)}</key><string>${xmlEscape(v)}</string>`).join("");
2595
+ return `<?xml version="1.0" encoding="UTF-8"?>
2596
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2597
+ <plist version="1.0">
2598
+ <dict>
2599
+ <key>Label</key><string>${MAC_LABEL}</string>
2600
+ <key>ProgramArguments</key>
2601
+ <array>
2602
+ ${items}
2603
+ </array>
2604
+ <key>RunAtLoad</key><true/>
2605
+ <key>KeepAlive</key><true/>
2606
+ <key>StandardOutPath</key><string>${join4(LOG_DIR, "up.log")}</string>
2607
+ <key>StandardErrorPath</key><string>${join4(LOG_DIR, "up.err.log")}</string>
2608
+ <key>EnvironmentVariables</key>
2609
+ <dict><key>PATH</key><string>${path2}</string>${extraEnv}</dict>
2610
+ </dict>
2611
+ </plist>
2612
+ `;
2613
+ }
2614
+ function systemdUnit(flags) {
2615
+ const argv = runArgv(flags);
2616
+ const execStart = argv.map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
2617
+ const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `Environment=${k}=${v}
2618
+ `).join("");
2619
+ return `[Unit]
2620
+ Description=Openship control plane
2621
+ After=network-online.target
2622
+ Wants=network-online.target
2623
+
2624
+ [Service]
2625
+ Type=simple
2626
+ ExecStart=${execStart}
2627
+ Restart=always
2628
+ RestartSec=2
2629
+ Environment=NODE_ENV=production
2630
+ ${extraEnv}
2631
+ [Install]
2632
+ WantedBy=default.target
2633
+ `;
2634
+ }
2635
+ function preview(flags) {
2636
+ const kind = detectKind();
2637
+ if (kind === "launchd") return { kind, path: MAC_PLIST, content: plist(flags) };
2638
+ if (kind === "systemd-user") return { kind, path: join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`), content: systemdUnit(flags) };
2639
+ if (kind === "systemd-system") return { kind, path: `/etc/systemd/system/${SYSTEMD_NAME}.service`, content: systemdUnit(flags) };
2640
+ if (kind === "schtasks") return { kind, path: WIN_TASK, content: runArgv(flags).join(" ") };
2641
+ return { kind, path: "", content: "" };
2642
+ }
2643
+ function installAndStart(flags) {
2644
+ mkdirSync4(LOG_DIR, { recursive: true });
2645
+ const kind = detectKind();
2646
+ if (kind === "launchd") {
2647
+ mkdirSync4(join4(HOME, "Library", "LaunchAgents"), { recursive: true });
2648
+ run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
2649
+ writeFileSync3(MAC_PLIST, plist(flags));
2650
+ const uid = String(process.getuid?.() ?? "");
2651
+ let r = run("launchctl", ["bootstrap", `gui/${uid}`, MAC_PLIST]);
2652
+ if (!r.ok) r = run("launchctl", ["load", "-w", MAC_PLIST]);
2653
+ if (!r.ok) throw new Error(`launchctl failed to load the agent: ${r.out}`);
2654
+ return { kind, detail: `launchd agent ${MAC_LABEL} (logs: ${LOG_DIR})` };
2655
+ }
2656
+ if (kind === "systemd-user" || kind === "systemd-system") {
2657
+ const sysArgs = kind === "systemd-user" ? ["--user"] : [];
2658
+ const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
2659
+ mkdirSync4(unitPath.slice(0, unitPath.lastIndexOf("/")), { recursive: true });
2660
+ writeFileSync3(unitPath, systemdUnit(flags));
2661
+ run("systemctl", [...sysArgs, "daemon-reload"]);
2662
+ const r = run("systemctl", [...sysArgs, "enable", "--now", SYSTEMD_NAME]);
2663
+ if (!r.ok) throw new Error(`systemctl enable failed: ${r.out}`);
2664
+ if (kind === "systemd-user") {
2665
+ run("loginctl", ["enable-linger", process.env.USER ?? ""]);
2666
+ }
2667
+ return { kind, detail: `systemd unit ${SYSTEMD_NAME} (${kind === "systemd-user" ? "--user" : "system"})` };
2668
+ }
2669
+ if (kind === "schtasks") {
2670
+ const tr = runArgv(flags).map((a) => `\\"${a}\\"`).join(" ");
2671
+ const r = run("schtasks", ["/Create", "/TN", WIN_TASK, "/SC", "ONLOGON", "/RL", "HIGHEST", "/TR", tr, "/F"]);
2672
+ if (!r.ok) throw new Error(`schtasks create failed: ${r.out}`);
2673
+ run("schtasks", ["/Run", "/TN", WIN_TASK]);
2674
+ return { kind, detail: `Scheduled Task ${WIN_TASK} (best-effort; runs at logon)` };
2675
+ }
2676
+ throw new Error(
2677
+ "No supported service manager found (need systemd on Linux). Run `openship up --foreground` instead, or use docker compose for always-on."
2678
+ );
2679
+ }
2680
+ function restart() {
2681
+ const kind = detectKind();
2682
+ if (kind === "launchd") {
2683
+ if (!existsSync3(MAC_PLIST)) return { restarted: false, detail: "no launchd agent installed" };
2684
+ const uid = String(process.getuid?.() ?? "");
2685
+ const r = run("launchctl", ["kickstart", "-k", `gui/${uid}/${MAC_LABEL}`]);
2686
+ return { restarted: r.ok, detail: r.ok ? `restarted ${MAC_LABEL}` : r.out };
2687
+ }
2688
+ if (kind === "systemd-user" || kind === "systemd-system") {
2689
+ const sysArgs = kind === "systemd-user" ? ["--user"] : [];
2690
+ const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
2691
+ if (!existsSync3(unitPath)) return { restarted: false, detail: "no systemd unit installed" };
2692
+ const r = run("systemctl", [...sysArgs, "restart", SYSTEMD_NAME]);
2693
+ return { restarted: r.ok, detail: r.ok ? `restarted ${SYSTEMD_NAME}` : r.out };
2694
+ }
2695
+ if (kind === "schtasks") {
2696
+ run("schtasks", ["/End", "/TN", WIN_TASK]);
2697
+ const r = run("schtasks", ["/Run", "/TN", WIN_TASK]);
2698
+ return { restarted: r.ok, detail: r.ok ? `restarted ${WIN_TASK}` : r.out };
2699
+ }
2700
+ return { restarted: false, detail: "no supported service manager" };
2701
+ }
2702
+ function stop() {
2703
+ const kind = detectKind();
2704
+ if (kind === "launchd") {
2705
+ run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
2706
+ if (existsSync3(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
2707
+ return { kind, detail: `launchd agent ${MAC_LABEL} stopped + removed` };
2708
+ }
2709
+ if (kind === "systemd-user" || kind === "systemd-system") {
2710
+ const sysArgs = kind === "systemd-user" ? ["--user"] : [];
2711
+ run("systemctl", [...sysArgs, "disable", "--now", SYSTEMD_NAME]);
2712
+ const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
2713
+ if (existsSync3(unitPath)) rmSync2(unitPath, { force: true });
2714
+ run("systemctl", [...sysArgs, "daemon-reload"]);
2715
+ return { kind, detail: `systemd unit ${SYSTEMD_NAME} stopped + disabled` };
2716
+ }
2717
+ if (kind === "schtasks") {
2718
+ run("schtasks", ["/End", "/TN", WIN_TASK]);
2719
+ run("schtasks", ["/Delete", "/TN", WIN_TASK, "/F"]);
2720
+ return { kind, detail: `Scheduled Task ${WIN_TASK} stopped + removed` };
2721
+ }
2722
+ return { kind, detail: "no supported service manager \u2014 nothing to stop" };
2723
+ }
2724
+
2513
2725
  // src/commands/up.ts
2726
+ function normalizeUrl(raw) {
2727
+ const value = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
2728
+ try {
2729
+ const u = new URL(value);
2730
+ return `${u.protocol}//${u.host}`;
2731
+ } catch {
2732
+ return null;
2733
+ }
2734
+ }
2735
+ function normalizePublicUrl(raw) {
2736
+ const url = normalizeUrl(raw);
2737
+ if (!url) {
2738
+ console.error(
2739
+ chalk5.red(`
2740
+ Invalid --public-url: ${raw}`) + chalk5.dim("\n Expected something like https://ops.example.com\n")
2741
+ );
2742
+ process.exit(1);
2743
+ }
2744
+ return url;
2745
+ }
2514
2746
  var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
2515
- var SERVER_DIR = join4(DIST_DIR, "server");
2516
- var OS_DIR = join4(homedir3(), ".openship");
2747
+ var SERVER_DIR = join5(DIST_DIR, "server");
2748
+ var OS_DIR2 = join5(homedir4(), ".openship");
2517
2749
  function ensureAuthSecret() {
2518
- const path2 = join4(OS_DIR, "auth-secret");
2519
- if (existsSync3(path2)) return readFileSync2(path2, "utf8").trim();
2520
- mkdirSync4(OS_DIR, { recursive: true, mode: 448 });
2750
+ const path2 = join5(OS_DIR2, "auth-secret");
2751
+ if (existsSync4(path2)) return readFileSync2(path2, "utf8").trim();
2752
+ mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
2521
2753
  const secret = randomBytes(32).toString("hex");
2522
- writeFileSync3(path2, secret, { mode: 384 });
2754
+ writeFileSync4(path2, secret, { mode: 384 });
2523
2755
  return secret;
2524
2756
  }
2525
- var upCommand = new Command4("up").description("Run the Openship control plane locally (bundled API + embedded database)").option("--port <port>", "API port to listen on", "4000").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port", "3001").option("--no-ui", "Run the API only \u2014 don't download/serve the dashboard").option("--ui-version <tag>", "Dashboard release tag to run (default: this CLI's version)").action(async (opts) => {
2526
- const serverEntry = join4(SERVER_DIR, "index.js");
2527
- if (!existsSync3(serverEntry)) {
2757
+ function ensureInternalToken() {
2758
+ const path2 = join5(OS_DIR2, "internal-token");
2759
+ if (existsSync4(path2)) return readFileSync2(path2, "utf8").trim();
2760
+ mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
2761
+ const token = randomBytes(32).toString("hex");
2762
+ writeFileSync4(path2, token, { mode: 384 });
2763
+ return token;
2764
+ }
2765
+ var upCommand = new Command4("up").description("Start Openship as a persistent service (boot + auto-restart); --foreground to run attached").option("--port <port>", "API port to listen on", "4000").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port", "3001").option("--no-ui", "Run the API only \u2014 don't download/serve the dashboard").option("--ui-version <tag>", "Dashboard release tag to run (default: this CLI's version)").option("-f, --foreground", "Run attached in this terminal instead of as a background service").option("--dry-run", "Print the service definition that would be installed, then exit").option(
2766
+ "--public-url <url>",
2767
+ "Serve remotely at this public URL (VPS): binds the dashboard to all interfaces, proxies the API same-origin, and requires login"
2768
+ ).option(
2769
+ "--trust-proxy",
2770
+ "Trust the X-Real-IP set by a reverse proxy in front (the proxy MUST overwrite X-Real-IP with the real client IP, e.g. `proxy_set_header X-Real-IP $remote_addr`, and the app port MUST be firewalled so only the proxy can reach it; enables per-client rate limiting)"
2771
+ ).option(
2772
+ "--managed-edge",
2773
+ "Managed edge: install OpenResty + a free Let's Encrypt cert on this box and route --public-url's domain to the dashboard (no reverse proxy needed)"
2774
+ ).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").action(async (opts) => {
2775
+ if (opts.foreground) return runForeground(opts);
2776
+ startService(opts);
2777
+ });
2778
+ function startService(opts, runOpts = {}) {
2779
+ const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
2780
+ const port = String(opts.port || "4000");
2781
+ const dashPort = String(opts.dashboardPort || "3001");
2782
+ const flags = {
2783
+ port: opts.port,
2784
+ dataDir: opts.dataDir,
2785
+ dashboardPort: opts.dashboardPort,
2786
+ ui: opts.ui,
2787
+ uiVersion: opts.uiVersion,
2788
+ publicUrl,
2789
+ trustProxy: opts.trustProxy || opts.managedEdge,
2790
+ // managed edge = OpenResty sets XFF
2791
+ managedEdge: opts.managedEdge,
2792
+ acmeEmail: opts.acmeEmail
2793
+ };
2794
+ if (opts.dryRun) {
2795
+ const p = preview(flags);
2796
+ console.log(
2797
+ chalk5.dim(`
2798
+ service manager: ${p.kind}
2799
+ path: ${p.path}
2800
+
2801
+ `) + p.content + "\n"
2802
+ );
2803
+ return { port, dashPort, publicUrl };
2804
+ }
2805
+ try {
2806
+ const res = installAndStart(flags);
2807
+ if (!runOpts.quiet) {
2808
+ const dashboardLine = publicUrl ? chalk5.dim(` Dashboard: ${publicUrl} (login required)
2809
+ `) : chalk5.dim(` Dashboard: http://localhost:${dashPort} (login required)
2810
+ `);
2811
+ console.log(
2812
+ chalk5.green("\n \u2714 Openship is running as a service.\n") + (opts.ui !== false ? dashboardLine : "") + (publicUrl ? chalk5.dim(" API is proxied through the dashboard (not exposed). Point your reverse proxy / DNS at the dashboard port.\n") : chalk5.dim(` API: http://localhost:${port}/api
2813
+ `)) + chalk5.dim(` ${res.detail}
2814
+ `) + chalk5.dim(" Starts on boot and auto-restarts. Stop with `openship stop`.\n")
2815
+ );
2816
+ }
2817
+ return { port, dashPort, publicUrl };
2818
+ } catch (e) {
2819
+ if (runOpts.quiet) throw e;
2820
+ console.error(
2821
+ chalk5.red(`
2822
+ Couldn't install the service: ${e.message}
2823
+ `) + chalk5.dim(" Run `openship up --foreground` to run it attached instead.\n")
2824
+ );
2825
+ process.exit(1);
2826
+ }
2827
+ }
2828
+ async function runForeground(opts) {
2829
+ const serverEntry = join5(SERVER_DIR, "index.js");
2830
+ if (!existsSync4(serverEntry)) {
2528
2831
  console.error(
2529
2832
  chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
2530
2833
  );
2531
2834
  process.exit(1);
2532
2835
  }
2533
2836
  const port = String(opts.port || "4000");
2534
- const dataDir = opts.dataDir || join4(OS_DIR, "data");
2535
- mkdirSync4(dataDir, { recursive: true });
2837
+ const dashPort = String(opts.dashboardPort || "3001");
2838
+ const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
2839
+ const managedEdge = Boolean(opts.managedEdge && publicUrl);
2840
+ const dataDir = opts.dataDir || join5(OS_DIR2, "data");
2841
+ mkdirSync5(dataDir, { recursive: true });
2536
2842
  const env = {
2537
2843
  ...process.env,
2538
2844
  PORT: port,
2539
2845
  NODE_ENV: "production",
2540
- // desktop mode → in-process job runner (no Redis) + loopback zero-auth,
2541
- // so a local single-user box needs no PAT over 127.0.0.1.
2846
+ // desktop mode → in-process job runner (no Redis).
2542
2847
  DEPLOY_MODE: "desktop",
2543
2848
  OPENSHIP_TARGET: "local",
2544
2849
  OPENSHIP_JOB_RUNNER: "in-process",
2545
- OPENSHIP_ALLOW_ZERO_AUTH: "true",
2546
2850
  PGLITE_DATA_DIR: dataDir,
2547
- OPENSHIP_MIGRATIONS_DIR: join4(SERVER_DIR, "migrations"),
2548
- OPENSHIP_PGLITE_ASSETS_DIR: join4(SERVER_DIR, "pglite"),
2851
+ OPENSHIP_MIGRATIONS_DIR: join5(SERVER_DIR, "migrations"),
2852
+ OPENSHIP_PGLITE_ASSETS_DIR: join5(SERVER_DIR, "pglite"),
2549
2853
  BETTER_AUTH_SECRET: ensureAuthSecret()
2550
2854
  };
2855
+ env.OPENSHIP_REQUIRE_AUTH = "true";
2856
+ env.INTERNAL_TOKEN = ensureInternalToken();
2857
+ env.OPENSHIP_API_HOST = "127.0.0.1";
2858
+ delete env.OPENSHIP_ALLOW_ZERO_AUTH;
2859
+ if (publicUrl) {
2860
+ env.OPENSHIP_PUBLIC_URL = publicUrl;
2861
+ }
2862
+ if (opts.trustProxy || managedEdge) env.TRUST_PROXY = "true";
2863
+ if (managedEdge) {
2864
+ env.OPENSHIP_MANAGED_EDGE = "true";
2865
+ env.OPENSHIP_DASHBOARD_PORT = dashPort;
2866
+ if (opts.acmeEmail) env.OPENSHIP_ACME_EMAIL = opts.acmeEmail;
2867
+ }
2551
2868
  delete env.DATABASE_URL;
2552
2869
  delete env.POSTGRES_URL;
2553
- const spinner2 = ora(`Starting Openship on http://localhost:${port} \u2026`).start();
2870
+ const spinner3 = ora(`Starting Openship on http://localhost:${port} \u2026`).start();
2554
2871
  const child = spawn(process.execPath, [serverEntry], { env, stdio: ["ignore", "pipe", "pipe"] });
2555
2872
  let buffered = "";
2556
2873
  const buffer = (d) => {
@@ -2560,7 +2877,7 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
2560
2877
  child.stderr.on("data", buffer);
2561
2878
  child.on("exit", (code) => {
2562
2879
  if (code && code !== 0) {
2563
- spinner2.fail(`Openship server exited (code ${code})`);
2880
+ spinner3.fail(`Openship server exited (code ${code})`);
2564
2881
  process.stderr.write(buffered.slice(-2e3));
2565
2882
  process.exit(code);
2566
2883
  }
@@ -2579,12 +2896,12 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
2579
2896
  }
2580
2897
  }
2581
2898
  if (!healthy) {
2582
- spinner2.fail("Openship did not become healthy in time");
2899
+ spinner3.fail("Openship did not become healthy in time");
2583
2900
  process.stderr.write(buffered.slice(-2e3));
2584
2901
  child.kill("SIGTERM");
2585
2902
  process.exit(1);
2586
2903
  }
2587
- spinner2.succeed(`Openship API running at http://localhost:${port}`);
2904
+ spinner3.succeed(`Openship API running at http://localhost:${port}`);
2588
2905
  const children = [child];
2589
2906
  const stopAll = () => {
2590
2907
  for (const c of children) {
@@ -2602,11 +2919,10 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
2602
2919
  };
2603
2920
  let dashboardUrl = null;
2604
2921
  if (opts.ui !== false) {
2605
- const dashPort = String(opts.dashboardPort || "3001");
2606
2922
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
2607
2923
  try {
2608
2924
  const bundle = await ensureDashboard({
2609
- tag: opts.uiVersion || `v${"0.1.10"}`,
2925
+ tag: opts.uiVersion || `v${"0.2.0"}`,
2610
2926
  onProgress: (received, total) => {
2611
2927
  if (total) {
2612
2928
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -2621,12 +2937,18 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
2621
2937
  NODE_ENV: "production",
2622
2938
  OPENSHIP_TARGET: "local",
2623
2939
  PORT: dashPort,
2624
- HOSTNAME: "127.0.0.1",
2625
- // The dashboard reads this (SSR) and mirrors it into the browser as
2626
- // window.__OPENSHIP_API_ORIGIN__ so both target our local API. It
2627
- // does NOT read API_INTERNAL_URL using that leaves it defaulting
2628
- // to :4000 (possibly a different instance).
2629
- OPENSHIP_LOCAL_API_URL: `http://127.0.0.1:${port}`
2940
+ // Reachable remotely when public; loopback-only otherwise. Under
2941
+ // managed edge the local OpenResty fronts the dashboard, so it stays
2942
+ // on loopback even though there's a public URL.
2943
+ HOSTNAME: publicUrl && !managedEdge ? "0.0.0.0" : "127.0.0.1",
2944
+ // The dashboard's same-origin proxy (NEXT_PUBLIC_API_PROXY, baked
2945
+ // into the release build) forwards /api/proxy/* to this address, so
2946
+ // the browser never needs to know where the API lives. Set in every
2947
+ // mode; loopback because the dashboard runs on the same box.
2948
+ INTERNAL_API_URL: `http://127.0.0.1:${port}`,
2949
+ // Public URL feeds the SSR proxy-origin resolver; local mode keeps
2950
+ // the window.__OPENSHIP_API_ORIGIN__ fallback for direct API calls.
2951
+ ...publicUrl ? { OPENSHIP_PUBLIC_URL: publicUrl } : { OPENSHIP_LOCAL_API_URL: `http://127.0.0.1:${port}` }
2630
2952
  },
2631
2953
  stdio: ["ignore", "pipe", "pipe"]
2632
2954
  });
@@ -2650,7 +2972,7 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
2650
2972
  }
2651
2973
  }
2652
2974
  if (dashUp) {
2653
- dashboardUrl = `http://localhost:${dashPort}`;
2975
+ dashboardUrl = publicUrl ?? `http://localhost:${dashPort}`;
2654
2976
  uiSpinner.succeed(`Dashboard running at ${dashboardUrl}`);
2655
2977
  dash.stdout.off("data", onDash);
2656
2978
  dash.stderr.off("data", onDash);
@@ -2669,12 +2991,20 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
2669
2991
  );
2670
2992
  }
2671
2993
  }
2672
- console.log(
2673
- chalk5.dim(` API: http://localhost:${port}/api
2674
- `) + (dashboardUrl ? chalk5.dim(` Dashboard: ${dashboardUrl}
2994
+ if (publicUrl) {
2995
+ console.log(
2996
+ (dashboardUrl ? chalk5.dim(` Dashboard: ${dashboardUrl} (login required)
2997
+ `) : "") + chalk5.dim(" API is proxied through the dashboard (bound to loopback, not exposed).\n") + chalk5.dim(` Data: ${dataDir}
2998
+ `) + (managedEdge ? chalk5.dim(" Managed edge (OpenResty + Let's Encrypt) fronts this box \u2014 point your domain's A record at this server's IP. Stop with Ctrl-C.\n") : chalk5.dim(" Point your reverse proxy / DNS at the dashboard port. Stop with Ctrl-C.\n"))
2999
+ );
3000
+ } else {
3001
+ console.log(
3002
+ chalk5.dim(` API: http://localhost:${port}/api
3003
+ `) + (dashboardUrl ? chalk5.dim(` Dashboard: ${dashboardUrl} (login required)
2675
3004
  `) : "") + chalk5.dim(` Data: ${dataDir}
2676
- `) + chalk5.dim(" Local access needs no token (loopback). Stop with Ctrl-C.\n")
2677
- );
3005
+ `) + chalk5.dim(" Log in with your admin account (run `openship` to create one). Stop with Ctrl-C.\n")
3006
+ );
3007
+ }
2678
3008
  child.stdout.off("data", buffer);
2679
3009
  child.stderr.off("data", buffer);
2680
3010
  child.stdout.on("data", (d) => process.stdout.write(d));
@@ -2685,19 +3015,35 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
2685
3015
  stopAll();
2686
3016
  process.exit(code ?? 0);
2687
3017
  });
3018
+ }
3019
+
3020
+ // src/commands/stop.ts
3021
+ import { Command as Command5 } from "commander";
3022
+ import chalk6 from "chalk";
3023
+ var stopCommand = new Command5("stop").description("Stop the Openship service (started by `openship up`) \u2014 it won't restart or return on reboot").action(() => {
3024
+ try {
3025
+ const res = stop();
3026
+ console.log(chalk6.green("\n \u2714 Openship stopped.\n") + chalk6.dim(` ${res.detail}
3027
+ `));
3028
+ } catch (e) {
3029
+ console.error(chalk6.red(`
3030
+ Couldn't stop the service: ${e.message}
3031
+ `));
3032
+ process.exit(1);
3033
+ }
2688
3034
  });
2689
3035
 
2690
3036
  // src/commands/init.ts
2691
- import { Command as Command5 } from "commander";
2692
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
3037
+ import { Command as Command6 } from "commander";
3038
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
2693
3039
  import { createInterface as createInterface2 } from "readline/promises";
2694
3040
  import { stdin as input2, stdout as output2 } from "process";
2695
- import { join as join5 } from "path";
2696
- var initCommand = new Command5("init").description("Link the current directory to an Openship project (.openship/project.json)").option("--project <id>", "Project id to link (skips the picker)").option("--environment <name>", "Default deploy environment", "production").option("--dir <path>", "Directory to initialize", process.cwd()).option("--force", "Overwrite an existing project link").option("-y, --yes", "Non-interactive: fail instead of prompting").action(async (opts) => {
3041
+ import { join as join6 } from "path";
3042
+ var initCommand = new Command6("init").description("Link the current directory to an Openship project (.openship/project.json)").option("--project <id>", "Project id to link (skips the picker)").option("--environment <name>", "Default deploy environment", "production").option("--dir <path>", "Directory to initialize", process.cwd()).option("--force", "Overwrite an existing project link").option("-y, --yes", "Non-interactive: fail instead of prompting").action(async (opts) => {
2697
3043
  const root = opts.dir || process.cwd();
2698
- const linkDir = join5(root, ".openship");
2699
- const linkPath = join5(linkDir, "project.json");
2700
- if (existsSync4(linkPath) && !opts.force) {
3044
+ const linkDir = join6(root, ".openship");
3045
+ const linkPath = join6(linkDir, "project.json");
3046
+ if (existsSync5(linkPath) && !opts.force) {
2701
3047
  err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
2702
3048
  process.exit(1);
2703
3049
  }
@@ -2750,8 +3096,8 @@ var initCommand = new Command5("init").description("Link the current directory t
2750
3096
  context: getActiveContext(),
2751
3097
  defaults: { environment: opts.environment || "production" }
2752
3098
  };
2753
- mkdirSync5(linkDir, { recursive: true });
2754
- writeFileSync4(linkPath, JSON.stringify(link, null, 2) + "\n");
3099
+ mkdirSync6(linkDir, { recursive: true });
3100
+ writeFileSync5(linkPath, JSON.stringify(link, null, 2) + "\n");
2755
3101
  if (isJsonMode()) {
2756
3102
  printJson({ path: linkPath, link });
2757
3103
  return;
@@ -2762,7 +3108,7 @@ var initCommand = new Command5("init").description("Link the current directory t
2762
3108
  });
2763
3109
 
2764
3110
  // src/commands/context.ts
2765
- import { Command as Command6 } from "commander";
3111
+ import { Command as Command7 } from "commander";
2766
3112
  function renderContexts() {
2767
3113
  const rows = listContexts().map((c) => ({
2768
3114
  current: c.current ? "*" : "",
@@ -2773,8 +3119,8 @@ function renderContexts() {
2773
3119
  }));
2774
3120
  printTable(rows, ["current", "name", "apiUrl", "dashboardUrl", "auth"]);
2775
3121
  }
2776
- var listCmd = new Command6("list").alias("ls").description("List configured contexts").action(renderContexts);
2777
- var useCmd = new Command6("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
3122
+ var listCmd = new Command7("list").alias("ls").description("List configured contexts").action(renderContexts);
3123
+ var useCmd = new Command7("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
2778
3124
  try {
2779
3125
  setActiveContext(name);
2780
3126
  ok(`
@@ -2785,7 +3131,7 @@ var useCmd = new Command6("use").description("Switch the active context").argume
2785
3131
  process.exit(1);
2786
3132
  }
2787
3133
  });
2788
- var addCmd = new Command6("add").description("Create or update a context's endpoints/token").argument("<name>", "Context name").option("--api-url <url>", "API base URL").option("--dashboard-url <url>", "Dashboard base URL").option("--token <token>", "Personal Access Token to store").option("--use", "Switch to this context after adding").action((name, opts) => {
3134
+ var addCmd = new Command7("add").description("Create or update a context's endpoints/token").argument("<name>", "Context name").option("--api-url <url>", "API base URL").option("--dashboard-url <url>", "Dashboard base URL").option("--token <token>", "Personal Access Token to store").option("--use", "Switch to this context after adding").action((name, opts) => {
2789
3135
  addContext(name, {
2790
3136
  apiUrl: opts.apiUrl,
2791
3137
  dashboardUrl: opts.dashboardUrl,
@@ -2796,7 +3142,7 @@ var addCmd = new Command6("add").description("Create or update a context's endpo
2796
3142
  Saved context "${name}"${opts.use ? " (now active)" : ""}.
2797
3143
  `);
2798
3144
  });
2799
- var rmCmd = new Command6("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
3145
+ var rmCmd = new Command7("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
2800
3146
  try {
2801
3147
  removeContext(name);
2802
3148
  ok(`
@@ -2807,15 +3153,15 @@ var rmCmd = new Command6("rm").alias("remove").description("Remove a context (ca
2807
3153
  process.exit(1);
2808
3154
  }
2809
3155
  });
2810
- var contextCommand = new Command6("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
3156
+ var contextCommand = new Command7("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
2811
3157
  ok(` Active context: ${getActiveContext()}`);
2812
3158
  renderContexts();
2813
3159
  }).addCommand(listCmd).addCommand(useCmd).addCommand(addCmd).addCommand(rmCmd);
2814
3160
 
2815
3161
  // src/commands/status.ts
2816
- import { Command as Command7 } from "commander";
2817
- import chalk6 from "chalk";
2818
- var statusCommand = new Command7("status").description("Show the active context's API health and deployment info").action(async () => {
3162
+ import { Command as Command8 } from "commander";
3163
+ import chalk7 from "chalk";
3164
+ var statusCommand = new Command8("status").description("Show the active context's API health and deployment info").action(async () => {
2819
3165
  const context = getActiveContext();
2820
3166
  const apiUrl = getApiUrl2();
2821
3167
  let health;
@@ -2838,18 +3184,18 @@ var statusCommand = new Command7("status").description("Show the active context'
2838
3184
  printJson({ context, apiUrl, reachable: true, health, env: envInfo });
2839
3185
  return;
2840
3186
  }
2841
- const row = (label, value) => ` ${chalk6.dim(label.padEnd(14))}${value ?? chalk6.dim("-")}
3187
+ const row = (label, value) => ` ${chalk7.dim(label.padEnd(14))}${value ?? chalk7.dim("-")}
2842
3188
  `;
2843
3189
  process.stdout.write(
2844
- chalk6.bold("\n Openship status\n\n") + row("Context", context) + row("API", apiUrl) + row("Health", chalk6.green(health.status ?? "ok")) + row("Mode", envInfo.selfHosted ? "self-hosted" : "cloud") + row("Deploy", envInfo.deployMode) + row("Auth", envInfo.authMode) + row("Team", envInfo.teamMode) + (envInfo.hostDomain ? row("Host domain", envInfo.hostDomain) : "") + (envInfo.machineName ? row("Machine", envInfo.machineName) : "") + "\n"
3190
+ chalk7.bold("\n Openship status\n\n") + row("Context", context) + row("API", apiUrl) + row("Health", chalk7.green(health.status ?? "ok")) + row("Mode", envInfo.selfHosted ? "self-hosted" : "cloud") + row("Deploy", envInfo.deployMode) + row("Auth", envInfo.authMode) + row("Team", envInfo.teamMode) + (envInfo.hostDomain ? row("Host domain", envInfo.hostDomain) : "") + (envInfo.machineName ? row("Machine", envInfo.machineName) : "") + "\n"
2845
3191
  );
2846
3192
  });
2847
3193
 
2848
3194
  // src/commands/doctor.ts
2849
- import { Command as Command8 } from "commander";
2850
- import { existsSync as existsSync5 } from "fs";
3195
+ import { Command as Command9 } from "commander";
3196
+ import { existsSync as existsSync6 } from "fs";
2851
3197
  import { execFileSync } from "child_process";
2852
- import chalk7 from "chalk";
3198
+ import chalk8 from "chalk";
2853
3199
  function bunVersion() {
2854
3200
  const embedded = process.versions.bun;
2855
3201
  if (embedded) return embedded;
@@ -2859,9 +3205,9 @@ function bunVersion() {
2859
3205
  return null;
2860
3206
  }
2861
3207
  }
2862
- var doctorCommand = new Command8("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
3208
+ var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
2863
3209
  const checks = [];
2864
- const hasConfig = existsSync5(CONFIG_PATH);
3210
+ const hasConfig = existsSync6(CONFIG_PATH);
2865
3211
  checks.push({
2866
3212
  name: "config",
2867
3213
  status: hasConfig ? "pass" : "warn",
@@ -2897,10 +3243,10 @@ var doctorCommand = new Command8("doctor").description("Diagnose the CLI setup (
2897
3243
  if (isJsonMode()) {
2898
3244
  printJson({ context, apiUrl, reachable, checks });
2899
3245
  } else {
2900
- const glyph = { pass: chalk7.green("\u2713"), warn: chalk7.yellow("!"), fail: chalk7.red("\u2717") };
2901
- process.stdout.write(chalk7.bold("\n Openship doctor\n\n"));
3246
+ const glyph = { pass: chalk8.green("\u2713"), warn: chalk8.yellow("!"), fail: chalk8.red("\u2717") };
3247
+ process.stdout.write(chalk8.bold("\n Openship doctor\n\n"));
2902
3248
  for (const c of checks) {
2903
- process.stdout.write(` ${glyph[c.status]} ${chalk7.bold(c.name.padEnd(8))} ${c.detail}
3249
+ process.stdout.write(` ${glyph[c.status]} ${chalk8.bold(c.name.padEnd(8))} ${c.detail}
2904
3250
  `);
2905
3251
  }
2906
3252
  process.stdout.write("\n");
@@ -2909,20 +3255,20 @@ var doctorCommand = new Command8("doctor").description("Diagnose the CLI setup (
2909
3255
  });
2910
3256
 
2911
3257
  // src/commands/deploy.ts
2912
- import { Command as Command9 } from "commander";
3258
+ import { Command as Command10 } from "commander";
2913
3259
  import { execFileSync as execFileSync3 } from "child_process";
2914
3260
  import ora2 from "ora";
2915
3261
 
2916
3262
  // src/lib/project-link.ts
2917
- import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
2918
- import { join as join6, dirname as dirname3, parse } from "path";
2919
- var LINK_REL = join6(".openship", "project.json");
3263
+ import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
3264
+ import { join as join7, dirname as dirname3, parse } from "path";
3265
+ var LINK_REL = join7(".openship", "project.json");
2920
3266
  function findProjectLinkPath(from = process.cwd()) {
2921
3267
  let dir = from;
2922
3268
  const root = parse(dir).root;
2923
3269
  for (; ; ) {
2924
- const candidate = join6(dir, LINK_REL);
2925
- if (existsSync6(candidate)) return candidate;
3270
+ const candidate = join7(dir, LINK_REL);
3271
+ if (existsSync7(candidate)) return candidate;
2926
3272
  if (dir === root) return null;
2927
3273
  dir = dirname3(dir);
2928
3274
  }
@@ -2939,21 +3285,21 @@ function readProjectLink(from) {
2939
3285
 
2940
3286
  // src/lib/folder-deploy.ts
2941
3287
  import { execFileSync as execFileSync2 } from "child_process";
2942
- import { readFileSync as readFileSync4, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
3288
+ import { readFileSync as readFileSync4, existsSync as existsSync8, rmSync as rmSync3 } from "fs";
2943
3289
  import { tmpdir } from "os";
2944
- import { join as join7, basename } from "path";
3290
+ import { join as join8, basename } from "path";
2945
3291
  function detectPackageManager(dir) {
2946
- if (existsSync7(join7(dir, "bun.lockb")) || existsSync7(join7(dir, "bun.lock"))) return "bun";
2947
- if (existsSync7(join7(dir, "pnpm-lock.yaml"))) return "pnpm";
2948
- if (existsSync7(join7(dir, "yarn.lock"))) return "yarn";
2949
- if (existsSync7(join7(dir, "package.json"))) return "npm";
3292
+ if (existsSync8(join8(dir, "bun.lockb")) || existsSync8(join8(dir, "bun.lock"))) return "bun";
3293
+ if (existsSync8(join8(dir, "pnpm-lock.yaml"))) return "pnpm";
3294
+ if (existsSync8(join8(dir, "yarn.lock"))) return "yarn";
3295
+ if (existsSync8(join8(dir, "package.json"))) return "npm";
2950
3296
  return void 0;
2951
3297
  }
2952
3298
  function detectStack(dir) {
2953
- if (existsSync7(join7(dir, "go.mod"))) return "go";
2954
- if (existsSync7(join7(dir, "Cargo.toml"))) return "rust";
2955
- if (existsSync7(join7(dir, "requirements.txt")) || existsSync7(join7(dir, "pyproject.toml"))) return "python";
2956
- if (existsSync7(join7(dir, "package.json"))) return "node";
3299
+ if (existsSync8(join8(dir, "go.mod"))) return "go";
3300
+ if (existsSync8(join8(dir, "Cargo.toml"))) return "rust";
3301
+ if (existsSync8(join8(dir, "requirements.txt")) || existsSync8(join8(dir, "pyproject.toml"))) return "python";
3302
+ if (existsSync8(join8(dir, "package.json"))) return "node";
2957
3303
  return void 0;
2958
3304
  }
2959
3305
  async function deployFolder(opts) {
@@ -2970,7 +3316,7 @@ async function deployFolder(opts) {
2970
3316
  throw new Error(session.error || "Failed to open upload session");
2971
3317
  }
2972
3318
  step("Packaging folder");
2973
- const tarball = join7(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3319
+ const tarball = join8(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
2974
3320
  execFileSync2(
2975
3321
  "tar",
2976
3322
  [
@@ -2996,7 +3342,7 @@ async function deployFolder(opts) {
2996
3342
  if (!res.ok) throw new Error(`upload failed (HTTP ${res.status})`);
2997
3343
  } finally {
2998
3344
  try {
2999
- rmSync2(tarball, { force: true });
3345
+ rmSync3(tarball, { force: true });
3000
3346
  } catch {
3001
3347
  }
3002
3348
  }
@@ -3170,7 +3516,7 @@ function git(args) {
3170
3516
  return void 0;
3171
3517
  }
3172
3518
  }
3173
- var deployCommand = new Command9("deploy").description("Trigger a deployment for the current project").option("--project <id>", "Project ID (defaults to the linked project in .openship/project.json)").option("--branch <name>", "Git branch to deploy (defaults to the current branch)").option("--commit <sha>", "Specific commit SHA (defaults to the latest commit on the branch)").option("--env <environment>", "Target environment: production | preview", "production").option("--force-all", "Rebuild every enabled service (skip smart per-service routing)").option("--service-ids <ids>", "Comma-separated service IDs to deploy (smart routing)").option("--smart-route", "Rebuild only services changed since the active deploy").option("--refresh", "Re-apply current env to the active deploy (no git pull, no rebuild)").option("--name <name>", "Project name for a folder (non-git) deploy (defaults to the directory name)").option("--watch", "Stream the deployment logs until it finishes").action(async (opts) => {
3519
+ var deployCommand = new Command10("deploy").description("Trigger a deployment for the current project").option("--project <id>", "Project ID (defaults to the linked project in .openship/project.json)").option("--branch <name>", "Git branch to deploy (defaults to the current branch)").option("--commit <sha>", "Specific commit SHA (defaults to the latest commit on the branch)").option("--env <environment>", "Target environment: production | preview", "production").option("--force-all", "Rebuild every enabled service (skip smart per-service routing)").option("--service-ids <ids>", "Comma-separated service IDs to deploy (smart routing)").option("--smart-route", "Rebuild only services changed since the active deploy").option("--refresh", "Re-apply current env to the active deploy (no git pull, no rebuild)").option("--name <name>", "Project name for a folder (non-git) deploy (defaults to the directory name)").option("--watch", "Stream the deployment logs until it finishes").action(async (opts) => {
3174
3520
  const link = readProjectLink();
3175
3521
  const env = opts.env;
3176
3522
  if (env !== "production" && env !== "preview") {
@@ -3182,7 +3528,7 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
3182
3528
  let deploymentId;
3183
3529
  let payload;
3184
3530
  if (!inGitRepo && !gitOnlyFlags) {
3185
- const spinner2 = isJsonMode() ? null : ora2("Deploying folder").start();
3531
+ const spinner3 = isJsonMode() ? null : ora2("Deploying folder").start();
3186
3532
  try {
3187
3533
  const result2 = await deployFolder({
3188
3534
  cwd: process.cwd(),
@@ -3190,14 +3536,14 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
3190
3536
  projectId: opts.project || link?.projectId,
3191
3537
  environment: env,
3192
3538
  onStep: (m) => {
3193
- if (spinner2) spinner2.text = m;
3539
+ if (spinner3) spinner3.text = m;
3194
3540
  }
3195
3541
  });
3196
3542
  deploymentId = result2.deploymentId;
3197
3543
  payload = { success: true, deployment_id: result2.deploymentId, project_id: result2.projectId };
3198
- spinner2?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
3544
+ spinner3?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
3199
3545
  } catch (e) {
3200
- spinner2?.fail("Folder deploy failed");
3546
+ spinner3?.fail("Folder deploy failed");
3201
3547
  err(e instanceof ApiError ? e.message : String(e));
3202
3548
  process.exit(1);
3203
3549
  }
@@ -3219,7 +3565,7 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
3219
3565
  smartRoute: opts.smartRoute || void 0,
3220
3566
  refresh: opts.refresh || void 0
3221
3567
  };
3222
- const spinner2 = isJsonMode() ? null : ora2("Triggering deployment").start();
3568
+ const spinner3 = isJsonMode() ? null : ora2("Triggering deployment").start();
3223
3569
  let res;
3224
3570
  try {
3225
3571
  res = await apiRequest("/deployments", {
@@ -3227,13 +3573,13 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
3227
3573
  body: JSON.stringify(body)
3228
3574
  });
3229
3575
  } catch (e) {
3230
- spinner2?.fail("Deployment failed to start");
3576
+ spinner3?.fail("Deployment failed to start");
3231
3577
  err(e instanceof ApiError ? e.message : String(e));
3232
3578
  process.exit(1);
3233
3579
  }
3234
3580
  deploymentId = res.data?.deployment_id;
3235
3581
  payload = res.data ?? res;
3236
- spinner2?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
3582
+ spinner3?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
3237
3583
  }
3238
3584
  if (isJsonMode() && !opts.watch) {
3239
3585
  printJson(payload ?? {});
@@ -3252,9 +3598,9 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
3252
3598
  });
3253
3599
 
3254
3600
  // src/commands/deployment.ts
3255
- import { Command as Command10 } from "commander";
3601
+ import { Command as Command11 } from "commander";
3256
3602
  import { createInterface as createInterface3 } from "readline";
3257
- function run(fn) {
3603
+ function run2(fn) {
3258
3604
  return async (...args) => {
3259
3605
  try {
3260
3606
  await fn(...args);
@@ -3274,12 +3620,12 @@ function shortSha(v) {
3274
3620
  async function confirm(question) {
3275
3621
  if (!process.stdin.isTTY) return true;
3276
3622
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
3277
- const answer = await new Promise((resolve) => rl.question(`${question} [y/N] `, resolve));
3623
+ const answer = await new Promise((resolve2) => rl.question(`${question} [y/N] `, resolve2));
3278
3624
  rl.close();
3279
3625
  return /^y(es)?$/i.test(answer.trim());
3280
3626
  }
3281
- var list = new Command10("list").description("List deployments (org-wide, or scoped to a project)").option("--project <id>", "Scope to a project (defaults to the linked project)").option("--env <environment>", "Filter by environment: production | preview").option("--limit <n>", "Max rows to fetch", "50").action(
3282
- run(async (opts) => {
3627
+ var list = new Command11("list").description("List deployments (org-wide, or scoped to a project)").option("--project <id>", "Scope to a project (defaults to the linked project)").option("--env <environment>", "Filter by environment: production | preview").option("--limit <n>", "Max rows to fetch", "50").action(
3628
+ run2(async (opts) => {
3283
3629
  const projectId = opts.project || readProjectLink()?.projectId;
3284
3630
  const params = new URLSearchParams();
3285
3631
  if (projectId) params.set("projectId", projectId);
@@ -3301,8 +3647,8 @@ var list = new Command10("list").description("List deployments (org-wide, or sco
3301
3647
  printTable(rows, ["id", "status", "env", "branch", "commit", "active", "created"]);
3302
3648
  })
3303
3649
  );
3304
- var get = new Command10("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
3305
- run(async (id) => {
3650
+ var get = new Command11("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
3651
+ run2(async (id) => {
3306
3652
  const res = await apiRequest(`/deployments/${id}`);
3307
3653
  const d = res.data ?? {};
3308
3654
  if (isJsonMode()) return printJson(d);
@@ -3322,20 +3668,20 @@ var get = new Command10("get").description("Show a single deployment").argument(
3322
3668
  );
3323
3669
  })
3324
3670
  );
3325
- var info2 = new Command10("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
3326
- run(async (id) => {
3671
+ var info2 = new Command11("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
3672
+ run2(async (id) => {
3327
3673
  const res = await apiRequest(`/deployments/${id}/info`);
3328
3674
  printJson(res.data ?? res);
3329
3675
  })
3330
3676
  );
3331
- var usage = new Command10("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
3332
- run(async (id) => {
3677
+ var usage = new Command11("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
3678
+ run2(async (id) => {
3333
3679
  const res = await apiRequest(`/deployments/${id}/usage`);
3334
3680
  printJson(res.data ?? res);
3335
3681
  })
3336
3682
  );
3337
- var redeploy = new Command10("redeploy").description("Redeploy from an existing deployment").argument("<id>", "Deployment ID").option("--use-existing-commit", "Rebuild the same commit instead of the latest on the branch").action(
3338
- run(async (id, opts) => {
3683
+ var redeploy = new Command11("redeploy").description("Redeploy from an existing deployment").argument("<id>", "Deployment ID").option("--use-existing-commit", "Rebuild the same commit instead of the latest on the branch").action(
3684
+ run2(async (id, opts) => {
3339
3685
  const res = await apiRequest(`/deployments/${id}/redeploy`, {
3340
3686
  method: "POST",
3341
3687
  body: JSON.stringify({ useExistingCommit: opts.useExistingCommit === true })
@@ -3343,14 +3689,14 @@ var redeploy = new Command10("redeploy").description("Redeploy from an existing
3343
3689
  report(res, `Redeploy triggered for ${id}`);
3344
3690
  })
3345
3691
  );
3346
- var rollback = new Command10("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
3347
- run(async (id) => {
3692
+ var rollback = new Command11("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
3693
+ run2(async (id) => {
3348
3694
  const res = await apiRequest(`/deployments/${id}/rollback`, { method: "POST" });
3349
3695
  report(res, `Rolled back to ${id}`);
3350
3696
  })
3351
3697
  );
3352
- var pin = new Command10("pin").description("Pin (or unpin) a deployment's rollback artifact").argument("<id>", "Deployment ID").option("--off", "Unpin instead of pin").action(
3353
- run(async (id, opts) => {
3698
+ var pin = new Command11("pin").description("Pin (or unpin) a deployment's rollback artifact").argument("<id>", "Deployment ID").option("--off", "Unpin instead of pin").action(
3699
+ run2(async (id, opts) => {
3354
3700
  const pinned = !opts.off;
3355
3701
  const res = await apiRequest(`/deployments/${id}/pin`, {
3356
3702
  method: "POST",
@@ -3359,32 +3705,32 @@ var pin = new Command10("pin").description("Pin (or unpin) a deployment's rollba
3359
3705
  report(res, `${pinned ? "Pinned" : "Unpinned"} ${id}`);
3360
3706
  })
3361
3707
  );
3362
- var cancel = new Command10("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
3363
- run(async (id) => {
3708
+ var cancel = new Command11("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
3709
+ run2(async (id) => {
3364
3710
  const res = await apiRequest(`/deployments/${id}/cancel`, { method: "POST" });
3365
3711
  report(res, `Cancelled ${id}`);
3366
3712
  })
3367
3713
  );
3368
- var restart = new Command10("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
3369
- run(async (id) => {
3714
+ var restart2 = new Command11("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
3715
+ run2(async (id) => {
3370
3716
  const res = await apiRequest(`/deployments/${id}/restart`, { method: "POST" });
3371
3717
  report(res, `Restarted ${id}`);
3372
3718
  })
3373
3719
  );
3374
- var reject = new Command10("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
3375
- run(async (id) => {
3720
+ var reject = new Command11("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
3721
+ run2(async (id) => {
3376
3722
  const res = await apiRequest(`/deployments/${id}/reject`, { method: "POST" });
3377
3723
  report(res, `Rejected ${id}`);
3378
3724
  })
3379
3725
  );
3380
- var keep = new Command10("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
3381
- run(async (id) => {
3726
+ var keep = new Command11("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
3727
+ run2(async (id) => {
3382
3728
  const res = await apiRequest(`/deployments/${id}/keep`, { method: "POST" });
3383
3729
  report(res, `Kept ${id}`);
3384
3730
  })
3385
3731
  );
3386
- var rm = new Command10("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
3387
- run(async (id, opts) => {
3732
+ var rm = new Command11("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
3733
+ run2(async (id, opts) => {
3388
3734
  if (!opts.yes && !isJsonMode() && !await confirm(`Delete deployment ${id}?`)) {
3389
3735
  err("Aborted.");
3390
3736
  process.exit(1);
@@ -3393,8 +3739,8 @@ var rm = new Command10("rm").description("Delete a deployment").argument("<id>",
3393
3739
  report(res, `Deleted ${id}`);
3394
3740
  })
3395
3741
  );
3396
- var sslStatus = new Command10("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
3397
- run(async (domain) => {
3742
+ var sslStatus = new Command11("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
3743
+ run2(async (domain) => {
3398
3744
  const res = await apiRequest("/deployments/ssl/status", {
3399
3745
  method: "POST",
3400
3746
  body: JSON.stringify({ domain })
@@ -3402,8 +3748,8 @@ var sslStatus = new Command10("status").description("Check SSL certificate statu
3402
3748
  printJson(res);
3403
3749
  })
3404
3750
  );
3405
- var sslRenew = new Command10("renew").description("Renew (issue) an SSL certificate for a domain").argument("<domain>", "Domain to renew").option("--www", "Also include the www subdomain").action(
3406
- run(async (domain, opts) => {
3751
+ var sslRenew = new Command11("renew").description("Renew (issue) an SSL certificate for a domain").argument("<domain>", "Domain to renew").option("--www", "Also include the www subdomain").action(
3752
+ run2(async (domain, opts) => {
3407
3753
  const res = await apiRequest("/deployments/ssl/renew", {
3408
3754
  method: "POST",
3409
3755
  body: JSON.stringify({ domain, includeWww: opts.www === true })
@@ -3411,12 +3757,12 @@ var sslRenew = new Command10("renew").description("Renew (issue) an SSL certific
3411
3757
  report(res, `SSL renewal requested for ${domain}`);
3412
3758
  })
3413
3759
  );
3414
- var ssl = new Command10("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
3415
- var deploymentCommand = new Command10("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(restart).addCommand(reject).addCommand(keep).addCommand(rm).addCommand(ssl);
3760
+ var ssl = new Command11("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
3761
+ var deploymentCommand = new Command11("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(restart2).addCommand(reject).addCommand(keep).addCommand(rm).addCommand(ssl);
3416
3762
 
3417
3763
  // src/commands/logs.ts
3418
- import { Command as Command11 } from "commander";
3419
- var logsCommand = new Command11("logs").description("View or stream a deployment's logs").argument("<deploymentId>", "Deployment ID").option("-f, --follow", "Stream live logs via SSE until the deployment finishes").option("--tail <n>", "Show only the last N log lines (snapshot mode)").action(async (deploymentId, opts) => {
3764
+ import { Command as Command12 } from "commander";
3765
+ var logsCommand = new Command12("logs").description("View or stream a deployment's logs").argument("<deploymentId>", "Deployment ID").option("-f, --follow", "Stream live logs via SSE until the deployment finishes").option("--tail <n>", "Show only the last N log lines (snapshot mode)").action(async (deploymentId, opts) => {
3420
3766
  if (opts.follow) {
3421
3767
  try {
3422
3768
  const result = await streamDeploymentLogs(deploymentId);
@@ -3448,8 +3794,8 @@ var logsCommand = new Command11("logs").description("View or stream a deployment
3448
3794
  });
3449
3795
 
3450
3796
  // src/commands/project.ts
3451
- import { Command as Command12 } from "commander";
3452
- import chalk8 from "chalk";
3797
+ import { Command as Command13 } from "commander";
3798
+ import chalk9 from "chalk";
3453
3799
  import { createInterface as createInterface4 } from "readline/promises";
3454
3800
  import { stdin as input3, stdout as output3 } from "process";
3455
3801
  function action(fn) {
@@ -3480,12 +3826,12 @@ function printProject(project) {
3480
3826
  ];
3481
3827
  for (const [k, v] of fields) {
3482
3828
  if (v === null || v === void 0 || v === "") continue;
3483
- process.stdout.write(` ${chalk8.dim(k.padEnd(12))} ${String(v)}
3829
+ process.stdout.write(` ${chalk9.dim(k.padEnd(12))} ${String(v)}
3484
3830
  `);
3485
3831
  }
3486
3832
  }
3487
3833
  var ENVIRONMENTS = ["production", "preview", "development"];
3488
- var listCmd2 = new Command12("list").alias("ls").description("List projects in the active organization").action(
3834
+ var listCmd2 = new Command13("list").alias("ls").description("List projects in the active organization").action(
3489
3835
  action(async () => {
3490
3836
  const rows = [];
3491
3837
  for await (const p of paginate("/projects")) {
@@ -3500,7 +3846,7 @@ var listCmd2 = new Command12("list").alias("ls").description("List projects in t
3500
3846
  printTable(rows, ["id", "name", "slug", "repo", "source"]);
3501
3847
  })
3502
3848
  );
3503
- var getCmd = new Command12("get").description("Show a single project").argument("<id>", "Project ID").action(
3849
+ var getCmd = new Command13("get").description("Show a single project").argument("<id>", "Project ID").action(
3504
3850
  action(async (id) => {
3505
3851
  const { data } = await apiRequest(
3506
3852
  `/projects/${encodeURIComponent(id)}`
@@ -3508,7 +3854,7 @@ var getCmd = new Command12("get").description("Show a single project").argument(
3508
3854
  printProject(data);
3509
3855
  })
3510
3856
  );
3511
- var createCmd = new Command12("create").description("Create a project").requiredOption("--name <name>", "Project name").option("--slug <slug>", "Free-subdomain slug (slug.opsh.io)").option("--git-owner <owner>", "GitHub owner/org").option("--git-repo <repo>", "GitHub repository name").option("--git-branch <branch>", "Git branch to deploy").option("--framework <framework>", "Stack/framework id").option("--local-path <path>", "Local source path").option("--port <port>", "Container port", (v) => Number(v)).option(
3857
+ var createCmd = new Command13("create").description("Create a project").requiredOption("--name <name>", "Project name").option("--slug <slug>", "Free-subdomain slug (slug.opsh.io)").option("--git-owner <owner>", "GitHub owner/org").option("--git-repo <repo>", "GitHub repository name").option("--git-branch <branch>", "Git branch to deploy").option("--framework <framework>", "Stack/framework id").option("--local-path <path>", "Local source path").option("--port <port>", "Container port", (v) => Number(v)).option(
3512
3858
  "--type <type>",
3513
3859
  "Project type: app | docker | services | monorepo"
3514
3860
  ).action(
@@ -3532,12 +3878,12 @@ var createCmd = new Command12("create").description("Create a project").required
3532
3878
  printProject(data);
3533
3879
  })
3534
3880
  );
3535
- var deleteCmd = new Command12("delete").alias("rm").description("Delete a project (tears down all resources)").argument("<id>", "Project ID").option("--force", "Cancel active work and delete anyway").option("--force-orphan", "Orphan resources that won't destroy, then drop the row").option("--wipe-volumes", "Also destroy persistent volumes").option("-y, --yes", "Skip the confirmation prompt").action(
3881
+ var deleteCmd = new Command13("delete").alias("rm").description("Delete a project (tears down all resources)").argument("<id>", "Project ID").option("--force", "Cancel active work and delete anyway").option("--force-orphan", "Orphan resources that won't destroy, then drop the row").option("--wipe-volumes", "Also destroy persistent volumes").option("-y, --yes", "Skip the confirmation prompt").action(
3536
3882
  action(async (id, opts) => {
3537
3883
  if (!opts.yes) {
3538
3884
  const rl = createInterface4({ input: input3, output: output3 });
3539
3885
  const answer = await rl.question(
3540
- chalk8.yellow(` Delete project ${id}? This cannot be undone. `) + "(y/N) "
3886
+ chalk9.yellow(` Delete project ${id}? This cannot be undone. `) + "(y/N) "
3541
3887
  );
3542
3888
  rl.close();
3543
3889
  if (answer.trim().toLowerCase() !== "y") {
@@ -3563,7 +3909,7 @@ var deleteCmd = new Command12("delete").alias("rm").description("Delete a projec
3563
3909
  `);
3564
3910
  })
3565
3911
  );
3566
- var envCmd = new Command12("env").description("Manage project environment variables");
3912
+ var envCmd = new Command13("env").description("Manage project environment variables");
3567
3913
  envCmd.command("get").description("List env vars (secret values are masked by the API)").argument("<id>", "Project ID").option("--environment <env>", "Filter by environment (production|preview|development)").action(
3568
3914
  action(async (id, opts) => {
3569
3915
  const qs = opts.environment ? `?environment=${encodeURIComponent(opts.environment)}` : "";
@@ -3637,7 +3983,7 @@ envCmd.command("set").description("Merge env vars: upsert KEY=VALUE pairs and/or
3637
3983
  );
3638
3984
  })
3639
3985
  );
3640
- var gitCmd = new Command12("git").description("Manage git linkage and auto-deploy");
3986
+ var gitCmd = new Command13("git").description("Manage git linkage and auto-deploy");
3641
3987
  gitCmd.command("link").description("Link a GitHub repository to a project").argument("<id>", "Project ID").requiredOption("--owner <owner>", "GitHub owner/org").requiredOption("--repo <repo>", "Repository name").option("--branch <branch>", "Branch (defaults to the repo's default branch)").option("--installation-id <id>", "GitHub App installation id", (v) => Number(v)).action(
3642
3988
  action(async (id, opts) => {
3643
3989
  const result = await apiRequest(
@@ -3725,7 +4071,7 @@ gitCmd.command("webhook-domain").description("Set or clear the domain that recei
3725
4071
  `);
3726
4072
  })
3727
4073
  );
3728
- var connectCmd = new Command12("connect").description("Connect a custom domain to a project").argument("<id>", "Project ID").argument("<domain>", "Custom domain hostname").option("--include-www", "Also connect the www. variant").action(
4074
+ var connectCmd = new Command13("connect").description("Connect a custom domain to a project").argument("<id>", "Project ID").argument("<domain>", "Custom domain hostname").option("--include-www", "Also connect the www. variant").action(
3729
4075
  action(async (id, domain, opts) => {
3730
4076
  const result = await apiRequest(
3731
4077
  `/projects/${encodeURIComponent(id)}/connect`,
@@ -3745,7 +4091,7 @@ var connectCmd = new Command12("connect").description("Connect a custom domain t
3745
4091
  printJson(result.records);
3746
4092
  })
3747
4093
  );
3748
- var enableCmd = new Command12("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
4094
+ var enableCmd = new Command13("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
3749
4095
  action(async (id) => {
3750
4096
  const result = await apiRequest(
3751
4097
  `/projects/${encodeURIComponent(id)}/enable`,
@@ -3757,7 +4103,7 @@ var enableCmd = new Command12("enable").description("Start a stopped project").a
3757
4103
  `);
3758
4104
  })
3759
4105
  );
3760
- var disableCmd = new Command12("disable").description("Stop a running project").argument("<id>", "Project ID").action(
4106
+ var disableCmd = new Command13("disable").description("Stop a running project").argument("<id>", "Project ID").action(
3761
4107
  action(async (id) => {
3762
4108
  const result = await apiRequest(
3763
4109
  `/projects/${encodeURIComponent(id)}/disable`,
@@ -3770,7 +4116,7 @@ var disableCmd = new Command12("disable").description("Stop a running project").
3770
4116
  })
3771
4117
  );
3772
4118
  var SLEEP_MODES = ["auto_sleep", "always_on"];
3773
- var sleepModeCmd = new Command12("sleep-mode").description("Set the project sleep mode").argument("<id>", "Project ID").argument("<mode>", `One of: ${SLEEP_MODES.join(", ")}`).action(
4119
+ var sleepModeCmd = new Command13("sleep-mode").description("Set the project sleep mode").argument("<id>", "Project ID").argument("<mode>", `One of: ${SLEEP_MODES.join(", ")}`).action(
3774
4120
  action(async (id, mode) => {
3775
4121
  if (!SLEEP_MODES.includes(mode)) {
3776
4122
  err(` mode must be one of: ${SLEEP_MODES.join(", ")}`);
@@ -3788,7 +4134,7 @@ var sleepModeCmd = new Command12("sleep-mode").description("Set the project slee
3788
4134
  })
3789
4135
  );
3790
4136
  var TRANSFER_DIRS = ["to-cloud", "to-self-hosted"];
3791
- var transferCmd = new Command12("transfer").description("Promote a project to Openship Cloud, or bring it back (self-hosted only)").argument("<id>", "Project ID").argument("<direction>", `One of: ${TRANSFER_DIRS.join(", ")}`).action(
4137
+ var transferCmd = new Command13("transfer").description("Promote a project to Openship Cloud, or bring it back (self-hosted only)").argument("<id>", "Project ID").argument("<direction>", `One of: ${TRANSFER_DIRS.join(", ")}`).action(
3792
4138
  action(async (id, direction) => {
3793
4139
  if (!TRANSFER_DIRS.includes(direction)) {
3794
4140
  err(` direction must be one of: ${TRANSFER_DIRS.join(", ")}`);
@@ -3811,7 +4157,7 @@ var transferCmd = new Command12("transfer").description("Promote a project to Op
3811
4157
  );
3812
4158
  })
3813
4159
  );
3814
- var logsCmd = new Command12("logs").description("Show or stream runtime (container) logs").argument("<id>", "Project ID").option("--tail <n>", "Number of recent lines", (v) => Number(v)).option("-f, --follow", "Stream logs until interrupted").action(
4160
+ var logsCmd = new Command13("logs").description("Show or stream runtime (container) logs").argument("<id>", "Project ID").option("--tail <n>", "Number of recent lines", (v) => Number(v)).option("-f, --follow", "Stream logs until interrupted").action(
3815
4161
  action(async (id, opts) => {
3816
4162
  const tailQs = opts.tail ? `?tail=${opts.tail}` : "";
3817
4163
  if (!opts.follow) {
@@ -3839,7 +4185,7 @@ var logsCmd = new Command12("logs").description("Show or stream runtime (contain
3839
4185
  }
3840
4186
  })
3841
4187
  );
3842
- var serverLogsCmd = new Command12("server-logs").description("Show or stream HTTP request logs (edge/OpenResty)").argument("<id>", "Project ID").option("--limit <n>", "Number of recent entries (max 200)", (v) => Number(v)).option("--domain <domain>", "Restrict to a specific domain").option("-f, --follow", "Stream request logs until interrupted").action(
4188
+ var serverLogsCmd = new Command13("server-logs").description("Show or stream HTTP request logs (edge/OpenResty)").argument("<id>", "Project ID").option("--limit <n>", "Number of recent entries (max 200)", (v) => Number(v)).option("--domain <domain>", "Restrict to a specific domain").option("-f, --follow", "Stream request logs until interrupted").action(
3843
4189
  action(async (id, opts) => {
3844
4190
  const base = `/projects/${encodeURIComponent(id)}/server-logs`;
3845
4191
  const domainQs = opts.domain ? `domain=${encodeURIComponent(opts.domain)}` : "";
@@ -3880,14 +4226,14 @@ function safeParse(s) {
3880
4226
  }
3881
4227
  }
3882
4228
  function printLogEntry(entry) {
3883
- const ts = entry.timestamp ? chalk8.dim(String(entry.timestamp)) : "";
4229
+ const ts = entry.timestamp ? chalk9.dim(String(entry.timestamp)) : "";
3884
4230
  const level = String(entry.level ?? "info");
3885
- const color = level === "error" ? chalk8.red : level === "warn" ? chalk8.yellow : chalk8.dim;
4231
+ const color = level === "error" ? chalk9.red : level === "warn" ? chalk9.yellow : chalk9.dim;
3886
4232
  const msg = entry.message ?? entry.data ?? "";
3887
4233
  process.stdout.write(` ${ts} ${color(level.padEnd(5))} ${String(msg)}
3888
4234
  `);
3889
4235
  }
3890
- var projectCommand = new Command12("project").alias("projects").description("Manage Openship projects");
4236
+ var projectCommand = new Command13("project").alias("projects").description("Manage Openship projects");
3891
4237
  projectCommand.addCommand(listCmd2);
3892
4238
  projectCommand.addCommand(getCmd);
3893
4239
  projectCommand.addCommand(createCmd);
@@ -3903,9 +4249,9 @@ projectCommand.addCommand(logsCmd);
3903
4249
  projectCommand.addCommand(serverLogsCmd);
3904
4250
 
3905
4251
  // src/commands/service.ts
3906
- import { Command as Command13 } from "commander";
3907
- import chalk9 from "chalk";
3908
- import { spawnSync as spawnSync2 } from "child_process";
4252
+ import { Command as Command14 } from "commander";
4253
+ import chalk10 from "chalk";
4254
+ import { spawnSync as spawnSync3 } from "child_process";
3909
4255
  import path from "path";
3910
4256
  import { createInterface as createInterface5 } from "readline/promises";
3911
4257
  import { stdin as input4, stdout as output4 } from "process";
@@ -3917,14 +4263,14 @@ function requireAuth() {
3917
4263
  }
3918
4264
  function fail(e) {
3919
4265
  if (e instanceof ApiError) {
3920
- err(` ${e.message}` + (e.status ? chalk9.dim(` (HTTP ${e.status})`) : ""));
4266
+ err(` ${e.message}` + (e.status ? chalk10.dim(` (HTTP ${e.status})`) : ""));
3921
4267
  } else {
3922
4268
  err(` ${e instanceof Error ? e.message : String(e)}`);
3923
4269
  }
3924
4270
  process.exit(1);
3925
4271
  }
3926
4272
  function stackCommand(name) {
3927
- return new Command13(name).requiredOption(
4273
+ return new Command14(name).requiredOption(
3928
4274
  "-p, --project <id|slug|name>",
3929
4275
  "Stack (project) id, slug, or name"
3930
4276
  );
@@ -4009,7 +4355,7 @@ var listCmd3 = stackCommand("list").description("List the services in a stack").
4009
4355
  image: s.image ?? "\u2014",
4010
4356
  enabled: s.enabled ? "yes" : "no",
4011
4357
  exposed: s.exposed ? "yes" : "no",
4012
- drift: s.drift ? chalk9.yellow("pending") : "\u2014"
4358
+ drift: s.drift ? chalk10.yellow("pending") : "\u2014"
4013
4359
  })),
4014
4360
  ["name", "kind", "image", "enabled", "exposed", "drift"]
4015
4361
  );
@@ -4172,7 +4518,7 @@ function mapComposeService(name, def, baseDir) {
4172
4518
  var syncCmd = stackCommand("sync").description("Sync a stack's services from a docker-compose file (services not in the file are removed)").argument("<compose-file>", "Path to docker-compose.yml / compose.yaml").option("-y, --yes", "Skip the confirmation prompt").action(async (composeFile, opts) => {
4173
4519
  requireAuth();
4174
4520
  const abs = path.resolve(composeFile);
4175
- const proc = spawnSync2(
4521
+ const proc = spawnSync3(
4176
4522
  "docker",
4177
4523
  ["compose", "-f", abs, "config", "--format", "json"],
4178
4524
  { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
@@ -4266,7 +4612,7 @@ var containersCmd = stackCommand("containers").description("List the stack's act
4266
4612
  fail(e);
4267
4613
  }
4268
4614
  });
4269
- var driftCmd = new Command13("drift").description(
4615
+ var driftCmd = new Command14("drift").description(
4270
4616
  "Resolve compose drift on a service (upstream compose changed a value you edited)"
4271
4617
  );
4272
4618
  function driftActionCommand(action2) {
@@ -4294,7 +4640,7 @@ function driftActionCommand(action2) {
4294
4640
  }
4295
4641
  driftCmd.addCommand(driftActionCommand("accept"));
4296
4642
  driftCmd.addCommand(driftActionCommand("keep"));
4297
- var envCmd2 = new Command13("env").description("Read and write a service's environment variables");
4643
+ var envCmd2 = new Command14("env").description("Read and write a service's environment variables");
4298
4644
  var envGetCmd = stackCommand("get").description("List a service's environment variables (secrets masked)").argument("<service>", "Service name or id").option("-e, --env <environment>", "Environment: production | preview | development").action(async (service, opts) => {
4299
4645
  requireAuth();
4300
4646
  try {
@@ -4377,8 +4723,8 @@ function printLogEntry2(entry) {
4377
4723
  process.stdout.write(JSON.stringify(entry) + "\n");
4378
4724
  return;
4379
4725
  }
4380
- const ts = entry.timestamp ? chalk9.dim(entry.timestamp) : "";
4381
- const line = entry.level === "error" ? chalk9.red(msg) : entry.level === "warn" ? chalk9.yellow(msg) : msg;
4726
+ const ts = entry.timestamp ? chalk10.dim(entry.timestamp) : "";
4727
+ const line = entry.level === "error" ? chalk10.red(msg) : entry.level === "warn" ? chalk10.yellow(msg) : msg;
4382
4728
  process.stdout.write(`${ts ? ts + " " : ""}${line}
4383
4729
  `);
4384
4730
  }
@@ -4427,7 +4773,7 @@ var execCmd = stackCommand("exec").description("Open an interactive shell in a s
4427
4773
  );
4428
4774
  process.exit(1);
4429
4775
  });
4430
- var serviceCommand = new Command13("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
4776
+ var serviceCommand = new Command14("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
4431
4777
  serviceCommand.addCommand(listCmd3);
4432
4778
  serviceCommand.addCommand(getCmd2);
4433
4779
  serviceCommand.addCommand(createCmd2);
@@ -4443,15 +4789,15 @@ serviceCommand.addCommand(logsCmd2);
4443
4789
  serviceCommand.addCommand(execCmd);
4444
4790
 
4445
4791
  // src/commands/domain.ts
4446
- import { Command as Command14 } from "commander";
4447
- import chalk10 from "chalk";
4792
+ import { Command as Command15 } from "commander";
4793
+ import chalk11 from "chalk";
4448
4794
  import ora3 from "ora";
4449
- function spin(text) {
4450
- return isJsonMode() ? null : ora3(text).start();
4795
+ function spin(text2) {
4796
+ return isJsonMode() ? null : ora3(text2).start();
4451
4797
  }
4452
4798
  function fail2(e) {
4453
4799
  if (e instanceof ApiError) {
4454
- err(` ${e.message}${e.status ? chalk10.dim(` (${e.status})`) : ""}`);
4800
+ err(` ${e.message}${e.status ? chalk11.dim(` (${e.status})`) : ""}`);
4455
4801
  } else {
4456
4802
  err(` ${e instanceof Error ? e.message : String(e)}`);
4457
4803
  }
@@ -4479,7 +4825,7 @@ function printRecords(result) {
4479
4825
  ["type", "host", "value"]
4480
4826
  );
4481
4827
  }
4482
- var listCmd4 = new Command14("list").description("List a project's custom domains").requiredOption("-p, --project <id>", "Project ID to list domains for").action(async (opts) => {
4828
+ var listCmd4 = new Command15("list").description("List a project's custom domains").requiredOption("-p, --project <id>", "Project ID to list domains for").action(async (opts) => {
4483
4829
  try {
4484
4830
  const res = await apiRequest(
4485
4831
  `/domains?projectId=${encodeURIComponent(opts.project)}`
@@ -4494,7 +4840,7 @@ var listCmd4 = new Command14("list").description("List a project's custom domain
4494
4840
  fail2(e);
4495
4841
  }
4496
4842
  });
4497
- var addCmd2 = new Command14("add").description("Add a custom domain to a project").argument("<hostname>", "Domain hostname (e.g. app.example.com)").requiredOption("-p, --project <id>", "Project ID to attach the domain to").option("--primary", "Mark this domain as the project's primary", false).action(async (hostname, opts) => {
4843
+ var addCmd2 = new Command15("add").description("Add a custom domain to a project").argument("<hostname>", "Domain hostname (e.g. app.example.com)").requiredOption("-p, --project <id>", "Project ID to attach the domain to").option("--primary", "Mark this domain as the project's primary", false).action(async (hostname, opts) => {
4498
4844
  const sp = spin(`Adding ${hostname}\u2026`);
4499
4845
  try {
4500
4846
  const res = await apiRequest("/domains", {
@@ -4513,7 +4859,7 @@ var addCmd2 = new Command14("add").description("Add a custom domain to a project
4513
4859
  fail2(e);
4514
4860
  }
4515
4861
  });
4516
- var previewCmd = new Command14("preview").description("Preview the DNS records a hostname would need (no changes saved)").argument("<hostname>", "Domain hostname to preview").action(async (hostname) => {
4862
+ var previewCmd = new Command15("preview").description("Preview the DNS records a hostname would need (no changes saved)").argument("<hostname>", "Domain hostname to preview").action(async (hostname) => {
4517
4863
  try {
4518
4864
  const res = await apiRequest("/domains/preview", {
4519
4865
  method: "POST",
@@ -4524,7 +4870,7 @@ var previewCmd = new Command14("preview").description("Preview the DNS records a
4524
4870
  fail2(e);
4525
4871
  }
4526
4872
  });
4527
- var verifyCmd = new Command14("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
4873
+ var verifyCmd = new Command15("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
4528
4874
  const sp = spin("Checking DNS records\u2026");
4529
4875
  try {
4530
4876
  const res = await apiRaw(`/domains/${encodeURIComponent(id)}/verify`, { method: "POST" });
@@ -4550,7 +4896,7 @@ var verifyCmd = new Command14("verify").description("Run DNS verification for a
4550
4896
  fail2(e);
4551
4897
  }
4552
4898
  });
4553
- var primaryCmd = new Command14("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
4899
+ var primaryCmd = new Command15("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
4554
4900
  const sp = spin("Setting primary\u2026");
4555
4901
  try {
4556
4902
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/primary`, {
@@ -4563,7 +4909,7 @@ var primaryCmd = new Command14("primary").description("Make a domain the project
4563
4909
  fail2(e);
4564
4910
  }
4565
4911
  });
4566
- var recordsCmd = new Command14("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
4912
+ var recordsCmd = new Command15("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
4567
4913
  try {
4568
4914
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/records`);
4569
4915
  printRecords(res.data);
@@ -4581,7 +4927,7 @@ function printSsl(data) {
4581
4927
  if (data.issuer) info(` issuer: ${data.issuer}`);
4582
4928
  if (data.expiresAt) info(` expires: ${data.expiresAt}`);
4583
4929
  }
4584
- var renewCmd = new Command14("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
4930
+ var renewCmd = new Command15("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
4585
4931
  const sp = spin("Renewing certificate\u2026");
4586
4932
  try {
4587
4933
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/renew`, {
@@ -4594,7 +4940,7 @@ var renewCmd = new Command14("renew").description("Renew the SSL certificate for
4594
4940
  fail2(e);
4595
4941
  }
4596
4942
  });
4597
- var verifySslCmd = new Command14("verify-ssl").description("Recheck that a domain's SSL certificate is issued and valid (no reissue)").argument("<id>", "Domain ID").action(async (id) => {
4943
+ var verifySslCmd = new Command15("verify-ssl").description("Recheck that a domain's SSL certificate is issued and valid (no reissue)").argument("<id>", "Domain ID").action(async (id) => {
4598
4944
  const sp = spin("Checking certificate\u2026");
4599
4945
  try {
4600
4946
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/verify-ssl`, {
@@ -4609,7 +4955,7 @@ var verifySslCmd = new Command14("verify-ssl").description("Recheck that a domai
4609
4955
  fail2(e);
4610
4956
  }
4611
4957
  });
4612
- var renewAllCmd = new Command14("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
4958
+ var renewAllCmd = new Command15("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
4613
4959
  const sp = spin("Renewing expiring certificates\u2026");
4614
4960
  try {
4615
4961
  const res = await apiRequest("/domains/renew-all", { method: "POST" });
@@ -4631,11 +4977,11 @@ var renewAllCmd = new Command14("renew-all").description("Renew SSL for every ne
4631
4977
  fail2(e);
4632
4978
  }
4633
4979
  });
4634
- var domainCommand = new Command14("domain").description("Manage custom domains, DNS verification, and SSL certificates").addCommand(listCmd4).addCommand(addCmd2).addCommand(previewCmd).addCommand(verifyCmd).addCommand(primaryCmd).addCommand(recordsCmd).addCommand(renewCmd).addCommand(verifySslCmd).addCommand(renewAllCmd);
4980
+ var domainCommand = new Command15("domain").description("Manage custom domains, DNS verification, and SSL certificates").addCommand(listCmd4).addCommand(addCmd2).addCommand(previewCmd).addCommand(verifyCmd).addCommand(primaryCmd).addCommand(recordsCmd).addCommand(renewCmd).addCommand(verifySslCmd).addCommand(renewAllCmd);
4635
4981
 
4636
4982
  // src/commands/server.ts
4637
- import { Command as Command15 } from "commander";
4638
- import chalk11 from "chalk";
4983
+ import { Command as Command16 } from "commander";
4984
+ import chalk12 from "chalk";
4639
4985
  import ora4 from "ora";
4640
4986
  var INSTALLABLE = ["docker", "git", "openresty", "certbot", "rsync"];
4641
4987
  function guard(fn) {
@@ -4667,7 +5013,7 @@ function connBody(o) {
4667
5013
  sshArgs: o.sshArgs
4668
5014
  };
4669
5015
  }
4670
- var server = new Command15("server").description("Manage self-hosted SSH servers");
5016
+ var server = new Command16("server").description("Manage self-hosted SSH servers");
4671
5017
  server.command("list").alias("ls").description("List servers in the active organization").action(
4672
5018
  guard(async () => {
4673
5019
  const servers = await apiRequest("/system/servers");
@@ -4705,31 +5051,31 @@ server.command("rm <id>").alias("remove").description("Delete a server").action(
4705
5051
  );
4706
5052
  server.command("test-connection").alias("test").description("Test an SSH connection without saving it").requiredOption("--host <host>", "SSH host / IP").option("--port <port>", "SSH port", "22").option("--user <user>", "SSH user", "root").option("--auth-method <method>", "Auth method (password|key|agent)").option("--password <password>", "SSH password").option("--key-path <path>", "Path to private key").option("--key-passphrase <passphrase>", "Private key passphrase").option("--jump-host <host>", "SSH jump / bastion host").option("--ssh-args <args>", "Extra raw ssh args").action(
4707
5053
  guard(async (o) => {
4708
- const spinner2 = isJsonMode() ? null : ora4(`Connecting to ${o.host}\u2026`).start();
5054
+ const spinner3 = isJsonMode() ? null : ora4(`Connecting to ${o.host}\u2026`).start();
4709
5055
  try {
4710
5056
  const res = await apiRequest("/system/test-connection", {
4711
5057
  method: "POST",
4712
5058
  body: JSON.stringify(connBody(o))
4713
5059
  });
4714
- spinner2?.stop();
5060
+ spinner3?.stop();
4715
5061
  if (isJsonMode()) return printJson(res);
4716
5062
  if (res.ok) return ok(` ${res.message}`);
4717
5063
  err(` ${res.message}`);
4718
5064
  process.exit(1);
4719
5065
  } catch (e) {
4720
- spinner2?.stop();
5066
+ spinner3?.stop();
4721
5067
  throw e;
4722
5068
  }
4723
5069
  })
4724
5070
  );
4725
5071
  server.command("check <serverId>").description("Run component health checks against a saved server").option("-c, --component <name...>", "Limit to specific components").action(
4726
5072
  guard(async (serverId, o) => {
4727
- const spinner2 = isJsonMode() ? null : ora4("Checking components\u2026").start();
5073
+ const spinner3 = isJsonMode() ? null : ora4("Checking components\u2026").start();
4728
5074
  const res = await apiRequest("/system/check", {
4729
5075
  method: "POST",
4730
5076
  body: JSON.stringify({ serverId, components: o.component })
4731
5077
  });
4732
- spinner2?.stop();
5078
+ spinner3?.stop();
4733
5079
  if (isJsonMode()) return printJson(res);
4734
5080
  printTable(
4735
5081
  res.components.map((c) => ({
@@ -4766,8 +5112,8 @@ server.command("install <serverId>").description("Install components on a server
4766
5112
  printJson({ event: ev.event, ...payload });
4767
5113
  } else if (ev.event === "log") {
4768
5114
  const p = payload;
4769
- const line = ` ${chalk11.dim(`[${p.component}]`)} ${p.message ?? ""}`;
4770
- process.stderr.write((p.level === "error" ? chalk11.red(line) : line) + "\n");
5115
+ const line = ` ${chalk12.dim(`[${p.component}]`)} ${p.message ?? ""}`;
5116
+ process.stderr.write((p.level === "error" ? chalk12.red(line) : line) + "\n");
4771
5117
  } else if (ev.event === "progress") {
4772
5118
  const p = payload;
4773
5119
  if (p.component) info(` ${p.component}: ${p.status}`);
@@ -4785,15 +5131,15 @@ server.command("install <serverId>").description("Install components on a server
4785
5131
  }
4786
5132
  const results = [];
4787
5133
  for (const component of components) {
4788
- const spinner2 = isJsonMode() ? null : ora4(`Installing ${component}\u2026`).start();
5134
+ const spinner3 = isJsonMode() ? null : ora4(`Installing ${component}\u2026`).start();
4789
5135
  const res = await apiRequest(
4790
5136
  "/system/install",
4791
5137
  { method: "POST", body: JSON.stringify({ serverId, component }) }
4792
5138
  );
4793
5139
  results.push(res);
4794
- if (isJsonMode()) spinner2?.stop();
4795
- else if (res.success) spinner2?.succeed(`${component} installed${res.version ? ` (${res.version})` : ""}`);
4796
- else spinner2?.fail(`${component} failed: ${res.error ?? "unknown error"}`);
5140
+ if (isJsonMode()) spinner3?.stop();
5141
+ else if (res.success) spinner3?.succeed(`${component} installed${res.version ? ` (${res.version})` : ""}`);
5142
+ else spinner3?.fail(`${component} failed: ${res.error ?? "unknown error"}`);
4797
5143
  }
4798
5144
  if (isJsonMode()) printJson(results);
4799
5145
  })
@@ -4885,9 +5231,9 @@ function fmtUptime(seconds) {
4885
5231
  var serverCommand = server;
4886
5232
 
4887
5233
  // src/commands/system.ts
4888
- import { Command as Command16 } from "commander";
5234
+ import { Command as Command17 } from "commander";
4889
5235
  import ora5 from "ora";
4890
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
5236
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
4891
5237
  import { createInterface as createInterface6 } from "readline/promises";
4892
5238
  import { stdin as input5, stdout as output5 } from "process";
4893
5239
  async function guarded(fn) {
@@ -4908,8 +5254,8 @@ function report2(obj, human) {
4908
5254
  if (isJsonMode()) printJson(obj);
4909
5255
  else human();
4910
5256
  }
4911
- function spinner(text) {
4912
- return isJsonMode() ? null : ora5(text).start();
5257
+ function spinner(text2) {
5258
+ return isJsonMode() ? null : ora5(text2).start();
4913
5259
  }
4914
5260
  async function confirm2(message, yes) {
4915
5261
  if (yes) return true;
@@ -4933,7 +5279,7 @@ async function promptHidden(query) {
4933
5279
  output5.write("\n");
4934
5280
  return answer;
4935
5281
  }
4936
- var settingsCommand = new Command16("settings").description("Read or update instance settings");
5282
+ var settingsCommand = new Command17("settings").description("Read or update instance settings");
4937
5283
  settingsCommand.command("get").description("Show current instance settings").action(async () => {
4938
5284
  await guarded(async () => {
4939
5285
  const s = await apiRequest("/system/settings");
@@ -4972,7 +5318,7 @@ settingsCommand.command("set").description("Update instance-level settings").opt
4972
5318
  `));
4973
5319
  });
4974
5320
  });
4975
- var onboardingCommand = new Command16("onboarding").description("First-run instance setup");
5321
+ var onboardingCommand = new Command17("onboarding").description("First-run instance setup");
4976
5322
  onboardingCommand.command("apply").description("Configure a fresh instance (fails once already configured)").option("--ssh-host <host>", "SSH host of the target server").option("--ssh-port <n>", "SSH port (default 22)").option("--ssh-user <user>", "SSH user (default root)").option("--ssh-auth-method <method>", "SSH auth method").option("--ssh-password <password>", "SSH password").option("--ssh-key-path <path>", "SSH private key path").option("--ssh-key-passphrase <pass>", "SSH key passphrase").option("--ssh-jump-host <host>", "SSH jump host").option("--ssh-args <args>", "Extra SSH args").option("--server-name <name>", "Display name for the server").option("--auth-mode <mode>", "Initial auth mode: none | local | cloud").option("--tunnel-provider <provider>", "Tunnel provider").option("--tunnel-token <token>", "Tunnel token").option("--default-build-mode <mode>", "Default build mode").option("--default-rollback-window <n>", "Default rollback window").action(async (opts) => {
4977
5323
  await guarded(async () => {
4978
5324
  const body = {
@@ -5006,17 +5352,17 @@ onboardingCommand.command("apply").description("Configure a fresh instance (fail
5006
5352
  }
5007
5353
  });
5008
5354
  });
5009
- var upgradeToAuthCommand = new Command16("upgrade-to-auth").description("Promote a zero-auth instance to email/password login").option("--name <name>", "Account display name").option("--email <email>", "Account email").option("--password <password>", "Account password (prompted if omitted)").option("--use-own-mail-server", "Warm the self-hosted mail server for auth emails").action(async (opts) => {
5355
+ var upgradeToAuthCommand = new Command17("upgrade-to-auth").description("Promote a zero-auth instance to email/password login").option("--name <name>", "Account display name").option("--email <email>", "Account email").option("--password <password>", "Account password (prompted if omitted)").option("--use-own-mail-server", "Warm the self-hosted mail server for auth emails").action(async (opts) => {
5010
5356
  await guarded(async () => {
5011
5357
  const name = opts.name;
5012
5358
  const email = opts.email;
5013
- let password = opts.password;
5014
- if (!password) {
5359
+ let password2 = opts.password;
5360
+ if (!password2) {
5015
5361
  if (!process.stdin.isTTY || isJsonMode()) {
5016
5362
  err("\n --password is required in non-interactive mode.\n");
5017
5363
  process.exit(1);
5018
5364
  }
5019
- password = await promptHidden(" New password: ");
5365
+ password2 = await promptHidden(" New password: ");
5020
5366
  }
5021
5367
  const res = await apiRequest(
5022
5368
  "/system/upgrade-to-auth",
@@ -5025,7 +5371,7 @@ var upgradeToAuthCommand = new Command16("upgrade-to-auth").description("Promote
5025
5371
  body: JSON.stringify({
5026
5372
  name,
5027
5373
  email,
5028
- password,
5374
+ password: password2,
5029
5375
  useOwnMailServer: opts.useOwnMailServer === true
5030
5376
  })
5031
5377
  }
@@ -5035,7 +5381,7 @@ var upgradeToAuthCommand = new Command16("upgrade-to-auth").description("Promote
5035
5381
  `));
5036
5382
  });
5037
5383
  });
5038
- var browseCommand = new Command16("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
5384
+ var browseCommand = new Command17("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
5039
5385
  await guarded(async () => {
5040
5386
  const qs = path2 ? `?path=${encodeURIComponent(path2)}` : "";
5041
5387
  const res = await apiRequest(`/system/browse${qs}`);
@@ -5064,7 +5410,7 @@ function buildDomain(opts) {
5064
5410
  err("\n A domain is required: pass --hostname <host> or --slug <slug>.\n");
5065
5411
  process.exit(1);
5066
5412
  }
5067
- var migrationCommand = new Command16("migration").description("Team-mode migration lifecycle");
5413
+ var migrationCommand = new Command17("migration").description("Team-mode migration lifecycle");
5068
5414
  migrationCommand.command("preflight").description("Read-only readiness check for the own-server migration").requiredOption("--server-id <id>", "Target server id").option("--hostname <host>", "Custom domain pointing at the server").option("--slug <slug>", "Free <slug>.opsh.io subdomain").action(async (opts) => {
5069
5415
  await guarded(async () => {
5070
5416
  const domain = buildDomain(opts);
@@ -5171,7 +5517,7 @@ migrationCommand.command("switch-back").description("Reverse migration back to s
5171
5517
  }
5172
5518
  });
5173
5519
  });
5174
- var dataTransferCommand = new Command16("data-transfer").description(
5520
+ var dataTransferCommand = new Command17("data-transfer").description(
5175
5521
  "Whole-instance export / import (owner-only)"
5176
5522
  );
5177
5523
  dataTransferCommand.command("export").description("Export the entire instance to a JSON file").option("--passphrase <passphrase>", "Seal secrets under this passphrase").option("--out <file>", "Write the export to this file instead of stdout").action(async (opts) => {
@@ -5187,7 +5533,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
5187
5533
  );
5188
5534
  spin4?.succeed("Export ready.");
5189
5535
  if (opts.out) {
5190
- writeFileSync5(opts.out, JSON.stringify(file));
5536
+ writeFileSync6(opts.out, JSON.stringify(file));
5191
5537
  const tables = Object.keys(file.dump?.tables ?? {}).length;
5192
5538
  report2(
5193
5539
  { out: opts.out, tables },
@@ -5241,11 +5587,11 @@ dataTransferCommand.command("import").description("Import an instance export fil
5241
5587
  }
5242
5588
  });
5243
5589
  });
5244
- var systemCommand = new Command16("system").description("Instance settings, onboarding, migration, and data transfer").addCommand(settingsCommand).addCommand(onboardingCommand).addCommand(upgradeToAuthCommand).addCommand(browseCommand).addCommand(migrationCommand).addCommand(dataTransferCommand);
5590
+ var systemCommand = new Command17("system").description("Instance settings, onboarding, migration, and data transfer").addCommand(settingsCommand).addCommand(onboardingCommand).addCommand(upgradeToAuthCommand).addCommand(browseCommand).addCommand(migrationCommand).addCommand(dataTransferCommand);
5245
5591
 
5246
5592
  // src/commands/mail.ts
5247
- import { Command as Command17 } from "commander";
5248
- import chalk12 from "chalk";
5593
+ import { Command as Command18 } from "commander";
5594
+ import chalk13 from "chalk";
5249
5595
  import ora6 from "ora";
5250
5596
  import { createInterface as createInterface7 } from "readline/promises";
5251
5597
  import { stdin as input6, stdout as output6 } from "process";
@@ -5264,8 +5610,8 @@ function guard2(fn) {
5264
5610
  }
5265
5611
  };
5266
5612
  }
5267
- function spin2(text) {
5268
- return isJsonMode() ? null : ora6(text).start();
5613
+ function spin2(text2) {
5614
+ return isJsonMode() ? null : ora6(text2).start();
5269
5615
  }
5270
5616
  function safeParse4(data) {
5271
5617
  try {
@@ -5286,7 +5632,7 @@ function printRecordsObject(records) {
5286
5632
  if (rows.length === 0) return info(" (no DNS records)");
5287
5633
  printTable(rows, ["key", "type", "host", "value"]);
5288
5634
  }
5289
- var stepsCmd = new Command17("steps").description("List the mail setup steps").action(
5635
+ var stepsCmd = new Command18("steps").description("List the mail setup steps").action(
5290
5636
  guard2(async () => {
5291
5637
  const res = await apiRequest(
5292
5638
  "/mail/steps"
@@ -5299,7 +5645,7 @@ var stepsCmd = new Command17("steps").description("List the mail setup steps").a
5299
5645
  info(` ${res.total} steps total.`);
5300
5646
  })
5301
5647
  );
5302
- var statusCmd = new Command17("status").description("Show the setup progress for a mail server").argument("[serverId]", "Mail server ID (omit for the empty welcome shell)").action(
5648
+ var statusCmd = new Command18("status").description("Show the setup progress for a mail server").argument("[serverId]", "Mail server ID (omit for the empty welcome shell)").action(
5303
5649
  guard2(async (serverId) => {
5304
5650
  const q = serverId ? `?serverId=${encodeURIComponent(serverId)}` : "";
5305
5651
  const res = await apiRequest(`/mail/status${q}`);
@@ -5320,7 +5666,7 @@ var statusCmd = new Command17("status").description("Show the setup progress for
5320
5666
  }
5321
5667
  })
5322
5668
  );
5323
- var serversCmd = new Command17("servers").description("List every server the mail stack is installed on").action(
5669
+ var serversCmd = new Command18("servers").description("List every server the mail stack is installed on").action(
5324
5670
  guard2(async () => {
5325
5671
  const res = await apiRequest(
5326
5672
  "/mail/servers"
@@ -5341,7 +5687,7 @@ var serversCmd = new Command17("servers").description("List every server the mai
5341
5687
  );
5342
5688
  })
5343
5689
  );
5344
- var scanCmd = new Command17("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
5690
+ var scanCmd = new Command18("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
5345
5691
  guard2(async (serverId) => {
5346
5692
  const sp = spin2("Scanning server\u2026");
5347
5693
  const res = await apiRequest("/mail/scan", { method: "POST", body: JSON.stringify({ serverId }) });
@@ -5356,7 +5702,7 @@ var scanCmd = new Command17("scan").description("Probe a server for an existing
5356
5702
  else info(" Nothing to adopt on this server.");
5357
5703
  })
5358
5704
  );
5359
- var adoptCmd = new Command17("adopt").description("Re-adopt an existing mail install whose orchestrator state was lost").argument("<serverId>", "Server ID to adopt").action(
5705
+ var adoptCmd = new Command18("adopt").description("Re-adopt an existing mail install whose orchestrator state was lost").argument("<serverId>", "Server ID to adopt").action(
5360
5706
  guard2(async (serverId) => {
5361
5707
  const sp = spin2("Adopting mail server\u2026");
5362
5708
  const res = await apiRequest(
@@ -5369,7 +5715,7 @@ var adoptCmd = new Command17("adopt").description("Re-adopt an existing mail ins
5369
5715
  info(` completed: ${res.completed ? "yes" : "no"}`);
5370
5716
  })
5371
5717
  );
5372
- var setupCmd = new Command17("setup").description("Start or resume the mail setup wizard (streams over SSE)").argument("<serverId>", "Server ID to install the mail stack on").requiredOption("-d, --domain <domain>", "Mail domain (e.g. example.com)").option("--start-step <n>", "Resume from a specific step (1-13)").option("--config <json>", "iRedMail config overrides as a JSON object").action(
5718
+ var setupCmd = new Command18("setup").description("Start or resume the mail setup wizard (streams over SSE)").argument("<serverId>", "Server ID to install the mail stack on").requiredOption("-d, --domain <domain>", "Mail domain (e.g. example.com)").option("--start-step <n>", "Resume from a specific step (1-13)").option("--config <json>", "iRedMail config overrides as a JSON object").action(
5373
5719
  guard2(async (serverId, opts) => {
5374
5720
  let config;
5375
5721
  if (opts.config) {
@@ -5402,7 +5748,7 @@ var setupCmd = new Command17("setup").description("Start or resume the mail setu
5402
5748
  break;
5403
5749
  case "log": {
5404
5750
  const line = ` ${String(p.message ?? "")}`;
5405
- process.stderr.write((p.level === "error" ? chalk12.red(line) : chalk12.dim(line)) + "\n");
5751
+ process.stderr.write((p.level === "error" ? chalk13.red(line) : chalk13.dim(line)) + "\n");
5406
5752
  break;
5407
5753
  }
5408
5754
  case "step_done":
@@ -5438,7 +5784,7 @@ var setupCmd = new Command17("setup").description("Start or resume the mail setu
5438
5784
  if (failed) process.exit(1);
5439
5785
  })
5440
5786
  );
5441
- var cancelCmd = new Command17("cancel").description("Cancel the mail setup currently running").action(
5787
+ var cancelCmd = new Command18("cancel").description("Cancel the mail setup currently running").action(
5442
5788
  guard2(async () => {
5443
5789
  const res = await apiRequest("/mail/setup/cancel", {
5444
5790
  method: "POST"
@@ -5448,7 +5794,7 @@ var cancelCmd = new Command17("cancel").description("Cancel the mail setup curre
5448
5794
  })
5449
5795
  );
5450
5796
  function ackCommand(name, path2, description, successMsg) {
5451
- return new Command17(name).description(description).argument("<serverId>", "Mail server ID").action(
5797
+ return new Command18(name).description(description).argument("<serverId>", "Mail server ID").action(
5452
5798
  guard2(async (serverId) => {
5453
5799
  const res = await apiRequest(path2, {
5454
5800
  method: "POST",
@@ -5471,7 +5817,7 @@ var ptrAckCmd = ackCommand(
5471
5817
  "Acknowledge that reverse DNS (PTR) is configured",
5472
5818
  "PTR acknowledged. Re-run `mail setup` with --start-step to continue."
5473
5819
  );
5474
- var resetCmd = new Command17("reset").description("Wipe the on-server setup state file (does NOT touch installed daemons)").argument("<serverId>", "Mail server ID").option("-y, --yes", "Skip the confirmation prompt").action(
5820
+ var resetCmd = new Command18("reset").description("Wipe the on-server setup state file (does NOT touch installed daemons)").argument("<serverId>", "Mail server ID").option("-y, --yes", "Skip the confirmation prompt").action(
5475
5821
  guard2(async (serverId, opts) => {
5476
5822
  if (!opts.yes && !isJsonMode()) {
5477
5823
  const rl = createInterface7({ input: input6, output: output6 });
@@ -5487,7 +5833,7 @@ var resetCmd = new Command17("reset").description("Wipe the on-server setup stat
5487
5833
  ok(" Setup state reset.");
5488
5834
  })
5489
5835
  );
5490
- var forgetCmd = new Command17("forget").description("Stop managing a mail server (drops the DB row; leaves the stack + state intact)").argument("<serverId>", "Mail server ID").action(
5836
+ var forgetCmd = new Command18("forget").description("Stop managing a mail server (drops the DB row; leaves the stack + state intact)").argument("<serverId>", "Mail server ID").action(
5491
5837
  guard2(async (serverId) => {
5492
5838
  const res = await apiRequest(`/mail/servers/${encodeURIComponent(serverId)}`, {
5493
5839
  method: "DELETE"
@@ -5496,7 +5842,7 @@ var forgetCmd = new Command17("forget").description("Stop managing a mail server
5496
5842
  ok(` Forgot mail server ${serverId} (re-adopt with \`mail scan\` + \`mail adopt\`).`);
5497
5843
  })
5498
5844
  );
5499
- var healthCmd = new Command17("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
5845
+ var healthCmd = new Command18("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
5500
5846
  guard2(async (serverId) => {
5501
5847
  const sp = spin2("Checking mail daemons\u2026");
5502
5848
  const res = await apiRequest(`/mail/health/${encodeURIComponent(serverId)}`);
@@ -5513,7 +5859,7 @@ var healthCmd = new Command17("health").description("Show live status of every m
5513
5859
  );
5514
5860
  })
5515
5861
  );
5516
- var logsCmd3 = new Command17("logs").description("Tail a mail component's journal (snapshot)").argument("<serverId>", "Mail server ID").argument("<component>", "Component key (postfix|dovecot|amavis|clamav|iredapd|postgresql|\u2026)").option("-n, --lines <n>", "Number of lines (max 1000)", "200").action(
5862
+ var logsCmd3 = new Command18("logs").description("Tail a mail component's journal (snapshot)").argument("<serverId>", "Mail server ID").argument("<component>", "Component key (postfix|dovecot|amavis|clamav|iredapd|postgresql|\u2026)").option("-n, --lines <n>", "Number of lines (max 1000)", "200").action(
5517
5863
  guard2(async (serverId, component, opts) => {
5518
5864
  const res = await apiRequest(
5519
5865
  `/mail/admin/${encodeURIComponent(serverId)}/components/${encodeURIComponent(component)}/logs?lines=${encodeURIComponent(opts.lines)}`
@@ -5523,38 +5869,38 @@ var logsCmd3 = new Command17("logs").description("Tail a mail component's journa
5523
5869
  for (const line of res.lines) process.stdout.write(line + "\n");
5524
5870
  })
5525
5871
  );
5526
- var postmasterCmd = new Command17("postmaster").description("Manage the postmaster mailbox");
5872
+ var postmasterCmd = new Command18("postmaster").description("Manage the postmaster mailbox");
5527
5873
  postmasterCmd.addCommand(
5528
- new Command17("set-password").description("Rotate the postmaster password").argument("<serverId>", "Mail server ID").option("--password <password>", "New password (min 12 chars); prompted if omitted").action(
5874
+ new Command18("set-password").description("Rotate the postmaster password").argument("<serverId>", "Mail server ID").option("--password <password>", "New password (min 12 chars); prompted if omitted").action(
5529
5875
  guard2(async (serverId, opts) => {
5530
- let password = opts.password;
5531
- if (!password) {
5876
+ let password2 = opts.password;
5877
+ if (!password2) {
5532
5878
  if (isJsonMode()) {
5533
5879
  err(" --password is required in JSON mode.");
5534
5880
  process.exit(1);
5535
5881
  }
5536
5882
  const rl = createInterface7({ input: input6, output: output6 });
5537
- password = (await rl.question(" New postmaster password (min 12 chars): ")).trim();
5883
+ password2 = (await rl.question(" New postmaster password (min 12 chars): ")).trim();
5538
5884
  rl.close();
5539
5885
  }
5540
- if (!password || password.length < 12) {
5886
+ if (!password2 || password2.length < 12) {
5541
5887
  err(" Password must be at least 12 characters.");
5542
5888
  process.exit(1);
5543
5889
  }
5544
5890
  const sp = spin2("Updating postmaster password\u2026");
5545
5891
  const res = await apiRequest("/mail/credentials/postmaster", {
5546
5892
  method: "POST",
5547
- body: JSON.stringify({ serverId, password })
5893
+ body: JSON.stringify({ serverId, password: password2 })
5548
5894
  });
5549
5895
  sp?.succeed("Postmaster password updated.");
5550
5896
  if (isJsonMode()) printJson(res);
5551
5897
  })
5552
5898
  )
5553
5899
  );
5554
- var mailCommand = new Command17("mail").description("Self-hosted mail server (iRedMail) setup and admin [self-host]").addCommand(stepsCmd).addCommand(statusCmd).addCommand(serversCmd).addCommand(scanCmd).addCommand(adoptCmd).addCommand(setupCmd).addCommand(cancelCmd).addCommand(dnsAckCmd).addCommand(ptrAckCmd).addCommand(resetCmd).addCommand(forgetCmd).addCommand(healthCmd).addCommand(logsCmd3).addCommand(postmasterCmd);
5900
+ var mailCommand = new Command18("mail").description("Self-hosted mail server (iRedMail) setup and admin [self-host]").addCommand(stepsCmd).addCommand(statusCmd).addCommand(serversCmd).addCommand(scanCmd).addCommand(adoptCmd).addCommand(setupCmd).addCommand(cancelCmd).addCommand(dnsAckCmd).addCommand(ptrAckCmd).addCommand(resetCmd).addCommand(forgetCmd).addCommand(healthCmd).addCommand(logsCmd3).addCommand(postmasterCmd);
5555
5901
 
5556
5902
  // src/commands/backup.ts
5557
- import { Command as Command18 } from "commander";
5903
+ import { Command as Command19 } from "commander";
5558
5904
  import ora7 from "ora";
5559
5905
  import { readFileSync as readFileSync6 } from "fs";
5560
5906
  async function guard3(fn) {
@@ -5600,7 +5946,7 @@ function fmtBytes2(n) {
5600
5946
  return `${v.toFixed(i === 0 ? 0 : 1)}${units[i]}`;
5601
5947
  }
5602
5948
  async function followStream(path2, label) {
5603
- const spinner2 = isJsonMode() ? null : ora7(`${label}: connecting\u2026`).start();
5949
+ const spinner3 = isJsonMode() ? null : ora7(`${label}: connecting\u2026`).start();
5604
5950
  let status = "unknown";
5605
5951
  try {
5606
5952
  for await (const ev of sseRequest(path2)) {
@@ -5615,31 +5961,31 @@ async function followStream(path2, label) {
5615
5961
  if (type === "snapshot") {
5616
5962
  const rec = payload.run ?? payload.restore;
5617
5963
  status = rec?.status ?? status;
5618
- if (spinner2) spinner2.text = `${label}: ${status}`;
5964
+ if (spinner3) spinner3.text = `${label}: ${status}`;
5619
5965
  } else if (type === "transition") {
5620
5966
  status = payload.status ?? status;
5621
5967
  const bytes = payload.bytesTransferred ?? payload.bytesRestored;
5622
- if (spinner2) spinner2.text = `${label}: ${status}${bytes ? ` (${fmtBytes2(bytes)})` : ""}`;
5968
+ if (spinner3) spinner3.text = `${label}: ${status}${bytes ? ` (${fmtBytes2(bytes)})` : ""}`;
5623
5969
  } else if (type === "progress") {
5624
5970
  const artifact = payload.currentArtifact ?? "working";
5625
5971
  const bytes = payload.bytesTransferred;
5626
- if (spinner2) spinner2.text = `${label}: ${artifact} ${fmtBytes2(bytes)}`.trimEnd();
5972
+ if (spinner3) spinner3.text = `${label}: ${artifact} ${fmtBytes2(bytes)}`.trimEnd();
5627
5973
  } else if (type === "complete") {
5628
5974
  status = payload.status ?? status;
5629
5975
  const errMsg = payload.errorMessage;
5630
- if (status === "succeeded") spinner2?.succeed(`${label} succeeded`);
5631
- else spinner2?.fail(`${label} ${status}${errMsg ? `: ${errMsg}` : ""}`);
5976
+ if (status === "succeeded") spinner3?.succeed(`${label} succeeded`);
5977
+ else spinner3?.fail(`${label} ${status}${errMsg ? `: ${errMsg}` : ""}`);
5632
5978
  break;
5633
5979
  }
5634
5980
  }
5635
5981
  } catch (e) {
5636
- spinner2?.stop();
5982
+ spinner3?.stop();
5637
5983
  throw e;
5638
5984
  }
5639
- spinner2?.stop();
5985
+ spinner3?.stop();
5640
5986
  return status;
5641
5987
  }
5642
- var policyCmd = new Command18("policy").description("Backup policies (schedules) for a project");
5988
+ var policyCmd = new Command19("policy").description("Backup policies (schedules) for a project");
5643
5989
  policyCmd.command("list").description("List backup policies for a project").requiredOption("--project <id>", "Project ID").action(
5644
5990
  (opts) => guard3(async () => {
5645
5991
  const { data } = await apiRequest(
@@ -5709,7 +6055,7 @@ policyCmd.command("run").description("Trigger a policy's backup now").argument("
5709
6055
  }
5710
6056
  })
5711
6057
  );
5712
- var runCmd = new Command18("run").description("Backup runs (executions)");
6058
+ var runCmd = new Command19("run").description("Backup runs (executions)");
5713
6059
  runCmd.command("list").description("List backup runs for a project").requiredOption("--project <id>", "Project ID").option("--service <id>", "Filter to a single service").option("--limit <n>", "Max rows (default 50)").action(
5714
6060
  (opts) => guard3(async () => {
5715
6061
  const qs = new URLSearchParams();
@@ -5785,7 +6131,7 @@ runCmd.command("restore").description("Prepare a restore from a run (stages it;
5785
6131
  }
5786
6132
  })
5787
6133
  );
5788
- var restoreCmd = new Command18("restore").description("Manage staged restores");
6134
+ var restoreCmd = new Command19("restore").description("Manage staged restores");
5789
6135
  restoreCmd.command("apply").description("Apply a staged restore (destructive)").argument("<restoreId>", "Restore ID from `backup run restore`").requiredOption("--token <token>", "Confirmation token from prepare").option("--follow", "Stream the restore to completion").action(
5790
6136
  (restoreId, opts) => guard3(async () => {
5791
6137
  await apiRequest(
@@ -5828,7 +6174,7 @@ restoreCmd.command("get").description("Show one restore (optionally stream it)")
5828
6174
  show(data);
5829
6175
  })
5830
6176
  );
5831
- var destinationCmd = new Command18("destination").description("Backup destinations (storage targets)");
6177
+ var destinationCmd = new Command19("destination").description("Backup destinations (storage targets)");
5832
6178
  destinationCmd.command("list").description("List backup destinations").action(
5833
6179
  () => guard3(async () => {
5834
6180
  const { data } = await apiRequest("/backup-destinations");
@@ -5884,7 +6230,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
5884
6230
  );
5885
6231
  destinationCmd.command("preflight").description("Verify a destination (write + read + delete a probe object)").argument("<destinationId>", "Destination ID").action(
5886
6232
  (destinationId) => guard3(async () => {
5887
- const spinner2 = isJsonMode() ? null : ora7("Running preflight\u2026").start();
6233
+ const spinner3 = isJsonMode() ? null : ora7("Running preflight\u2026").start();
5888
6234
  const { data } = await apiRequest(
5889
6235
  `/backup-destinations/${encodeURIComponent(destinationId)}/preflight`,
5890
6236
  { method: "POST", body: JSON.stringify({}) }
@@ -5893,28 +6239,28 @@ destinationCmd.command("preflight").description("Verify a destination (write + r
5893
6239
  printJson(data);
5894
6240
  return;
5895
6241
  }
5896
- if (data.ok) spinner2?.succeed("Destination reachable");
6242
+ if (data.ok) spinner3?.succeed("Destination reachable");
5897
6243
  else {
5898
- spinner2?.fail(`Preflight failed: ${data.reason ?? "unknown"}`);
6244
+ spinner3?.fail(`Preflight failed: ${data.reason ?? "unknown"}`);
5899
6245
  process.exit(1);
5900
6246
  }
5901
6247
  })
5902
6248
  );
5903
- var backupCommand = new Command18("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
6249
+ var backupCommand = new Command19("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
5904
6250
 
5905
6251
  // src/commands/token.ts
5906
- import { Command as Command19 } from "commander";
5907
- import chalk14 from "chalk";
6252
+ import { Command as Command20 } from "commander";
6253
+ import chalk15 from "chalk";
5908
6254
 
5909
6255
  // src/lib/cmd-helpers.ts
5910
- import chalk13 from "chalk";
6256
+ import chalk14 from "chalk";
5911
6257
  import ora8 from "ora";
5912
- function spin3(text) {
5913
- return isJsonMode() ? null : ora8(text).start();
6258
+ function spin3(text2) {
6259
+ return isJsonMode() ? null : ora8(text2).start();
5914
6260
  }
5915
6261
  function fail3(e) {
5916
6262
  if (e instanceof ApiError) {
5917
- err(` ${e.message}${e.status ? chalk13.dim(` (${e.status})`) : ""}`);
6263
+ err(` ${e.message}${e.status ? chalk14.dim(` (${e.status})`) : ""}`);
5918
6264
  } else {
5919
6265
  err(` ${e instanceof Error ? e.message : String(e)}`);
5920
6266
  }
@@ -5931,7 +6277,7 @@ function collectGrant(value, acc) {
5931
6277
  acc.push({ resourceType, resourceId, permissions });
5932
6278
  return acc;
5933
6279
  }
5934
- var listCmd5 = new Command19("list").description("List your personal access tokens").action(async () => {
6280
+ var listCmd5 = new Command20("list").description("List your personal access tokens").action(async () => {
5935
6281
  try {
5936
6282
  const res = await apiRequest("/tokens");
5937
6283
  const rows = res.data ?? [];
@@ -5956,7 +6302,7 @@ var listCmd5 = new Command19("list").description("List your personal access toke
5956
6302
  fail3(e);
5957
6303
  }
5958
6304
  });
5959
- var createCmd3 = new Command19("create").description("Mint a new personal access token (the secret is shown once)").argument("<name>", "Human-readable token name").option("--read-only", "Reject mutation methods (POST/PUT/PATCH/DELETE)", false).option("--expires <days>", "Expire after N days (1\u2013365); omit for non-expiring", (v) => parseInt(v, 10)).option(
6305
+ var createCmd3 = new Command20("create").description("Mint a new personal access token (the secret is shown once)").argument("<name>", "Human-readable token name").option("--read-only", "Reject mutation methods (POST/PUT/PATCH/DELETE)", false).option("--expires <days>", "Expire after N days (1\u2013365); omit for non-expiring", (v) => parseInt(v, 10)).option(
5960
6306
  "--grant <type:id:perms>",
5961
6307
  "Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
5962
6308
  collectGrant,
@@ -5981,14 +6327,14 @@ var createCmd3 = new Command19("create").description("Mint a new personal access
5981
6327
  return;
5982
6328
  }
5983
6329
  info(" Copy this token now \u2014 it will not be shown again:");
5984
- process.stdout.write(chalk14.cyan(` ${res.data.token}
6330
+ process.stdout.write(chalk15.cyan(` ${res.data.token}
5985
6331
  `));
5986
6332
  } catch (e) {
5987
6333
  sp?.fail("Create failed");
5988
6334
  fail3(e);
5989
6335
  }
5990
6336
  });
5991
- var revokeCmd = new Command19("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
6337
+ var revokeCmd = new Command20("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
5992
6338
  const sp = spin3("Revoking token\u2026");
5993
6339
  try {
5994
6340
  await apiRequest(`/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
@@ -6000,11 +6346,11 @@ var revokeCmd = new Command19("revoke").description("Revoke one of your tokens")
6000
6346
  fail3(e);
6001
6347
  }
6002
6348
  });
6003
- var tokenCommand = new Command19("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
6349
+ var tokenCommand = new Command20("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
6004
6350
 
6005
6351
  // src/commands/api.ts
6006
- import { Command as Command20 } from "commander";
6007
- var apiCommand = new Command20("api").description("Make an authenticated request to any Openship API route (like `gh api`)").argument("<path>", "Path under /api, e.g. /projects or /deployments/<id>").option("-X, --method <method>", "HTTP method (defaults to GET, or POST when --data is given)").option("-d, --data <json>", "Request body as a JSON string").option("-q, --query <kv...>", "Query parameter key=value (repeatable)").action(async (path2, opts) => {
6352
+ import { Command as Command21 } from "commander";
6353
+ var apiCommand = new Command21("api").description("Make an authenticated request to any Openship API route (like `gh api`)").argument("<path>", "Path under /api, e.g. /projects or /deployments/<id>").option("-X, --method <method>", "HTTP method (defaults to GET, or POST when --data is given)").option("-d, --data <json>", "Request body as a JSON string").option("-q, --query <kv...>", "Query parameter key=value (repeatable)").action(async (path2, opts) => {
6008
6354
  const method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
6009
6355
  let url = path2.startsWith("/") ? path2 : `/${path2}`;
6010
6356
  if (opts.query?.length) {
@@ -6019,10 +6365,10 @@ var apiCommand = new Command20("api").description("Make an authenticated request
6019
6365
  if (opts.data) init.body = opts.data;
6020
6366
  try {
6021
6367
  const res = await apiRaw(url, init);
6022
- const text = await res.text();
6023
- let body = text;
6368
+ const text2 = await res.text();
6369
+ let body = text2;
6024
6370
  try {
6025
- body = JSON.parse(text);
6371
+ body = JSON.parse(text2);
6026
6372
  } catch {
6027
6373
  }
6028
6374
  if (typeof body === "string") {
@@ -6038,11 +6384,11 @@ var apiCommand = new Command20("api").description("Make an authenticated request
6038
6384
  });
6039
6385
 
6040
6386
  // src/commands/install.ts
6041
- import { Command as Command21 } from "commander";
6042
- import { chmodSync as chmodSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
6043
- import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
6044
- import { homedir as homedir4 } from "os";
6045
- import { join as join8 } from "path";
6387
+ import { Command as Command22 } from "commander";
6388
+ import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
6389
+ import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
6390
+ import { homedir as homedir5 } from "os";
6391
+ import { join as join9 } from "path";
6046
6392
  import ora9 from "ora";
6047
6393
  function assetForPlatform() {
6048
6394
  const { platform, arch } = process;
@@ -6054,33 +6400,33 @@ function assetForPlatform() {
6054
6400
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
6055
6401
  }
6056
6402
  function installDmg(dmg) {
6057
- const homeApps = join8(homedir4(), "Applications");
6403
+ const homeApps = join9(homedir5(), "Applications");
6058
6404
  let dest = homeApps;
6059
6405
  try {
6060
- mkdirSync6(homeApps, { recursive: true });
6406
+ mkdirSync7(homeApps, { recursive: true });
6061
6407
  } catch {
6062
6408
  dest = "/Applications";
6063
6409
  }
6064
- const attach = spawnSync3("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6410
+ const attach = spawnSync4("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6065
6411
  encoding: "utf8"
6066
6412
  });
6067
6413
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
6068
6414
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
6069
6415
  if (!mount) throw new Error("Could not determine the mounted volume");
6070
- let target = join8(dest, "Openship.app");
6416
+ let target = join9(dest, "Openship.app");
6071
6417
  try {
6072
- const appInDmg = join8(mount, "Openship.app");
6073
- if (!existsSync8(appInDmg)) throw new Error("Openship.app not found in the disk image");
6074
- spawnSync3("rm", ["-rf", target]);
6075
- let copy = spawnSync3("ditto", [appInDmg, target], { encoding: "utf8" });
6418
+ const appInDmg = join9(mount, "Openship.app");
6419
+ if (!existsSync9(appInDmg)) throw new Error("Openship.app not found in the disk image");
6420
+ spawnSync4("rm", ["-rf", target]);
6421
+ let copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
6076
6422
  if (copy.status !== 0 && dest === homeApps) {
6077
- target = join8("/Applications", "Openship.app");
6078
- spawnSync3("rm", ["-rf", target]);
6079
- copy = spawnSync3("ditto", [appInDmg, target], { encoding: "utf8" });
6423
+ target = join9("/Applications", "Openship.app");
6424
+ spawnSync4("rm", ["-rf", target]);
6425
+ copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
6080
6426
  }
6081
6427
  if (copy.status !== 0) throw new Error(`ditto copy failed: ${copy.stderr?.trim()}`);
6082
6428
  } finally {
6083
- spawnSync3("hdiutil", ["detach", mount, "-quiet"]);
6429
+ spawnSync4("hdiutil", ["detach", mount, "-quiet"]);
6084
6430
  }
6085
6431
  return target;
6086
6432
  }
@@ -6089,10 +6435,10 @@ function installAppImage(appImage) {
6089
6435
  return appImage;
6090
6436
  }
6091
6437
  function installZip(zip) {
6092
- const localAppData = process.env.LOCALAPPDATA || join8(homedir4(), "AppData", "Local");
6093
- const target = join8(localAppData, "Programs", "Openship");
6094
- mkdirSync6(target, { recursive: true });
6095
- const expand = spawnSync3(
6438
+ const localAppData = process.env.LOCALAPPDATA || join9(homedir5(), "AppData", "Local");
6439
+ const target = join9(localAppData, "Programs", "Openship");
6440
+ mkdirSync7(target, { recursive: true });
6441
+ const expand = spawnSync4(
6096
6442
  "powershell",
6097
6443
  [
6098
6444
  "-NoProfile",
@@ -6107,7 +6453,7 @@ function installZip(zip) {
6107
6453
  }
6108
6454
  function launch(kind, target) {
6109
6455
  if (kind === "dmg") {
6110
- spawnSync3("open", [target]);
6456
+ spawnSync4("open", [target]);
6111
6457
  return;
6112
6458
  }
6113
6459
  if (kind === "appimage") {
@@ -6118,11 +6464,11 @@ function launch(kind, target) {
6118
6464
  child.unref();
6119
6465
  return;
6120
6466
  }
6121
- const exe = join8(target, "Openship.exe");
6122
- const path2 = existsSync8(exe) ? exe : target;
6123
- spawnSync3("cmd", ["/c", "start", "", path2]);
6467
+ const exe = join9(target, "Openship.exe");
6468
+ const path2 = existsSync9(exe) ? exe : target;
6469
+ spawnSync4("cmd", ["/c", "start", "", path2]);
6124
6470
  }
6125
- var installCommand = new Command21("install").description("Download and install the Openship desktop app for this OS").option("--version <tag>", "Release tag to install (e.g. v1.2.3)").option("--latest", "Install the latest release (default)").option("--force", "Re-download even if a verified copy is cached").option("--no-verify", "Skip SHA-256 verification (allowed when no sidecar exists)").option("--no-launch", "Install without launching the app").action(async (opts) => {
6471
+ var installCommand = new Command22("install").description("Download and install the Openship desktop app for this OS").option("--version <tag>", "Release tag to install (e.g. v1.2.3)").option("--latest", "Install the latest release (default)").option("--force", "Re-download even if a verified copy is cached").option("--no-verify", "Skip SHA-256 verification (allowed when no sidecar exists)").option("--no-launch", "Install without launching the app").action(async (opts) => {
6126
6472
  let asset;
6127
6473
  try {
6128
6474
  asset = assetForPlatform();
@@ -6130,7 +6476,7 @@ var installCommand = new Command21("install").description("Download and install
6130
6476
  err(e.message);
6131
6477
  process.exit(1);
6132
6478
  }
6133
- const spin4 = (text) => isJsonMode() ? null : ora9(text).start();
6479
+ const spin4 = (text2) => isJsonMode() ? null : ora9(text2).start();
6134
6480
  let tag;
6135
6481
  try {
6136
6482
  if (opts.version) {
@@ -6145,13 +6491,13 @@ var installCommand = new Command21("install").description("Download and install
6145
6491
  process.exit(1);
6146
6492
  }
6147
6493
  const dir = releaseDir(tag);
6148
- const assetPath = join8(dir, asset.name);
6494
+ const assetPath = join9(dir, asset.name);
6149
6495
  const sidecarPath = `${assetPath}.sha256`;
6150
6496
  const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
6151
6497
  const sidecarUrl = `${assetUrl2}.sha256`;
6152
6498
  let downloaded = false;
6153
6499
  let sha;
6154
- const cachedUsable = !opts.force && existsSync8(assetPath) && (existsSync8(sidecarPath) || opts.verify === false);
6500
+ const cachedUsable = !opts.force && existsSync9(assetPath) && (existsSync9(sidecarPath) || opts.verify === false);
6155
6501
  if (cachedUsable) {
6156
6502
  info(` Using cached ${asset.name} (${tag}).`);
6157
6503
  } else {
@@ -6173,7 +6519,7 @@ var installCommand = new Command21("install").description("Download and install
6173
6519
  const s2 = spin4("Verifying checksum\u2026");
6174
6520
  try {
6175
6521
  let sidecarBody;
6176
- if (existsSync8(sidecarPath) && !downloaded) {
6522
+ if (existsSync9(sidecarPath) && !downloaded) {
6177
6523
  sidecarBody = readFileSync7(sidecarPath, "utf8");
6178
6524
  } else {
6179
6525
  sidecarBody = await fetchSidecar(sidecarUrl);
@@ -6197,8 +6543,8 @@ var installCommand = new Command21("install").description("Download and install
6197
6543
  err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
6198
6544
  process.exit(1);
6199
6545
  }
6200
- mkdirSync6(dir, { recursive: true });
6201
- writeFileSync6(sidecarPath, sidecarBody);
6546
+ mkdirSync7(dir, { recursive: true });
6547
+ writeFileSync7(sidecarPath, sidecarBody);
6202
6548
  s2?.succeed("Checksum verified");
6203
6549
  } catch (e) {
6204
6550
  s2?.fail("Verification failed");
@@ -6237,15 +6583,15 @@ var installCommand = new Command21("install").description("Download and install
6237
6583
  });
6238
6584
 
6239
6585
  // src/commands/update.ts
6240
- import { Command as Command22 } from "commander";
6241
- import { spawnSync as spawnSync4 } from "child_process";
6586
+ import { Command as Command23 } from "commander";
6587
+ import { spawnSync as spawnSync5 } from "child_process";
6242
6588
  function detectPackageManager2(override) {
6243
6589
  if (override === "bun" || override === "npm") return override;
6244
- const hasBun = spawnSync4("bun", ["--version"], { stdio: "ignore" }).status === 0;
6590
+ const hasBun = spawnSync5("bun", ["--version"], { stdio: "ignore" }).status === 0;
6245
6591
  return hasBun ? "bun" : "npm";
6246
6592
  }
6247
- var updateCommand = new Command22("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").action(async (opts) => {
6248
- const current = "0.1.10";
6593
+ var updateCommand = new Command23("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").action(async (opts) => {
6594
+ const current = "0.2.0";
6249
6595
  let latest;
6250
6596
  try {
6251
6597
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -6273,32 +6619,35 @@ var updateCommand = new Command22("update").description("Update the Openship CLI
6273
6619
  const ref = `openship@${latest}`;
6274
6620
  const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
6275
6621
  info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
6276
- const res = spawnSync4(pm, argv, { stdio: "inherit" });
6622
+ const res = spawnSync5(pm, argv, { stdio: "inherit" });
6277
6623
  if (res.status !== 0) {
6278
6624
  err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
6279
6625
  process.exitCode = 1;
6280
6626
  return;
6281
6627
  }
6628
+ const { restarted } = restart();
6282
6629
  if (isJsonMode()) {
6283
- printJson({ updated: true, from: current, to: latest, via: pm });
6630
+ printJson({ updated: true, from: current, to: latest, via: pm, restarted });
6631
+ } else if (restarted) {
6632
+ ok(`Updated to v${latest} and restarted the service \u2014 you're on the new version.`);
6284
6633
  } else {
6285
6634
  ok(`Updated to v${latest}. Restart the server to run the new version: openship up`);
6286
6635
  }
6287
6636
  });
6288
6637
 
6289
6638
  // src/commands/cache.ts
6290
- import { Command as Command23 } from "commander";
6291
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8, rmSync as rmSync3, statSync } from "fs";
6292
- import { join as join9 } from "path";
6639
+ import { Command as Command24 } from "commander";
6640
+ import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync8, rmSync as rmSync4, statSync } from "fs";
6641
+ import { join as join10 } from "path";
6293
6642
  function listAssets() {
6294
- if (!existsSync9(RELEASES_DIR)) return [];
6643
+ if (!existsSync10(RELEASES_DIR)) return [];
6295
6644
  const out = [];
6296
6645
  for (const tag of readdirSync(RELEASES_DIR)) {
6297
6646
  const dir = releaseDir(tag);
6298
6647
  if (!statSync(dir).isDirectory()) continue;
6299
6648
  for (const name of readdirSync(dir)) {
6300
6649
  if (name.endsWith(".sha256")) continue;
6301
- const path2 = join9(dir, name);
6650
+ const path2 = join10(dir, name);
6302
6651
  const st = statSync(path2);
6303
6652
  if (!st.isFile()) continue;
6304
6653
  out.push({
@@ -6306,17 +6655,17 @@ function listAssets() {
6306
6655
  name,
6307
6656
  path: path2,
6308
6657
  size: st.size,
6309
- hasSidecar: existsSync9(`${path2}.sha256`)
6658
+ hasSidecar: existsSync10(`${path2}.sha256`)
6310
6659
  });
6311
6660
  }
6312
6661
  }
6313
6662
  return out;
6314
6663
  }
6315
- var pathCmd = new Command23("path").description("Print the cache directory path").action(() => {
6664
+ var pathCmd = new Command24("path").description("Print the cache directory path").action(() => {
6316
6665
  if (isJsonMode()) printJson({ path: CACHE_DIR });
6317
6666
  else process.stdout.write(CACHE_DIR + "\n");
6318
6667
  });
6319
- var listCmd6 = new Command23("list").alias("ls").description("List cached release assets").action(() => {
6668
+ var listCmd6 = new Command24("list").alias("ls").description("List cached release assets").action(() => {
6320
6669
  const assets = listAssets();
6321
6670
  printTable(
6322
6671
  assets.map((a) => ({
@@ -6328,7 +6677,7 @@ var listCmd6 = new Command23("list").alias("ls").description("List cached releas
6328
6677
  ["tag", "asset", "size", "sidecar"]
6329
6678
  );
6330
6679
  });
6331
- var verifyCmd2 = new Command23("verify").description("Re-hash cached assets and compare to their .sha256 sidecar").argument("[tag]", "Only verify assets under this release tag").action(async (tag) => {
6680
+ var verifyCmd2 = new Command24("verify").description("Re-hash cached assets and compare to their .sha256 sidecar").argument("[tag]", "Only verify assets under this release tag").action(async (tag) => {
6332
6681
  const assets = listAssets().filter((a) => !tag || a.tag === tag);
6333
6682
  const results = [];
6334
6683
  let bad = 0;
@@ -6351,30 +6700,478 @@ var verifyCmd2 = new Command23("verify").description("Re-hash cached assets and
6351
6700
  }
6352
6701
  if (bad > 0) process.exit(1);
6353
6702
  });
6354
- var cleanCmd = new Command23("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
6703
+ var cleanCmd = new Command24("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
6355
6704
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
6356
- if (!existsSync9(target)) {
6705
+ if (!existsSync10(target)) {
6357
6706
  if (isJsonMode()) printJson({ removed: false, path: target });
6358
6707
  else info(` Nothing to clean (${target}).`);
6359
6708
  return;
6360
6709
  }
6361
- rmSync3(target, { recursive: true, force: true });
6710
+ rmSync4(target, { recursive: true, force: true });
6362
6711
  if (isJsonMode()) printJson({ removed: true, path: target });
6363
6712
  else ok(`
6364
6713
  Removed ${target}
6365
6714
  `);
6366
6715
  });
6367
- var cacheCommand = new Command23("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
6716
+ var cacheCommand = new Command24("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
6368
6717
  err("Specify a subcommand: path | list | verify | clean");
6369
6718
  process.exit(1);
6370
6719
  }).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
6371
6720
 
6721
+ // src/commands/wizard.ts
6722
+ import chalk16 from "chalk";
6723
+ import open from "open";
6724
+ import { createServer } from "http";
6725
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
6726
+ import {
6727
+ intro,
6728
+ outro,
6729
+ text,
6730
+ password,
6731
+ select,
6732
+ confirm as confirm3,
6733
+ spinner as spinner2,
6734
+ note,
6735
+ log,
6736
+ cancel as cancel2,
6737
+ isCancel
6738
+ } from "@clack/prompts";
6739
+ function ensure(value) {
6740
+ if (isCancel(value)) {
6741
+ cancel2("Setup cancelled.");
6742
+ process.exit(0);
6743
+ }
6744
+ return value;
6745
+ }
6746
+ var SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
6747
+ async function internalGet(port, path2) {
6748
+ try {
6749
+ const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
6750
+ headers: { "X-Internal-Token": ensureInternalToken() },
6751
+ signal: AbortSignal.timeout(1e4)
6752
+ });
6753
+ if (!res.ok) return null;
6754
+ return await res.json();
6755
+ } catch {
6756
+ return null;
6757
+ }
6758
+ }
6759
+ async function internalPost(port, path2, body) {
6760
+ try {
6761
+ const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
6762
+ method: "POST",
6763
+ headers: { "Content-Type": "application/json", "X-Internal-Token": ensureInternalToken() },
6764
+ body: JSON.stringify(body),
6765
+ signal: AbortSignal.timeout(3e4)
6766
+ });
6767
+ const data = await res.json().catch(() => ({}));
6768
+ return { ok: res.ok, data };
6769
+ } catch (err2) {
6770
+ return { ok: false, data: { error: err2.message } };
6771
+ }
6772
+ }
6773
+ async function bootstrapAdmin(apiPort, admin) {
6774
+ const { ok: ok3, data } = await internalPost(apiPort, "/api/system/bootstrap-admin", admin);
6775
+ if (ok3) return { ok: true };
6776
+ if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
6777
+ return { ok: false, message: data?.error || "failed" };
6778
+ }
6779
+ async function waitHealthy(apiPort, seconds = 90) {
6780
+ for (let i = 0; i < seconds; i++) {
6781
+ await new Promise((r) => setTimeout(r, 1e3));
6782
+ try {
6783
+ await fetch(`http://127.0.0.1:${apiPort}/api/health`, { signal: AbortSignal.timeout(2e3) });
6784
+ return true;
6785
+ } catch {
6786
+ }
6787
+ }
6788
+ return false;
6789
+ }
6790
+ async function detectPublicIp() {
6791
+ for (const url of ["https://api.ipify.org", "https://ifconfig.me/ip"]) {
6792
+ try {
6793
+ const res = await fetch(url, { signal: AbortSignal.timeout(3e3) });
6794
+ if (!res.ok) continue;
6795
+ const ip = (await res.text()).trim();
6796
+ if (/^[0-9.]+$/.test(ip) || ip.includes(":")) return ip;
6797
+ } catch {
6798
+ }
6799
+ }
6800
+ return null;
6801
+ }
6802
+ var b64url = (buf) => buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6803
+ async function connectOpenshipCloud(port) {
6804
+ const already = await internalGet(port, "/api/system/cloud-status");
6805
+ if (already?.connected) {
6806
+ log.success(`Already connected to Openship Cloud${already.user?.email ? ` as ${already.user.email}` : ""}.`);
6807
+ return true;
6808
+ }
6809
+ const capsEnv = await internalGet(port, "/api/health/env");
6810
+ const cloudApiUrl = capsEnv?.cloudApiUrl;
6811
+ if (!cloudApiUrl) {
6812
+ log.error("Couldn't discover the Openship Cloud URL \u2014 free domain unavailable. Use a custom domain instead.");
6813
+ return false;
6814
+ }
6815
+ const verifier = b64url(randomBytes2(32));
6816
+ const challenge = b64url(createHash2("sha256").update(verifier).digest());
6817
+ const state = b64url(randomBytes2(16));
6818
+ const codePromise = new Promise((resolve2) => {
6819
+ const server2 = createServer((req, res2) => {
6820
+ const u = new URL(req.url || "/", "http://127.0.0.1");
6821
+ if (!u.pathname.startsWith("/callback")) {
6822
+ res2.writeHead(404).end();
6823
+ return;
6824
+ }
6825
+ const code2 = u.searchParams.get("code");
6826
+ const gotState = u.searchParams.get("state");
6827
+ res2.writeHead(200, { "Content-Type": "text/html" }).end(
6828
+ "<html><body style='font:16px system-ui;padding:3rem;text-align:center'><h2>Openship Cloud connected</h2><p>You can close this window and return to your terminal.</p></body></html>"
6829
+ );
6830
+ server2.close();
6831
+ resolve2(code2 && gotState === state ? code2 : null);
6832
+ });
6833
+ server2.on("error", () => resolve2(null));
6834
+ server2.listen(0, "127.0.0.1", () => {
6835
+ const cbPort = server2.address().port;
6836
+ const redirect = `http://127.0.0.1:${cbPort}/callback`;
6837
+ const handoff = `${cloudApiUrl.replace(/\/$/, "")}/api/cloud/connect-handoff?redirect=${encodeURIComponent(redirect)}&state=${state}&code_challenge=${challenge}`;
6838
+ note(handoff, "Open this URL to authorize (opening your browser\u2026)");
6839
+ void open(handoff).catch(() => {
6840
+ });
6841
+ });
6842
+ setTimeout(() => {
6843
+ try {
6844
+ server2.close();
6845
+ } catch {
6846
+ }
6847
+ resolve2(null);
6848
+ }, 3e5);
6849
+ });
6850
+ const s = spinner2();
6851
+ s.start("Waiting for Openship Cloud authorization in your browser");
6852
+ const code = await codePromise;
6853
+ if (!code) {
6854
+ s.stop("Openship Cloud wasn't authorized.", 1);
6855
+ return false;
6856
+ }
6857
+ s.message("Linking this instance to Openship Cloud");
6858
+ const res = await internalPost(port, "/api/system/cloud-connect", { code, codeVerifier: verifier });
6859
+ if (!res.ok) {
6860
+ s.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
6861
+ return false;
6862
+ }
6863
+ s.stop("Connected to Openship Cloud.");
6864
+ return true;
6865
+ }
6866
+ async function promptLocalAdmin() {
6867
+ const name = ensure(await text({ message: "Your name", validate: (v) => v?.trim() ? void 0 : "Required" })).trim();
6868
+ const email = ensure(
6869
+ await text({
6870
+ message: "Email",
6871
+ placeholder: "you@example.com",
6872
+ validate: (v) => v?.includes("@") ? void 0 : "Enter a valid email"
6873
+ })
6874
+ ).trim().toLowerCase();
6875
+ const pw = ensure(
6876
+ await password({ message: "Password", validate: (v) => v && v.length >= 8 ? void 0 : "At least 8 characters" })
6877
+ );
6878
+ ensure(await password({ message: "Confirm password", validate: (v) => v === pw ? void 0 : "Passwords don't match" }));
6879
+ return { name, email, password: pw };
6880
+ }
6881
+ async function streamProvision(port, sessionId, s) {
6882
+ let ok3 = false;
6883
+ try {
6884
+ const res = await fetch(`http://127.0.0.1:${port}/api/system/self-register/stream?id=${sessionId}`, {
6885
+ headers: { "X-Internal-Token": ensureInternalToken() },
6886
+ signal: AbortSignal.timeout(3e5)
6887
+ });
6888
+ if (!res.ok || !res.body) return false;
6889
+ const reader = res.body.getReader();
6890
+ const decoder = new TextDecoder();
6891
+ let buffer = "";
6892
+ for (; ; ) {
6893
+ const { value, done } = await reader.read();
6894
+ if (done) break;
6895
+ buffer += decoder.decode(value, { stream: true });
6896
+ let sep;
6897
+ while ((sep = buffer.indexOf("\n\n")) >= 0) {
6898
+ const frame = buffer.slice(0, sep);
6899
+ buffer = buffer.slice(sep + 2);
6900
+ const event = /event:\s*(.*)/.exec(frame)?.[1]?.trim();
6901
+ const dataRaw = /data:\s*([\s\S]*)/.exec(frame)?.[1]?.trim();
6902
+ if (!event) continue;
6903
+ if (event === "log" && dataRaw) {
6904
+ try {
6905
+ const d = JSON.parse(dataRaw);
6906
+ if (d.message) s.message(String(d.message).replace(/\s+/g, " ").slice(0, 68));
6907
+ } catch {
6908
+ }
6909
+ } else if (event === "complete" && dataRaw) {
6910
+ try {
6911
+ ok3 = JSON.parse(dataRaw).status === "completed";
6912
+ } catch {
6913
+ }
6914
+ } else if (event === "end") {
6915
+ return ok3;
6916
+ }
6917
+ }
6918
+ }
6919
+ } catch {
6920
+ return ok3;
6921
+ }
6922
+ return ok3;
6923
+ }
6924
+ async function runWizard() {
6925
+ intro(`${chalk16.bgCyan(chalk16.black(" Openship "))}${chalk16.dim(" setup")}`);
6926
+ log.message(
6927
+ chalk16.dim(
6928
+ "Deploy Openship on this machine \u2014 a few questions, then it installs itself\nas a service, registers as an app, and prints the URL to log in."
6929
+ )
6930
+ );
6931
+ let publicUrl;
6932
+ let behindProxy = false;
6933
+ let managedEdge = false;
6934
+ let domainPlan = { type: "none" };
6935
+ const reach = ensure(
6936
+ await select({
6937
+ message: "How should this instance be reachable?",
6938
+ initialValue: "private",
6939
+ options: [
6940
+ { value: "private", label: "Private", hint: "this machine only (localhost)" },
6941
+ { value: "public", label: "Public", hint: "a server / VPS, reachable from other machines" }
6942
+ ]
6943
+ })
6944
+ );
6945
+ if (reach === "public") {
6946
+ const canManage = process.platform === "linux";
6947
+ const domainType = ensure(
6948
+ await select({
6949
+ message: "How do you want a domain + HTTPS?",
6950
+ initialValue: "free",
6951
+ options: [
6952
+ { value: "free", label: "Free domain", hint: "name.opsh.io via Openship Cloud \u2014 HTTPS handled for you" },
6953
+ ...canManage ? [{ value: "custom", label: "Custom domain", hint: "your domain + free Let's Encrypt on this box" }] : [],
6954
+ { value: "byo", label: "Bring your own", hint: "your domain, behind your own reverse proxy" }
6955
+ ]
6956
+ })
6957
+ );
6958
+ if (domainType === "free") {
6959
+ const slug = ensure(
6960
+ await text({
6961
+ message: "Choose your subdomain",
6962
+ placeholder: "my-openship",
6963
+ validate: (v) => v && SLUG_RE.test(v.trim().toLowerCase()) ? void 0 : "Lowercase letters, digits, hyphens"
6964
+ })
6965
+ ).trim().toLowerCase();
6966
+ const s2 = spinner2();
6967
+ s2.start("Detecting this server's public IP");
6968
+ const publicHost = await detectPublicIp();
6969
+ s2.stop(publicHost ? `Public IP: ${chalk16.bold(publicHost)}` : "Couldn't detect the public IP automatically.");
6970
+ publicUrl = `https://${slug}.opsh.io`;
6971
+ behindProxy = true;
6972
+ domainPlan = { type: "free", slug, publicHost };
6973
+ } else if (domainType === "custom") {
6974
+ const raw = ensure(
6975
+ await text({
6976
+ message: "Your domain",
6977
+ placeholder: "ops.example.com",
6978
+ validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
6979
+ })
6980
+ );
6981
+ publicUrl = normalizeUrl(raw).replace(/^http:/i, "https:");
6982
+ const hostname = new URL(publicUrl).hostname;
6983
+ managedEdge = true;
6984
+ behindProxy = true;
6985
+ if (typeof process.getuid === "function" && process.getuid() !== 0) {
6986
+ log.warn("Managed HTTPS installs OpenResty + certbot \u2014 that needs root. Re-run with sudo if it can't install.");
6987
+ }
6988
+ const s2 = spinner2();
6989
+ s2.start("Detecting this server's public IP");
6990
+ const ip = await detectPublicIp();
6991
+ s2.stop(ip ? `Public IP: ${chalk16.bold(ip)}` : "Couldn't detect the public IP automatically.");
6992
+ note(
6993
+ `Add a DNS ${chalk16.bold("A record")}:
6994
+
6995
+ ${chalk16.cyan(hostname)} \u2192 ${chalk16.cyan(ip ?? "<this server's public IP>")}
6996
+
6997
+ ` + chalk16.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
6998
+ "DNS"
6999
+ );
7000
+ ensure(await confirm3({ message: "A record set? (continue either way \u2014 it retries)", initialValue: true }));
7001
+ domainPlan = { type: "custom", hostname };
7002
+ } else {
7003
+ const raw = ensure(
7004
+ await text({
7005
+ message: "Your domain (served behind your proxy)",
7006
+ placeholder: "ops.example.com",
7007
+ validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
7008
+ })
7009
+ );
7010
+ publicUrl = normalizeUrl(raw);
7011
+ behindProxy = true;
7012
+ if (publicUrl.startsWith("http://")) {
7013
+ log.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
7014
+ }
7015
+ domainPlan = { type: "byo", hostname: new URL(publicUrl).hostname };
7016
+ }
7017
+ }
7018
+ const isCloudDomain = domainPlan.type === "free";
7019
+ const admin = isCloudDomain ? null : await promptLocalAdmin();
7020
+ const s = spinner2();
7021
+ s.start("Installing Openship as a service");
7022
+ let started;
7023
+ try {
7024
+ started = startService(
7025
+ { publicUrl, trustProxy: behindProxy, managedEdge, acmeEmail: managedEdge ? admin?.email : void 0 },
7026
+ { quiet: true }
7027
+ );
7028
+ } catch (e) {
7029
+ s.stop("Couldn't install the service.", 1);
7030
+ log.error(e.message);
7031
+ log.info("Run `openship up --foreground` to run it attached and see the error.");
7032
+ process.exit(1);
7033
+ }
7034
+ s.message("Waiting for Openship to come up");
7035
+ if (!await waitHealthy(started.port)) {
7036
+ s.stop("Openship didn't become healthy in time.", 1);
7037
+ log.info("Check logs: `openship logs` (or `openship up --foreground`).");
7038
+ process.exit(1);
7039
+ }
7040
+ if (admin) {
7041
+ s.message("Creating your admin account");
7042
+ const adminRes = await bootstrapAdmin(started.port, admin);
7043
+ if (!adminRes.ok) {
7044
+ s.stop(`Couldn't create the admin account: ${adminRes.message}`, 1);
7045
+ process.exit(1);
7046
+ }
7047
+ s.stop(
7048
+ adminRes.message === "already-exists" ? "An admin already exists \u2014 use your existing login." : `Admin account created for ${admin.email}.`
7049
+ );
7050
+ } else {
7051
+ s.stop("Openship is up.");
7052
+ }
7053
+ let liveUrl = publicUrl ?? `http://localhost:${started.dashPort}`;
7054
+ const port = started.port;
7055
+ if (domainPlan.type === "free") {
7056
+ const linked = await connectOpenshipCloud(port);
7057
+ if (!linked) {
7058
+ log.warn("Openship Cloud wasn't connected \u2014 set up a local admin instead. You can add the free domain later in Settings \u2192 Cloud.");
7059
+ const fb = await promptLocalAdmin();
7060
+ const abr = await bootstrapAdmin(port, fb);
7061
+ if (!abr.ok) {
7062
+ log.error(`Couldn't create the admin account: ${abr.message}`);
7063
+ process.exit(1);
7064
+ }
7065
+ await internalPost(port, "/api/system/self-register", { domainType: "byo" });
7066
+ } else {
7067
+ const s2 = spinner2();
7068
+ s2.start("Registering your free domain with Openship Cloud");
7069
+ const res = await internalPost(port, "/api/system/self-register", {
7070
+ domainType: "free",
7071
+ slug: domainPlan.slug,
7072
+ publicHost: domainPlan.publicHost,
7073
+ dashPort: Number(started.dashPort)
7074
+ });
7075
+ if (res.ok && res.data?.url) {
7076
+ liveUrl = res.data.url;
7077
+ s2.stop(`Free domain live: ${res.data.url}`);
7078
+ } else {
7079
+ s2.stop(`Couldn't register the free domain: ${res.data?.error || "failed"}`, 1);
7080
+ }
7081
+ }
7082
+ } else if (domainPlan.type === "custom") {
7083
+ let edgeTakeover = false;
7084
+ let edgeMigrate = false;
7085
+ let proceedCustom = true;
7086
+ const pf = await internalPost(port, "/api/system/self-edge/preflight", {});
7087
+ const status = pf.ok ? pf.data?.status : void 0;
7088
+ const importable = pf.ok && Array.isArray(pf.data?.sites) ? pf.data.sites.length : 0;
7089
+ if (status && !status.canProceedClean && status.occupants?.length) {
7090
+ const owner = status.occupants.map((o) => o.command ?? `port ${o.port}`).join(", ");
7091
+ const known = status.classification === "known";
7092
+ const choice = ensure(
7093
+ await select({
7094
+ message: known ? `An existing reverse proxy (${owner}) is serving ports 80/443.` : `Ports 80/443 are in use by ${owner}, which we couldn't identify.`,
7095
+ options: [
7096
+ ...importable > 0 ? [{
7097
+ value: "migrate",
7098
+ label: `Migrate ${importable} site${importable === 1 ? "" : "s"} & take over`,
7099
+ hint: "import the existing sites into Openship, then take 80/443"
7100
+ }] : [],
7101
+ {
7102
+ value: "override",
7103
+ label: "Stop it & take over 80/443",
7104
+ hint: known ? "the existing sites stop being served" : "may interrupt a running service"
7105
+ },
7106
+ { value: "cancel", label: "Cancel \u2014 leave it running" }
7107
+ ],
7108
+ // Per product decision: unknown owner pre-selects takeover; a known
7109
+ // proxy defaults to cancel so the user chooses deliberately.
7110
+ initialValue: known ? "cancel" : "override"
7111
+ })
7112
+ );
7113
+ if (choice === "cancel") proceedCustom = false;
7114
+ else if (choice === "migrate") edgeMigrate = true;
7115
+ else edgeTakeover = true;
7116
+ }
7117
+ if (!proceedCustom) {
7118
+ log.warn(
7119
+ "Left the existing proxy on 80/443 running. Registering Openship without managed HTTPS \u2014 front it with your proxy, or re-run setup to take over."
7120
+ );
7121
+ await internalPost(port, "/api/system/self-register", {
7122
+ domainType: "byo",
7123
+ hostname: domainPlan.hostname
7124
+ });
7125
+ liveUrl = `https://${domainPlan.hostname}`;
7126
+ } else {
7127
+ const res = await internalPost(port, "/api/system/self-register", {
7128
+ domainType: "custom",
7129
+ hostname: domainPlan.hostname,
7130
+ dashPort: Number(started.dashPort),
7131
+ acmeEmail: admin?.email,
7132
+ edgeTakeover,
7133
+ edgeMigrate
7134
+ });
7135
+ if (res.ok && res.data?.sessionId) {
7136
+ const s2 = spinner2();
7137
+ s2.start("Issuing HTTPS certificate (OpenResty + Let's Encrypt)");
7138
+ const done = await streamProvision(port, res.data.sessionId, s2);
7139
+ liveUrl = res.data.url ?? liveUrl;
7140
+ if (done) s2.stop(`HTTPS ready: ${liveUrl}`);
7141
+ else s2.stop("HTTPS isn't ready yet \u2014 it retries on reboot; the site serves over HTTP meanwhile.", 1);
7142
+ } else {
7143
+ log.warn(`Couldn't start domain provisioning: ${res.data?.error || "failed"}`);
7144
+ }
7145
+ }
7146
+ } else if (domainPlan.type === "byo") {
7147
+ const res = await internalPost(port, "/api/system/self-register", {
7148
+ domainType: "byo",
7149
+ hostname: domainPlan.hostname
7150
+ });
7151
+ if (res.ok && res.data?.url) liveUrl = res.data.url;
7152
+ } else {
7153
+ await internalPost(port, "/api/system/self-register", { domainType: "byo" });
7154
+ }
7155
+ note(
7156
+ `${chalk16.bold(liveUrl)}
7157
+
7158
+ ` + chalk16.dim(`${admin ? `Log in as ${admin.email}` : "Log in with Openship Cloud"}. Openship now appears under your Apps, and runs as a service (restarts on boot).`),
7159
+ "Openship is live"
7160
+ );
7161
+ outro(
7162
+ domainPlan.type === "byo" ? chalk16.dim("Point your reverse proxy at the dashboard port above.") : chalk16.green("Happy shipping.")
7163
+ );
7164
+ }
7165
+
6372
7166
  // src/index.ts
6373
- var program = new Command24();
6374
- program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.1.10").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7167
+ var program = new Command25();
7168
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.0").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
6375
7169
  if (thisCommand.opts().json) setJsonMode(true);
7170
+ }).action(async () => {
7171
+ await runWizard();
6376
7172
  });
6377
7173
  program.addCommand(upCommand);
7174
+ program.addCommand(stopCommand);
6378
7175
  program.addCommand(installCommand);
6379
7176
  program.addCommand(updateCommand);
6380
7177
  program.addCommand(openCommand);