openship 0.5.0 → 0.6.1

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 (104) hide show
  1. package/README.md +26 -1
  2. package/dist/bare-IWS2QHJ3.js +23 -0
  3. package/dist/{bowser-O3AEA6AN.js → bowser-HNOTPGJO.js} +2 -2
  4. package/dist/{chunk-5MZ624VX.js → chunk-2GWYYHOR.js} +1 -1
  5. package/dist/{chunk-BS2AXYZX.js → chunk-2NZSUXOB.js} +647 -214
  6. package/dist/chunk-3FYIXVX5.js +8051 -0
  7. package/dist/{chunk-ILFETZOM.js → chunk-3LDWWY7U.js} +21 -1
  8. package/dist/{chunk-W73X43R7.js → chunk-3NQQXMDR.js} +3 -3
  9. package/dist/{chunk-NNPEG6HM.js → chunk-3NZICQHF.js} +2 -2
  10. package/dist/{chunk-OWED2GRM.js → chunk-42WQABXM.js} +2 -2
  11. package/dist/{chunk-TYVME3XD.js → chunk-6FH4AXVY.js} +20 -3
  12. package/dist/{chunk-ZCOQJCZO.js → chunk-6ODUBKUB.js} +3 -3
  13. package/dist/{chunk-DYPH4HCI.js → chunk-AO252K7Z.js} +1 -1
  14. package/dist/{chunk-IXCHURVK.js → chunk-DOD2BXHQ.js} +4 -4
  15. package/dist/{chunk-VWPEKOH6.js → chunk-DRK3MEDS.js} +3 -3
  16. package/dist/{chunk-V7BZMGOT.js → chunk-EMXI4UCU.js} +5 -5
  17. package/dist/{chunk-RB35YN3J.js → chunk-EW6KB2J7.js} +391 -63
  18. package/dist/chunk-I3DPMTSF.js +29 -0
  19. package/dist/{chunk-NXFIRK2Z.js → chunk-IOT5ZX23.js} +573 -459
  20. package/dist/{chunk-KZOBYJUW.js → chunk-IW2UKBVF.js} +3 -3
  21. package/dist/{chunk-YWMODKWL.js → chunk-IXM5245F.js} +165 -13
  22. package/dist/{chunk-KNTJ7PSF.js → chunk-JIMUYEKN.js} +4 -4
  23. package/dist/{chunk-O6ZZF52V.js → chunk-ONKEYSMY.js} +2 -2
  24. package/dist/{chunk-UMQP7SYT.js → chunk-OTLSJYAC.js} +1316 -53
  25. package/dist/{chunk-EI575GHN.js → chunk-PDW4L3NV.js} +3 -3
  26. package/dist/{chunk-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
  27. package/dist/{chunk-6APIPIJY.js → chunk-QWHBRY2K.js} +5 -5
  28. package/dist/{chunk-JLKKYUZA.js → chunk-RRJJGVKE.js} +7 -7
  29. package/dist/{chunk-6B5YCFFV.js → chunk-RVER2EOO.js} +5 -5
  30. package/dist/{chunk-Q2CIRAOK.js → chunk-RXQBD4ZY.js} +1 -1
  31. package/dist/{chunk-6LKHXB6T.js → chunk-TWJCLD4W.js} +48 -13
  32. package/dist/{chunk-K2RPVGPN.js → chunk-UC7YIEHT.js} +36 -17
  33. package/dist/{chunk-I6PSTQIR.js → chunk-VDDMADOF.js} +3 -3
  34. package/dist/{chunk-6PIYRG5I.js → chunk-VZ2ZHPS2.js} +52 -42
  35. package/dist/{chunk-AVIB7CT7.js → chunk-WNWQBB7F.js} +2 -2
  36. package/dist/{chunk-FRNAR33O.js → chunk-XZYFZ6A5.js} +3 -3
  37. package/dist/chunk-YIPMDGG6.js +703 -0
  38. package/dist/{chunk-RPHRPFEH.js → chunk-ZCFVHA25.js} +1 -1
  39. package/dist/{chunk-FPRHYBY2.js → chunk-ZFJXPCQP.js} +1 -1
  40. package/dist/{chunk-JYGD5WSN.js → chunk-ZWLDLTJP.js} +1 -1
  41. package/dist/{cloud-7QYOGF7F.js → cloud-KRTZ2IL5.js} +3 -3
  42. package/dist/cloud-M2YLWEXO.js +31 -0
  43. package/dist/{detect-BAJMY6KM.js → detect-ALF4GXRX.js} +7 -8
  44. package/dist/{dist-IQJ6H6UZ.js → dist-BM6NPWIH.js} +3 -3
  45. package/dist/{dist-es-X3B5QYKB.js → dist-es-2YRDXMUU.js} +6 -6
  46. package/dist/{dist-es-OSNJBLXB.js → dist-es-CCVZEFRZ.js} +5 -5
  47. package/dist/{dist-es-VNKHQVIB.js → dist-es-IOH6G4GM.js} +4 -4
  48. package/dist/{dist-es-RYHIPOTN.js → dist-es-JGBPABJI.js} +14 -14
  49. package/dist/{dist-es-JSTDUS7Q.js → dist-es-KWSMDPIP.js} +6 -6
  50. package/dist/{dist-es-5DY7K32L.js → dist-es-UYDDOVZ3.js} +6 -6
  51. package/dist/{dist-es-WV4AMOII.js → dist-es-XUOR4NQN.js} +8 -8
  52. package/dist/docker-QECQJ5P4.js +32 -0
  53. package/dist/docker-edge-executor-62N64MGS.js +17 -0
  54. package/dist/edge-import-SAYTRJLO.js +48 -0
  55. package/dist/ensure-container-edge-CFSCPGZK.js +32 -0
  56. package/dist/{event-streams-EHRAKL75.js → event-streams-TR4GAC53.js} +4 -4
  57. package/dist/{executor-LOYMPU67.js → executor-PT6UEJ5M.js} +8 -8
  58. package/dist/index.js +1228 -463
  59. package/dist/{loadSso-46FQRIZS.js → loadSso-UGF4JMRK.js} +10 -10
  60. package/dist/{local-executor-V7U5VOZW.js → local-executor-WOFZD72X.js} +4 -4
  61. package/dist/nginx-YOEEFBTE.js +31 -0
  62. package/dist/node-entry.js +4 -0
  63. package/dist/{noop-FMMFG2EZ.js → noop-NJ72F46T.js} +3 -3
  64. package/dist/openresty-lua-WGBO2M3Z.js +81 -0
  65. package/dist/server/index.js +23276 -8015
  66. package/dist/server/lua/geo_country.lua +34 -8
  67. package/dist/server/lua/maxminddb.lua +403 -0
  68. package/dist/server/lua/mgmt_api.lua +431 -68
  69. package/dist/server/lua/pipe_stream.lua +52 -39
  70. package/dist/server/lua/site_logger.lua +236 -30
  71. package/dist/server/migrations/0082_edge_target_verification.sql +39 -0
  72. package/dist/server/migrations/0083_service_incident.sql +84 -0
  73. package/dist/server/migrations/0084_analytics_daily_rollup.sql +72 -0
  74. package/dist/server/migrations/0085_resource_usage.sql +68 -0
  75. package/dist/server/migrations/0086_remote_infra_updates.sql +41 -0
  76. package/dist/server/migrations/0087_server_container_version_columns.sql +17 -0
  77. package/dist/server/migrations/0088_audit_source_and_settings.sql +27 -0
  78. package/dist/server/migrations/0089_auto_scan_infra.sql +7 -0
  79. package/dist/server/migrations/0090_project_collect_paths.sql +18 -0
  80. package/dist/server/migrations/0091_project_server_id.sql +26 -0
  81. package/dist/server/migrations/0092_project_internal_alias.sql +13 -0
  82. package/dist/server/migrations/0093_service_incident_org_index.sql +13 -0
  83. package/dist/server/migrations/0094_backup_restore_meta.sql +15 -0
  84. package/dist/server/migrations/0095_backup_restore_cancel.sql +19 -0
  85. package/dist/server/migrations/0096_backup_policy_retention_defaults.sql +20 -0
  86. package/dist/server/migrations/0097_update_status_upstream_only.sql +35 -0
  87. package/dist/server/migrations/meta/_journal.json +112 -0
  88. package/dist/setup-M2ZDGH3O.js +21 -0
  89. package/dist/{signin-ITOPKKUB.js → signin-3M3UPB6K.js} +10 -10
  90. package/dist/{sso-oidc-NON2PD7Y.js → sso-oidc-3VDIYOEJ.js} +10 -10
  91. package/dist/{sts-HHFFSAY2.js → sts-B3WIYLXR.js} +9 -9
  92. package/package.json +6 -3
  93. package/dist/bare-VRGE5MXH.js +0 -23
  94. package/dist/chunk-CUJXGK7R.js +0 -3167
  95. package/dist/chunk-EAAGSQXN.js +0 -377
  96. package/dist/cloud-3M2WZ624.js +0 -30
  97. package/dist/docker-2NGBRJFJ.js +0 -18
  98. package/dist/docker-edge-executor-5I4M5QT7.js +0 -13
  99. package/dist/edge-import-RIWNEIXQ.js +0 -47
  100. package/dist/ensure-container-edge-3NSYKHRX.js +0 -31
  101. package/dist/nginx-QE4DOM5J.js +0 -24
  102. package/dist/openresty-lua-BTAKECCF.js +0 -42
  103. package/dist/server/lua/pipe_log.lua +0 -71
  104. package/dist/setup-KELALK7H.js +0 -20
package/dist/index.js CHANGED
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env sh
2
- ":" //# ; exec "$(command -v bun || command -v node)" "$0" "$@"
2
+ ":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
3
3
  import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
- DEFAULT_API_PORT,
7
- DEFAULT_DASHBOARD_PORT,
6
+ AUTH_SECRET_FILE,
7
+ DATA_DIR,
8
+ DEFAULT_REPO,
9
+ INTERNAL_TOKEN_FILE,
8
10
  IS_ALT_HOME,
11
+ LOG_DIR,
9
12
  OS_DIR,
13
+ PORTS_FILE,
14
+ SOURCE_CHECKOUT_DIR,
15
+ SOURCE_DIST_DIR,
10
16
  composeDown,
11
17
  composeInternalToken,
12
- composeIsViableDefault,
18
+ composePlan,
13
19
  composePrefetch,
14
20
  composePs,
15
21
  composeRestart,
@@ -20,7 +26,9 @@ import {
20
26
  composeUp,
21
27
  composeUpdate,
22
28
  dockerGap,
29
+ dockerInstallPreview,
23
30
  ensureDocker,
31
+ has,
24
32
  hasDockerCompose,
25
33
  importMigratedSites,
26
34
  portMoveNotice,
@@ -38,24 +46,25 @@ import {
38
46
  sourceBuildDir,
39
47
  storedApiPort,
40
48
  storedDashboardPort
41
- } from "./chunk-BS2AXYZX.js";
42
- import "./chunk-FRNAR33O.js";
43
- import "./chunk-EI575GHN.js";
44
- import "./chunk-6APIPIJY.js";
45
- import "./chunk-IXCHURVK.js";
46
- import "./chunk-OWED2GRM.js";
47
- import "./chunk-VWPEKOH6.js";
48
- import "./chunk-W73X43R7.js";
49
- import "./chunk-6LKHXB6T.js";
50
- import "./chunk-JLKKYUZA.js";
51
- import "./chunk-AVIB7CT7.js";
52
- import "./chunk-5MZ624VX.js";
53
- import "./chunk-Q2CIRAOK.js";
54
- import "./chunk-NNPEG6HM.js";
49
+ } from "./chunk-2NZSUXOB.js";
50
+ import "./chunk-XZYFZ6A5.js";
51
+ import "./chunk-PDW4L3NV.js";
52
+ import "./chunk-QWHBRY2K.js";
53
+ import "./chunk-DOD2BXHQ.js";
54
+ import "./chunk-42WQABXM.js";
55
+ import "./chunk-DRK3MEDS.js";
56
+ import "./chunk-3NQQXMDR.js";
57
+ import "./chunk-TWJCLD4W.js";
58
+ import "./chunk-RRJJGVKE.js";
59
+ import "./chunk-WNWQBB7F.js";
60
+ import "./chunk-2GWYYHOR.js";
61
+ import "./chunk-RXQBD4ZY.js";
62
+ import "./chunk-3NZICQHF.js";
55
63
  import {
56
64
  beginEdgeTakeover,
57
65
  collectProxyCerts,
58
66
  completeEdgeTakeover,
67
+ copyStaticRootIntoEdge,
59
68
  detectInstalledProxy,
60
69
  edgeProxy,
61
70
  edgeProxyFor,
@@ -63,42 +72,45 @@ import {
63
72
  importSites,
64
73
  recoverInterruptedTakeover,
65
74
  rollbackEdgeTakeover,
66
- scanImportableSites
67
- } from "./chunk-NXFIRK2Z.js";
68
- import "./chunk-O6ZZF52V.js";
69
- import "./chunk-JYGD5WSN.js";
70
- import "./chunk-CUJXGK7R.js";
71
- import "./chunk-V7BZMGOT.js";
72
- import "./chunk-YWMODKWL.js";
73
- import "./chunk-KNTJ7PSF.js";
74
- import "./chunk-KZOBYJUW.js";
75
- import "./chunk-I6PSTQIR.js";
76
- import "./chunk-K2RPVGPN.js";
77
- import "./chunk-ZCOQJCZO.js";
78
- import "./chunk-RPHRPFEH.js";
75
+ scanImportableSites,
76
+ unreachableStaticRoots
77
+ } from "./chunk-IOT5ZX23.js";
78
+ import "./chunk-ONKEYSMY.js";
79
+ import "./chunk-ZWLDLTJP.js";
80
+ import "./chunk-3FYIXVX5.js";
81
+ import "./chunk-EMXI4UCU.js";
82
+ import "./chunk-IXM5245F.js";
83
+ import "./chunk-JIMUYEKN.js";
84
+ import "./chunk-IW2UKBVF.js";
85
+ import "./chunk-VDDMADOF.js";
86
+ import "./chunk-UC7YIEHT.js";
87
+ import "./chunk-6ODUBKUB.js";
88
+ import "./chunk-ZCFVHA25.js";
79
89
  import {
80
90
  LocalExecutor
81
- } from "./chunk-TYVME3XD.js";
82
- import "./chunk-RB35YN3J.js";
83
- import "./chunk-FPRHYBY2.js";
91
+ } from "./chunk-6FH4AXVY.js";
92
+ import "./chunk-EW6KB2J7.js";
93
+ import "./chunk-I3DPMTSF.js";
84
94
  import {
85
95
  edgeCrashReason,
86
96
  edgeIsBroken,
87
97
  ourEdgeContainerRunning
88
- } from "./chunk-6PIYRG5I.js";
89
- import "./chunk-ILFETZOM.js";
90
- import "./chunk-EAAGSQXN.js";
98
+ } from "./chunk-VZ2ZHPS2.js";
99
+ import "./chunk-3LDWWY7U.js";
100
+ import "./chunk-YIPMDGG6.js";
101
+ import "./chunk-ZFJXPCQP.js";
91
102
  import {
92
103
  CLOUD_DASHBOARD_URL,
93
104
  LOCAL_API_URL,
94
105
  LOCAL_DASHBOARD_URL,
106
+ buildMailImageRef,
95
107
  cliInstallCommand,
96
108
  commandToArgv,
97
109
  isValidEmail,
98
110
  parseOpenshipConfigJson,
99
111
  resolveCliUpdatePlan
100
- } from "./chunk-UMQP7SYT.js";
101
- import "./chunk-KI2EY3WB.js";
112
+ } from "./chunk-OTLSJYAC.js";
113
+ import "./chunk-PXKCZB2I.js";
102
114
 
103
115
  // src/index.ts
104
116
  import { Command as Command28 } from "commander";
@@ -256,6 +268,7 @@ function getDashboardUrl(name) {
256
268
  }
257
269
 
258
270
  // src/lib/api-client.ts
271
+ var USER_AGENT = `openship-cli/${true ? "0.6.1" : "dev"}`;
259
272
  function getApiUrl2() {
260
273
  return `${getApiUrl()}/api`;
261
274
  }
@@ -272,6 +285,7 @@ var ApiError = class extends Error {
272
285
  function buildHeaders(extra) {
273
286
  const headers = new Headers(extra);
274
287
  if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
288
+ if (!headers.has("User-Agent")) headers.set("User-Agent", USER_AGENT);
275
289
  const token = getToken();
276
290
  if (token && !headers.has("Authorization")) headers.set("Authorization", `Bearer ${token}`);
277
291
  return headers;
@@ -496,12 +510,12 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
496
510
 
497
511
  // src/commands/up.ts
498
512
  import { Command as Command4 } from "commander";
499
- import chalk6 from "chalk";
513
+ import chalk7 from "chalk";
500
514
  import ora from "ora";
501
515
  import { spawn } from "child_process";
502
516
  import { randomBytes as randomBytes2 } from "crypto";
503
- import { createWriteStream as createWriteStream2, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync6 } from "fs";
504
- import { dirname as dirname2, join as join7 } from "path";
517
+ import { createWriteStream as createWriteStream2, existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
518
+ import { dirname as dirname2, join as join8 } from "path";
505
519
  import { fileURLToPath } from "url";
506
520
 
507
521
  // src/lib/dashboard.ts
@@ -550,8 +564,8 @@ async function downloadToFile(url, dest, onProgress) {
550
564
  } finally {
551
565
  file.end();
552
566
  }
553
- await new Promise((resolve2, reject2) => {
554
- file.on("finish", () => resolve2());
567
+ await new Promise((resolve3, reject2) => {
568
+ file.on("finish", () => resolve3());
555
569
  file.on("error", reject2);
556
570
  });
557
571
  return { sha256: hash.digest("hex"), size: received };
@@ -671,16 +685,54 @@ async function fetchBundle(tag, onProgress) {
671
685
 
672
686
  // src/lib/service.ts
673
687
  import { spawnSync as spawnSync2 } from "child_process";
674
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
688
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
675
689
  import { homedir } from "os";
676
- import { join as join4, resolve } from "path";
690
+ import { join as join5, resolve } from "path";
691
+
692
+ // src/lib/cli-install.ts
693
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
694
+ import { join as join4 } from "path";
695
+ var MARKER = join4(OS_DIR, "cli-install.json");
696
+ function parseCliInstall(raw) {
697
+ try {
698
+ const obj = JSON.parse(raw);
699
+ if (!obj || obj.method !== "tarball" || typeof obj.tag !== "string" || !obj.tag) return null;
700
+ return {
701
+ method: "tarball",
702
+ tag: obj.tag,
703
+ launcher: typeof obj.launcher === "string" ? obj.launcher : join4(OS_DIR, "bin", "openship"),
704
+ runtime: obj.runtime === "vendored" ? "vendored" : "system"
705
+ };
706
+ } catch {
707
+ return null;
708
+ }
709
+ }
710
+ function readCliInstall() {
711
+ if (!existsSync3(MARKER)) return null;
712
+ try {
713
+ return parseCliInstall(readFileSync2(MARKER, "utf8"));
714
+ } catch {
715
+ return null;
716
+ }
717
+ }
718
+ function writeCliInstall(info3) {
719
+ mkdirSync4(OS_DIR, { recursive: true });
720
+ writeFileSync3(MARKER, `${JSON.stringify(info3, null, 2)}
721
+ `);
722
+ }
723
+
724
+ // src/lib/service.ts
677
725
  var HOME = homedir();
678
- var LOG_DIR = join4(OS_DIR, "logs");
726
+ var OPENSHIP_BIN = join5(OS_DIR, "bin");
679
727
  var MAC_LABEL = IS_ALT_HOME ? "io.openship-dev.up" : "io.openship.up";
680
- var MAC_PLIST = join4(HOME, "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
728
+ var MAC_PLIST = join5(HOME, "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
681
729
  var SYSTEMD_NAME = IS_ALT_HOME ? "openship-dev" : "openship";
682
730
  var WIN_TASK = IS_ALT_HOME ? "OpenshipDev" : "Openship";
683
731
  function selfInvocation() {
732
+ const cli = readCliInstall();
733
+ if (cli?.method === "tarball" && existsSync4(cli.launcher)) {
734
+ return { runtime: cli.launcher, args: [] };
735
+ }
684
736
  const runtime = process.execPath;
685
737
  const entry = resolve(process.argv[1] ?? "");
686
738
  return { runtime, args: [entry] };
@@ -749,7 +801,8 @@ function serviceEnv() {
749
801
  function plist(flags) {
750
802
  const argv = runArgv(flags);
751
803
  const items = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
752
- const path2 = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", join4(HOME, ".bun/bin")].join(":");
804
+ const runtimeBin = readCliInstall()?.method === "tarball" ? OPENSHIP_BIN : join5(HOME, ".bun/bin");
805
+ const path2 = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", runtimeBin].join(":");
753
806
  const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `<key>${xmlEscape(k)}</key><string>${xmlEscape(v)}</string>`).join("");
754
807
  return `<?xml version="1.0" encoding="UTF-8"?>
755
808
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -762,8 +815,8 @@ ${items}
762
815
  </array>
763
816
  <key>RunAtLoad</key><true/>
764
817
  <key>KeepAlive</key><true/>
765
- <key>StandardOutPath</key><string>${join4(LOG_DIR, "up.log")}</string>
766
- <key>StandardErrorPath</key><string>${join4(LOG_DIR, "up.err.log")}</string>
818
+ <key>StandardOutPath</key><string>${join5(LOG_DIR, "up.log")}</string>
819
+ <key>StandardErrorPath</key><string>${join5(LOG_DIR, "up.err.log")}</string>
767
820
  <key>EnvironmentVariables</key>
768
821
  <dict><key>PATH</key><string>${path2}</string>${extraEnv}</dict>
769
822
  </dict>
@@ -775,6 +828,8 @@ function systemdUnit(flags) {
775
828
  const execStart = argv.map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
776
829
  const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `Environment=${k}=${v}
777
830
  `).join("");
831
+ const pathEnv = readCliInstall()?.method === "tarball" ? `Environment=PATH=${["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin", OPENSHIP_BIN].join(":")}
832
+ ` : "";
778
833
  return `[Unit]
779
834
  Description=Openship control plane
780
835
  After=network-online.target
@@ -786,26 +841,40 @@ ExecStart=${execStart}
786
841
  Restart=always
787
842
  RestartSec=2
788
843
  Environment=NODE_ENV=production
789
- ${extraEnv}
844
+ ${pathEnv}${extraEnv}
790
845
  [Install]
791
846
  WantedBy=default.target
792
847
  `;
793
848
  }
849
+ function installStepFor(kind) {
850
+ switch (kind) {
851
+ case "launchd":
852
+ return "launchctl bootstrap (starts it now and at login)";
853
+ case "systemd-user":
854
+ return "systemctl --user enable --now + loginctl enable-linger (starts it now, on boot, without a login session)";
855
+ case "systemd-system":
856
+ return "systemctl enable --now (starts it now and on boot)";
857
+ case "schtasks":
858
+ return "schtasks /Create /SC ONLOGON (starts it at logon)";
859
+ default:
860
+ return "nothing \u2014 no supported service manager on this box (needs systemd on Linux); `up --foreground` or `up --compose` instead";
861
+ }
862
+ }
794
863
  function preview(flags) {
795
864
  const kind = detectKind();
796
865
  if (kind === "launchd") return { kind, path: MAC_PLIST, content: plist(flags) };
797
- if (kind === "systemd-user") return { kind, path: join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`), content: systemdUnit(flags) };
866
+ if (kind === "systemd-user") return { kind, path: join5(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`), content: systemdUnit(flags) };
798
867
  if (kind === "systemd-system") return { kind, path: `/etc/systemd/system/${SYSTEMD_NAME}.service`, content: systemdUnit(flags) };
799
868
  if (kind === "schtasks") return { kind, path: WIN_TASK, content: runArgv(flags).join(" ") };
800
869
  return { kind, path: "", content: "" };
801
870
  }
802
871
  function installAndStart(flags) {
803
- mkdirSync4(LOG_DIR, { recursive: true });
872
+ mkdirSync5(LOG_DIR, { recursive: true });
804
873
  const kind = detectKind();
805
874
  if (kind === "launchd") {
806
- mkdirSync4(join4(HOME, "Library", "LaunchAgents"), { recursive: true });
875
+ mkdirSync5(join5(HOME, "Library", "LaunchAgents"), { recursive: true });
807
876
  run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
808
- writeFileSync3(MAC_PLIST, plist(flags));
877
+ writeFileSync4(MAC_PLIST, plist(flags));
809
878
  const uid = String(process.getuid?.() ?? "");
810
879
  let r = run("launchctl", ["bootstrap", `gui/${uid}`, MAC_PLIST]);
811
880
  if (!r.ok) r = run("launchctl", ["load", "-w", MAC_PLIST]);
@@ -814,9 +883,9 @@ function installAndStart(flags) {
814
883
  }
815
884
  if (kind === "systemd-user" || kind === "systemd-system") {
816
885
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
817
- const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
818
- mkdirSync4(unitPath.slice(0, unitPath.lastIndexOf("/")), { recursive: true });
819
- writeFileSync3(unitPath, systemdUnit(flags));
886
+ const unitPath = kind === "systemd-user" ? join5(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
887
+ mkdirSync5(unitPath.slice(0, unitPath.lastIndexOf("/")), { recursive: true });
888
+ writeFileSync4(unitPath, systemdUnit(flags));
820
889
  run("systemctl", [...sysArgs, "daemon-reload"]);
821
890
  const r = run("systemctl", [...sysArgs, "enable", "--now", SYSTEMD_NAME]);
822
891
  if (!r.ok) throw new Error(`systemctl enable failed: ${r.out}`);
@@ -839,7 +908,7 @@ function installAndStart(flags) {
839
908
  function restart() {
840
909
  const kind = detectKind();
841
910
  if (kind === "launchd") {
842
- if (!existsSync3(MAC_PLIST)) return { restarted: false, detail: "no launchd agent installed" };
911
+ if (!existsSync4(MAC_PLIST)) return { restarted: false, detail: "no launchd agent installed" };
843
912
  const uid = String(process.getuid?.() ?? "");
844
913
  const r = run("launchctl", ["kickstart", "-k", `gui/${uid}/${MAC_LABEL}`]);
845
914
  if (!r.ok) return { restarted: false, detail: r.out || "launchctl kickstart failed" };
@@ -855,8 +924,8 @@ function restart() {
855
924
  }
856
925
  if (kind === "systemd-user" || kind === "systemd-system") {
857
926
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
858
- const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
859
- if (!existsSync3(unitPath)) return { restarted: false, detail: "no systemd unit installed" };
927
+ const unitPath = kind === "systemd-user" ? join5(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
928
+ if (!existsSync4(unitPath)) return { restarted: false, detail: "no systemd unit installed" };
860
929
  const r = run("systemctl", [...sysArgs, "restart", SYSTEMD_NAME]);
861
930
  return { restarted: r.ok, detail: r.ok ? `restarted ${SYSTEMD_NAME}` : r.out };
862
931
  }
@@ -870,14 +939,14 @@ function restart() {
870
939
  function serviceStatus() {
871
940
  const kind = detectKind();
872
941
  if (kind === "launchd") {
873
- return { kind, installed: existsSync3(MAC_PLIST), running: launchdPid() != null };
942
+ return { kind, installed: existsSync4(MAC_PLIST), running: launchdPid() != null };
874
943
  }
875
944
  if (kind === "systemd-user" || kind === "systemd-system") {
876
945
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
877
- const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
946
+ const unitPath = kind === "systemd-user" ? join5(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
878
947
  return {
879
948
  kind,
880
- installed: existsSync3(unitPath),
949
+ installed: existsSync4(unitPath),
881
950
  running: run("systemctl", [...sysArgs, "is-active", SYSTEMD_NAME]).out.trim() === "active"
882
951
  };
883
952
  }
@@ -912,13 +981,13 @@ function stop() {
912
981
  let result;
913
982
  if (kind === "launchd") {
914
983
  run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
915
- if (existsSync3(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
984
+ if (existsSync4(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
916
985
  result = { kind, detail: `launchd agent ${MAC_LABEL} stopped + removed` };
917
986
  } else if (kind === "systemd-user" || kind === "systemd-system") {
918
987
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
919
988
  run("systemctl", [...sysArgs, "disable", "--now", SYSTEMD_NAME]);
920
- const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
921
- if (existsSync3(unitPath)) rmSync2(unitPath, { force: true });
989
+ const unitPath = kind === "systemd-user" ? join5(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
990
+ if (existsSync4(unitPath)) rmSync2(unitPath, { force: true });
922
991
  run("systemctl", [...sysArgs, "daemon-reload"]);
923
992
  result = { kind, detail: `systemd unit ${SYSTEMD_NAME} stopped + disabled` };
924
993
  } else if (kind === "schtasks") {
@@ -932,10 +1001,15 @@ function stop() {
932
1001
  return result;
933
1002
  }
934
1003
 
1004
+ // src/lib/up-plan.ts
1005
+ import { homedir as homedir2 } from "os";
1006
+ import { join as join7 } from "path";
1007
+ import chalk6 from "chalk";
1008
+
935
1009
  // src/lib/edge-preflight.ts
936
1010
  import { spawnSync as spawnSync3 } from "child_process";
937
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
938
- import { join as join5 } from "path";
1011
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
1012
+ import { join as join6 } from "path";
939
1013
  import chalk5 from "chalk";
940
1014
  import { isCancel, log, note, select } from "@clack/prompts";
941
1015
  async function planAndApplyHostEdge(opts, overrides = {}) {
@@ -956,12 +1030,39 @@ async function planAndApplyHostEdge(opts, overrides = {}) {
956
1030
  const action2 = await resolveAction(opts.edge, { status, owner, sites, warnings }, deps);
957
1031
  if (action2 === "cancel") return { proceed: false };
958
1032
  const certPems = action2 === "migrate" ? await deps.collectCerts(executor, sites, { status }) : void 0;
959
- await deps.beginEdgeTakeover(
1033
+ const staticRootOverrides = action2 === "migrate" ? await remediateUnreachableStaticRoots({
1034
+ unreachable: unreachableStaticRoots(sites, { containerEdge: true }),
1035
+ executor,
1036
+ interactive: deps.interactive
1037
+ }) : void 0;
1038
+ const freed = await deps.beginEdgeTakeover(
960
1039
  executor,
961
1040
  status,
962
1041
  (m, l) => deps.warn(l === "info" ? m : chalk5.yellow(m))
963
1042
  );
964
- return action2 === "migrate" ? { proceed: true, action: action2, sites, certPems } : { proceed: true, action: action2 };
1043
+ if (!freed.freed) {
1044
+ const plural = freed.stillBound.length > 1;
1045
+ const restored = await deps.rollbackHostEdge();
1046
+ return {
1047
+ proceed: false,
1048
+ blockedBy: `port${plural ? "s" : ""} ${freed.stillBound.join(" and ")} ${plural ? "are" : "is"} still in use after stopping ${owner}` + (restored ? " (the previous proxy has been restored)" : "")
1049
+ };
1050
+ }
1051
+ return action2 === "migrate" ? { proceed: true, action: action2, sites, certPems, staticRootOverrides } : { proceed: true, action: action2 };
1052
+ }
1053
+ async function previewHostEdge(overrides = {}) {
1054
+ const deps = { ...defaultDeps(), ...overrides };
1055
+ const empty = { owner: null, blocked: false, sites: [], warnings: [] };
1056
+ if (deps.platform !== "linux") return empty;
1057
+ try {
1058
+ const executor = deps.makeExecutor();
1059
+ const { status, blocked, owner } = await deps.foreignProxyOnEdge(executor);
1060
+ if (!blocked) return { ...empty, owner: owner || null };
1061
+ const { sites, warnings } = await deps.importSites(executor, status);
1062
+ return { owner: owner || null, blocked: true, sites, warnings };
1063
+ } catch {
1064
+ return empty;
1065
+ }
965
1066
  }
966
1067
  async function rollbackHostEdge() {
967
1068
  if (process.platform !== "linux") return false;
@@ -974,7 +1075,7 @@ async function rollbackHostEdge() {
974
1075
  return false;
975
1076
  }
976
1077
  }
977
- var IMPORTED_MARKER = join5(OS_DIR, "imported-proxy");
1078
+ var IMPORTED_MARKER = join6(OS_DIR, "imported-proxy");
978
1079
  function edgeServedHostnames() {
979
1080
  const served = /* @__PURE__ */ new Set();
980
1081
  const r = spawnSync3(
@@ -998,13 +1099,13 @@ function edgeServedHostnames() {
998
1099
  }
999
1100
  function markStoppedProxyImported() {
1000
1101
  try {
1001
- mkdirSync5(OS_DIR, { recursive: true, mode: 448 });
1002
- writeFileSync4(IMPORTED_MARKER, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
1102
+ mkdirSync6(OS_DIR, { recursive: true, mode: 448 });
1103
+ writeFileSync5(IMPORTED_MARKER, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
1003
1104
  } catch {
1004
1105
  }
1005
1106
  }
1006
1107
  async function offerStoppedProxyImport(executor, deps) {
1007
- if (!deps.interactive || existsSync4(IMPORTED_MARKER)) return { proceed: true };
1108
+ if (!deps.interactive || existsSync5(IMPORTED_MARKER)) return { proceed: true };
1008
1109
  let proxy = null;
1009
1110
  try {
1010
1111
  proxy = await deps.detectInstalledProxy(executor);
@@ -1113,6 +1214,51 @@ async function confirmEdgeAction(info3) {
1113
1214
  });
1114
1215
  return isCancel(choice) ? "cancel" : choice;
1115
1216
  }
1217
+ async function remediateUnreachableStaticRoots(opts) {
1218
+ const { unreachable } = opts;
1219
+ if (unreachable.length === 0) return void 0;
1220
+ note(
1221
+ unreachable.map((u) => `${chalk5.bold(u.host)} \u2192 ${chalk5.dim(u.root)}`).join("\n"),
1222
+ `${unreachable.length} static site${unreachable.length === 1 ? "" : "s"} rooted outside the edge`
1223
+ );
1224
+ log.warn("Their files live outside the edge container's mounts, so they'd return 500 after cutover.");
1225
+ let copy = true;
1226
+ if (opts.interactive) {
1227
+ const choice = await select({
1228
+ message: "Copy these sites' files into the edge so they keep serving?",
1229
+ options: [
1230
+ {
1231
+ value: "copy",
1232
+ label: `Copy ${unreachable.length === 1 ? "it" : "all"} into the edge`,
1233
+ hint: "snapshot the files under /opt/openship/static (recommended)"
1234
+ },
1235
+ {
1236
+ value: "leave",
1237
+ label: "Leave as-is",
1238
+ hint: "they stop serving until you mount their directory yourself"
1239
+ }
1240
+ ],
1241
+ initialValue: "copy"
1242
+ });
1243
+ copy = !isCancel(choice) && choice === "copy";
1244
+ } else {
1245
+ log.info("Non-interactive: copying unreachable static roots into the edge.");
1246
+ }
1247
+ if (!copy) return void 0;
1248
+ const overrides = {};
1249
+ for (const u of unreachable) {
1250
+ try {
1251
+ const newRoot = await copyStaticRootIntoEdge(opts.executor, { root: u.root, host: u.host });
1252
+ overrides[u.host] = newRoot;
1253
+ log.success(`Copied ${u.host} \u2192 ${newRoot}`);
1254
+ } catch (err2) {
1255
+ log.warn(
1256
+ `Couldn't copy ${u.host} (${u.root}) \u2014 it will migrate as-is: ${err2 instanceof Error ? err2.message : String(err2)}`
1257
+ );
1258
+ }
1259
+ }
1260
+ return Object.keys(overrides).length > 0 ? overrides : void 0;
1261
+ }
1116
1262
  function defaultDeps() {
1117
1263
  return {
1118
1264
  platform: process.platform,
@@ -1121,6 +1267,7 @@ function defaultDeps() {
1121
1267
  foreignProxyOnEdge,
1122
1268
  importSites,
1123
1269
  beginEdgeTakeover: (executor, status, onLog) => beginEdgeTakeover(executor, status, (entry) => onLog(entry.message, entry.level)),
1270
+ rollbackHostEdge,
1124
1271
  recoverInterruptedTakeover: (executor, onLog, isEdgeHealthy) => recoverInterruptedTakeover(
1125
1272
  executor,
1126
1273
  (entry) => onLog(entry.message, entry.level),
@@ -1148,16 +1295,209 @@ function defaultDeps() {
1148
1295
  };
1149
1296
  }
1150
1297
 
1298
+ // src/lib/up-plan.ts
1299
+ function planMethod(opts, dockerUsable) {
1300
+ if (opts.bare) return { method: "bare", reason: "--bare was passed" };
1301
+ if (opts.compose) return { method: "compose", reason: "--compose was passed" };
1302
+ if (process.platform !== "linux") {
1303
+ return {
1304
+ method: "bare",
1305
+ reason: `the compose edge needs host networking, which Docker Desktop on ${process.platform} doesn't provide`
1306
+ };
1307
+ }
1308
+ return dockerUsable ? { method: "compose", reason: "the default on Linux when Docker is usable" } : { method: "bare", reason: "Docker isn't usable here, so `up` falls back to the process service" };
1309
+ }
1310
+ async function planUp(opts) {
1311
+ const mode = opts.fromSource || opts.source ? "from-source" : opts.foreground ? "foreground" : "service";
1312
+ const attached = mode !== "service";
1313
+ const docker = dockerInstallPreview();
1314
+ const dockerUsable = docker.gap === null || docker.wouldInstall;
1315
+ const { method, reason } = attached ? { method: "bare", reason: `--${mode === "foreground" ? "foreground" : "from-source"} runs attached (no service, no compose)` } : planMethod(opts, dockerUsable);
1316
+ const plan = {
1317
+ mode,
1318
+ method,
1319
+ methodReason: reason,
1320
+ docker,
1321
+ ports: method === "compose" ? await resolveComposePorts({ api: opts.port, dashboard: opts.dashboardPort }, { persist: false }) : await resolvePorts(
1322
+ {
1323
+ api: opts.port ? Number(opts.port) : void 0,
1324
+ dashboard: opts.dashboardPort ? Number(opts.dashboardPort) : void 0
1325
+ },
1326
+ { persist: false }
1327
+ ),
1328
+ ...opts.publicUrl ? { publicUrl: opts.publicUrl } : {},
1329
+ writes: [],
1330
+ runs: []
1331
+ };
1332
+ if (mode === "from-source") {
1333
+ plan.fromSource = {
1334
+ ref: opts.source ? "local" : (opts.ref || "main").trim(),
1335
+ ...opts.source ? { sourceDir: opts.source } : { repo: opts.repo || DEFAULT_REPO }
1336
+ };
1337
+ }
1338
+ if (method === "compose") {
1339
+ const stack = composePlan({
1340
+ apiPort: String(plan.ports.api),
1341
+ dashboardPort: String(plan.ports.dashboard),
1342
+ ...opts.publicUrl ? { publicUrl: opts.publicUrl } : {},
1343
+ ...opts.trustProxy ? { trustProxy: true } : {},
1344
+ ...opts.hostControl === false ? { noHostControl: true } : {}
1345
+ });
1346
+ plan.compose = stack;
1347
+ if (process.platform === "linux") plan.edge = await previewHostEdge();
1348
+ plan.writes.push(
1349
+ stack.composeFile,
1350
+ `${stack.envFile}${stack.newSecrets.length ? ` (generates ${stack.newSecrets.join(", ")})` : ""}`,
1351
+ ...stack.buildFile ? [stack.buildFile] : [],
1352
+ ...stack.hostChannel ? [
1353
+ stack.hostChannel.keyPath,
1354
+ `${join7(homedir2(), ".ssh/authorized_keys")} (authorizes that key for ${stack.hostChannel.user}'s host ops; --no-host-control skips it)`
1355
+ ] : [],
1356
+ ...stack.mountDirs.map((d) => `${d}/ (edge routing state + certs)`),
1357
+ stack.installMethodFile
1358
+ );
1359
+ plan.runs.push(
1360
+ stack.buildDir ? `docker compose pull postgres redis && docker compose build (from ${stack.buildDir})` : `docker compose pull (postgres, redis, api, dashboard, edge \u2014 several hundred MB)`,
1361
+ `docker compose up -d (the edge container binds host :80 and :443)`
1362
+ );
1363
+ } else {
1364
+ plan.writes.push(PORTS_FILE, `${LOG_DIR}/`);
1365
+ if (mode === "service") {
1366
+ const svc = preview({
1367
+ port: opts.port,
1368
+ dataDir: opts.dataDir,
1369
+ dashboardPort: opts.dashboardPort,
1370
+ ui: opts.ui,
1371
+ uiVersion: opts.uiVersion,
1372
+ publicUrl: opts.publicUrl,
1373
+ trustProxy: opts.trustProxy || opts.managedEdge,
1374
+ host: opts.host,
1375
+ managedEdge: opts.managedEdge,
1376
+ acmeEmail: opts.acmeEmail
1377
+ });
1378
+ plan.service = svc;
1379
+ if (svc.path) plan.writes.push(svc.path);
1380
+ plan.runs.push(installStepFor(svc.kind));
1381
+ }
1382
+ plan.writes.push(
1383
+ AUTH_SECRET_FILE,
1384
+ INTERNAL_TOKEN_FILE,
1385
+ `${opts.dataDir || DATA_DIR}/ (embedded Postgres)`
1386
+ );
1387
+ if (opts.ui !== false) {
1388
+ plan.runs.push(`download the dashboard bundle \u2192 ${DASHBOARD_CACHE} (--no-ui skips it)`);
1389
+ }
1390
+ if (mode === "from-source") {
1391
+ const src = plan.fromSource;
1392
+ plan.runs.unshift(
1393
+ ...src.sourceDir ? [`build the checkout at ${src.sourceDir}`] : [`git clone/fetch ${src.repo} @ ${src.ref} \u2192 ${SOURCE_CHECKOUT_DIR}`],
1394
+ `bun install + build the release dist \u2192 ${SOURCE_DIST_DIR} (compiles the dashboard)`
1395
+ );
1396
+ }
1397
+ if (opts.managedEdge) {
1398
+ plan.runs.push(
1399
+ "install OpenResty + issue a Let's Encrypt certificate on this box (--managed-edge)"
1400
+ );
1401
+ }
1402
+ if (attached) {
1403
+ plan.runs.push("run the API + dashboard attached to this terminal until Ctrl-C");
1404
+ }
1405
+ }
1406
+ return plan;
1407
+ }
1408
+ function renderUpPlan(plan) {
1409
+ const L = [];
1410
+ const head = (s) => L.push("", chalk6.bold(` ${s}`));
1411
+ const item = (s) => L.push(chalk6.dim(` ${s}`));
1412
+ L.push(
1413
+ "",
1414
+ chalk6.cyan(" Dry run \u2014 nothing on this machine has been changed."),
1415
+ chalk6.dim(` ${methodLabel(plan)} \xB7 ${plan.methodReason}`)
1416
+ );
1417
+ head("Docker");
1418
+ const { docker } = plan;
1419
+ if (!docker.gap) {
1420
+ item("already installed and usable \u2014 nothing to install.");
1421
+ } else if (docker.wouldInstall) {
1422
+ item(chalk6.yellow(`${docker.gap.summary}. A real run WOULD install it:`));
1423
+ item(` ${docker.installCommand}`);
1424
+ item("~150 MB of packages, and it enables the docker service on this box.");
1425
+ if (plan.method === "compose") item("`openship up --bare` installs without Docker (embedded database).");
1426
+ } else {
1427
+ item(`${docker.gap.summary} \u2014 a real run would NOT install it.`);
1428
+ if (docker.gap.hint) item(docker.gap.hint);
1429
+ }
1430
+ head("Ports");
1431
+ item(
1432
+ `API ${plan.ports.api} \xB7 dashboard ${plan.ports.dashboard}` + (plan.ports.switched.api || plan.ports.switched.dashboard ? ` (preferred ${plan.ports.preferred.api}/${plan.ports.preferred.dashboard} \u2014 busy right now)` : "")
1433
+ );
1434
+ if (plan.publicUrl) item(`public URL: ${plan.publicUrl}`);
1435
+ if (plan.compose) {
1436
+ head("Compose stack");
1437
+ item(plan.compose.existing ? "re-runs an existing install (secrets preserved)" : "fresh install");
1438
+ for (const { key, value } of plan.compose.settings) item(`${key}=${value}`);
1439
+ }
1440
+ if (plan.edge) {
1441
+ head("Ports 80/443");
1442
+ if (!plan.edge.blocked) {
1443
+ item("free (or already Openship's edge) \u2014 the edge container binds them.");
1444
+ } else {
1445
+ item(chalk6.yellow(`held by ${plan.edge.owner}.`));
1446
+ item(
1447
+ plan.edge.sites.length ? `A real run would ask before touching it: migrate its ${plan.edge.sites.length} site(s) into Openship's edge, take over (its sites stop serving), or cancel.` : "A real run would ask before stopping it (--edge=migrate|takeover|cancel)."
1448
+ );
1449
+ for (const site of plan.edge.sites.slice(0, 8)) {
1450
+ item(` \u2022 ${site.serverNames.join(", ") || "(no server_name)"}${site.ssl ? " [TLS]" : ""}`);
1451
+ }
1452
+ if (plan.edge.sites.length > 8) item(` \u2022 \u2026and ${plan.edge.sites.length - 8} more`);
1453
+ }
1454
+ }
1455
+ head("Would write");
1456
+ for (const w of plan.writes) item(w);
1457
+ if (plan.runs.length) {
1458
+ head("Would run");
1459
+ for (const r of plan.runs) item(r);
1460
+ }
1461
+ if (plan.service) {
1462
+ head(`${plan.service.kind} definition (${plan.service.path})`);
1463
+ L.push(plan.service.content.replace(/\n$/, ""));
1464
+ }
1465
+ if (plan.compose) {
1466
+ head(`docker-compose.yml (${plan.compose.composeFile})`);
1467
+ L.push(plan.compose.yaml.replace(/\n$/, ""));
1468
+ }
1469
+ L.push("", chalk6.dim(" Re-run without --dry-run to apply this."), "");
1470
+ return L.join("\n");
1471
+ }
1472
+ function methodLabel(plan) {
1473
+ if (plan.mode === "from-source") return "Build from source and run attached";
1474
+ if (plan.mode === "foreground") return "Run attached in this terminal";
1475
+ return plan.method === "compose" ? "Install the Docker Compose stack" : `Install the ${serviceWord(plan.service?.kind)} service`;
1476
+ }
1477
+ function serviceWord(kind) {
1478
+ switch (kind) {
1479
+ case "launchd":
1480
+ return "launchd";
1481
+ case "systemd-user":
1482
+ return "systemd (--user)";
1483
+ case "systemd-system":
1484
+ return "systemd (system)";
1485
+ case "schtasks":
1486
+ return "Scheduled Task";
1487
+ default:
1488
+ return "process";
1489
+ }
1490
+ }
1491
+
1151
1492
  // src/lib/loopback-api.ts
1152
- import { existsSync as existsSync5, readFileSync as readFileSync3, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
1493
+ import { existsSync as existsSync6, readFileSync as readFileSync4, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
1153
1494
  import { randomBytes } from "crypto";
1154
- import { join as join6 } from "path";
1155
1495
  function ensureInternalToken() {
1156
- const path2 = join6(OS_DIR, "internal-token");
1157
- if (existsSync5(path2)) return readFileSync3(path2, "utf8").trim();
1158
- mkdirSync6(OS_DIR, { recursive: true, mode: 448 });
1496
+ const path2 = INTERNAL_TOKEN_FILE;
1497
+ if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
1498
+ mkdirSync7(OS_DIR, { recursive: true, mode: 448 });
1159
1499
  const token = randomBytes(32).toString("hex");
1160
- writeFileSync5(path2, token, { mode: 384 });
1500
+ writeFileSync6(path2, token, { mode: 384 });
1161
1501
  return token;
1162
1502
  }
1163
1503
  async function internalGet(port, path2, token) {
@@ -1421,7 +1761,8 @@ async function headlessProvision(opts) {
1421
1761
  dashPort,
1422
1762
  localEdge: d.localEdge,
1423
1763
  edgeTakeover: d.edgeTakeover,
1424
- edgeMigrate: d.edgeMigrate
1764
+ edgeMigrate: d.edgeMigrate,
1765
+ ...d.staticRootOverrides ? { staticRootOverrides: d.staticRootOverrides } : {}
1425
1766
  });
1426
1767
  let slug = d.slug;
1427
1768
  let res = await internalPost(port, "/api/system/self-register", freeBody(slug), token);
@@ -1469,24 +1810,27 @@ function normalizePublicUrl(raw) {
1469
1810
  const url = normalizeUrl(raw);
1470
1811
  if (!url) {
1471
1812
  console.error(
1472
- chalk6.red(`
1473
- Invalid --public-url: ${raw}`) + chalk6.dim("\n Expected something like https://ops.example.com\n")
1813
+ chalk7.red(`
1814
+ Invalid --public-url: ${raw}`) + chalk7.dim("\n Expected something like https://ops.example.com\n")
1474
1815
  );
1475
1816
  process.exit(1);
1476
1817
  }
1477
1818
  return url;
1478
1819
  }
1479
1820
  var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
1480
- var SERVER_DIR = join7(DIST_DIR, "server");
1821
+ var SERVER_DIR = join8(DIST_DIR, "server");
1481
1822
  function ensureAuthSecret() {
1482
- const path2 = join7(OS_DIR, "auth-secret");
1483
- if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
1484
- mkdirSync7(OS_DIR, { recursive: true, mode: 448 });
1823
+ const path2 = AUTH_SECRET_FILE;
1824
+ if (existsSync7(path2)) return readFileSync5(path2, "utf8").trim();
1825
+ mkdirSync8(OS_DIR, { recursive: true, mode: 448 });
1485
1826
  const secret = randomBytes2(32).toString("hex");
1486
- writeFileSync6(path2, secret, { mode: 384 });
1827
+ writeFileSync7(path2, secret, { mode: 384 });
1487
1828
  return secret;
1488
1829
  }
1489
- 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 (default: 4000, or the next free port if it's taken)").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port (default: 3001, or the next free port if it's taken)").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(
1830
+ 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 (default: 4000, or the next free port if it's taken)").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port (default: 3001, or the next free port if it's taken)").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(
1831
+ "--dry-run",
1832
+ "Preview only: print what `up` would install (method, ports, files, commands, the service definition / compose file) and exit WITHOUT changing this machine"
1833
+ ).option(
1490
1834
  "--public-url <url>",
1491
1835
  "Serve remotely at this public URL (VPS): binds the dashboard to all interfaces, proxies the API same-origin, and requires login"
1492
1836
  ).option(
@@ -1502,34 +1846,31 @@ var upCommand = new Command4("up").description("Start Openship as a persistent s
1502
1846
  "--no-host-control",
1503
1847
  "Harden: don't give the control plane a channel to this machine's OS. No host SSH key is generated or mounted, host operations refuse, and this box stops being offered as a deploy target. Recommended when this box only manages REMOTE servers \u2014 it loses :80/:443 takeover, the host terminal and host port scans. The Docker socket is still mounted (deployments need it), so this is defense in depth, not isolation."
1504
1848
  ).option("--edge <action>", "Compose mode: how to handle an existing proxy on :80/:443 \u2014 'migrate' (import its sites into Openship's edge), 'takeover' (stop it; its sites stop serving), or 'cancel'. Default: prompt when interactive, else cancel.").option("--non-interactive", "Headless install: after the service starts, create the admin + register the domain from the flags below (no prompts). Alias: --yes.").option("--yes", "Alias for --non-interactive.").option("--admin-name <name>", "Admin display name (headless install)").option("--admin-email <email>", "Admin email \u2014 required for a headless install").option("--admin-password <password>", "Admin password (min 8). Prefer the OPENSHIP_ADMIN_PASSWORD env var to keep it out of shell history.").option("--domain-kind <kind>", "Headless install domain: byo | custom | free | none (default: byo if --public-url set, else none)").option("--hostname <host>", "Domain/hostname for --domain-kind byo|custom (or derived from --public-url)").option("--slug <slug>", "Free .opsh.io subdomain for --domain-kind free (box must already be Cloud-connected)").action(async (opts) => {
1849
+ if (opts.dryRun) {
1850
+ console.log(renderUpPlan(await planUp({ ...opts, publicUrl: effectivePublicUrl(opts.publicUrl) })));
1851
+ return;
1852
+ }
1505
1853
  if (opts.fromSource || opts.source) return runFromSource(opts);
1506
1854
  if (opts.foreground) return runForeground(opts);
1507
1855
  const headless = !!(opts.nonInteractive || opts.yes);
1508
- let method;
1509
- if (opts.bare) {
1510
- method = "bare";
1511
- } else if (opts.compose) {
1512
- if (!await ensureDocker()) {
1513
- console.error(
1514
- chalk6.red("\n --compose needs Docker + docker compose, and they couldn't be installed automatically.") + chalk6.dim(
1515
- "\n Install Docker (https://docs.docker.com/engine/install/) and re-run, or use --bare.\n"
1516
- )
1517
- );
1518
- process.exit(1);
1519
- }
1520
- method = "compose";
1521
- } else if (process.platform === "linux") {
1522
- method = await ensureDocker() ? "compose" : "bare";
1523
- } else {
1524
- method = composeIsViableDefault() ? "compose" : "bare";
1856
+ const needsDocker = !opts.bare && (Boolean(opts.compose) || process.platform === "linux");
1857
+ const dockerUsable = needsDocker ? await ensureDocker() : false;
1858
+ const { method } = planMethod(opts, dockerUsable);
1859
+ if (method === "compose" && !dockerUsable) {
1860
+ console.error(
1861
+ chalk7.red("\n --compose needs Docker + docker compose, and they couldn't be installed automatically.") + chalk7.dim(
1862
+ "\n Install Docker (https://docs.docker.com/engine/install/) and re-run, or use --bare.\n"
1863
+ )
1864
+ );
1865
+ process.exit(1);
1525
1866
  }
1526
1867
  if (method === "compose") {
1527
1868
  const started2 = await runCompose(opts);
1528
- if (headless && !opts.dryRun) {
1869
+ if (headless) {
1529
1870
  const token = composeInternalToken();
1530
1871
  if (!token) {
1531
1872
  console.warn(
1532
- chalk6.yellow(
1873
+ chalk7.yellow(
1533
1874
  "\n Couldn't read the stack's internal token (compose/.env) \u2014 create the admin from the dashboard.\n"
1534
1875
  )
1535
1876
  );
@@ -1544,7 +1885,7 @@ var upCommand = new Command4("up").description("Start Openship as a persistent s
1544
1885
  return;
1545
1886
  }
1546
1887
  const started = await startService(opts);
1547
- if (headless && !opts.dryRun) await runHeadlessProvision(opts, started, { method: "bare" });
1888
+ if (headless) await runHeadlessProvision(opts, started, { method: "bare" });
1548
1889
  });
1549
1890
  async function runHeadlessProvision(opts, started, extra) {
1550
1891
  let inputs;
@@ -1562,7 +1903,7 @@ async function runHeadlessProvision(opts, started, extra) {
1562
1903
  });
1563
1904
  } catch (err2) {
1564
1905
  if (err2 instanceof HeadlessInputError) {
1565
- console.error(chalk6.red(`
1906
+ console.error(chalk7.red(`
1566
1907
  ${err2.message}
1567
1908
  `));
1568
1909
  process.exit(1);
@@ -1576,13 +1917,13 @@ async function runHeadlessProvision(opts, started, extra) {
1576
1917
  inputs,
1577
1918
  token: extra?.token,
1578
1919
  method: extra?.method,
1579
- onLog: (m) => console.log(chalk6.dim(` ${m}`))
1920
+ onLog: (m) => console.log(chalk7.dim(` ${m}`))
1580
1921
  });
1581
- console.log(chalk6.green(`
1922
+ console.log(chalk7.green(`
1582
1923
  \u2713 Openship provisioned${result.liveUrl ? `: ${result.liveUrl}` : "."}`));
1583
- for (const w of result.warnings) console.warn(chalk6.yellow(` \u26A0 ${w}`));
1924
+ for (const w of result.warnings) console.warn(chalk7.yellow(` \u26A0 ${w}`));
1584
1925
  } catch (err2) {
1585
- console.error(chalk6.red(`
1926
+ console.error(chalk7.red(`
1586
1927
  Headless provisioning failed: ${err2.message}
1587
1928
  `));
1588
1929
  process.exit(1);
@@ -1592,14 +1933,14 @@ async function runCompose(opts) {
1592
1933
  const headless = !!(opts.nonInteractive || opts.yes);
1593
1934
  if (!hasDockerCompose()) {
1594
1935
  console.error(
1595
- chalk6.red("\n Docker + `docker compose` are required for the Compose install.\n") + chalk6.dim(" Install Docker, or run `openship up --bare` for the process mode.\n")
1936
+ chalk7.red("\n Docker + `docker compose` are required for the Compose install.\n") + chalk7.dim(" Install Docker, or run `openship up --bare` for the process mode.\n")
1596
1937
  );
1597
1938
  process.exit(1);
1598
1939
  }
1599
1940
  if (opts.edge && !EDGE_ACTIONS.includes(opts.edge)) {
1600
1941
  console.error(
1601
- chalk6.red(`
1602
- Invalid --edge value: ${opts.edge}`) + chalk6.dim(`
1942
+ chalk7.red(`
1943
+ Invalid --edge value: ${opts.edge}`) + chalk7.dim(`
1603
1944
  Expected one of: ${EDGE_ACTIONS.join(", ")}
1604
1945
  `)
1605
1946
  );
@@ -1611,7 +1952,7 @@ async function runCompose(opts) {
1611
1952
  const dashboardPort = String(ports.dashboard);
1612
1953
  const portNotice = portMoveNotice(ports, [...composeTrustedOriginUrls(), publicUrl]);
1613
1954
  if (portNotice.length) {
1614
- console.log("\n" + portNotice.map((l) => chalk6.yellow(` ${l}`)).join("\n"));
1955
+ console.log("\n" + portNotice.map((l) => chalk7.yellow(` ${l}`)).join("\n"));
1615
1956
  }
1616
1957
  const prefetch = ora(
1617
1958
  sourceBuildDir() ? "Building images before touching :80/:443\u2026" : "Pulling images before touching :80/:443\u2026"
@@ -1626,7 +1967,7 @@ async function runCompose(opts) {
1626
1967
  if (!fetched) {
1627
1968
  prefetch.fail("Couldn't fetch the stack's images \u2014 nothing was changed on this box.");
1628
1969
  console.error(
1629
- chalk6.dim("\n Your current proxy is untouched and still serving. Fix the pull/build error above and re-run.\n")
1970
+ chalk7.dim("\n Your current proxy is untouched and still serving. Fix the pull/build error above and re-run.\n")
1630
1971
  );
1631
1972
  process.exit(1);
1632
1973
  }
@@ -1636,15 +1977,17 @@ async function runCompose(opts) {
1636
1977
  edgePlan = await planAndApplyHostEdge({ edge: opts.edge });
1637
1978
  } catch (e) {
1638
1979
  console.error(
1639
- chalk6.red(`
1980
+ chalk7.red(`
1640
1981
  Edge preflight failed: ${e.message}
1641
- `) + chalk6.dim(" Re-run, or pass --edge=cancel to skip taking over :80/:443.\n")
1982
+ `) + chalk7.dim(" Re-run, or pass --edge=cancel to skip taking over :80/:443.\n")
1642
1983
  );
1643
1984
  process.exit(1);
1644
1985
  }
1645
1986
  if (!edgePlan.proceed) {
1646
1987
  console.log(
1647
- chalk6.yellow("\n Left the existing proxy on :80/:443 running \u2014 not starting the stack.\n") + chalk6.dim(" Re-run and choose migrate / take-over (or pass --edge=migrate|takeover) when ready.\n")
1988
+ edgePlan.blockedBy ? chalk7.red(`
1989
+ Can't take over the edge: ${edgePlan.blockedBy}.
1990
+ `) + chalk7.dim(" Find what else is bound to those ports (`ss -ltnp | grep -E ':80|:443'`), then re-run.\n") : chalk7.yellow("\n Left the existing proxy on :80/:443 running \u2014 not starting the stack.\n") + chalk7.dim(" Re-run and choose migrate / take-over (or pass --edge=migrate|takeover) when ready.\n")
1648
1991
  );
1649
1992
  process.exit(1);
1650
1993
  }
@@ -1668,7 +2011,7 @@ async function runCompose(opts) {
1668
2011
  spinner5.fail("docker compose failed to start the stack");
1669
2012
  const restored = edgePlan.action ? await rollbackHostEdge() : false;
1670
2013
  console.error(
1671
- chalk6.dim("\n Check `docker compose -f ~/.openship/compose/docker-compose.yml logs`.\n") + (restored ? chalk6.yellow(" Restored the previous proxy on :80/:443 \u2014 your sites are serving again.\n") : chalk6.dim(" If ports 80/443 are held by another proxy, re-run \u2014 the preflight will offer to migrate or take over.\n"))
2014
+ chalk7.dim("\n Check `docker compose -f ~/.openship/compose/docker-compose.yml logs`.\n") + (restored ? chalk7.yellow(" Restored the previous proxy on :80/:443 \u2014 your sites are serving again.\n") : chalk7.dim(" If ports 80/443 are held by another proxy, re-run \u2014 the preflight will offer to migrate or take over.\n"))
1672
2015
  );
1673
2016
  process.exit(1);
1674
2017
  }
@@ -1676,16 +2019,16 @@ async function runCompose(opts) {
1676
2019
  if (edgePlan.action && await edgeIsBroken(new LocalExecutor())) {
1677
2020
  const reason = await edgeCrashReason(new LocalExecutor());
1678
2021
  console.error(
1679
- chalk6.red(`
2022
+ chalk7.red(`
1680
2023
  Openship's edge container is not running${reason ? ` \u2014 ${reason}` : "."}`)
1681
2024
  );
1682
2025
  const restored = await rollbackHostEdge();
1683
2026
  console.error(
1684
- restored ? chalk6.yellow(
2027
+ restored ? chalk7.yellow(
1685
2028
  ` Your previous proxy has been restarted \u2014 the box is serving again, on it.
1686
2029
  Nothing was migrated. Fix the cause above, then re-run \`openship up\`.
1687
2030
  `
1688
- ) : chalk6.red(
2031
+ ) : chalk7.red(
1689
2032
  ` AND your previous proxy could NOT be restarted automatically \u2014 the box is
1690
2033
  serving nothing right now. Start it by hand:
1691
2034
  docker stop openship-edge && sudo systemctl enable --now nginx
@@ -1696,68 +2039,47 @@ async function runCompose(opts) {
1696
2039
  }
1697
2040
  let importedOk = true;
1698
2041
  if (edgePlan.action === "migrate" && edgePlan.sites?.length) {
1699
- const outcome = await importMigratedSites(res.apiPort, edgePlan.sites, edgePlan.certPems);
2042
+ const outcome = await importMigratedSites(
2043
+ res.apiPort,
2044
+ edgePlan.sites,
2045
+ edgePlan.certPems,
2046
+ edgePlan.staticRootOverrides
2047
+ );
1700
2048
  importedOk = outcome.registered.length > 0;
1701
2049
  if (importedOk) markStoppedProxyImported();
1702
2050
  }
1703
2051
  if (edgePlan.action && importedOk) await completeHostEdge();
1704
2052
  const dashboardUrl = publicUrl ?? `http://localhost:${res.dashPort}`;
1705
2053
  console.log(
1706
- chalk6.dim(` Dashboard: ${dashboardUrl} (login required)
1707
- `) + chalk6.dim(" Images: api + dashboard + edge (OpenResty on :80/:443)\n") + chalk6.dim(" Manage: openship stop \xB7 openship update \xB7 openship status\n") + // In headless mode the admin is bootstrapped below — don't tell the user to do it by hand.
1708
- (headless ? "" : chalk6.dim(" Create an admin: open the dashboard and register the first account.\n"))
2054
+ chalk7.dim(` Dashboard: ${dashboardUrl} (login required)
2055
+ `) + chalk7.dim(" Images: api + dashboard + edge (OpenResty on :80/:443)\n") + chalk7.dim(" Manage: openship stop \xB7 openship update \xB7 openship status\n") + // In headless mode the admin is bootstrapped below — don't tell the user to do it by hand.
2056
+ (headless ? "" : chalk7.dim(" Create an admin: open the dashboard and register the first account.\n"))
1709
2057
  );
1710
2058
  return { apiPort: res.apiPort, dashPort: res.dashPort };
1711
2059
  }
1712
2060
  async function runFromSource(opts) {
1713
- console.log(chalk6.cyan("\n Building Openship from source (preview mode)\u2026"));
2061
+ console.log(chalk7.cyan("\n Building Openship from source (preview mode)\u2026"));
1714
2062
  console.log(
1715
- chalk6.dim(" Unverified dev build \u2014 for previewing a branch, not production self-hosting.\n")
2063
+ chalk7.dim(" Unverified dev build \u2014 for previewing a branch, not production self-hosting.\n")
1716
2064
  );
1717
2065
  let src;
1718
2066
  try {
1719
2067
  src = await prepareFromSource({ ref: opts.ref, source: opts.source, repo: opts.repo });
1720
2068
  } catch (e) {
1721
2069
  console.error(
1722
- chalk6.red(`
2070
+ chalk7.red(`
1723
2071
  Build from source failed: ${e.message}
1724
- `) + chalk6.dim(" Small boxes can OOM on the dashboard build \u2014 build on a bigger machine and pass --source, or use a published release.\n")
2072
+ `) + chalk7.dim(" Small boxes can OOM on the dashboard build \u2014 build on a bigger machine and pass --source, or use a published release.\n")
1725
2073
  );
1726
2074
  process.exit(1);
1727
2075
  }
1728
- console.log(chalk6.green(`
2076
+ console.log(chalk7.green(`
1729
2077
  Built ${src.ref} (${src.sha}). Starting\u2026
1730
2078
  `));
1731
2079
  await runForeground(opts, src);
1732
2080
  }
1733
2081
  async function startService(opts, runOpts = {}) {
1734
2082
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1735
- if (opts.dryRun) {
1736
- const p = preview({
1737
- port: opts.port,
1738
- dataDir: opts.dataDir,
1739
- dashboardPort: opts.dashboardPort,
1740
- ui: opts.ui,
1741
- uiVersion: opts.uiVersion,
1742
- publicUrl,
1743
- trustProxy: opts.trustProxy || opts.managedEdge,
1744
- host: opts.host,
1745
- managedEdge: opts.managedEdge,
1746
- acmeEmail: opts.acmeEmail
1747
- });
1748
- console.log(
1749
- chalk6.dim(`
1750
- service manager: ${p.kind}
1751
- path: ${p.path}
1752
-
1753
- `) + p.content + "\n"
1754
- );
1755
- return {
1756
- port: String(opts.port || DEFAULT_API_PORT),
1757
- dashPort: String(opts.dashboardPort || DEFAULT_DASHBOARD_PORT),
1758
- publicUrl
1759
- };
1760
- }
1761
2083
  const resolved = await resolvePorts({
1762
2084
  api: opts.port ? Number(opts.port) : void 0,
1763
2085
  dashboard: opts.dashboardPort ? Number(opts.dashboardPort) : void 0
@@ -1782,24 +2104,24 @@ async function startService(opts, runOpts = {}) {
1782
2104
  if (!runOpts.quiet) {
1783
2105
  const portNotice = portMoveNotice(resolved, [publicUrl]);
1784
2106
  if (portNotice.length) {
1785
- console.log("\n" + portNotice.map((l) => chalk6.yellow(` ${l}`)).join("\n"));
2107
+ console.log("\n" + portNotice.map((l) => chalk7.yellow(` ${l}`)).join("\n"));
1786
2108
  }
1787
- const dashboardLine = publicUrl ? chalk6.dim(` Dashboard: ${publicUrl} (login required)
1788
- `) : chalk6.dim(` Dashboard: http://localhost:${dashPort} (login required)
2109
+ const dashboardLine = publicUrl ? chalk7.dim(` Dashboard: ${publicUrl} (login required)
2110
+ `) : chalk7.dim(` Dashboard: http://localhost:${dashPort} (login required)
1789
2111
  `);
1790
2112
  console.log(
1791
- chalk6.green("\n \u2714 Openship is running as a service.\n") + (opts.ui !== false ? dashboardLine : "") + (publicUrl ? chalk6.dim(" API is proxied through the dashboard (not exposed). Point your reverse proxy / DNS at the dashboard port.\n") : chalk6.dim(` API: http://localhost:${port}/api
1792
- `)) + chalk6.dim(` ${res.detail}
1793
- `) + chalk6.dim(" Starts on boot and auto-restarts. Stop with `openship stop`.\n")
2113
+ chalk7.green("\n \u2714 Openship is running as a service.\n") + (opts.ui !== false ? dashboardLine : "") + (publicUrl ? chalk7.dim(" API is proxied through the dashboard (not exposed). Point your reverse proxy / DNS at the dashboard port.\n") : chalk7.dim(` API: http://localhost:${port}/api
2114
+ `)) + chalk7.dim(` ${res.detail}
2115
+ `) + chalk7.dim(" Starts on boot and auto-restarts. Stop with `openship stop`.\n")
1794
2116
  );
1795
2117
  }
1796
2118
  return { port, dashPort, publicUrl };
1797
2119
  } catch (e) {
1798
2120
  if (runOpts.quiet) throw e;
1799
2121
  console.error(
1800
- chalk6.red(`
2122
+ chalk7.red(`
1801
2123
  Couldn't install the service: ${e.message}
1802
- `) + chalk6.dim(" Run `openship up --foreground` to run it attached instead.\n")
2124
+ `) + chalk7.dim(" Run `openship up --foreground` to run it attached instead.\n")
1803
2125
  );
1804
2126
  process.exit(1);
1805
2127
  }
@@ -1813,10 +2135,10 @@ async function runForeground(opts, source) {
1813
2135
  apiArgs = ["run", "src/index.ts"];
1814
2136
  apiCwd = source.apiDir;
1815
2137
  } else {
1816
- const serverEntry = join7(SERVER_DIR, "index.js");
1817
- if (!existsSync6(serverEntry)) {
2138
+ const serverEntry = join8(SERVER_DIR, "index.js");
2139
+ if (!existsSync7(serverEntry)) {
1818
2140
  console.error(
1819
- chalk6.red("\n Bundled server not found in this install.") + chalk6.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
2141
+ chalk7.red("\n Bundled server not found in this install.") + chalk7.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
1820
2142
  );
1821
2143
  process.exit(1);
1822
2144
  }
@@ -1830,11 +2152,10 @@ async function runForeground(opts, source) {
1830
2152
  const dashPort = String(resolved.dashboard);
1831
2153
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1832
2154
  const managedEdge = Boolean(opts.managedEdge && publicUrl);
1833
- const dataDir = opts.dataDir || join7(OS_DIR, "data");
1834
- mkdirSync7(dataDir, { recursive: true });
1835
- const logDir = join7(OS_DIR, "logs");
1836
- mkdirSync7(logDir, { recursive: true });
1837
- const instanceLogPath = join7(logDir, "instance.log");
2155
+ const dataDir = opts.dataDir || DATA_DIR;
2156
+ mkdirSync8(dataDir, { recursive: true });
2157
+ mkdirSync8(LOG_DIR, { recursive: true });
2158
+ const instanceLogPath = join8(LOG_DIR, "instance.log");
1838
2159
  const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
1839
2160
  const env = {
1840
2161
  ...process.env,
@@ -1862,10 +2183,10 @@ async function runForeground(opts, source) {
1862
2183
  BETTER_AUTH_SECRET: ensureAuthSecret()
1863
2184
  };
1864
2185
  if (!source) {
1865
- env.OPENSHIP_MIGRATIONS_DIR = join7(SERVER_DIR, "migrations");
1866
- env.OPENSHIP_PGLITE_ASSETS_DIR = join7(SERVER_DIR, "pglite");
1867
- const engineDir = join7(SERVER_DIR, "engine");
1868
- if (existsSync6(engineDir)) env.MAIL_SERVER_ENGINE_DIR = engineDir;
2186
+ env.OPENSHIP_MIGRATIONS_DIR = join8(SERVER_DIR, "migrations");
2187
+ env.OPENSHIP_PGLITE_ASSETS_DIR = join8(SERVER_DIR, "pglite");
2188
+ const engineDir = join8(SERVER_DIR, "engine");
2189
+ if (existsSync7(engineDir)) env.MAIL_SERVER_ENGINE_DIR = engineDir;
1869
2190
  }
1870
2191
  env.OPENSHIP_REQUIRE_AUTH = "true";
1871
2192
  env.INTERNAL_TOKEN = ensureInternalToken();
@@ -1957,7 +2278,7 @@ async function runForeground(opts, source) {
1957
2278
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
1958
2279
  try {
1959
2280
  const bundle = await ensureDashboard({
1960
- tag: source ? "local" : opts.uiVersion || `v${"0.5.0"}`,
2281
+ tag: source ? "local" : opts.uiVersion || `v${"0.6.1"}`,
1961
2282
  onProgress: (received, total) => {
1962
2283
  if (total) {
1963
2284
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -2032,7 +2353,7 @@ async function runForeground(opts, source) {
2032
2353
  } catch (e) {
2033
2354
  uiSpinner.warn(`Dashboard unavailable: ${e.message}`);
2034
2355
  console.log(
2035
- chalk6.dim(
2356
+ chalk7.dim(
2036
2357
  " The API is still running. Retry `openship up`, pass --no-ui, or use `openship install` for the desktop app.\n"
2037
2358
  )
2038
2359
  );
@@ -2040,16 +2361,16 @@ async function runForeground(opts, source) {
2040
2361
  }
2041
2362
  if (publicUrl) {
2042
2363
  console.log(
2043
- (dashboardUrl ? chalk6.dim(` Dashboard: ${dashboardUrl} (login required)
2044
- `) : "") + chalk6.dim(" API is proxied through the dashboard (bound to loopback, not exposed).\n") + chalk6.dim(` Data: ${dataDir}
2045
- `) + (managedEdge ? chalk6.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") : chalk6.dim(" Point your reverse proxy / DNS at the dashboard port. Stop with Ctrl-C.\n"))
2364
+ (dashboardUrl ? chalk7.dim(` Dashboard: ${dashboardUrl} (login required)
2365
+ `) : "") + chalk7.dim(" API is proxied through the dashboard (bound to loopback, not exposed).\n") + chalk7.dim(` Data: ${dataDir}
2366
+ `) + (managedEdge ? chalk7.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") : chalk7.dim(" Point your reverse proxy / DNS at the dashboard port. Stop with Ctrl-C.\n"))
2046
2367
  );
2047
2368
  } else {
2048
2369
  console.log(
2049
- chalk6.dim(` API: http://localhost:${port}/api
2050
- `) + (dashboardUrl ? chalk6.dim(` Dashboard: ${dashboardUrl} (login required)
2051
- `) : "") + chalk6.dim(` Data: ${dataDir}
2052
- `) + chalk6.dim(" Log in with your admin account (run `openship` to create one). Stop with Ctrl-C.\n")
2370
+ chalk7.dim(` API: http://localhost:${port}/api
2371
+ `) + (dashboardUrl ? chalk7.dim(` Dashboard: ${dashboardUrl} (login required)
2372
+ `) : "") + chalk7.dim(` Data: ${dataDir}
2373
+ `) + chalk7.dim(" Log in with your admin account (run `openship` to create one). Stop with Ctrl-C.\n")
2053
2374
  );
2054
2375
  }
2055
2376
  child.stdout.off("data", buffer);
@@ -2063,20 +2384,20 @@ async function runForeground(opts, source) {
2063
2384
 
2064
2385
  // src/commands/stop.ts
2065
2386
  import { Command as Command5 } from "commander";
2066
- import chalk7 from "chalk";
2387
+ import chalk8 from "chalk";
2067
2388
  var stopCommand = new Command5("stop").description("Stop Openship (started by `openship up`) \u2014 it won't restart or return on reboot").action(() => {
2068
2389
  if (readInstallMethod() === "compose") {
2069
2390
  const ok3 = composeDown();
2070
- if (ok3) console.log(chalk7.green("\n \u2714 Openship stopped (docker compose down).\n"));
2071
- else console.error(chalk7.red("\n docker compose down failed (or no stack found).\n"));
2391
+ if (ok3) console.log(chalk8.green("\n \u2714 Openship stopped (docker compose down).\n"));
2392
+ else console.error(chalk8.red("\n docker compose down failed (or no stack found).\n"));
2072
2393
  process.exit(ok3 ? 0 : 1);
2073
2394
  }
2074
2395
  try {
2075
2396
  const res = stop();
2076
- console.log(chalk7.green("\n \u2714 Openship stopped.\n") + chalk7.dim(` ${res.detail}
2397
+ console.log(chalk8.green("\n \u2714 Openship stopped.\n") + chalk8.dim(` ${res.detail}
2077
2398
  `));
2078
2399
  } catch (e) {
2079
- console.error(chalk7.red(`
2400
+ console.error(chalk8.red(`
2080
2401
  Couldn't stop the service: ${e.message}
2081
2402
  `));
2082
2403
  process.exit(1);
@@ -2085,9 +2406,9 @@ var stopCommand = new Command5("stop").description("Stop Openship (started by `o
2085
2406
 
2086
2407
  // src/commands/uninstall.ts
2087
2408
  import { Command as Command6 } from "commander";
2088
- import chalk8 from "chalk";
2409
+ import chalk9 from "chalk";
2089
2410
  import { confirm, isCancel as isCancel2 } from "@clack/prompts";
2090
- import { existsSync as existsSync7, rmSync as rmSync3 } from "fs";
2411
+ import { existsSync as existsSync8, rmSync as rmSync3 } from "fs";
2091
2412
  var uninstallCommand = new Command6("uninstall").description(
2092
2413
  "Remove Openship from this machine: stop it, delete its data (database, certs, edge config), its images, and ~/.openship. Deployed apps are left running."
2093
2414
  ).option("-y, --yes", "Skip the confirmation prompt (for scripts)").option("--keep-data", "Keep the database/certs volumes and ~/.openship \u2014 remove only the running stack").option("--keep-images", "Don't delete the Openship container images").action(async (opts) => {
@@ -2101,66 +2422,66 @@ var uninstallCommand = new Command6("uninstall").description(
2101
2422
  !opts.keepImages && method === "compose" ? " \u2022 delete the Openship container images" : null,
2102
2423
  " \u2022 leave your DEPLOYED apps and their data untouched"
2103
2424
  ].filter(Boolean);
2104
- console.log(chalk8.yellow("\n This will:\n") + lines.join("\n") + "\n");
2425
+ console.log(chalk9.yellow("\n This will:\n") + lines.join("\n") + "\n");
2105
2426
  const go = await confirm({
2106
2427
  message: destructive ? "Permanently remove Openship and its data?" : "Remove Openship (keeping data)?",
2107
2428
  initialValue: false
2108
2429
  });
2109
2430
  if (isCancel2(go) || !go) {
2110
- console.log(chalk8.dim("\n Cancelled \u2014 nothing was changed.\n"));
2431
+ console.log(chalk9.dim("\n Cancelled \u2014 nothing was changed.\n"));
2111
2432
  return;
2112
2433
  }
2113
2434
  }
2114
2435
  const restored = await rollbackHostEdge().catch(() => false);
2115
- if (restored) console.log(chalk8.green(" Restored the proxy that was running before Openship."));
2436
+ if (restored) console.log(chalk9.green(" Restored the proxy that was running before Openship."));
2116
2437
  if (method === "compose") {
2117
2438
  const { ok: ok3, removedImages } = composeUninstall({ removeImages: !opts.keepImages });
2118
2439
  if (!ok3) {
2119
2440
  console.log(
2120
- chalk8.yellow(" No compose stack found (or `down` failed) \u2014 continuing with local cleanup.")
2441
+ chalk9.yellow(" No compose stack found (or `down` failed) \u2014 continuing with local cleanup.")
2121
2442
  );
2122
2443
  } else {
2123
2444
  console.log(
2124
- chalk8.green(` Stack removed${destructive ? " (including its volumes)" : ""}.`)
2445
+ chalk9.green(` Stack removed${destructive ? " (including its volumes)" : ""}.`)
2125
2446
  );
2126
2447
  }
2127
- for (const ref of removedImages) console.log(chalk8.dim(` removed image ${ref}`));
2448
+ for (const ref of removedImages) console.log(chalk9.dim(` removed image ${ref}`));
2128
2449
  } else {
2129
2450
  try {
2130
2451
  const res = stop();
2131
- console.log(chalk8.green(` Service removed \u2014 ${res.detail}`));
2452
+ console.log(chalk9.green(` Service removed \u2014 ${res.detail}`));
2132
2453
  } catch (err2) {
2133
- console.log(chalk8.yellow(` Couldn't remove the service: ${err2.message}`));
2454
+ console.log(chalk9.yellow(` Couldn't remove the service: ${err2.message}`));
2134
2455
  }
2135
2456
  }
2136
- if (destructive && existsSync7(OS_DIR)) {
2457
+ if (destructive && existsSync8(OS_DIR)) {
2137
2458
  try {
2138
2459
  rmSync3(OS_DIR, { recursive: true, force: true });
2139
- console.log(chalk8.green(` Removed ${OS_DIR}`));
2460
+ console.log(chalk9.green(` Removed ${OS_DIR}`));
2140
2461
  } catch (err2) {
2141
- console.log(chalk8.yellow(` Couldn't remove ${OS_DIR}: ${err2.message}`));
2462
+ console.log(chalk9.yellow(` Couldn't remove ${OS_DIR}: ${err2.message}`));
2142
2463
  }
2143
2464
  }
2144
2465
  console.log(
2145
- chalk8.green("\n \u2714 Openship uninstalled.\n") + // A takeover that SUCCEEDED cleared its journal, so we have no record of
2466
+ chalk9.green("\n \u2714 Openship uninstalled.\n") + // A takeover that SUCCEEDED cleared its journal, so we have no record of
2146
2467
  // what to restart — say so rather than leaving :80/:443 quietly dark.
2147
- (restored ? "" : chalk8.dim(
2468
+ (restored ? "" : chalk9.dim(
2148
2469
  " If Openship took over :80/:443 from another proxy, re-enable it:\n sudo systemctl enable --now nginx (or caddy / apache2)\n"
2149
- )) + chalk8.dim(" Deployed apps are still running \u2014 `docker ps` to review them.\n")
2470
+ )) + chalk9.dim(" Deployed apps are still running \u2014 `docker ps` to review them.\n")
2150
2471
  );
2151
2472
  });
2152
2473
 
2153
2474
  // src/commands/init.ts
2154
2475
  import { Command as Command7 } from "commander";
2155
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
2476
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
2156
2477
  import { createInterface as createInterface2 } from "readline/promises";
2157
2478
  import { stdin as input2, stdout as output2 } from "process";
2158
- import { join as join8 } from "path";
2479
+ import { join as join9 } from "path";
2159
2480
  var initCommand = new Command7("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) => {
2160
2481
  const root = opts.dir || process.cwd();
2161
- const linkDir = join8(root, ".openship");
2162
- const linkPath = join8(linkDir, "project.json");
2163
- if (existsSync8(linkPath) && !opts.force) {
2482
+ const linkDir = join9(root, ".openship");
2483
+ const linkPath = join9(linkDir, "project.json");
2484
+ if (existsSync9(linkPath) && !opts.force) {
2164
2485
  err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
2165
2486
  process.exit(1);
2166
2487
  }
@@ -2213,8 +2534,8 @@ var initCommand = new Command7("init").description("Link the current directory t
2213
2534
  context: getActiveContext(),
2214
2535
  defaults: { environment: opts.environment || "production" }
2215
2536
  };
2216
- mkdirSync8(linkDir, { recursive: true });
2217
- writeFileSync7(linkPath, JSON.stringify(link, null, 2) + "\n");
2537
+ mkdirSync9(linkDir, { recursive: true });
2538
+ writeFileSync8(linkPath, JSON.stringify(link, null, 2) + "\n");
2218
2539
  if (isJsonMode()) {
2219
2540
  printJson({ path: linkPath, link });
2220
2541
  return;
@@ -2226,8 +2547,8 @@ var initCommand = new Command7("init").description("Link the current directory t
2226
2547
 
2227
2548
  // src/commands/config.ts
2228
2549
  import { Command as Command8 } from "commander";
2229
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync8 } from "fs";
2230
- import { join as join9 } from "path";
2550
+ import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync9 } from "fs";
2551
+ import { join as join10 } from "path";
2231
2552
  var SCHEMA_URL = "https://openship.io/openship.schema.json";
2232
2553
  var CONFIG_FILE = "openship.json";
2233
2554
  function detectHints(dir) {
@@ -2239,15 +2560,15 @@ function detectHints(dir) {
2239
2560
  ["package-lock.json", "npm"]
2240
2561
  ];
2241
2562
  for (const [file, pm] of lock) {
2242
- if (existsSync9(join9(dir, file))) {
2563
+ if (existsSync10(join10(dir, file))) {
2243
2564
  hints.packageManager = pm;
2244
2565
  break;
2245
2566
  }
2246
2567
  }
2247
- const pkgPath = join9(dir, "package.json");
2248
- if (existsSync9(pkgPath)) {
2568
+ const pkgPath = join10(dir, "package.json");
2569
+ if (existsSync10(pkgPath)) {
2249
2570
  try {
2250
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
2571
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
2251
2572
  const runner = hints.packageManager ?? "npm";
2252
2573
  const exec = runner === "npm" ? "npm run" : runner;
2253
2574
  if (pkg.scripts?.build) hints.buildCommand = `${exec} build`;
@@ -2259,15 +2580,15 @@ function detectHints(dir) {
2259
2580
  }
2260
2581
  var initCmd = new Command8("init").description("Scaffold an openship.json in the current directory").option("--dir <path>", "Directory to write into", process.cwd()).option("--force", "Overwrite an existing openship.json").action((opts) => {
2261
2582
  const dir = opts.dir || process.cwd();
2262
- const path2 = join9(dir, CONFIG_FILE);
2263
- if (existsSync9(path2) && !opts.force) {
2583
+ const path2 = join10(dir, CONFIG_FILE);
2584
+ if (existsSync10(path2) && !opts.force) {
2264
2585
  err(`${CONFIG_FILE} already exists. Re-run with --force to overwrite.`);
2265
2586
  process.exit(1);
2266
2587
  }
2267
2588
  const hints = detectHints(dir);
2268
2589
  const scaffold = { $schema: SCHEMA_URL, ...hints };
2269
2590
  const text2 = JSON.stringify(scaffold, null, 2) + "\n";
2270
- writeFileSync8(path2, text2);
2591
+ writeFileSync9(path2, text2);
2271
2592
  if (isJsonMode()) {
2272
2593
  printJson({ path: path2, config: scaffold });
2273
2594
  return;
@@ -2277,13 +2598,13 @@ var initCmd = new Command8("init").description("Scaffold an openship.json in the
2277
2598
  info(" Edit it to declare framework, env, domains, resources, services\u2026 then `openship config validate`.\n");
2278
2599
  });
2279
2600
  var validateCmd = new Command8("validate").description("Validate an openship.json against the deploy schema").argument("[file]", "Path to the config file", CONFIG_FILE).action((file) => {
2280
- const path2 = file === CONFIG_FILE ? join9(process.cwd(), CONFIG_FILE) : file;
2281
- if (!existsSync9(path2)) {
2601
+ const path2 = file === CONFIG_FILE ? join10(process.cwd(), CONFIG_FILE) : file;
2602
+ if (!existsSync10(path2)) {
2282
2603
  if (isJsonMode()) printJson({ valid: false, errors: [`${path2} not found`], warnings: [] });
2283
2604
  else err(`Not found: ${path2}`);
2284
2605
  process.exit(1);
2285
2606
  }
2286
- const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync5(path2, "utf8"));
2607
+ const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync6(path2, "utf8"));
2287
2608
  const valid = errors.length === 0 && config !== null;
2288
2609
  if (isJsonMode()) {
2289
2610
  printJson({ valid, errors, warnings });
@@ -2355,21 +2676,21 @@ var contextCommand = new Command9("context").alias("ctx").description("Manage co
2355
2676
 
2356
2677
  // src/commands/status.ts
2357
2678
  import { Command as Command10 } from "commander";
2358
- import chalk9 from "chalk";
2679
+ import chalk10 from "chalk";
2359
2680
  var statusCommand = new Command10("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
2360
2681
  const context = getActiveContext();
2361
2682
  const apiUrl = getApiUrl2();
2362
2683
  if (readInstallMethod() === "compose" && !isJsonMode()) {
2363
- console.log(chalk9.bold("\n Openship status (Docker Compose)\n"));
2684
+ console.log(chalk10.bold("\n Openship status (Docker Compose)\n"));
2364
2685
  composePs();
2365
2686
  try {
2366
2687
  const h = await apiRequest("/health", { signal: AbortSignal.timeout(8e3) });
2367
- console.log(chalk9.dim(`
2368
- API: ${apiUrl} \u2014 `) + chalk9.green(h.status ?? "ok") + "\n");
2688
+ console.log(chalk10.dim(`
2689
+ API: ${apiUrl} \u2014 `) + chalk10.green(h.status ?? "ok") + "\n");
2369
2690
  } catch (e) {
2370
2691
  const msg = e instanceof ApiError ? e.message : e.message;
2371
- console.log(chalk9.dim(`
2372
- API: ${apiUrl} \u2014 `) + chalk9.red("not reachable") + chalk9.dim(` ${msg}
2692
+ console.log(chalk10.dim(`
2693
+ API: ${apiUrl} \u2014 `) + chalk10.red("not reachable") + chalk10.dim(` ${msg}
2373
2694
  `));
2374
2695
  }
2375
2696
  return;
@@ -2391,15 +2712,15 @@ var statusCommand = new Command10("status").description("Show the local Openship
2391
2712
  printJson({ context, apiUrl, service: svc, ports, reachable, health, env: envInfo });
2392
2713
  process.exit(reachable ? 0 : 1);
2393
2714
  }
2394
- const row2 = (label, value) => ` ${chalk9.dim(label.padEnd(14))}${value ?? chalk9.dim("-")}
2715
+ const row2 = (label, value) => ` ${chalk10.dim(label.padEnd(14))}${value ?? chalk10.dim("-")}
2395
2716
  `;
2396
- const serviceState = svc.running ? chalk9.green("running") : svc.installed ? chalk9.yellow("installed \xB7 stopped") : chalk9.dim("not installed");
2397
- let out = chalk9.bold("\n Openship status\n\n") + row2("Service", serviceState) + row2("Manager", svc.kind === "unsupported" ? chalk9.dim("none") : svc.kind) + (ports.api ? row2("API port", ports.api) : "") + (ports.dashboard ? row2("Dashboard port", ports.dashboard) : "") + row2("Context", context) + row2("API", apiUrl);
2717
+ const serviceState = svc.running ? chalk10.green("running") : svc.installed ? chalk10.yellow("installed \xB7 stopped") : chalk10.dim("not installed");
2718
+ let out = chalk10.bold("\n Openship status\n\n") + row2("Service", serviceState) + row2("Manager", svc.kind === "unsupported" ? chalk10.dim("none") : svc.kind) + (ports.api ? row2("API port", ports.api) : "") + (ports.dashboard ? row2("Dashboard port", ports.dashboard) : "") + row2("Context", context) + row2("API", apiUrl);
2398
2719
  if (reachable && health && envInfo) {
2399
- out += row2("Health", chalk9.green(health.status ?? "ok")) + row2("Mode", envInfo.selfHosted ? "self-hosted" : "cloud") + row2("Deploy", envInfo.deployMode) + row2("Auth", envInfo.authMode) + row2("Team", envInfo.teamMode) + (envInfo.hostDomain ? row2("Host domain", envInfo.hostDomain) : "") + (envInfo.machineName ? row2("Machine", envInfo.machineName) : "");
2720
+ out += row2("Health", chalk10.green(health.status ?? "ok")) + row2("Mode", envInfo.selfHosted ? "self-hosted" : "cloud") + row2("Deploy", envInfo.deployMode) + row2("Auth", envInfo.authMode) + row2("Team", envInfo.teamMode) + (envInfo.hostDomain ? row2("Host domain", envInfo.hostDomain) : "") + (envInfo.machineName ? row2("Machine", envInfo.machineName) : "");
2400
2721
  } else {
2401
- out += row2("Health", chalk9.red("not reachable")) + chalk9.dim(` ${unreachableMsg}
2402
- `) + chalk9.dim(svc.running ? " (service is up \u2014 it may still be starting)\n" : " Start it with `openship up`.\n");
2722
+ out += row2("Health", chalk10.red("not reachable")) + chalk10.dim(` ${unreachableMsg}
2723
+ `) + chalk10.dim(svc.running ? " (service is up \u2014 it may still be starting)\n" : " Start it with `openship up`.\n");
2403
2724
  }
2404
2725
  process.stdout.write(out + "\n");
2405
2726
  if (!reachable) process.exit(1);
@@ -2407,50 +2728,50 @@ var statusCommand = new Command10("status").description("Show the local Openship
2407
2728
 
2408
2729
  // src/commands/doctor.ts
2409
2730
  import { Command as Command11 } from "commander";
2410
- import { existsSync as existsSync12 } from "fs";
2731
+ import { existsSync as existsSync13 } from "fs";
2411
2732
  import { execFileSync as execFileSync2 } from "child_process";
2412
- import chalk11 from "chalk";
2733
+ import chalk12 from "chalk";
2413
2734
  import open from "open";
2414
2735
  import { intro, outro, note as note2, select as select3, log as log3, spinner as spinner2, confirm as confirm3 } from "@clack/prompts";
2415
2736
 
2416
2737
  // src/lib/repair.ts
2417
2738
  import { spawnSync as spawnSync4 } from "child_process";
2418
- import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
2419
- import { join as join11 } from "path";
2420
- import chalk10 from "chalk";
2739
+ import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
2740
+ import { join as join12 } from "path";
2741
+ import chalk11 from "chalk";
2421
2742
  import { spinner, log as log2, select as select2, isCancel as isCancel3, cancel } from "@clack/prompts";
2422
2743
 
2423
2744
  // src/lib/heal.ts
2424
2745
  import {
2425
- existsSync as existsSync10,
2746
+ existsSync as existsSync11,
2426
2747
  readdirSync,
2427
2748
  statSync,
2428
2749
  unlinkSync,
2429
- mkdirSync as mkdirSync9,
2750
+ mkdirSync as mkdirSync10,
2430
2751
  copyFileSync,
2431
2752
  renameSync
2432
2753
  } from "fs";
2433
- import { homedir as homedir2 } from "os";
2434
- import { join as join10 } from "path";
2754
+ import { homedir as homedir3 } from "os";
2755
+ import { join as join11 } from "path";
2435
2756
  import { execFileSync } from "child_process";
2436
2757
  function resolveDataDir() {
2437
2758
  const fromEnv = process.env.PGLITE_DATA_DIR?.trim();
2438
2759
  if (fromEnv) {
2439
- return fromEnv.startsWith("~") ? join10(homedir2(), fromEnv.slice(1).replace(/^[/\\]/, "")) : fromEnv;
2760
+ return fromEnv.startsWith("~") ? join11(homedir3(), fromEnv.slice(1).replace(/^[/\\]/, "")) : fromEnv;
2440
2761
  }
2441
- return join10(OS_DIR, "data");
2762
+ return DATA_DIR;
2442
2763
  }
2443
2764
  function dataDirExists(dataDir = resolveDataDir()) {
2444
- return existsSync10(dataDir);
2765
+ return existsSync11(dataDir);
2445
2766
  }
2446
2767
  function stamp() {
2447
2768
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2448
2769
  }
2449
2770
  function copyDirRecursive(src, dest) {
2450
- if (!existsSync10(dest)) mkdirSync9(dest, { recursive: true });
2771
+ if (!existsSync11(dest)) mkdirSync10(dest, { recursive: true });
2451
2772
  for (const entry of readdirSync(src)) {
2452
- const s = join10(src, entry);
2453
- const d = join10(dest, entry);
2773
+ const s = join11(src, entry);
2774
+ const d = join11(dest, entry);
2454
2775
  if (statSync(s).isDirectory()) copyDirRecursive(s, d);
2455
2776
  else copyFileSync(s, d);
2456
2777
  }
@@ -2462,30 +2783,30 @@ function backupDataDir(dataDir = resolveDataDir()) {
2462
2783
  }
2463
2784
  function healDataDir(dataDir = resolveDataDir()) {
2464
2785
  const result = { removedPostmasterPid: false, trimmedWalSegment: null };
2465
- const pidPath = join10(dataDir, "postmaster.pid");
2466
- if (existsSync10(pidPath)) {
2786
+ const pidPath = join11(dataDir, "postmaster.pid");
2787
+ if (existsSync11(pidPath)) {
2467
2788
  unlinkSync(pidPath);
2468
2789
  result.removedPostmasterPid = true;
2469
2790
  }
2470
- const walDir = join10(dataDir, "pg_wal");
2471
- if (existsSync10(walDir)) {
2472
- const segments = readdirSync(walDir).filter((name) => /^[0-9A-F]{24}$/.test(name)).map((name) => ({ name, mtime: statSync(join10(walDir, name)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2791
+ const walDir = join11(dataDir, "pg_wal");
2792
+ if (existsSync11(walDir)) {
2793
+ const segments = readdirSync(walDir).filter((name) => /^[0-9A-F]{24}$/.test(name)).map((name) => ({ name, mtime: statSync(join11(walDir, name)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2473
2794
  if (segments.length > 0) {
2474
2795
  const newest = segments[0];
2475
- unlinkSync(join10(walDir, newest.name));
2796
+ unlinkSync(join11(walDir, newest.name));
2476
2797
  result.trimmedWalSegment = newest.name;
2477
2798
  }
2478
2799
  }
2479
2800
  return result;
2480
2801
  }
2481
2802
  function restoreBackup(backupDir, dataDir = resolveDataDir()) {
2482
- if (!existsSync10(backupDir)) throw new Error(`Backup not found: ${backupDir}`);
2483
- if (existsSync10(dataDir)) renameSync(dataDir, `${dataDir}.discarded-${stamp()}`);
2803
+ if (!existsSync11(backupDir)) throw new Error(`Backup not found: ${backupDir}`);
2804
+ if (existsSync11(dataDir)) renameSync(dataDir, `${dataDir}.discarded-${stamp()}`);
2484
2805
  copyDirRecursive(backupDir, dataDir);
2485
2806
  }
2486
2807
  function freshStart(dataDir = resolveDataDir()) {
2487
2808
  const aside = `${dataDir}.corrupt-${stamp()}`;
2488
- if (existsSync10(dataDir)) renameSync(dataDir, aside);
2809
+ if (existsSync11(dataDir)) renameSync(dataDir, aside);
2489
2810
  return aside;
2490
2811
  }
2491
2812
  function findResetwal() {
@@ -2497,7 +2818,7 @@ function findResetwal() {
2497
2818
  "/usr/lib/postgresql/16/bin/pg_resetwal",
2498
2819
  "/usr/bin/pg_resetwal"
2499
2820
  ];
2500
- for (const c of candidates) if (existsSync10(c)) return c;
2821
+ for (const c of candidates) if (existsSync11(c)) return c;
2501
2822
  try {
2502
2823
  const found = execFileSync("bash", ["-lc", "command -v pg_resetwal"], {
2503
2824
  encoding: "utf8",
@@ -2518,7 +2839,6 @@ function deepHeal(dataDir = resolveDataDir()) {
2518
2839
  }
2519
2840
 
2520
2841
  // src/lib/repair.ts
2521
- var LOG_DIR2 = join11(OS_DIR, "logs");
2522
2842
  function ensure(value) {
2523
2843
  if (isCancel3(value)) {
2524
2844
  cancel("Cancelled.");
@@ -2564,10 +2884,10 @@ async function waitApiDown(seconds = 20) {
2564
2884
  }
2565
2885
  function lastServiceError() {
2566
2886
  for (const name of ["up.err.log", "up.log", "instance.log"]) {
2567
- const p = join11(LOG_DIR2, name);
2568
- if (!existsSync11(p)) continue;
2887
+ const p = join12(LOG_DIR, name);
2888
+ if (!existsSync12(p)) continue;
2569
2889
  try {
2570
- const lines = readFileSync6(p, "utf8").trim().split("\n");
2890
+ const lines = readFileSync7(p, "utf8").trim().split("\n");
2571
2891
  const hit = [...lines].reverse().find((l) => /error|locked|aborted|malformed|corrupt|throw|cannot|EADDRINUSE/i.test(l));
2572
2892
  if (hit) return hit.trim().slice(0, 240);
2573
2893
  } catch {
@@ -2682,7 +3002,7 @@ async function runRepair() {
2682
3002
  bk.start("Backing up the database");
2683
3003
  try {
2684
3004
  backupDir = backupDataDir();
2685
- bk.stop(`Backup saved \u2192 ${chalk10.dim(backupDir)}`);
3005
+ bk.stop(`Backup saved \u2192 ${chalk11.dim(backupDir)}`);
2686
3006
  } catch (err2) {
2687
3007
  bk.stop("Backup failed \u2014 aborting to protect your data.", 1);
2688
3008
  await bringUp();
@@ -2698,7 +3018,7 @@ async function runRepair() {
2698
3018
  up.start("Restarting and verifying");
2699
3019
  let healthy = await bringUp();
2700
3020
  let verified = healthy && !!(await internalGet2("/api/system/health"))?.db?.ok;
2701
- up.stop(verified ? chalk10.green("Database is healthy again.") : "Still not healthy after the light repair.", verified ? 0 : 1);
3021
+ up.stop(verified ? chalk11.green("Database is healthy again.") : "Still not healthy after the light repair.", verified ? 0 : 1);
2702
3022
  if (verified) {
2703
3023
  return { healed: true, backupDir, detail: "In-place repair succeeded \u2014 your data is intact." };
2704
3024
  }
@@ -2746,7 +3066,7 @@ async function runRepair() {
2746
3066
  const f = spinner();
2747
3067
  f.start("Starting a fresh database");
2748
3068
  const aside = freshStart();
2749
- f.stop(`Corrupt data parked \u2192 ${chalk10.dim(aside)}`);
3069
+ f.stop(`Corrupt data parked \u2192 ${chalk11.dim(aside)}`);
2750
3070
  const ok3 = await bringUp();
2751
3071
  return {
2752
3072
  healed: ok3,
@@ -2759,9 +3079,9 @@ async function runRepair() {
2759
3079
 
2760
3080
  // src/commands/doctor.ts
2761
3081
  var GLYPH = {
2762
- pass: chalk11.green("\u2713"),
2763
- warn: chalk11.yellow("!"),
2764
- fail: chalk11.red("\u2717")
3082
+ pass: chalk12.green("\u2713"),
3083
+ warn: chalk12.yellow("!"),
3084
+ fail: chalk12.red("\u2717")
2765
3085
  };
2766
3086
  function isLocalContext() {
2767
3087
  return /localhost|127\.0\.0\.1|\[::1\]/i.test(getApiUrl());
@@ -2797,7 +3117,7 @@ function servicesSummary(s) {
2797
3117
  };
2798
3118
  }
2799
3119
  function row(name, c) {
2800
- return `${GLYPH[c.state]} ${chalk11.bold(name.padEnd(10))} ${chalk11.dim(c.detail)}`;
3120
+ return `${GLYPH[c.state]} ${chalk12.bold(name.padEnd(10))} ${chalk12.dim(c.detail)}`;
2801
3121
  }
2802
3122
  function statusPanel(s) {
2803
3123
  const lines = [
@@ -2809,10 +3129,10 @@ function statusPanel(s) {
2809
3129
  if (s.services.length > 0) {
2810
3130
  for (const svc of s.services.slice(0, 8)) {
2811
3131
  const g = svc.state === "running" ? GLYPH.pass : svc.state === "failed" ? GLYPH.fail : GLYPH.warn;
2812
- lines.push(chalk11.dim(` ${g} ${svc.name} \u2014 ${svc.state}`));
3132
+ lines.push(chalk12.dim(` ${g} ${svc.name} \u2014 ${svc.state}`));
2813
3133
  }
2814
3134
  }
2815
- if (s.corrupted && s.lastError) lines.push("", chalk11.red(`Last error: ${s.lastError}`));
3135
+ if (s.corrupted && s.lastError) lines.push("", chalk12.red(`Last error: ${s.lastError}`));
2816
3136
  return lines.join("\n");
2817
3137
  }
2818
3138
  function bunVersion() {
@@ -2826,7 +3146,7 @@ function bunVersion() {
2826
3146
  }
2827
3147
  async function remotePreflight() {
2828
3148
  const checks = [];
2829
- const hasConfig = existsSync12(CONFIG_PATH);
3149
+ const hasConfig = existsSync13(CONFIG_PATH);
2830
3150
  checks.push({ name: "config", status: hasConfig ? "pass" : "warn", detail: hasConfig ? CONFIG_PATH : `not found; run \`openship login\`` });
2831
3151
  const context = getActiveContext();
2832
3152
  const apiUrl = getApiUrl();
@@ -2846,8 +3166,8 @@ async function remotePreflight() {
2846
3166
  if (isJsonMode()) {
2847
3167
  printJson({ context, apiUrl, reachable, checks });
2848
3168
  } else {
2849
- process.stdout.write(chalk11.bold("\n Openship doctor\n\n"));
2850
- for (const c of checks) process.stdout.write(` ${GLYPH[c.status]} ${chalk11.bold(c.name.padEnd(8))} ${c.detail}
3169
+ process.stdout.write(chalk12.bold("\n Openship doctor\n\n"));
3170
+ for (const c of checks) process.stdout.write(` ${GLYPH[c.status]} ${chalk12.bold(c.name.padEnd(8))} ${c.detail}
2851
3171
  `);
2852
3172
  process.stdout.write("\n");
2853
3173
  }
@@ -2871,16 +3191,16 @@ async function reportOnce() {
2871
3191
  lastError: s.lastError
2872
3192
  });
2873
3193
  } else {
2874
- process.stdout.write(chalk11.bold("\n Openship doctor\n\n"));
2875
- process.stdout.write(` ${GLYPH[svc.state]} ${chalk11.bold("Service".padEnd(10))} ${svc.detail}
3194
+ process.stdout.write(chalk12.bold("\n Openship doctor\n\n"));
3195
+ process.stdout.write(` ${GLYPH[svc.state]} ${chalk12.bold("Service".padEnd(10))} ${svc.detail}
2876
3196
  `);
2877
- process.stdout.write(` ${GLYPH[db.state]} ${chalk11.bold("Database".padEnd(10))} ${db.detail}
3197
+ process.stdout.write(` ${GLYPH[db.state]} ${chalk12.bold("Database".padEnd(10))} ${db.detail}
2878
3198
  `);
2879
- for (const c of s.components) process.stdout.write(` ${GLYPH[c.state]} ${chalk11.bold(c.name.padEnd(10))} ${c.detail}
3199
+ for (const c of s.components) process.stdout.write(` ${GLYPH[c.state]} ${chalk12.bold(c.name.padEnd(10))} ${c.detail}
2880
3200
  `);
2881
- process.stdout.write(` ${GLYPH[services.state]} ${chalk11.bold("Services".padEnd(10))} ${services.detail}
3201
+ process.stdout.write(` ${GLYPH[services.state]} ${chalk12.bold("Services".padEnd(10))} ${services.detail}
2882
3202
  `);
2883
- if (s.corrupted) process.stdout.write(chalk11.yellow(`
3203
+ if (s.corrupted) process.stdout.write(chalk12.yellow(`
2884
3204
  Database looks corrupted \u2014 run \`openship doctor\` (interactive) or \`openship doctor --fix\`.
2885
3205
  `));
2886
3206
  process.stdout.write("\n");
@@ -2892,11 +3212,11 @@ async function autoFix() {
2892
3212
  const out = (m) => process.stdout.write(` ${m}
2893
3213
  `);
2894
3214
  if (!dataDirExists()) {
2895
- out(chalk11.dim(`No embedded database at ${resolveDataDir()} \u2014 nothing to repair.`));
3215
+ out(chalk12.dim(`No embedded database at ${resolveDataDir()} \u2014 nothing to repair.`));
2896
3216
  return;
2897
3217
  }
2898
3218
  if ((await internalGet2("/api/system/health"))?.db?.ok) {
2899
- out(chalk11.green("Database is healthy \u2014 nothing to fix."));
3219
+ out(chalk12.green("Database is healthy \u2014 nothing to fix."));
2900
3220
  return;
2901
3221
  }
2902
3222
  out("Stopping the service\u2026");
@@ -2911,17 +3231,17 @@ async function autoFix() {
2911
3231
  const healthy = await waitApiHealthy(60);
2912
3232
  const verified = healthy && !!(await internalGet2("/api/system/health"))?.db?.ok;
2913
3233
  if (verified) {
2914
- out(chalk11.green("Database is healthy again. Your data is intact."));
3234
+ out(chalk12.green("Database is healthy again. Your data is intact."));
2915
3235
  } else {
2916
- out(chalk11.red(`Still not healthy. Backup preserved at ${backup}. Run \`openship doctor\` for guided recovery.`));
3236
+ out(chalk12.red(`Still not healthy. Backup preserved at ${backup}. Run \`openship doctor\` for guided recovery.`));
2917
3237
  process.exit(1);
2918
3238
  }
2919
3239
  }
2920
3240
  async function interactiveDoctor() {
2921
- intro(`${chalk11.bgCyan(chalk11.black(" Openship "))}${chalk11.dim(" doctor")}`);
3241
+ intro(`${chalk12.bgCyan(chalk12.black(" Openship "))}${chalk12.dim(" doctor")}`);
2922
3242
  for (; ; ) {
2923
3243
  const s = await gatherStatus();
2924
- note2(statusPanel(s), s.corrupted ? chalk11.red("Database looks corrupted") : "Health");
3244
+ note2(statusPanel(s), s.corrupted ? chalk12.red("Database looks corrupted") : "Health");
2925
3245
  const options = [];
2926
3246
  if (s.corrupted || !s.apiUp && dataDirExists() || s.health?.db && !s.health.db.ok) {
2927
3247
  options.push({ value: "repair", label: "Repair database", hint: "backup \u2192 heal \u2192 verify" });
@@ -2935,7 +3255,7 @@ async function interactiveDoctor() {
2935
3255
  await select3({ message: "What would you like to do?", options, initialValue: options[0]?.value })
2936
3256
  );
2937
3257
  if (action2 === "quit") {
2938
- outro(chalk11.dim("Bye."));
3258
+ outro(chalk12.dim("Bye."));
2939
3259
  return;
2940
3260
  }
2941
3261
  if (action2 === "recheck") continue;
@@ -2954,12 +3274,12 @@ async function interactiveDoctor() {
2954
3274
  sp.start("Restarting");
2955
3275
  const r = restart();
2956
3276
  const up = r.restarted ? await waitApiHealthy(45) : false;
2957
- sp.stop(r.restarted && up ? chalk11.green("Restarted and healthy.") : chalk11.yellow(r.detail), r.restarted && up ? 0 : 1);
3277
+ sp.stop(r.restarted && up ? chalk12.green("Restarted and healthy.") : chalk12.yellow(r.detail), r.restarted && up ? 0 : 1);
2958
3278
  continue;
2959
3279
  }
2960
3280
  if (action2 === "logs") {
2961
3281
  const err2 = lastServiceError();
2962
- note2(err2 ? chalk11.red(err2) : chalk11.dim("No recent errors in the service logs."), "Recent errors");
3282
+ note2(err2 ? chalk12.red(err2) : chalk12.dim("No recent errors in the service logs."), "Recent errors");
2963
3283
  continue;
2964
3284
  }
2965
3285
  if (action2 === "open") {
@@ -2967,7 +3287,7 @@ async function interactiveDoctor() {
2967
3287
  const target = url && !/^https?:\/\/localhost/i.test(url) ? url : `http://localhost:${storedDashboardPort()}`;
2968
3288
  await open(target).catch(() => {
2969
3289
  });
2970
- log3.message(chalk11.dim(`Opening ${target}`));
3290
+ log3.message(chalk12.dim(`Opening ${target}`));
2971
3291
  continue;
2972
3292
  }
2973
3293
  }
@@ -2994,15 +3314,15 @@ import { execFileSync as execFileSync4 } from "child_process";
2994
3314
  import ora2 from "ora";
2995
3315
 
2996
3316
  // src/lib/project-link.ts
2997
- import { readFileSync as readFileSync7, existsSync as existsSync13 } from "fs";
2998
- import { join as join12, dirname as dirname3, parse } from "path";
2999
- var LINK_REL = join12(".openship", "project.json");
3317
+ import { readFileSync as readFileSync8, existsSync as existsSync14 } from "fs";
3318
+ import { join as join13, dirname as dirname3, parse } from "path";
3319
+ var LINK_REL = join13(".openship", "project.json");
3000
3320
  function findProjectLinkPath(from = process.cwd()) {
3001
3321
  let dir = from;
3002
3322
  const root = parse(dir).root;
3003
3323
  for (; ; ) {
3004
- const candidate = join12(dir, LINK_REL);
3005
- if (existsSync13(candidate)) return candidate;
3324
+ const candidate = join13(dir, LINK_REL);
3325
+ if (existsSync14(candidate)) return candidate;
3006
3326
  if (dir === root) return null;
3007
3327
  dir = dirname3(dir);
3008
3328
  }
@@ -3011,7 +3331,7 @@ function readProjectLink(from) {
3011
3331
  const path2 = findProjectLinkPath(from);
3012
3332
  if (!path2) return null;
3013
3333
  try {
3014
- return JSON.parse(readFileSync7(path2, "utf8"));
3334
+ return JSON.parse(readFileSync8(path2, "utf8"));
3015
3335
  } catch {
3016
3336
  return null;
3017
3337
  }
@@ -3019,21 +3339,21 @@ function readProjectLink(from) {
3019
3339
 
3020
3340
  // src/lib/folder-deploy.ts
3021
3341
  import { execFileSync as execFileSync3 } from "child_process";
3022
- import { readFileSync as readFileSync8, existsSync as existsSync14, rmSync as rmSync4 } from "fs";
3342
+ import { readFileSync as readFileSync9, existsSync as existsSync15, rmSync as rmSync4 } from "fs";
3023
3343
  import { tmpdir } from "os";
3024
- import { join as join13, basename } from "path";
3344
+ import { join as join14, basename } from "path";
3025
3345
  function detectPackageManager(dir) {
3026
- if (existsSync14(join13(dir, "bun.lockb")) || existsSync14(join13(dir, "bun.lock"))) return "bun";
3027
- if (existsSync14(join13(dir, "pnpm-lock.yaml"))) return "pnpm";
3028
- if (existsSync14(join13(dir, "yarn.lock"))) return "yarn";
3029
- if (existsSync14(join13(dir, "package.json"))) return "npm";
3346
+ if (existsSync15(join14(dir, "bun.lockb")) || existsSync15(join14(dir, "bun.lock"))) return "bun";
3347
+ if (existsSync15(join14(dir, "pnpm-lock.yaml"))) return "pnpm";
3348
+ if (existsSync15(join14(dir, "yarn.lock"))) return "yarn";
3349
+ if (existsSync15(join14(dir, "package.json"))) return "npm";
3030
3350
  return void 0;
3031
3351
  }
3032
3352
  function detectStack(dir) {
3033
- if (existsSync14(join13(dir, "go.mod"))) return "go";
3034
- if (existsSync14(join13(dir, "Cargo.toml"))) return "rust";
3035
- if (existsSync14(join13(dir, "requirements.txt")) || existsSync14(join13(dir, "pyproject.toml"))) return "python";
3036
- if (existsSync14(join13(dir, "package.json"))) return "node";
3353
+ if (existsSync15(join14(dir, "go.mod"))) return "go";
3354
+ if (existsSync15(join14(dir, "Cargo.toml"))) return "rust";
3355
+ if (existsSync15(join14(dir, "requirements.txt")) || existsSync15(join14(dir, "pyproject.toml"))) return "python";
3356
+ if (existsSync15(join14(dir, "package.json"))) return "node";
3037
3357
  return void 0;
3038
3358
  }
3039
3359
  async function deployFolder(opts) {
@@ -3050,7 +3370,7 @@ async function deployFolder(opts) {
3050
3370
  throw new Error(session.error || "Failed to open upload session");
3051
3371
  }
3052
3372
  step("Packaging folder");
3053
- const tarball = join13(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3373
+ const tarball = join14(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3054
3374
  execFileSync3(
3055
3375
  "tar",
3056
3376
  [
@@ -3069,7 +3389,7 @@ async function deployFolder(opts) {
3069
3389
  );
3070
3390
  step("Uploading source");
3071
3391
  try {
3072
- const body = readFileSync8(tarball);
3392
+ const body = readFileSync9(tarball);
3073
3393
  const up = session.upload;
3074
3394
  const method = up.method || "POST";
3075
3395
  const res = /^https?:\/\//i.test(up.url) ? await fetch(up.url, { method, headers: up.headers, body }) : await apiRaw(`/${up.url.replace(/^\/+/, "")}`, { method, headers: up.headers, body });
@@ -3395,7 +3715,7 @@ function shortSha2(v) {
3395
3715
  async function confirm4(question) {
3396
3716
  if (!process.stdin.isTTY) return true;
3397
3717
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
3398
- const answer = await new Promise((resolve2) => rl.question(`${question} [y/N] `, resolve2));
3718
+ const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
3399
3719
  rl.close();
3400
3720
  return /^y(es)?$/i.test(answer.trim());
3401
3721
  }
@@ -3600,7 +3920,7 @@ var logsCommand = new Command14("logs").description("View or stream a deployment
3600
3920
 
3601
3921
  // src/commands/project.ts
3602
3922
  import { Command as Command15 } from "commander";
3603
- import chalk12 from "chalk";
3923
+ import chalk13 from "chalk";
3604
3924
  import { createInterface as createInterface4 } from "readline/promises";
3605
3925
  import { stdin as input3, stdout as output3 } from "process";
3606
3926
  function action(fn) {
@@ -3631,7 +3951,7 @@ function printProject(project) {
3631
3951
  ];
3632
3952
  for (const [k, v] of fields) {
3633
3953
  if (v === null || v === void 0 || v === "") continue;
3634
- process.stdout.write(` ${chalk12.dim(k.padEnd(12))} ${String(v)}
3954
+ process.stdout.write(` ${chalk13.dim(k.padEnd(12))} ${String(v)}
3635
3955
  `);
3636
3956
  }
3637
3957
  }
@@ -3688,7 +4008,7 @@ var deleteCmd = new Command15("delete").alias("rm").description("Delete a projec
3688
4008
  if (!opts.yes) {
3689
4009
  const rl = createInterface4({ input: input3, output: output3 });
3690
4010
  const answer = await rl.question(
3691
- chalk12.yellow(` Delete project ${id}? This cannot be undone. `) + "(y/N) "
4011
+ chalk13.yellow(` Delete project ${id}? This cannot be undone. `) + "(y/N) "
3692
4012
  );
3693
4013
  rl.close();
3694
4014
  if (answer.trim().toLowerCase() !== "y") {
@@ -4031,9 +4351,9 @@ function safeParse(s) {
4031
4351
  }
4032
4352
  }
4033
4353
  function printLogEntry(entry) {
4034
- const ts = entry.timestamp ? chalk12.dim(String(entry.timestamp)) : "";
4354
+ const ts = entry.timestamp ? chalk13.dim(String(entry.timestamp)) : "";
4035
4355
  const level = String(entry.level ?? "info");
4036
- const color = level === "error" ? chalk12.red : level === "warn" ? chalk12.yellow : chalk12.dim;
4356
+ const color = level === "error" ? chalk13.red : level === "warn" ? chalk13.yellow : chalk13.dim;
4037
4357
  const msg = entry.message ?? entry.data ?? "";
4038
4358
  process.stdout.write(` ${ts} ${color(level.padEnd(5))} ${String(msg)}
4039
4359
  `);
@@ -4055,7 +4375,7 @@ projectCommand.addCommand(serverLogsCmd);
4055
4375
 
4056
4376
  // src/commands/service.ts
4057
4377
  import { Command as Command16 } from "commander";
4058
- import chalk13 from "chalk";
4378
+ import chalk14 from "chalk";
4059
4379
  import { spawnSync as spawnSync5 } from "child_process";
4060
4380
  import path from "path";
4061
4381
  import { createInterface as createInterface5 } from "readline/promises";
@@ -4068,7 +4388,7 @@ function requireAuth() {
4068
4388
  }
4069
4389
  function fail(e) {
4070
4390
  if (e instanceof ApiError) {
4071
- err(` ${e.message}` + (e.status ? chalk13.dim(` (HTTP ${e.status})`) : ""));
4391
+ err(` ${e.message}` + (e.status ? chalk14.dim(` (HTTP ${e.status})`) : ""));
4072
4392
  } else {
4073
4393
  err(` ${e instanceof Error ? e.message : String(e)}`);
4074
4394
  }
@@ -4160,7 +4480,7 @@ var listCmd3 = stackCommand("list").description("List the services in a stack").
4160
4480
  image: s.image ?? "\u2014",
4161
4481
  enabled: s.enabled ? "yes" : "no",
4162
4482
  exposed: s.exposed ? "yes" : "no",
4163
- drift: s.drift ? chalk13.yellow("pending") : "\u2014"
4483
+ drift: s.drift ? chalk14.yellow("pending") : "\u2014"
4164
4484
  })),
4165
4485
  ["name", "kind", "image", "enabled", "exposed", "drift"]
4166
4486
  );
@@ -4530,8 +4850,8 @@ function printLogEntry2(entry) {
4530
4850
  process.stdout.write(JSON.stringify(entry) + "\n");
4531
4851
  return;
4532
4852
  }
4533
- const ts = entry.timestamp ? chalk13.dim(entry.timestamp) : "";
4534
- const line = entry.level === "error" ? chalk13.red(msg) : entry.level === "warn" ? chalk13.yellow(msg) : msg;
4853
+ const ts = entry.timestamp ? chalk14.dim(entry.timestamp) : "";
4854
+ const line = entry.level === "error" ? chalk14.red(msg) : entry.level === "warn" ? chalk14.yellow(msg) : msg;
4535
4855
  process.stdout.write(`${ts ? ts + " " : ""}${line}
4536
4856
  `);
4537
4857
  }
@@ -4597,14 +4917,14 @@ serviceCommand.addCommand(execCmd);
4597
4917
 
4598
4918
  // src/commands/domain.ts
4599
4919
  import { Command as Command17 } from "commander";
4600
- import chalk14 from "chalk";
4920
+ import chalk15 from "chalk";
4601
4921
  import ora3 from "ora";
4602
4922
  function spin(text2) {
4603
4923
  return isJsonMode() ? null : ora3(text2).start();
4604
4924
  }
4605
4925
  function fail2(e) {
4606
4926
  if (e instanceof ApiError) {
4607
- err(` ${e.message}${e.status ? chalk14.dim(` (${e.status})`) : ""}`);
4927
+ err(` ${e.message}${e.status ? chalk15.dim(` (${e.status})`) : ""}`);
4608
4928
  } else {
4609
4929
  err(` ${e instanceof Error ? e.message : String(e)}`);
4610
4930
  }
@@ -4788,7 +5108,7 @@ var domainCommand = new Command17("domain").description("Manage custom domains,
4788
5108
 
4789
5109
  // src/commands/server.ts
4790
5110
  import { Command as Command18 } from "commander";
4791
- import chalk15 from "chalk";
5111
+ import chalk16 from "chalk";
4792
5112
  import ora4 from "ora";
4793
5113
  var INSTALLABLE = ["docker", "git", "edge", "rsync"];
4794
5114
  function guard(fn) {
@@ -4963,8 +5283,8 @@ server.command("install <serverId>").description("Install components on a server
4963
5283
  printJson({ event: ev.event, ...payload });
4964
5284
  } else if (ev.event === "log") {
4965
5285
  const p = payload;
4966
- const line = ` ${chalk15.dim(`[${p.component}]`)} ${p.message ?? ""}`;
4967
- process.stderr.write((p.level === "error" ? chalk15.red(line) : line) + "\n");
5286
+ const line = ` ${chalk16.dim(`[${p.component}]`)} ${p.message ?? ""}`;
5287
+ process.stderr.write((p.level === "error" ? chalk16.red(line) : line) + "\n");
4968
5288
  } else if (ev.event === "progress") {
4969
5289
  const p = payload;
4970
5290
  if (p.component) info(` ${p.component}: ${p.status}`);
@@ -5084,7 +5404,7 @@ var serverCommand = server;
5084
5404
  // src/commands/system.ts
5085
5405
  import { Command as Command19 } from "commander";
5086
5406
  import ora5 from "ora";
5087
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
5407
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
5088
5408
  import { createInterface as createInterface6 } from "readline/promises";
5089
5409
  import { stdin as input5, stdout as output5 } from "process";
5090
5410
  async function guarded(fn) {
@@ -5384,7 +5704,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
5384
5704
  );
5385
5705
  spin4?.succeed("Export ready.");
5386
5706
  if (opts.out) {
5387
- writeFileSync9(opts.out, JSON.stringify(file));
5707
+ writeFileSync10(opts.out, JSON.stringify(file));
5388
5708
  const tables = Object.keys(file.dump?.tables ?? {}).length;
5389
5709
  report2(
5390
5710
  { out: opts.out, tables },
@@ -5410,7 +5730,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
5410
5730
  }
5411
5731
  let file;
5412
5732
  try {
5413
- file = JSON.parse(readFileSync9(opts.file, "utf8"));
5733
+ file = JSON.parse(readFileSync10(opts.file, "utf8"));
5414
5734
  } catch {
5415
5735
  err(`
5416
5736
  Could not read or parse ${opts.file}.
@@ -5450,8 +5770,11 @@ var systemCommand = new Command19("system").description("Instance settings, onbo
5450
5770
 
5451
5771
  // src/commands/mail.ts
5452
5772
  import { Command as Command20 } from "commander";
5453
- import chalk16 from "chalk";
5773
+ import chalk17 from "chalk";
5454
5774
  import ora6 from "ora";
5775
+ import { spawnSync as spawnSync6 } from "child_process";
5776
+ import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
5777
+ import { join as join15, resolve as resolve2 } from "path";
5455
5778
  import { createInterface as createInterface7 } from "readline/promises";
5456
5779
  import { stdin as input6, stdout as output6 } from "process";
5457
5780
  function guard2(fn) {
@@ -5607,7 +5930,7 @@ var setupCmd = new Command20("setup").description("Start or resume the mail setu
5607
5930
  break;
5608
5931
  case "log": {
5609
5932
  const line = ` ${String(p.message ?? "")}`;
5610
- process.stderr.write((p.level === "error" ? chalk16.red(line) : chalk16.dim(line)) + "\n");
5933
+ process.stderr.write((p.level === "error" ? chalk17.red(line) : chalk17.dim(line)) + "\n");
5611
5934
  break;
5612
5935
  }
5613
5936
  case "step_done":
@@ -5756,12 +6079,66 @@ postmasterCmd.addCommand(
5756
6079
  })
5757
6080
  )
5758
6081
  );
5759
- var mailCommand = new Command20("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);
6082
+ var DOCKERFILE_REL = join15("apps", "email", "Dockerfile");
6083
+ function resolveBuildContext(explicit) {
6084
+ if (explicit?.trim()) return resolve2(explicit.trim());
6085
+ const marker = readSourceInstall();
6086
+ if (marker?.dir && existsSync16(join15(marker.dir, DOCKERFILE_REL))) return marker.dir;
6087
+ const cwd = process.cwd();
6088
+ if (existsSync16(join15(cwd, DOCKERFILE_REL))) return cwd;
6089
+ return null;
6090
+ }
6091
+ function defaultMailRef(context) {
6092
+ let fallbackTag;
6093
+ try {
6094
+ const pkg = JSON.parse(readFileSync11(join15(context, "apps", "api", "package.json"), "utf8"));
6095
+ if (typeof pkg.version === "string") fallbackTag = pkg.version;
6096
+ } catch {
6097
+ }
6098
+ return buildMailImageRef({ fallbackTag });
6099
+ }
6100
+ var buildCmd = new Command20("build").description("Build the openship-mail engine image locally from a source checkout [dev]").option("--context <dir>", "Repo root containing apps/email/Dockerfile (default: checkout or cwd)").option("--tag <ref>", "Image ref to tag (default: the pinned openship-mail ref)").action(async (opts) => {
6101
+ try {
6102
+ if (!has("docker")) {
6103
+ err("Docker isn't on PATH \u2014 install Docker, then retry.");
6104
+ process.exit(1);
6105
+ }
6106
+ const context = resolveBuildContext(opts.context);
6107
+ if (!context) {
6108
+ err(
6109
+ "No source checkout to build from. Run from the repo root, or pass --context <path>."
6110
+ );
6111
+ process.exit(1);
6112
+ }
6113
+ const dockerfile = join15(context, DOCKERFILE_REL);
6114
+ if (!existsSync16(dockerfile)) {
6115
+ err(`No apps/email/Dockerfile under ${context} \u2014 is that the repo root?`);
6116
+ process.exit(1);
6117
+ }
6118
+ const ref = opts.tag?.trim() || defaultMailRef(context);
6119
+ info(` Building ${ref}`);
6120
+ info(` context: ${context}`);
6121
+ info(` dockerfile: ${dockerfile}`);
6122
+ const res = spawnSync6("docker", ["build", "-t", ref, "-f", dockerfile, context], {
6123
+ stdio: [isJsonMode() ? "ignore" : "inherit", isJsonMode() ? "ignore" : "inherit", "inherit"]
6124
+ });
6125
+ if (res.status !== 0) {
6126
+ err(`docker build failed (exit ${res.status ?? "signal"}).`);
6127
+ process.exit(1);
6128
+ }
6129
+ if (isJsonMode()) printJson({ ok: true, image: ref, context });
6130
+ else ok(` Built ${ref}. Run \`mail setup\` (or redeploy) and it's used without a registry pull.`);
6131
+ } catch (e) {
6132
+ err(e instanceof Error ? e.message : String(e));
6133
+ process.exit(1);
6134
+ }
6135
+ });
6136
+ var mailCommand = new Command20("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).addCommand(buildCmd);
5760
6137
 
5761
6138
  // src/commands/backup.ts
5762
6139
  import { Command as Command21 } from "commander";
5763
6140
  import ora7 from "ora";
5764
- import { readFileSync as readFileSync10 } from "fs";
6141
+ import { readFileSync as readFileSync12 } from "fs";
5765
6142
  async function guard3(fn) {
5766
6143
  try {
5767
6144
  await fn();
@@ -6008,13 +6385,34 @@ restoreCmd.command("apply").description("Apply a staged restore (destructive)").
6008
6385
  );
6009
6386
  restoreCmd.command("cancel").description("Cancel a staged or in-flight restore").argument("<restoreId>", "Restore ID").action(
6010
6387
  (restoreId) => guard3(async () => {
6011
- await apiRequest(
6012
- `/backup-restores/${encodeURIComponent(restoreId)}/cancel`,
6013
- { method: "POST", body: JSON.stringify({}) }
6014
- );
6015
- ok(`
6016
- Restore cancelled: ${restoreId}
6388
+ const { data } = await apiRequest(`/backup-restores/${encodeURIComponent(restoreId)}/cancel`, {
6389
+ method: "POST",
6390
+ body: JSON.stringify({})
6391
+ });
6392
+ if (data.status === "cancelled") {
6393
+ ok(`
6394
+ Restore cancelled: ${restoreId}${data.forced ? " (forced)" : ""}
6395
+ `);
6396
+ } else if (data.accepted) {
6397
+ info(
6398
+ `
6399
+ Cancellation requested: ${restoreId} (status=${data.status}).
6400
+ Follow it with: openship backup restore get ${restoreId} --follow
6401
+ `
6402
+ );
6403
+ } else {
6404
+ info(`
6405
+ Restore ${restoreId} already finished (status=${data.status}) \u2014 nothing to cancel.
6017
6406
  `);
6407
+ }
6408
+ if (data.destructive) {
6409
+ err(
6410
+ ` The restore had already begun writing its target, so that target now holds
6411
+ PARTIAL data. Re-run this restore (or a newer one) to completion before
6412
+ starting the service.
6413
+ `
6414
+ );
6415
+ }
6018
6416
  })
6019
6417
  );
6020
6418
  restoreCmd.command("get").description("Show one restore (optionally stream it)").argument("<restoreId>", "Restore ID").option("--follow", "Stream progress to completion").action(
@@ -6054,7 +6452,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
6054
6452
  let sftpPrivateKey = opts.sftpPrivateKey;
6055
6453
  if (opts.sftpPrivateKeyFile) {
6056
6454
  try {
6057
- sftpPrivateKey = readFileSync10(opts.sftpPrivateKeyFile, "utf8");
6455
+ sftpPrivateKey = readFileSync12(opts.sftpPrivateKeyFile, "utf8");
6058
6456
  } catch {
6059
6457
  throw new Error(`Cannot read key file: ${opts.sftpPrivateKeyFile}`);
6060
6458
  }
@@ -6109,17 +6507,17 @@ var backupCommand = new Command21("backup").description("Manage backups: policie
6109
6507
 
6110
6508
  // src/commands/token.ts
6111
6509
  import { Command as Command22 } from "commander";
6112
- import chalk18 from "chalk";
6510
+ import chalk19 from "chalk";
6113
6511
 
6114
6512
  // src/lib/cmd-helpers.ts
6115
- import chalk17 from "chalk";
6513
+ import chalk18 from "chalk";
6116
6514
  import ora8 from "ora";
6117
6515
  function spin3(text2) {
6118
6516
  return isJsonMode() ? null : ora8(text2).start();
6119
6517
  }
6120
6518
  function fail3(e) {
6121
6519
  if (e instanceof ApiError) {
6122
- err(` ${e.message}${e.status ? chalk17.dim(` (${e.status})`) : ""}`);
6520
+ err(` ${e.message}${e.status ? chalk18.dim(` (${e.status})`) : ""}`);
6123
6521
  } else {
6124
6522
  err(` ${e instanceof Error ? e.message : String(e)}`);
6125
6523
  }
@@ -6201,7 +6599,7 @@ var createCmd3 = new Command22("create").description("Mint a new personal access
6201
6599
  return;
6202
6600
  }
6203
6601
  info(" Copy this token now \u2014 it will not be shown again:");
6204
- process.stdout.write(chalk18.cyan(` ${res.data.token}
6602
+ process.stdout.write(chalk19.cyan(` ${res.data.token}
6205
6603
  `));
6206
6604
  } catch (e) {
6207
6605
  sp?.fail("Create failed");
@@ -6259,10 +6657,10 @@ var apiCommand = new Command23("api").description("Make an authenticated request
6259
6657
 
6260
6658
  // src/commands/reset-admin.ts
6261
6659
  import { Command as Command24 } from "commander";
6262
- import chalk19 from "chalk";
6660
+ import chalk20 from "chalk";
6263
6661
  import { intro as intro2, outro as outro2, password as passwordPrompt, isCancel as isCancel4, cancel as cancel3, log as log4 } from "@clack/prompts";
6264
6662
  var resetAdminCommand = new Command24("reset-admin-password").description("Reset the local admin login on THIS machine (no sign-in required)").option("--port <port>", "API port of the running service (default: the port this install resolved to, else 4000)").option("--email <email>", "Also set the admin email").option("--name <name>", "Also set the admin display name").option("--password <password>", "New password (prompted if omitted)").action(async (opts) => {
6265
- intro2(chalk19.cyan("Reset Openship admin password"));
6663
+ intro2(chalk20.cyan("Reset Openship admin password"));
6266
6664
  let pw = opts.password;
6267
6665
  if (!pw) {
6268
6666
  if (!process.stdin.isTTY) {
@@ -6305,15 +6703,15 @@ var resetAdminCommand = new Command24("reset-admin-password").description("Reset
6305
6703
  log4.error(`Reset failed: ${data.error || res.statusText}`);
6306
6704
  process.exit(1);
6307
6705
  }
6308
- outro2(chalk19.green(`Password reset. Log in as ${data.email} with your new password.`));
6706
+ outro2(chalk20.green(`Password reset. Log in as ${data.email} with your new password.`));
6309
6707
  });
6310
6708
 
6311
6709
  // src/commands/install.ts
6312
6710
  import { Command as Command25 } from "commander";
6313
- import { chmodSync as chmodSync2, existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
6314
- import { spawn as spawn2, spawnSync as spawnSync6 } from "child_process";
6315
- import { homedir as homedir3 } from "os";
6316
- import { join as join14 } from "path";
6711
+ import { chmodSync as chmodSync2, existsSync as existsSync17, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
6712
+ import { spawn as spawn2, spawnSync as spawnSync7 } from "child_process";
6713
+ import { homedir as homedir4 } from "os";
6714
+ import { join as join16 } from "path";
6317
6715
  import ora9 from "ora";
6318
6716
  function assetForPlatform() {
6319
6717
  const { platform, arch } = process;
@@ -6327,33 +6725,33 @@ function assetForPlatform() {
6327
6725
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
6328
6726
  }
6329
6727
  function installDmg(dmg) {
6330
- const homeApps = join14(homedir3(), "Applications");
6728
+ const homeApps = join16(homedir4(), "Applications");
6331
6729
  let dest = homeApps;
6332
6730
  try {
6333
- mkdirSync10(homeApps, { recursive: true });
6731
+ mkdirSync11(homeApps, { recursive: true });
6334
6732
  } catch {
6335
6733
  dest = "/Applications";
6336
6734
  }
6337
- const attach = spawnSync6("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6735
+ const attach = spawnSync7("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6338
6736
  encoding: "utf8"
6339
6737
  });
6340
6738
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
6341
6739
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
6342
6740
  if (!mount) throw new Error("Could not determine the mounted volume");
6343
- let target = join14(dest, "Openship.app");
6741
+ let target = join16(dest, "Openship.app");
6344
6742
  try {
6345
- const appInDmg = join14(mount, "Openship.app");
6346
- if (!existsSync15(appInDmg)) throw new Error("Openship.app not found in the disk image");
6347
- spawnSync6("rm", ["-rf", target]);
6348
- let copy = spawnSync6("ditto", [appInDmg, target], { encoding: "utf8" });
6743
+ const appInDmg = join16(mount, "Openship.app");
6744
+ if (!existsSync17(appInDmg)) throw new Error("Openship.app not found in the disk image");
6745
+ spawnSync7("rm", ["-rf", target]);
6746
+ let copy = spawnSync7("ditto", [appInDmg, target], { encoding: "utf8" });
6349
6747
  if (copy.status !== 0 && dest === homeApps) {
6350
- target = join14("/Applications", "Openship.app");
6351
- spawnSync6("rm", ["-rf", target]);
6352
- copy = spawnSync6("ditto", [appInDmg, target], { encoding: "utf8" });
6748
+ target = join16("/Applications", "Openship.app");
6749
+ spawnSync7("rm", ["-rf", target]);
6750
+ copy = spawnSync7("ditto", [appInDmg, target], { encoding: "utf8" });
6353
6751
  }
6354
6752
  if (copy.status !== 0) throw new Error(`ditto copy failed: ${copy.stderr?.trim()}`);
6355
6753
  } finally {
6356
- spawnSync6("hdiutil", ["detach", mount, "-quiet"]);
6754
+ spawnSync7("hdiutil", ["detach", mount, "-quiet"]);
6357
6755
  }
6358
6756
  return target;
6359
6757
  }
@@ -6362,10 +6760,10 @@ function installAppImage(appImage) {
6362
6760
  return appImage;
6363
6761
  }
6364
6762
  function installZip(zip) {
6365
- const localAppData = process.env.LOCALAPPDATA || join14(homedir3(), "AppData", "Local");
6366
- const target = join14(localAppData, "Programs", "Openship");
6367
- mkdirSync10(target, { recursive: true });
6368
- const expand = spawnSync6(
6763
+ const localAppData = process.env.LOCALAPPDATA || join16(homedir4(), "AppData", "Local");
6764
+ const target = join16(localAppData, "Programs", "Openship");
6765
+ mkdirSync11(target, { recursive: true });
6766
+ const expand = spawnSync7(
6369
6767
  "powershell",
6370
6768
  [
6371
6769
  "-NoProfile",
@@ -6380,7 +6778,7 @@ function installZip(zip) {
6380
6778
  }
6381
6779
  function launch(kind, target) {
6382
6780
  if (kind === "dmg") {
6383
- spawnSync6("open", [target]);
6781
+ spawnSync7("open", [target]);
6384
6782
  return;
6385
6783
  }
6386
6784
  if (kind === "appimage") {
@@ -6391,9 +6789,9 @@ function launch(kind, target) {
6391
6789
  child.unref();
6392
6790
  return;
6393
6791
  }
6394
- const exe = join14(target, "Openship.exe");
6395
- const path2 = existsSync15(exe) ? exe : target;
6396
- spawnSync6("cmd", ["/c", "start", "", path2]);
6792
+ const exe = join16(target, "Openship.exe");
6793
+ const path2 = existsSync17(exe) ? exe : target;
6794
+ spawnSync7("cmd", ["/c", "start", "", path2]);
6397
6795
  }
6398
6796
  var installCommand = new Command25("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) => {
6399
6797
  let asset;
@@ -6418,13 +6816,13 @@ var installCommand = new Command25("install").description("Download and install
6418
6816
  process.exit(1);
6419
6817
  }
6420
6818
  const dir = releaseDir(tag);
6421
- const assetPath = join14(dir, asset.name);
6819
+ const assetPath = join16(dir, asset.name);
6422
6820
  const sidecarPath = `${assetPath}.sha256`;
6423
6821
  const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
6424
6822
  const sidecarUrl = `${assetUrl2}.sha256`;
6425
6823
  let downloaded = false;
6426
6824
  let sha;
6427
- const cachedUsable = !opts.force && existsSync15(assetPath) && (existsSync15(sidecarPath) || opts.verify === false);
6825
+ const cachedUsable = !opts.force && existsSync17(assetPath) && (existsSync17(sidecarPath) || opts.verify === false);
6428
6826
  if (cachedUsable) {
6429
6827
  info(` Using cached ${asset.name} (${tag}).`);
6430
6828
  } else {
@@ -6446,8 +6844,8 @@ var installCommand = new Command25("install").description("Download and install
6446
6844
  const s2 = spin4("Verifying checksum\u2026");
6447
6845
  try {
6448
6846
  let sidecarBody;
6449
- if (existsSync15(sidecarPath) && !downloaded) {
6450
- sidecarBody = readFileSync11(sidecarPath, "utf8");
6847
+ if (existsSync17(sidecarPath) && !downloaded) {
6848
+ sidecarBody = readFileSync13(sidecarPath, "utf8");
6451
6849
  } else {
6452
6850
  sidecarBody = await fetchSidecar(sidecarUrl);
6453
6851
  }
@@ -6470,8 +6868,8 @@ var installCommand = new Command25("install").description("Download and install
6470
6868
  err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
6471
6869
  process.exit(1);
6472
6870
  }
6473
- mkdirSync10(dir, { recursive: true });
6474
- writeFileSync10(sidecarPath, sidecarBody);
6871
+ mkdirSync11(dir, { recursive: true });
6872
+ writeFileSync11(sidecarPath, sidecarBody);
6475
6873
  s2?.succeed("Checksum verified");
6476
6874
  } catch (e) {
6477
6875
  s2?.fail("Verification failed");
@@ -6510,8 +6908,257 @@ var installCommand = new Command25("install").description("Download and install
6510
6908
  });
6511
6909
 
6512
6910
  // src/commands/update.ts
6513
- import { Command as Command26 } from "commander";
6514
- import { spawnSync as spawnSync7 } from "child_process";
6911
+ import { Command as Command26, Option } from "commander";
6912
+ import { spawnSync as spawnSync10 } from "child_process";
6913
+
6914
+ // src/lib/cli-payload.ts
6915
+ import { spawnSync as spawnSync8 } from "child_process";
6916
+ import {
6917
+ existsSync as existsSync18,
6918
+ lstatSync,
6919
+ mkdirSync as mkdirSync12,
6920
+ readdirSync as readdirSync2,
6921
+ rmSync as rmSync5,
6922
+ symlinkSync,
6923
+ writeFileSync as writeFileSync12
6924
+ } from "fs";
6925
+ import { join as join17 } from "path";
6926
+ var CLI_DIR = join17(OS_DIR, "cli");
6927
+ var CLI_CURRENT = join17(CLI_DIR, "current");
6928
+ var KEEP = 2;
6929
+ function assetName2(tag) {
6930
+ return `openship-cli-${tag}.tar.gz`;
6931
+ }
6932
+ function repoint(link, targetTag) {
6933
+ try {
6934
+ if (lstatSync(link)) rmSync5(link, { recursive: true, force: true });
6935
+ } catch {
6936
+ }
6937
+ symlinkSync(targetTag, link, process.platform === "win32" ? "junction" : void 0);
6938
+ }
6939
+ function prune(activeTag) {
6940
+ let entries;
6941
+ try {
6942
+ entries = readdirSync2(CLI_DIR);
6943
+ } catch {
6944
+ return;
6945
+ }
6946
+ const tags = entries.filter((e) => e !== "current" && existsSync18(join17(CLI_DIR, e, ".extracted")));
6947
+ const ranked = tags.map((t) => ({ t, m: safeMtime(join17(CLI_DIR, t)) })).sort((a, b) => b.m - a.m).map((x) => x.t);
6948
+ const keep2 = /* @__PURE__ */ new Set([activeTag, ...ranked.slice(0, KEEP)]);
6949
+ for (const t of tags) {
6950
+ if (!keep2.has(t)) rmSync5(join17(CLI_DIR, t), { recursive: true, force: true });
6951
+ }
6952
+ }
6953
+ function safeMtime(p) {
6954
+ try {
6955
+ return lstatSync(p).mtimeMs;
6956
+ } catch {
6957
+ return 0;
6958
+ }
6959
+ }
6960
+ async function ensureCliPayload(tag, onProgress) {
6961
+ const resolved = tag ?? await resolveLatestTag();
6962
+ const dir = join17(CLI_DIR, resolved);
6963
+ const entry = join17(dir, "dist", "index.js");
6964
+ const marker = join17(dir, ".extracted");
6965
+ if (existsSync18(marker) && existsSync18(entry)) {
6966
+ repoint(CLI_CURRENT, resolved);
6967
+ prune(resolved);
6968
+ return { tag: resolved, dir, entry };
6969
+ }
6970
+ rmSync5(dir, { recursive: true, force: true });
6971
+ mkdirSync12(dir, { recursive: true });
6972
+ const name = assetName2(resolved);
6973
+ const override = process.env.OPENSHIP_CLI_ASSET_URL?.trim();
6974
+ const url = override || assetUrl(resolved, name);
6975
+ const tarball = join17(dir, name);
6976
+ const { sha256 } = await downloadToFile(url, tarball, onProgress);
6977
+ const expected = override ? parseSha256(await fetchSidecar(`${url}.sha256`) ?? "") : await expectedSha256(resolved, name);
6978
+ if (!expected) {
6979
+ rmSync5(dir, { recursive: true, force: true });
6980
+ throw new Error(`No .sha256 sidecar for ${name}; refusing to install an unverified CLI payload.`);
6981
+ }
6982
+ if (expected !== sha256) {
6983
+ rmSync5(dir, { recursive: true, force: true });
6984
+ throw new Error(`CLI payload checksum mismatch (expected ${expected}, got ${sha256}).`);
6985
+ }
6986
+ const untar = spawnSync8("tar", ["-xzf", tarball, "-C", dir], { stdio: "inherit" });
6987
+ if (untar.status !== 0) {
6988
+ rmSync5(dir, { recursive: true, force: true });
6989
+ throw new Error("Failed to extract the CLI payload (is `tar` available?).");
6990
+ }
6991
+ rmSync5(tarball, { force: true });
6992
+ if (!existsSync18(entry)) {
6993
+ rmSync5(dir, { recursive: true, force: true });
6994
+ throw new Error(`CLI payload extracted but ${entry} is missing (unexpected layout).`);
6995
+ }
6996
+ writeFileSync12(marker, `${resolved}
6997
+ `);
6998
+ repoint(CLI_CURRENT, resolved);
6999
+ prune(resolved);
7000
+ return { tag: resolved, dir, entry };
7001
+ }
7002
+
7003
+ // src/lib/node-runtime.ts
7004
+ import { execFileSync as execFileSync5, spawnSync as spawnSync9 } from "child_process";
7005
+ import { chmodSync as chmodSync3, existsSync as existsSync19, lstatSync as lstatSync2, mkdirSync as mkdirSync13, rmSync as rmSync6, symlinkSync as symlinkSync2, writeFileSync as writeFileSync13 } from "fs";
7006
+ import { join as join18 } from "path";
7007
+ var NODE_MAJOR = 22;
7008
+ var NODE_DIST = "https://nodejs.org/dist";
7009
+ var RUNTIME_DIR = join18(OS_DIR, "runtime");
7010
+ var RUNTIME_CURRENT = join18(RUNTIME_DIR, "current");
7011
+ var BIN_DIR = join18(OS_DIR, "bin");
7012
+ var LAUNCHER_PATH = join18(BIN_DIR, "openship");
7013
+ var LAUNCHER_SH = `#!/bin/sh
7014
+ # Openship CLI launcher \u2014 runs the tarball-installed CLI under Node.
7015
+ # Managed by scripts/install.sh and \`openship update\`; manual edits are overwritten.
7016
+ SELF="$0"
7017
+ while [ -L "$SELF" ]; do
7018
+ _t="$(readlink "$SELF")"
7019
+ case "$_t" in /*) SELF="$_t" ;; *) SELF="$(dirname "$SELF")/$_t" ;; esac
7020
+ done
7021
+ BIN_DIR="$(cd "$(dirname "$SELF")" && pwd)"
7022
+ HOME_DIR="$(dirname "$BIN_DIR")"
7023
+ CLI="$HOME_DIR/cli/current/dist/index.js"
7024
+ VENDORED_NODE="$HOME_DIR/runtime/current/bin/node"
7025
+ # Prefer a system node >= ${NODE_MAJOR}; fall back to the vendored runtime.
7026
+ if command -v node >/dev/null 2>&1 && node -e 'process.exit(+process.versions.node.split(".")[0]>=${NODE_MAJOR}?0:1)' 2>/dev/null; then
7027
+ exec node "$CLI" "$@"
7028
+ elif [ -x "$VENDORED_NODE" ]; then
7029
+ exec "$VENDORED_NODE" "$CLI" "$@"
7030
+ else
7031
+ echo "openship: no Node >= ${NODE_MAJOR} found (system or vendored). Reinstall: curl -fsSL https://get.openship.io | sh" >&2
7032
+ exit 127
7033
+ fi
7034
+ `;
7035
+ function nodeDistOs() {
7036
+ if (process.platform === "darwin") return "darwin";
7037
+ if (process.platform === "linux") return "linux";
7038
+ throw new Error(`Unsupported platform for vendored Node: ${process.platform}`);
7039
+ }
7040
+ function nodeDistArch() {
7041
+ if (process.arch === "x64") return "x64";
7042
+ if (process.arch === "arm64") return "arm64";
7043
+ throw new Error(`Unsupported arch for vendored Node: ${process.arch}`);
7044
+ }
7045
+ function majorOf(bin) {
7046
+ try {
7047
+ const out = execFileSync5(bin, ["-e", "process.stdout.write(process.versions.node)"], {
7048
+ encoding: "utf8",
7049
+ stdio: ["ignore", "pipe", "ignore"]
7050
+ });
7051
+ const m = Number(out.trim().split(".")[0]);
7052
+ return Number.isInteger(m) ? m : null;
7053
+ } catch {
7054
+ return null;
7055
+ }
7056
+ }
7057
+ function writeLauncher() {
7058
+ mkdirSync13(BIN_DIR, { recursive: true });
7059
+ writeFileSync13(LAUNCHER_PATH, LAUNCHER_SH);
7060
+ chmodSync3(LAUNCHER_PATH, 493);
7061
+ return LAUNCHER_PATH;
7062
+ }
7063
+ function repoint2(link, target) {
7064
+ try {
7065
+ if (lstatSync2(link)) rmSync6(link, { recursive: true, force: true });
7066
+ } catch {
7067
+ }
7068
+ symlinkSync2(target, link);
7069
+ }
7070
+ async function resolveLatestNode() {
7071
+ const res = await fetch(`${NODE_DIST}/index.json`, {
7072
+ headers: { "User-Agent": "openship-cli" },
7073
+ signal: AbortSignal.timeout(15e3)
7074
+ });
7075
+ if (!res.ok) throw new Error(`Node dist index lookup failed: HTTP ${res.status}`);
7076
+ const rows = await res.json();
7077
+ const prefix = `v${NODE_MAJOR}.`;
7078
+ const row2 = rows.find((r) => r.version.startsWith(prefix) && r.lts) ?? rows.find((r) => r.version.startsWith(prefix));
7079
+ if (!row2) throw new Error(`No Node ${NODE_MAJOR}.x release found on nodejs.org`);
7080
+ return row2.version;
7081
+ }
7082
+ async function fetchVendoredNode(onLog) {
7083
+ const ver = await resolveLatestNode();
7084
+ const os = nodeDistOs();
7085
+ const arch = nodeDistArch();
7086
+ const name = `node-${ver}-${os}-${arch}.tar.gz`;
7087
+ mkdirSync13(RUNTIME_DIR, { recursive: true });
7088
+ const tarball = join18(RUNTIME_DIR, name);
7089
+ onLog?.(`Fetching Node ${ver} (${os}-${arch}) from nodejs.org\u2026`);
7090
+ const { sha256 } = await downloadToFile(`${NODE_DIST}/${ver}/${name}`, tarball);
7091
+ const shaRes = await fetch(`${NODE_DIST}/${ver}/SHASUMS256.txt`, {
7092
+ headers: { "User-Agent": "openship-cli" },
7093
+ signal: AbortSignal.timeout(15e3)
7094
+ });
7095
+ if (!shaRes.ok) {
7096
+ rmSync6(tarball, { force: true });
7097
+ throw new Error(`Node checksum list fetch failed: HTTP ${shaRes.status}`);
7098
+ }
7099
+ const expected = (await shaRes.text()).split("\n").map((l) => l.trim()).find((l) => l.endsWith(` ${name}`) || l.endsWith(` ${name}`))?.split(/\s+/)[0]?.toLowerCase();
7100
+ if (!expected) {
7101
+ rmSync6(tarball, { force: true });
7102
+ throw new Error(`No checksum for ${name} in SHASUMS256.txt`);
7103
+ }
7104
+ if (expected !== sha256) {
7105
+ rmSync6(tarball, { force: true });
7106
+ throw new Error(`Node download checksum mismatch (expected ${expected}, got ${sha256}).`);
7107
+ }
7108
+ const unpacked = join18(RUNTIME_DIR, `node-${ver}-${os}-${arch}`);
7109
+ rmSync6(unpacked, { recursive: true, force: true });
7110
+ const untar = spawnSync9("tar", ["-xzf", tarball, "-C", RUNTIME_DIR], { stdio: "inherit" });
7111
+ if (untar.status !== 0) throw new Error("Failed to extract the Node runtime (is `tar` available?).");
7112
+ rmSync6(tarball, { force: true });
7113
+ const nodeBin = join18(unpacked, "bin", "node");
7114
+ if (!existsSync19(nodeBin)) throw new Error(`Node unpacked but ${nodeBin} is missing (unexpected layout).`);
7115
+ repoint2(RUNTIME_CURRENT, unpacked);
7116
+ return nodeBin;
7117
+ }
7118
+ async function ensureNodeRuntime(onLog) {
7119
+ if ((majorOf("node") ?? 0) >= NODE_MAJOR) return { node: "node", source: "system" };
7120
+ const existing = join18(RUNTIME_CURRENT, "bin", "node");
7121
+ if (existsSync19(existing) && (majorOf(existing) ?? 0) >= NODE_MAJOR) {
7122
+ return { node: existing, source: "vendored" };
7123
+ }
7124
+ return { node: await fetchVendoredNode(onLog), source: "vendored" };
7125
+ }
7126
+
7127
+ // src/lib/update-reexec.ts
7128
+ var REEXEC_ENV = "OPENSHIP_UPDATE_REEXEC";
7129
+ function isReexecChild(env = process.env) {
7130
+ return env[REEXEC_ENV] === "1";
7131
+ }
7132
+ function applyOnlyArgv(selfArgs, json) {
7133
+ return [...selfArgs, ...json ? ["--json"] : [], "update", "--apply-only"];
7134
+ }
7135
+
7136
+ // src/commands/update.ts
7137
+ async function runReconcile() {
7138
+ if (readInstallMethod() === "compose") {
7139
+ const applied = await composeUpdate();
7140
+ if (isJsonMode()) printJson({ reconciled: applied, method: "compose" });
7141
+ else if (applied) ok("Compose stack regenerated, images pulled, services recreated.");
7142
+ else err("Bringing the compose stack up failed. Re-run `openship update`, or `openship up` to retry.");
7143
+ if (!applied) process.exitCode = 1;
7144
+ return applied;
7145
+ }
7146
+ const { restarted } = restart();
7147
+ if (isJsonMode()) printJson({ reconciled: restarted, method: "bare" });
7148
+ else if (restarted) ok("Restarted the service onto the new version.");
7149
+ else ok("Restart the server to run the new version: openship up");
7150
+ return restarted;
7151
+ }
7152
+ function reexecApply() {
7153
+ if (isReexecChild()) return { spawned: false, status: null };
7154
+ const { runtime, args } = selfInvocation();
7155
+ const argv = applyOnlyArgv(args, isJsonMode());
7156
+ const res = spawnSync10(runtime, argv, {
7157
+ stdio: "inherit",
7158
+ env: { ...process.env, [REEXEC_ENV]: "1" }
7159
+ });
7160
+ return { spawned: res.error == null, status: res.status };
7161
+ }
6515
7162
  async function runSourceUpdate(source, opts) {
6516
7163
  const current = shortSha(source.dir);
6517
7164
  const remote = remoteSha(source.repo, source.ref);
@@ -6541,24 +7188,105 @@ async function runSourceUpdate(source, opts) {
6541
7188
  process.exitCode = 1;
6542
7189
  return;
6543
7190
  }
7191
+ info(`Rebuilt from source (${source.ref} @ ${sha}).`);
7192
+ const r = reexecApply();
7193
+ if (r.spawned) {
7194
+ process.exitCode = r.status ?? 0;
7195
+ return;
7196
+ }
6544
7197
  const { restarted } = restart();
6545
7198
  if (isJsonMode()) {
6546
7199
  printJson({ updated: true, source: true, ref: source.ref, from: current, to: sha, restarted });
6547
7200
  } else if (restarted) {
6548
- ok(`Rebuilt from source (${source.ref} @ ${sha}) and restarted the service.`);
7201
+ ok(`Restarted the service \u2014 you're on ${sha}.`);
6549
7202
  } else {
6550
- ok(`Rebuilt from source (${source.ref} @ ${sha}). Restart to run it: openship up`);
7203
+ ok(`Restart to run it: openship up`);
7204
+ }
7205
+ }
7206
+ var REINSTALL_HINT = "curl -fsSL https://get.openship.io | sh";
7207
+ async function runTarballUpdate(install, opts) {
7208
+ const current = "0.6.1";
7209
+ let latest;
7210
+ try {
7211
+ latest = (await resolveLatestTag()).replace(/^v/, "");
7212
+ } catch {
7213
+ err(`Could not reach GitHub to check for updates. Try again, or reinstall: ${REINSTALL_HINT}`);
7214
+ process.exitCode = 1;
7215
+ return;
7216
+ }
7217
+ const plan = resolveCliUpdatePlan(current, latest);
7218
+ if (opts.check) {
7219
+ if (isJsonMode()) {
7220
+ printJson({ current, latest, updateAvailable: plan.action === "install", method: "tarball" });
7221
+ } else if (plan.action === "install") {
7222
+ info(`Update available: v${current} \u2192 v${latest}. Run \`openship update\`.`);
7223
+ } else {
7224
+ ok(`Up to date (v${current}).`);
7225
+ }
7226
+ return;
7227
+ }
7228
+ if (plan.action === "up-to-date") {
7229
+ ok(`Already on the latest version (v${current}).`);
7230
+ return;
7231
+ }
7232
+ const tag = `v${latest}`;
7233
+ info(`Updating v${current} \u2192 v${latest} (verified payload)\u2026`);
7234
+ let runtime = install.runtime;
7235
+ try {
7236
+ runtime = (await ensureNodeRuntime((m) => info(m))).source;
7237
+ } catch (e) {
7238
+ info(`Keeping the current Node runtime (couldn't refresh it: ${e.message}).`);
7239
+ }
7240
+ try {
7241
+ await ensureCliPayload(tag);
7242
+ if (process.platform !== "win32") writeLauncher();
7243
+ } catch (e) {
7244
+ err(`Update failed: ${e.message} Reinstall: ${REINSTALL_HINT}`);
7245
+ process.exitCode = 1;
7246
+ return;
7247
+ }
7248
+ writeCliInstall({ ...install, tag, runtime });
7249
+ const applyViaChild = reexecApply();
7250
+ if (applyViaChild.spawned) {
7251
+ process.exitCode = applyViaChild.status ?? 0;
7252
+ return;
7253
+ }
7254
+ if (readInstallMethod() === "compose") {
7255
+ const applied = await composeUpdate(latest);
7256
+ if (isJsonMode()) {
7257
+ printJson({ updated: true, from: current, to: latest, method: "compose", applied });
7258
+ } else if (applied) {
7259
+ ok(`Updated to v${latest} \u2014 compose stack regenerated, images pulled, services recreated.`);
7260
+ } else {
7261
+ err(`Updated the CLI to v${latest}, but bringing the compose stack up failed. Re-run \`openship update\`, or \`openship up\` to retry.`);
7262
+ process.exitCode = 1;
7263
+ }
7264
+ return;
7265
+ }
7266
+ const { restarted } = restart();
7267
+ if (isJsonMode()) {
7268
+ printJson({ updated: true, from: current, to: latest, method: "tarball", restarted });
7269
+ } else if (restarted) {
7270
+ ok(`Updated to v${latest} and restarted the service \u2014 you're on the new version.`);
7271
+ } else {
7272
+ ok(`Updated to v${latest}. Restart the server to run the new version: openship up`);
6551
7273
  }
6552
7274
  }
6553
7275
  function detectPackageManager2(override) {
6554
7276
  if (override === "bun" || override === "npm") return override;
6555
- const hasBun = spawnSync7("bun", ["--version"], { stdio: "ignore" }).status === 0;
7277
+ const hasBun = spawnSync10("bun", ["--version"], { stdio: "ignore" }).status === 0;
6556
7278
  return hasBun ? "bun" : "npm";
6557
7279
  }
6558
- var updateCommand = new Command26("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").option("--rebuild", "From-source installs: rebuild even if already at the remote tip").action(async (opts) => {
7280
+ var updateCommand = new Command26("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").option("--rebuild", "From-source installs: rebuild even if already at the remote tip").addOption(new Option("--apply-only").hideHelp()).action(async (opts) => {
7281
+ if (opts.applyOnly) {
7282
+ await runReconcile();
7283
+ return;
7284
+ }
6559
7285
  const source = readSourceInstall();
6560
7286
  if (source) return runSourceUpdate(source, opts);
6561
- const current = "0.5.0";
7287
+ const cliInstall = readCliInstall();
7288
+ if (cliInstall?.method === "tarball") return runTarballUpdate(cliInstall, opts);
7289
+ const current = "0.6.1";
6562
7290
  let latest;
6563
7291
  try {
6564
7292
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -6586,12 +7314,17 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
6586
7314
  const ref = `openship@${latest}`;
6587
7315
  const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
6588
7316
  info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
6589
- const res = spawnSync7(pm, argv, { stdio: "inherit", shell: process.platform === "win32" });
7317
+ const res = spawnSync10(pm, argv, { stdio: "inherit", shell: process.platform === "win32" });
6590
7318
  if (res.status !== 0) {
6591
7319
  err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
6592
7320
  process.exitCode = 1;
6593
7321
  return;
6594
7322
  }
7323
+ const applyViaChild = reexecApply();
7324
+ if (applyViaChild.spawned) {
7325
+ process.exitCode = applyViaChild.status ?? 0;
7326
+ return;
7327
+ }
6595
7328
  if (readInstallMethod() === "compose") {
6596
7329
  const applied = await composeUpdate(latest);
6597
7330
  if (isJsonMode()) {
@@ -6616,17 +7349,17 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
6616
7349
 
6617
7350
  // src/commands/cache.ts
6618
7351
  import { Command as Command27 } from "commander";
6619
- import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync2 } from "fs";
6620
- import { join as join15 } from "path";
7352
+ import { existsSync as existsSync20, readdirSync as readdirSync3, readFileSync as readFileSync14, rmSync as rmSync7, statSync as statSync2 } from "fs";
7353
+ import { join as join19 } from "path";
6621
7354
  function listAssets() {
6622
- if (!existsSync16(RELEASES_DIR)) return [];
7355
+ if (!existsSync20(RELEASES_DIR)) return [];
6623
7356
  const out = [];
6624
- for (const tag of readdirSync2(RELEASES_DIR)) {
7357
+ for (const tag of readdirSync3(RELEASES_DIR)) {
6625
7358
  const dir = releaseDir(tag);
6626
7359
  if (!statSync2(dir).isDirectory()) continue;
6627
- for (const name of readdirSync2(dir)) {
7360
+ for (const name of readdirSync3(dir)) {
6628
7361
  if (name.endsWith(".sha256")) continue;
6629
- const path2 = join15(dir, name);
7362
+ const path2 = join19(dir, name);
6630
7363
  const st = statSync2(path2);
6631
7364
  if (!st.isFile()) continue;
6632
7365
  out.push({
@@ -6634,7 +7367,7 @@ function listAssets() {
6634
7367
  name,
6635
7368
  path: path2,
6636
7369
  size: st.size,
6637
- hasSidecar: existsSync16(`${path2}.sha256`)
7370
+ hasSidecar: existsSync20(`${path2}.sha256`)
6638
7371
  });
6639
7372
  }
6640
7373
  }
@@ -6665,7 +7398,7 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
6665
7398
  results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
6666
7399
  continue;
6667
7400
  }
6668
- const expected = parseSha256(readFileSync12(`${a.path}.sha256`, "utf8"));
7401
+ const expected = parseSha256(readFileSync14(`${a.path}.sha256`, "utf8"));
6669
7402
  const actual = await hashFile(a.path);
6670
7403
  const okMatch = expected !== null && expected === actual;
6671
7404
  if (!okMatch) bad += 1;
@@ -6681,12 +7414,12 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
6681
7414
  });
6682
7415
  var cleanCmd = new Command27("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
6683
7416
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
6684
- if (!existsSync16(target)) {
7417
+ if (!existsSync20(target)) {
6685
7418
  if (isJsonMode()) printJson({ removed: false, path: target });
6686
7419
  else info(` Nothing to clean (${target}).`);
6687
7420
  return;
6688
7421
  }
6689
- rmSync5(target, { recursive: true, force: true });
7422
+ rmSync7(target, { recursive: true, force: true });
6690
7423
  if (isJsonMode()) printJson({ removed: true, path: target });
6691
7424
  else ok(`
6692
7425
  Removed ${target}
@@ -6698,11 +7431,11 @@ var cacheCommand = new Command27("cache").description("Manage the local download
6698
7431
  }).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
6699
7432
 
6700
7433
  // src/commands/wizard.ts
6701
- import chalk20 from "chalk";
7434
+ import chalk21 from "chalk";
6702
7435
  import open2 from "open";
6703
7436
  import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
6704
- import { existsSync as existsSync17, mkdirSync as mkdirSync11, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
6705
- import { join as join16 } from "path";
7437
+ import { existsSync as existsSync21, mkdirSync as mkdirSync14, rmSync as rmSync8, writeFileSync as writeFileSync14 } from "fs";
7438
+ import { join as join20 } from "path";
6706
7439
  import {
6707
7440
  intro as intro3,
6708
7441
  outro as outro3,
@@ -6743,7 +7476,7 @@ async function connectOpenshipCloud(port, token) {
6743
7476
  const handoff = `${apiBase}/api/cloud/connect-handoff?state=${encodeURIComponent(state)}&code_challenge=${challenge}&mode=device`;
6744
7477
  const overSsh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT);
6745
7478
  log5.step("Open this URL in your browser to authorize, then click Authorize:");
6746
- console.log("\n" + chalk20.cyan.underline(handoff) + "\n");
7479
+ console.log("\n" + chalk21.cyan.underline(handoff) + "\n");
6747
7480
  if (!overSsh) void open2(handoff).catch(() => {
6748
7481
  });
6749
7482
  const s = spinner4();
@@ -6848,16 +7581,16 @@ async function streamProvision(port, sessionId, s) {
6848
7581
  }
6849
7582
  return { ok: ok3, detail };
6850
7583
  }
6851
- var SETUP_LOCK = join16(OS_DIR, "setup-in-progress");
7584
+ var SETUP_LOCK = join20(OS_DIR, "setup-in-progress");
6852
7585
  function isSetupInProgress() {
6853
- return existsSync17(SETUP_LOCK);
7586
+ return existsSync21(SETUP_LOCK);
6854
7587
  }
6855
7588
  function markSetupStarted() {
6856
- mkdirSync11(OS_DIR, { recursive: true });
6857
- writeFileSync11(SETUP_LOCK, "1");
7589
+ mkdirSync14(OS_DIR, { recursive: true });
7590
+ writeFileSync14(SETUP_LOCK, "1");
6858
7591
  }
6859
7592
  function markSetupDone() {
6860
- rmSync6(SETUP_LOCK, { force: true });
7593
+ rmSync8(SETUP_LOCK, { force: true });
6861
7594
  }
6862
7595
  function wizardInputs(admin, plan) {
6863
7596
  switch (plan.type) {
@@ -6874,20 +7607,20 @@ function wizardInputs(admin, plan) {
6874
7607
  function finishSetup(opts) {
6875
7608
  saveInstanceUrl(opts.liveUrl);
6876
7609
  markSetupDone();
6877
- const pad = (label) => chalk20.dim(label.padEnd(11));
6878
- log5.success(chalk20.bold("Openship is live"));
7610
+ const pad = (label) => chalk21.dim(label.padEnd(11));
7611
+ log5.success(chalk21.bold("Openship is live"));
6879
7612
  log5.message(
6880
- `${pad("URL")}${chalk20.bold(opts.liveUrl)}
7613
+ `${pad("URL")}${chalk21.bold(opts.liveUrl)}
6881
7614
  ${pad("Dashboard")}http://localhost:${opts.dashPort}
6882
7615
  ${pad("API")}http://localhost:${opts.apiPort}
6883
- ${pad("Login")}${opts.adminEmail} ${chalk20.dim("(email + password you set)")}
6884
- ` + (opts.cloudEmail ? `${pad("Cloud")}${chalk20.dim("connected as ")}${opts.cloudEmail}${chalk20.dim(" \u2014 free domain + mail only")}
6885
- ` : "") + `${pad("Status")}${chalk20.green("running")} ${chalk20.dim(opts.method === "compose" ? "\xB7 Docker Compose stack (restarts on boot)" : "\xB7 service (restarts on boot)")}`
7616
+ ${pad("Login")}${opts.adminEmail} ${chalk21.dim("(email + password you set)")}
7617
+ ` + (opts.cloudEmail ? `${pad("Cloud")}${chalk21.dim("connected as ")}${opts.cloudEmail}${chalk21.dim(" \u2014 free domain + mail only")}
7618
+ ` : "") + `${pad("Status")}${chalk21.green("running")} ${chalk21.dim(opts.method === "compose" ? "\xB7 Docker Compose stack (restarts on boot)" : "\xB7 service (restarts on boot)")}`
6886
7619
  );
6887
7620
  log5.message(
6888
- chalk20.dim("Sign in with the email + password you just set. Openship appears under your Apps.\n") + chalk20.dim("Change the domain, Openship Cloud, team, and everything else anytime in Settings.\n") + chalk20.dim(`Locked out? Run ${chalk20.reset("openship reset-admin-password")}${chalk20.dim(" on this machine \u2014 resets your login without signing in.")}`)
7621
+ chalk21.dim("Sign in with the email + password you just set. Openship appears under your Apps.\n") + chalk21.dim("Change the domain, Openship Cloud, team, and everything else anytime in Settings.\n") + chalk21.dim(`Locked out? Run ${chalk21.reset("openship reset-admin-password")}${chalk21.dim(" on this machine \u2014 resets your login without signing in.")}`)
6889
7622
  );
6890
- outro3(opts.byo ? chalk20.dim("Point your reverse proxy at the dashboard port above.") : chalk20.green("Happy shipping."));
7623
+ outro3(opts.byo ? chalk21.dim("Point your reverse proxy at the dashboard port above.") : chalk21.green("Happy shipping."));
6891
7624
  }
6892
7625
  async function promptLocalEdgeTakeover(port) {
6893
7626
  const pf = await internalPost(port, "/api/system/self-edge/preflight", {});
@@ -6902,22 +7635,36 @@ async function promptLocalEdgeTakeover(port) {
6902
7635
  renderEdgeConflict({ owner, sites, warnings });
6903
7636
  const action2 = await confirmEdgeAction({ owner, known, importable: sites.length });
6904
7637
  if (action2 === "cancel") return { proceed: false, edgeMigrate: false, edgeTakeover: false };
6905
- return { proceed: true, edgeMigrate: action2 === "migrate", edgeTakeover: action2 === "takeover" };
7638
+ let staticRootOverrides;
7639
+ if (action2 === "migrate") {
7640
+ const unreachable = pf.ok && Array.isArray(pf.data?.unreachableStaticRoots) ? pf.data.unreachableStaticRoots : [];
7641
+ staticRootOverrides = await remediateUnreachableStaticRoots({
7642
+ unreachable,
7643
+ executor: new LocalExecutor(),
7644
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
7645
+ });
7646
+ }
7647
+ return {
7648
+ proceed: true,
7649
+ edgeMigrate: action2 === "migrate",
7650
+ edgeTakeover: action2 === "takeover",
7651
+ ...staticRootOverrides ? { staticRootOverrides } : {}
7652
+ };
6906
7653
  }
6907
7654
  async function runWizard() {
6908
7655
  const resuming = isSetupInProgress();
6909
- intro3(`${chalk20.bgCyan(chalk20.black(" Openship "))}${chalk20.dim(" setup")}`);
7656
+ intro3(`${chalk21.bgCyan(chalk21.black(" Openship "))}${chalk21.dim(" setup")}`);
6910
7657
  if (resuming) {
6911
7658
  log5.warn(
6912
7659
  "Your last setup didn't finish \u2014 picking it back up. Re-enter your details to complete it (or run `openship up` to just keep the server running)."
6913
7660
  );
6914
7661
  }
6915
7662
  log5.message(
6916
- chalk20.dim(
7663
+ chalk21.dim(
6917
7664
  "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."
6918
7665
  )
6919
7666
  );
6920
- log5.message(chalk20.dim("First, your instance login (email + password) \u2014 this is how you sign in. Domain and Openship Cloud come next and never replace it."));
7667
+ log5.message(chalk21.dim("First, your instance login (email + password) \u2014 this is how you sign in. Domain and Openship Cloud come next and never replace it."));
6921
7668
  const admin = await promptLocalAdmin();
6922
7669
  let cloudEmail = null;
6923
7670
  let publicUrl;
@@ -6936,7 +7683,7 @@ async function runWizard() {
6936
7683
  sp.start("Detecting this server's public IP");
6937
7684
  const detected = await detectPublicIp();
6938
7685
  if (detected) {
6939
- sp.stop(`Public IP: ${chalk20.bold(detected)}`);
7686
+ sp.stop(`Public IP: ${chalk21.bold(detected)}`);
6940
7687
  publicHost = detected;
6941
7688
  return detected;
6942
7689
  }
@@ -6951,7 +7698,7 @@ async function runWizard() {
6951
7698
  return publicHost;
6952
7699
  }
6953
7700
  let stage = "reach";
6954
- log5.message(chalk20.dim("These are just starting choices \u2014 domain, Cloud, team, and the rest are all editable later in Settings."));
7701
+ log5.message(chalk21.dim("These are just starting choices \u2014 domain, Cloud, team, and the rest are all editable later in Settings."));
6955
7702
  planning: while (true) {
6956
7703
  if (stage === "reach") {
6957
7704
  const reach = ensure2(
@@ -7007,11 +7754,11 @@ async function runWizard() {
7007
7754
  ).trim().toLowerCase();
7008
7755
  const host = await resolvePublicHost();
7009
7756
  note3(
7010
- `${chalk20.cyan(`https://${slug}.opsh.io`)}
7757
+ `${chalk21.cyan(`https://${slug}.opsh.io`)}
7011
7758
 
7012
- ${chalk20.dim("served via")} Openship Cloud edge ${chalk20.dim("\u2192")} ${chalk20.cyan(host)}
7759
+ ${chalk21.dim("served via")} Openship Cloud edge ${chalk21.dim("\u2192")} ${chalk21.cyan(host)}
7013
7760
 
7014
- ` + chalk20.dim("Openship Cloud terminates HTTPS and forwards to this server."),
7761
+ ` + chalk21.dim("Openship Cloud terminates HTTPS and forwards to this server."),
7015
7762
  "Confirm free domain"
7016
7763
  );
7017
7764
  const go2 = ensure2(
@@ -7049,11 +7796,11 @@ async function runWizard() {
7049
7796
  }
7050
7797
  const host = await resolvePublicHost();
7051
7798
  note3(
7052
- `Add a DNS ${chalk20.bold("A record")}:
7799
+ `Add a DNS ${chalk21.bold("A record")}:
7053
7800
 
7054
- ${chalk20.cyan(hostname2)} \u2192 ${chalk20.cyan(host)}
7801
+ ${chalk21.cyan(hostname2)} \u2192 ${chalk21.cyan(host)}
7055
7802
 
7056
- ` + chalk20.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
7803
+ ` + chalk21.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
7057
7804
  "DNS"
7058
7805
  );
7059
7806
  const go2 = ensure2(
@@ -7090,9 +7837,9 @@ async function runWizard() {
7090
7837
  log5.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
7091
7838
  }
7092
7839
  note3(
7093
- `${chalk20.cyan(url)}
7840
+ `${chalk21.cyan(url)}
7094
7841
 
7095
- ` + chalk20.dim("Point your reverse proxy at the dashboard port shown at the end."),
7842
+ ` + chalk21.dim("Point your reverse proxy at the dashboard port shown at the end."),
7096
7843
  "Confirm"
7097
7844
  );
7098
7845
  const go = ensure2(
@@ -7130,7 +7877,7 @@ async function runWizard() {
7130
7877
  }
7131
7878
  }
7132
7879
  }
7133
- const uiTag = `v${"0.5.0"}`;
7880
+ const uiTag = `v${"0.6.1"}`;
7134
7881
  if (method === "bare") {
7135
7882
  const dl = spinner4();
7136
7883
  dl.start("Pulling the Openship dist from GitHub");
@@ -7155,6 +7902,7 @@ async function runWizard() {
7155
7902
  let edgeAction;
7156
7903
  let migratedSites;
7157
7904
  let migratedCertPems;
7905
+ let migratedStaticRootOverrides;
7158
7906
  let allowHostControl = true;
7159
7907
  if (method === "compose") {
7160
7908
  allowHostControl = ensure2(
@@ -7177,7 +7925,7 @@ async function runWizard() {
7177
7925
  ) === "allow";
7178
7926
  if (!allowHostControl) {
7179
7927
  log5.message(
7180
- chalk20.dim(
7928
+ chalk21.dim(
7181
7929
  " No host key will be created or mounted, and this box won't be offered as a deploy target.\n The Docker socket stays mounted (deployments need it), so this is defense in depth, not isolation."
7182
7930
  )
7183
7931
  );
@@ -7197,7 +7945,7 @@ async function runWizard() {
7197
7945
  dashboardPort,
7198
7946
  publicUrl,
7199
7947
  trustProxy: behindProxy,
7200
- version: "0.5.0",
7948
+ version: "0.6.1",
7201
7949
  noHostControl: !allowHostControl
7202
7950
  })) {
7203
7951
  cancel4("Couldn't fetch the Openship images \u2014 nothing on this box was changed.");
@@ -7205,12 +7953,15 @@ async function runWizard() {
7205
7953
  }
7206
7954
  const edgePlan = await planAndApplyHostEdge({});
7207
7955
  if (!edgePlan.proceed) {
7208
- cancel4("Left the existing proxy on 80/443 running \u2014 re-run and choose migrate/takeover when ready.");
7209
- process.exit(0);
7956
+ cancel4(
7957
+ edgePlan.blockedBy ? `Can't take over the edge: ${edgePlan.blockedBy}. Nothing was started.` : "Left the existing proxy on 80/443 running \u2014 re-run and choose migrate/takeover when ready."
7958
+ );
7959
+ process.exit(edgePlan.blockedBy ? 1 : 0);
7210
7960
  }
7211
7961
  edgeAction = edgePlan.action;
7212
7962
  migratedSites = edgePlan.sites;
7213
7963
  migratedCertPems = edgePlan.certPems;
7964
+ migratedStaticRootOverrides = edgePlan.staticRootOverrides;
7214
7965
  log5.step("Starting the Docker Compose stack\u2026");
7215
7966
  const up = await composeUp({
7216
7967
  // Prefetched above, before the preflight stopped anything.
@@ -7219,7 +7970,7 @@ async function runWizard() {
7219
7970
  dashboardPort,
7220
7971
  publicUrl,
7221
7972
  trustProxy: behindProxy,
7222
- version: "0.5.0",
7973
+ version: "0.6.1",
7223
7974
  noHostControl: !allowHostControl
7224
7975
  });
7225
7976
  if (!up.ok) {
@@ -7268,7 +8019,12 @@ async function runWizard() {
7268
8019
  await waitDashboard(started.dashPort);
7269
8020
  s.stop("Deployed.");
7270
8021
  if (edgeAction === "migrate" && migratedSites?.length) {
7271
- const imported = await importMigratedSites(started.port, migratedSites, migratedCertPems);
8022
+ const imported = await importMigratedSites(
8023
+ started.port,
8024
+ migratedSites,
8025
+ migratedCertPems,
8026
+ migratedStaticRootOverrides
8027
+ );
7272
8028
  if (!imported.ok) {
7273
8029
  log5.warn(
7274
8030
  `Your ${migratedSites.length} existing site${migratedSites.length === 1 ? "" : "s"} aren't served yet \u2014 re-run \`openship up\` to retry the import.`
@@ -7291,7 +8047,7 @@ async function runWizard() {
7291
8047
  inputs: wizardInputs(admin, domainPlan),
7292
8048
  token: provisionToken,
7293
8049
  method: "compose",
7294
- onLog: (msg) => log5.message(chalk20.dim(msg)),
8050
+ onLog: (msg) => log5.message(chalk21.dim(msg)),
7295
8051
  // .opsh.io is a SHARED zone, and Cloud only connects after the stack is up —
7296
8052
  // so a taken subdomain can't be detected at prompt time. Recover here, where
7297
8053
  // we still have a TTY, instead of ending the run with no domain.
@@ -7377,10 +8133,11 @@ async function runWizard() {
7377
8133
  // the operator declined the takeover (their proxy keeps 80/443).
7378
8134
  localEdge: edge.proceed,
7379
8135
  edgeTakeover: edge.edgeTakeover,
7380
- edgeMigrate: edge.edgeMigrate
8136
+ edgeMigrate: edge.edgeMigrate,
8137
+ ...edge.staticRootOverrides ? { staticRootOverrides: edge.staticRootOverrides } : {}
7381
8138
  }
7382
8139
  },
7383
- onLog: (msg) => log5.message(chalk20.dim(msg)),
8140
+ onLog: (msg) => log5.message(chalk21.dim(msg)),
7384
8141
  onSlugTaken: async (taken) => {
7385
8142
  log5.warn(`"${taken}.opsh.io" is already taken.`);
7386
8143
  const next = await text({
@@ -7415,7 +8172,8 @@ async function runWizard() {
7415
8172
  dashPort: Number(started.dashPort),
7416
8173
  acmeEmail: admin?.email,
7417
8174
  edgeTakeover,
7418
- edgeMigrate
8175
+ edgeMigrate,
8176
+ ...edge.staticRootOverrides ? { staticRootOverrides: edge.staticRootOverrides } : {}
7419
8177
  });
7420
8178
  if (res.ok && res.data?.sessionId) {
7421
8179
  const s2 = spinner4();
@@ -7460,18 +8218,18 @@ async function runControl() {
7460
8218
  const dashUrl = `http://localhost:${storedDashboardPort()}`;
7461
8219
  const publicUrl = readInstanceUrl();
7462
8220
  const primaryUrl = publicUrl && !/^https?:\/\/localhost/i.test(publicUrl) ? publicUrl : dashUrl;
7463
- intro3(`${chalk20.bgCyan(chalk20.black(" Openship "))}${chalk20.dim(" control")}`);
8221
+ intro3(`${chalk21.bgCyan(chalk21.black(" Openship "))}${chalk21.dim(" control")}`);
7464
8222
  note3(
7465
- `${chalk20.dim("URL".padEnd(11))}${chalk20.bold(primaryUrl)}
7466
- ${chalk20.dim("Service".padEnd(11))}${running ? chalk20.green("running") : chalk20.yellow("stopped")}
7467
- ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7468
- ` + (ports.api ? `${chalk20.dim("API".padEnd(11))}http://localhost:${ports.api}
7469
- ` : "") + `${chalk20.dim("Manager".padEnd(11))}${managerLabel}`,
8223
+ `${chalk21.dim("URL".padEnd(11))}${chalk21.bold(primaryUrl)}
8224
+ ${chalk21.dim("Service".padEnd(11))}${running ? chalk21.green("running") : chalk21.yellow("stopped")}
8225
+ ${chalk21.dim("Dashboard".padEnd(11))}${dashUrl}
8226
+ ` + (ports.api ? `${chalk21.dim("API".padEnd(11))}http://localhost:${ports.api}
8227
+ ` : "") + `${chalk21.dim("Manager".padEnd(11))}${managerLabel}`,
7470
8228
  "Openship is already set up"
7471
8229
  );
7472
8230
  const corrupted = looksCorrupted();
7473
8231
  if (corrupted) {
7474
- note3(chalk20.red("The service is installed but keeps failing to start \u2014 the database looks corrupted."), "Needs repair");
8232
+ note3(chalk21.red("The service is installed but keeps failing to start \u2014 the database looks corrupted."), "Needs repair");
7475
8233
  }
7476
8234
  const action2 = ensure2(
7477
8235
  await select4({
@@ -7491,18 +8249,18 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7491
8249
  switch (action2) {
7492
8250
  case "repair": {
7493
8251
  const res = await runRepair();
7494
- outro3(res.healed ? chalk20.green(res.detail) : chalk20.yellow(res.detail));
8252
+ outro3(res.healed ? chalk21.green(res.detail) : chalk21.yellow(res.detail));
7495
8253
  return;
7496
8254
  }
7497
8255
  case "open":
7498
8256
  await open2(primaryUrl).catch(() => {
7499
8257
  });
7500
- outro3(chalk20.dim(`Opening ${primaryUrl}`));
8258
+ outro3(chalk21.dim(`Opening ${primaryUrl}`));
7501
8259
  return;
7502
8260
  case "start": {
7503
8261
  if (isCompose) {
7504
8262
  const ok3 = composeStart();
7505
- outro3(ok3 ? chalk20.green("Started.") : chalk20.yellow("Couldn't start the stack \u2014 run `openship up` to see the error."));
8263
+ outro3(ok3 ? chalk21.green("Started.") : chalk21.yellow("Couldn't start the stack \u2014 run `openship up` to see the error."));
7506
8264
  return;
7507
8265
  }
7508
8266
  await startService({});
@@ -7511,21 +8269,21 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7511
8269
  case "restart": {
7512
8270
  if (isCompose) {
7513
8271
  const ok3 = composeRestart();
7514
- outro3(ok3 ? chalk20.green("Restarted.") : chalk20.yellow("Couldn't restart the stack."));
8272
+ outro3(ok3 ? chalk21.green("Restarted.") : chalk21.yellow("Couldn't restart the stack."));
7515
8273
  return;
7516
8274
  }
7517
8275
  const r = restart();
7518
- outro3(r.restarted ? chalk20.green("Restarted.") : chalk20.yellow(r.detail));
8276
+ outro3(r.restarted ? chalk21.green("Restarted.") : chalk21.yellow(r.detail));
7519
8277
  return;
7520
8278
  }
7521
8279
  case "stop": {
7522
8280
  if (isCompose) {
7523
8281
  const ok3 = composeDown();
7524
- outro3(ok3 ? chalk20.green("Stopped. Won't restart on boot.") : chalk20.yellow("Couldn't stop the stack."));
8282
+ outro3(ok3 ? chalk21.green("Stopped. Won't restart on boot.") : chalk21.yellow("Couldn't stop the stack."));
7525
8283
  return;
7526
8284
  }
7527
8285
  const r = stop();
7528
- outro3(chalk20.green(`Stopped. ${chalk20.dim(r.detail)}`));
8286
+ outro3(chalk21.green(`Stopped. ${chalk21.dim(r.detail)}`));
7529
8287
  return;
7530
8288
  }
7531
8289
  case "reset": {
@@ -7534,7 +8292,7 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7534
8292
  );
7535
8293
  const rr = await internalPost(apiPort, "/api/system/reset-admin-password", { password: pw });
7536
8294
  outro3(
7537
- rr.ok ? chalk20.green(`Password reset. Sign in at ${dashUrl} with your email + new password.`) : chalk20.red(`Couldn't reset: ${rr.data?.error || "failed"}`)
8295
+ rr.ok ? chalk21.green(`Password reset. Sign in at ${dashUrl} with your email + new password.`) : chalk21.red(`Couldn't reset: ${rr.data?.error || "failed"}`)
7538
8296
  );
7539
8297
  return;
7540
8298
  }
@@ -7542,13 +8300,19 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7542
8300
  await runWizard();
7543
8301
  return;
7544
8302
  default:
7545
- outro3(chalk20.dim("Nothing changed."));
8303
+ outro3(chalk21.dim("Nothing changed."));
7546
8304
  }
7547
8305
  }
7548
8306
 
8307
+ // src/commands/completion.ts
8308
+ import tab from "@bomb.sh/tab/commander";
8309
+ function attachCompletion(program2) {
8310
+ tab(program2, { completionCommandName: "completion" });
8311
+ }
8312
+
7549
8313
  // src/index.ts
7550
8314
  var program = new Command28();
7551
- program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.5.0").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
8315
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.6.1").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7552
8316
  if (thisCommand.opts().json) setJsonMode(true);
7553
8317
  }).action(async () => {
7554
8318
  const installed = serviceStatus().installed || readInstallMethod() === "compose";
@@ -7582,4 +8346,5 @@ program.addCommand(tokenCommand);
7582
8346
  program.addCommand(apiCommand);
7583
8347
  program.addCommand(resetAdminCommand);
7584
8348
  installCommand.addCommand(cacheCommand);
8349
+ attachCompletion(program);
7585
8350
  program.parse();