openship 0.4.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +27 -1
  2. package/dist/bare-FSUAM5V5.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-3FYIXVX5.js +8051 -0
  6. package/dist/{chunk-ILFETZOM.js → chunk-3LDWWY7U.js} +21 -1
  7. package/dist/{chunk-W73X43R7.js → chunk-3NQQXMDR.js} +3 -3
  8. package/dist/{chunk-OWED2GRM.js → chunk-42WQABXM.js} +2 -2
  9. package/dist/{chunk-TYVME3XD.js → chunk-6FH4AXVY.js} +20 -3
  10. package/dist/{chunk-OXWDY22G.js → chunk-6ODUBKUB.js} +3 -3
  11. package/dist/{chunk-DYPH4HCI.js → chunk-AO252K7Z.js} +1 -1
  12. package/dist/{chunk-XPPZ7B2J.js → chunk-B3CE2OMR.js} +14 -12
  13. package/dist/{chunk-IXCHURVK.js → chunk-DOD2BXHQ.js} +4 -4
  14. package/dist/{chunk-VWPEKOH6.js → chunk-DRK3MEDS.js} +3 -3
  15. package/dist/{chunk-V6AN6XLZ.js → chunk-EMXI4UCU.js} +125 -47
  16. package/dist/{chunk-GKHUSZCD.js → chunk-EP2TNALB.js} +2 -2
  17. package/dist/{chunk-SLTYVHU7.js → chunk-GUQ3QCJ7.js} +942 -519
  18. package/dist/chunk-I3DPMTSF.js +29 -0
  19. package/dist/{chunk-L2VOGB2X.js → chunk-IW2UKBVF.js} +3 -3
  20. package/dist/{chunk-VDMQJHPI.js → chunk-IXM5245F.js} +228 -13
  21. package/dist/{chunk-MZ2WEHHY.js → chunk-JIMUYEKN.js} +4 -4
  22. package/dist/chunk-OGY63DGD.js +705 -0
  23. package/dist/{chunk-O6ZZF52V.js → chunk-ONKEYSMY.js} +2 -2
  24. package/dist/{chunk-6APULWSU.js → chunk-OTLSJYAC.js} +1744 -118
  25. package/dist/{chunk-EI575GHN.js → chunk-PDW4L3NV.js} +3 -3
  26. package/dist/{chunk-5NYUCONN.js → chunk-PSZSKEPA.js} +33 -51
  27. package/dist/{chunk-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
  28. package/dist/{chunk-FXTBHTWY.js → chunk-QEH4QUBX.js} +649 -85
  29. package/dist/{chunk-6APIPIJY.js → chunk-QWHBRY2K.js} +5 -5
  30. package/dist/{chunk-6B5YCFFV.js → chunk-RVER2EOO.js} +5 -5
  31. package/dist/{chunk-Q2CIRAOK.js → chunk-RXQBD4ZY.js} +1 -1
  32. package/dist/{chunk-TGIINPVM.js → chunk-TWJCLD4W.js} +60 -10
  33. package/dist/{chunk-4Z5QO7N6.js → chunk-VDDMADOF.js} +3 -3
  34. package/dist/{chunk-BBHX7T4R.js → chunk-VE2YOK4I.js} +991 -254
  35. package/dist/{chunk-2D3I5G6P.js → chunk-VZ2ZHPS2.js} +73 -43
  36. package/dist/{chunk-AVIB7CT7.js → chunk-WNWQBB7F.js} +2 -2
  37. package/dist/{chunk-FRNAR33O.js → chunk-XZYFZ6A5.js} +3 -3
  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-E5M6VMCG.js +31 -0
  42. package/dist/{cloud-7QYOGF7F.js → cloud-KRTZ2IL5.js} +3 -3
  43. package/dist/{detect-AZLOHUFX.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-CQWHN2Q3.js +48 -0
  55. package/dist/ensure-container-edge-MJVXJQG5.js +32 -0
  56. package/dist/{event-streams-EHRAKL75.js → event-streams-TR4GAC53.js} +4 -4
  57. package/dist/{executor-XJ6RP4Q2.js → executor-JELARVKC.js} +8 -8
  58. package/dist/index.js +1209 -560
  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-AXGNWRKZ.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-IE4P5YQX.js +81 -0
  65. package/dist/server/index.js +116248 -94749
  66. package/dist/server/lua/geo_country.lua +31 -6
  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_log.lua +10 -4
  70. package/dist/server/lua/site_logger.lua +220 -12
  71. package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
  72. package/dist/server/migrations/0074_project_compose_path.sql +1 -0
  73. package/dist/server/migrations/0075_service_command_argv.sql +1 -0
  74. package/dist/server/migrations/0076_rollback_retention.sql +17 -0
  75. package/dist/server/migrations/0077_domain_redirect.sql +13 -0
  76. package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
  77. package/dist/server/migrations/0079_project_readiness.sql +18 -0
  78. package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
  79. package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
  80. package/dist/server/migrations/0082_edge_target_verification.sql +39 -0
  81. package/dist/server/migrations/0083_service_incident.sql +84 -0
  82. package/dist/server/migrations/0084_analytics_daily_rollup.sql +72 -0
  83. package/dist/server/migrations/0085_resource_usage.sql +68 -0
  84. package/dist/server/migrations/0086_remote_infra_updates.sql +41 -0
  85. package/dist/server/migrations/0087_server_container_version_columns.sql +17 -0
  86. package/dist/server/migrations/0088_audit_source_and_settings.sql +27 -0
  87. package/dist/server/migrations/0089_auto_scan_infra.sql +7 -0
  88. package/dist/server/migrations/0090_project_collect_paths.sql +18 -0
  89. package/dist/server/migrations/0091_project_server_id.sql +26 -0
  90. package/dist/server/migrations/0092_project_internal_alias.sql +13 -0
  91. package/dist/server/migrations/0093_service_incident_org_index.sql +13 -0
  92. package/dist/server/migrations/0094_backup_restore_meta.sql +15 -0
  93. package/dist/server/migrations/0095_backup_restore_cancel.sql +19 -0
  94. package/dist/server/migrations/0096_backup_policy_retention_defaults.sql +20 -0
  95. package/dist/server/migrations/0097_update_status_upstream_only.sql +35 -0
  96. package/dist/server/migrations/meta/_journal.json +175 -0
  97. package/dist/setup-FA4PW4NU.js +21 -0
  98. package/dist/{signin-ITOPKKUB.js → signin-3M3UPB6K.js} +10 -10
  99. package/dist/{sso-oidc-NON2PD7Y.js → sso-oidc-3VDIYOEJ.js} +10 -10
  100. package/dist/{sts-HHFFSAY2.js → sts-B3WIYLXR.js} +9 -9
  101. package/package.json +6 -3
  102. package/dist/bare-JY5Y5ELU.js +0 -23
  103. package/dist/chunk-SJIFLLX4.js +0 -2931
  104. package/dist/chunk-VFINNTUL.js +0 -362
  105. package/dist/cloud-KYWY3RP4.js +0 -30
  106. package/dist/docker-LODK5Y2S.js +0 -16
  107. package/dist/docker-edge-executor-VHW3JPJS.js +0 -13
  108. package/dist/edge-import-ABUDKKJI.js +0 -45
  109. package/dist/ensure-container-edge-SW6HENSG.js +0 -31
  110. package/dist/nginx-3L4SIBJA.js +0 -24
  111. package/dist/openresty-lua-4WLGDSER.js +0 -42
  112. package/dist/setup-NU6T2PDU.js +0 -20
package/dist/index.js CHANGED
@@ -1,49 +1,70 @@
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
+ AUTH_SECRET_FILE,
7
+ DATA_DIR,
8
+ DEFAULT_REPO,
9
+ INTERNAL_TOKEN_FILE,
6
10
  IS_ALT_HOME,
11
+ LOG_DIR,
7
12
  OS_DIR,
13
+ PORTS_FILE,
14
+ SOURCE_CHECKOUT_DIR,
15
+ SOURCE_DIST_DIR,
8
16
  composeDown,
9
17
  composeInternalToken,
10
- composeIsViableDefault,
18
+ composePlan,
11
19
  composePrefetch,
12
20
  composePs,
13
21
  composeRestart,
14
22
  composeRunning,
15
23
  composeStart,
24
+ composeTrustedOriginUrls,
16
25
  composeUninstall,
17
26
  composeUp,
18
27
  composeUpdate,
28
+ dockerGap,
29
+ dockerInstallPreview,
19
30
  ensureDocker,
31
+ has,
20
32
  hasDockerCompose,
21
33
  importMigratedSites,
34
+ portMoveNotice,
22
35
  prepareFromSource,
23
36
  readInstallMethod,
37
+ readInstanceUrl,
24
38
  readSourceInstall,
39
+ readStoredPorts,
25
40
  rebuildFromSource,
26
41
  remoteSha,
42
+ resolveComposePorts,
43
+ resolvePorts,
44
+ saveInstanceUrl,
27
45
  shortSha,
28
- sourceBuildDir
29
- } from "./chunk-BBHX7T4R.js";
30
- import "./chunk-FRNAR33O.js";
31
- import "./chunk-EI575GHN.js";
32
- import "./chunk-6APIPIJY.js";
33
- import "./chunk-IXCHURVK.js";
34
- import "./chunk-OWED2GRM.js";
35
- import "./chunk-VWPEKOH6.js";
36
- import "./chunk-W73X43R7.js";
37
- import "./chunk-TGIINPVM.js";
38
- import "./chunk-5NYUCONN.js";
39
- import "./chunk-AVIB7CT7.js";
40
- import "./chunk-5MZ624VX.js";
41
- import "./chunk-Q2CIRAOK.js";
42
- import "./chunk-GKHUSZCD.js";
46
+ sourceBuildDir,
47
+ storedApiPort,
48
+ storedDashboardPort
49
+ } from "./chunk-VE2YOK4I.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-PSZSKEPA.js";
59
+ import "./chunk-WNWQBB7F.js";
60
+ import "./chunk-2GWYYHOR.js";
61
+ import "./chunk-RXQBD4ZY.js";
62
+ import "./chunk-EP2TNALB.js";
43
63
  import {
44
64
  beginEdgeTakeover,
45
65
  collectProxyCerts,
46
66
  completeEdgeTakeover,
67
+ copyStaticRootIntoEdge,
47
68
  detectInstalledProxy,
48
69
  edgeProxy,
49
70
  edgeProxyFor,
@@ -51,41 +72,45 @@ import {
51
72
  importSites,
52
73
  recoverInterruptedTakeover,
53
74
  rollbackEdgeTakeover,
54
- scanImportableSites
55
- } from "./chunk-SLTYVHU7.js";
56
- import "./chunk-O6ZZF52V.js";
57
- import "./chunk-JYGD5WSN.js";
58
- import "./chunk-SJIFLLX4.js";
59
- import "./chunk-V6AN6XLZ.js";
60
- import "./chunk-VDMQJHPI.js";
61
- import "./chunk-MZ2WEHHY.js";
62
- import "./chunk-L2VOGB2X.js";
63
- import "./chunk-4Z5QO7N6.js";
64
- import "./chunk-XPPZ7B2J.js";
65
- import "./chunk-OXWDY22G.js";
66
- import "./chunk-RPHRPFEH.js";
75
+ scanImportableSites,
76
+ unreachableStaticRoots
77
+ } from "./chunk-GUQ3QCJ7.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-B3CE2OMR.js";
87
+ import "./chunk-6ODUBKUB.js";
88
+ import "./chunk-ZCFVHA25.js";
67
89
  import {
68
90
  LocalExecutor
69
- } from "./chunk-TYVME3XD.js";
70
- import "./chunk-FXTBHTWY.js";
71
- import "./chunk-FPRHYBY2.js";
91
+ } from "./chunk-6FH4AXVY.js";
92
+ import "./chunk-QEH4QUBX.js";
93
+ import "./chunk-I3DPMTSF.js";
72
94
  import {
73
95
  edgeCrashReason,
74
96
  edgeIsBroken,
75
97
  ourEdgeContainerRunning
76
- } from "./chunk-2D3I5G6P.js";
77
- import "./chunk-ILFETZOM.js";
78
- import "./chunk-VFINNTUL.js";
98
+ } from "./chunk-VZ2ZHPS2.js";
99
+ import "./chunk-3LDWWY7U.js";
100
+ import "./chunk-OGY63DGD.js";
101
+ import "./chunk-ZFJXPCQP.js";
79
102
  import {
80
103
  CLOUD_DASHBOARD_URL,
81
104
  LOCAL_API_URL,
82
105
  LOCAL_DASHBOARD_URL,
106
+ buildMailImageRef,
83
107
  cliInstallCommand,
108
+ commandToArgv,
84
109
  isValidEmail,
85
110
  parseOpenshipConfigJson,
86
111
  resolveCliUpdatePlan
87
- } from "./chunk-6APULWSU.js";
88
- import "./chunk-KI2EY3WB.js";
112
+ } from "./chunk-OTLSJYAC.js";
113
+ import "./chunk-PXKCZB2I.js";
89
114
 
90
115
  // src/index.ts
91
116
  import { Command as Command28 } from "commander";
@@ -243,6 +268,7 @@ function getDashboardUrl(name) {
243
268
  }
244
269
 
245
270
  // src/lib/api-client.ts
271
+ var USER_AGENT = `openship-cli/${true ? "0.6.0" : "dev"}`;
246
272
  function getApiUrl2() {
247
273
  return `${getApiUrl()}/api`;
248
274
  }
@@ -259,6 +285,7 @@ var ApiError = class extends Error {
259
285
  function buildHeaders(extra) {
260
286
  const headers = new Headers(extra);
261
287
  if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
288
+ if (!headers.has("User-Agent")) headers.set("User-Agent", USER_AGENT);
262
289
  const token = getToken();
263
290
  if (token && !headers.has("Authorization")) headers.set("Authorization", `Bearer ${token}`);
264
291
  return headers;
@@ -483,7 +510,7 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
483
510
 
484
511
  // src/commands/up.ts
485
512
  import { Command as Command4 } from "commander";
486
- import chalk6 from "chalk";
513
+ import chalk7 from "chalk";
487
514
  import ora from "ora";
488
515
  import { spawn } from "child_process";
489
516
  import { randomBytes as randomBytes2 } from "crypto";
@@ -537,8 +564,8 @@ async function downloadToFile(url, dest, onProgress) {
537
564
  } finally {
538
565
  file.end();
539
566
  }
540
- await new Promise((resolve2, reject2) => {
541
- file.on("finish", () => resolve2());
567
+ await new Promise((resolve3, reject2) => {
568
+ file.on("finish", () => resolve3());
542
569
  file.on("error", reject2);
543
570
  });
544
571
  return { sha256: hash.digest("hex"), size: received };
@@ -658,16 +685,54 @@ async function fetchBundle(tag, onProgress) {
658
685
 
659
686
  // src/lib/service.ts
660
687
  import { spawnSync as spawnSync2 } from "child_process";
661
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2, readFileSync as readFileSync2 } from "fs";
688
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
662
689
  import { homedir } from "os";
663
- 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
664
725
  var HOME = homedir();
665
- var LOG_DIR = join4(OS_DIR, "logs");
726
+ var OPENSHIP_BIN = join5(OS_DIR, "bin");
666
727
  var MAC_LABEL = IS_ALT_HOME ? "io.openship-dev.up" : "io.openship.up";
667
- var MAC_PLIST = join4(HOME, "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
728
+ var MAC_PLIST = join5(HOME, "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
668
729
  var SYSTEMD_NAME = IS_ALT_HOME ? "openship-dev" : "openship";
669
730
  var WIN_TASK = IS_ALT_HOME ? "OpenshipDev" : "Openship";
670
731
  function selfInvocation() {
732
+ const cli = readCliInstall();
733
+ if (cli?.method === "tarball" && existsSync4(cli.launcher)) {
734
+ return { runtime: cli.launcher, args: [] };
735
+ }
671
736
  const runtime = process.execPath;
672
737
  const entry = resolve(process.argv[1] ?? "");
673
738
  return { runtime, args: [entry] };
@@ -736,7 +801,8 @@ function serviceEnv() {
736
801
  function plist(flags) {
737
802
  const argv = runArgv(flags);
738
803
  const items = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
739
- 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(":");
740
806
  const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `<key>${xmlEscape(k)}</key><string>${xmlEscape(v)}</string>`).join("");
741
807
  return `<?xml version="1.0" encoding="UTF-8"?>
742
808
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -749,8 +815,8 @@ ${items}
749
815
  </array>
750
816
  <key>RunAtLoad</key><true/>
751
817
  <key>KeepAlive</key><true/>
752
- <key>StandardOutPath</key><string>${join4(LOG_DIR, "up.log")}</string>
753
- <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>
754
820
  <key>EnvironmentVariables</key>
755
821
  <dict><key>PATH</key><string>${path2}</string>${extraEnv}</dict>
756
822
  </dict>
@@ -762,6 +828,8 @@ function systemdUnit(flags) {
762
828
  const execStart = argv.map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
763
829
  const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `Environment=${k}=${v}
764
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
+ ` : "";
765
833
  return `[Unit]
766
834
  Description=Openship control plane
767
835
  After=network-online.target
@@ -773,26 +841,40 @@ ExecStart=${execStart}
773
841
  Restart=always
774
842
  RestartSec=2
775
843
  Environment=NODE_ENV=production
776
- ${extraEnv}
844
+ ${pathEnv}${extraEnv}
777
845
  [Install]
778
846
  WantedBy=default.target
779
847
  `;
780
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
+ }
781
863
  function preview(flags) {
782
864
  const kind = detectKind();
783
865
  if (kind === "launchd") return { kind, path: MAC_PLIST, content: plist(flags) };
784
- 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) };
785
867
  if (kind === "systemd-system") return { kind, path: `/etc/systemd/system/${SYSTEMD_NAME}.service`, content: systemdUnit(flags) };
786
868
  if (kind === "schtasks") return { kind, path: WIN_TASK, content: runArgv(flags).join(" ") };
787
869
  return { kind, path: "", content: "" };
788
870
  }
789
871
  function installAndStart(flags) {
790
- mkdirSync4(LOG_DIR, { recursive: true });
872
+ mkdirSync5(LOG_DIR, { recursive: true });
791
873
  const kind = detectKind();
792
874
  if (kind === "launchd") {
793
- mkdirSync4(join4(HOME, "Library", "LaunchAgents"), { recursive: true });
875
+ mkdirSync5(join5(HOME, "Library", "LaunchAgents"), { recursive: true });
794
876
  run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
795
- writeFileSync3(MAC_PLIST, plist(flags));
877
+ writeFileSync4(MAC_PLIST, plist(flags));
796
878
  const uid = String(process.getuid?.() ?? "");
797
879
  let r = run("launchctl", ["bootstrap", `gui/${uid}`, MAC_PLIST]);
798
880
  if (!r.ok) r = run("launchctl", ["load", "-w", MAC_PLIST]);
@@ -801,9 +883,9 @@ function installAndStart(flags) {
801
883
  }
802
884
  if (kind === "systemd-user" || kind === "systemd-system") {
803
885
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
804
- const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
805
- mkdirSync4(unitPath.slice(0, unitPath.lastIndexOf("/")), { recursive: true });
806
- 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));
807
889
  run("systemctl", [...sysArgs, "daemon-reload"]);
808
890
  const r = run("systemctl", [...sysArgs, "enable", "--now", SYSTEMD_NAME]);
809
891
  if (!r.ok) throw new Error(`systemctl enable failed: ${r.out}`);
@@ -826,7 +908,7 @@ function installAndStart(flags) {
826
908
  function restart() {
827
909
  const kind = detectKind();
828
910
  if (kind === "launchd") {
829
- 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" };
830
912
  const uid = String(process.getuid?.() ?? "");
831
913
  const r = run("launchctl", ["kickstart", "-k", `gui/${uid}/${MAC_LABEL}`]);
832
914
  if (!r.ok) return { restarted: false, detail: r.out || "launchctl kickstart failed" };
@@ -842,8 +924,8 @@ function restart() {
842
924
  }
843
925
  if (kind === "systemd-user" || kind === "systemd-system") {
844
926
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
845
- const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
846
- 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" };
847
929
  const r = run("systemctl", [...sysArgs, "restart", SYSTEMD_NAME]);
848
930
  return { restarted: r.ok, detail: r.ok ? `restarted ${SYSTEMD_NAME}` : r.out };
849
931
  }
@@ -857,14 +939,14 @@ function restart() {
857
939
  function serviceStatus() {
858
940
  const kind = detectKind();
859
941
  if (kind === "launchd") {
860
- return { kind, installed: existsSync3(MAC_PLIST), running: launchdPid() != null };
942
+ return { kind, installed: existsSync4(MAC_PLIST), running: launchdPid() != null };
861
943
  }
862
944
  if (kind === "systemd-user" || kind === "systemd-system") {
863
945
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
864
- 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`;
865
947
  return {
866
948
  kind,
867
- installed: existsSync3(unitPath),
949
+ installed: existsSync4(unitPath),
868
950
  running: run("systemctl", [...sysArgs, "is-active", SYSTEMD_NAME]).out.trim() === "active"
869
951
  };
870
952
  }
@@ -876,14 +958,9 @@ function serviceStatus() {
876
958
  }
877
959
  function sweepOrphanPorts() {
878
960
  if (process.platform === "win32") return;
879
- let ports = [];
880
- try {
881
- const raw = readFileSync2(join4(HOME, ".openship", "ports.json"), "utf8");
882
- const p = JSON.parse(raw);
883
- ports = [p.api, p.dashboard].filter((n) => typeof n === "number");
884
- } catch {
885
- return;
886
- }
961
+ const stored = readStoredPorts();
962
+ const ports = [stored.api, stored.dashboard].filter((n) => typeof n === "number");
963
+ if (ports.length === 0) return;
887
964
  const self = process.pid;
888
965
  for (const port of ports) {
889
966
  const q = run("lsof", ["-ti", `tcp:${port}`]);
@@ -904,13 +981,13 @@ function stop() {
904
981
  let result;
905
982
  if (kind === "launchd") {
906
983
  run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
907
- if (existsSync3(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
984
+ if (existsSync4(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
908
985
  result = { kind, detail: `launchd agent ${MAC_LABEL} stopped + removed` };
909
986
  } else if (kind === "systemd-user" || kind === "systemd-system") {
910
987
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
911
988
  run("systemctl", [...sysArgs, "disable", "--now", SYSTEMD_NAME]);
912
- const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
913
- 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 });
914
991
  run("systemctl", [...sysArgs, "daemon-reload"]);
915
992
  result = { kind, detail: `systemd unit ${SYSTEMD_NAME} stopped + disabled` };
916
993
  } else if (kind === "schtasks") {
@@ -924,92 +1001,10 @@ function stop() {
924
1001
  return result;
925
1002
  }
926
1003
 
927
- // src/lib/ports.ts
928
- import { createServer } from "net";
929
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
930
- import { join as join5 } from "path";
931
- var PORTS_FILE = join5(OS_DIR, "ports.json");
932
- var INSTANCE_FILE = join5(OS_DIR, "instance.json");
933
- function saveInstanceUrl(publicUrl) {
934
- try {
935
- if (!existsSync4(OS_DIR)) mkdirSync5(OS_DIR, { recursive: true, mode: 448 });
936
- writeFileSync4(INSTANCE_FILE, JSON.stringify({ publicUrl: publicUrl ?? null }));
937
- } catch {
938
- }
939
- }
940
- function readInstanceUrl() {
941
- try {
942
- return JSON.parse(readFileSync3(INSTANCE_FILE, "utf8")).publicUrl ?? null;
943
- } catch {
944
- return null;
945
- }
946
- }
947
- var DEFAULT_API = 4e3;
948
- var DEFAULT_DASHBOARD = 3001;
949
- function isPortFree(port) {
950
- return new Promise((resolve2) => {
951
- const srv = createServer();
952
- srv.once("error", () => resolve2(false));
953
- srv.listen(port, "127.0.0.1", () => srv.close(() => resolve2(true)));
954
- });
955
- }
956
- async function waitPortFree(port, opts = {}) {
957
- const timeoutMs = opts.timeoutMs ?? 6e3;
958
- const intervalMs = opts.intervalMs ?? 250;
959
- const deadline = Date.now() + timeoutMs;
960
- for (; ; ) {
961
- if (await isPortFree(port)) return true;
962
- if (Date.now() >= deadline) return false;
963
- await new Promise((r) => setTimeout(r, intervalMs));
964
- }
965
- }
966
- function getFreePort() {
967
- return new Promise((resolve2, reject2) => {
968
- const srv = createServer();
969
- srv.once("error", reject2);
970
- srv.listen(0, "127.0.0.1", () => {
971
- const addr = srv.address();
972
- const port = addr && typeof addr === "object" ? addr.port : 0;
973
- srv.close(() => port ? resolve2(port) : reject2(new Error("no free port")));
974
- });
975
- });
976
- }
977
- function loadStoredPorts() {
978
- try {
979
- return JSON.parse(readFileSync3(PORTS_FILE, "utf-8"));
980
- } catch {
981
- return {};
982
- }
983
- }
984
- function saveStoredPorts(api, dashboard) {
985
- try {
986
- if (!existsSync4(OS_DIR)) mkdirSync5(OS_DIR, { recursive: true, mode: 448 });
987
- writeFileSync4(PORTS_FILE, JSON.stringify({ api, dashboard }));
988
- } catch {
989
- }
990
- }
991
- async function resolvePorts(prefs) {
992
- const stored = loadStoredPorts();
993
- const apiPref = prefs.api ?? stored.api ?? DEFAULT_API;
994
- const dashPref = prefs.dashboard ?? stored.dashboard ?? DEFAULT_DASHBOARD;
995
- const apiRemembered = prefs.api === void 0 && stored.api === apiPref;
996
- const dashRemembered = prefs.dashboard === void 0 && stored.dashboard === dashPref;
997
- let api;
998
- if (await isPortFree(apiPref)) api = apiPref;
999
- else if (apiRemembered && await waitPortFree(apiPref)) api = apiPref;
1000
- else api = await getFreePort();
1001
- let dashboard;
1002
- if (dashPref !== api && await isPortFree(dashPref)) dashboard = dashPref;
1003
- else if (dashPref !== api && dashRemembered && await waitPortFree(dashPref)) dashboard = dashPref;
1004
- else dashboard = await getFreePort();
1005
- if (dashboard === api) dashboard = await getFreePort();
1006
- saveStoredPorts(api, dashboard);
1007
- return {
1008
- api,
1009
- dashboard,
1010
- switched: { api: api !== apiPref, dashboard: dashboard !== dashPref }
1011
- };
1012
- }
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";
1013
1008
 
1014
1009
  // src/lib/edge-preflight.ts
1015
1010
  import { spawnSync as spawnSync3 } from "child_process";
@@ -1035,12 +1030,39 @@ async function planAndApplyHostEdge(opts, overrides = {}) {
1035
1030
  const action2 = await resolveAction(opts.edge, { status, owner, sites, warnings }, deps);
1036
1031
  if (action2 === "cancel") return { proceed: false };
1037
1032
  const certPems = action2 === "migrate" ? await deps.collectCerts(executor, sites, { status }) : void 0;
1038
- 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(
1039
1039
  executor,
1040
1040
  status,
1041
1041
  (m, l) => deps.warn(l === "info" ? m : chalk5.yellow(m))
1042
1042
  );
1043
- 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
+ }
1044
1066
  }
1045
1067
  async function rollbackHostEdge() {
1046
1068
  if (process.platform !== "linux") return false;
@@ -1192,6 +1214,51 @@ async function confirmEdgeAction(info3) {
1192
1214
  });
1193
1215
  return isCancel(choice) ? "cancel" : choice;
1194
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
+ }
1195
1262
  function defaultDeps() {
1196
1263
  return {
1197
1264
  platform: process.platform,
@@ -1200,6 +1267,7 @@ function defaultDeps() {
1200
1267
  foreignProxyOnEdge,
1201
1268
  importSites,
1202
1269
  beginEdgeTakeover: (executor, status, onLog) => beginEdgeTakeover(executor, status, (entry) => onLog(entry.message, entry.level)),
1270
+ rollbackHostEdge,
1203
1271
  recoverInterruptedTakeover: (executor, onLog, isEdgeHealthy) => recoverInterruptedTakeover(
1204
1272
  executor,
1205
1273
  (entry) => onLog(entry.message, entry.level),
@@ -1227,12 +1295,205 @@ function defaultDeps() {
1227
1295
  };
1228
1296
  }
1229
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
+
1230
1492
  // src/lib/loopback-api.ts
1231
1493
  import { existsSync as existsSync6, readFileSync as readFileSync4, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
1232
1494
  import { randomBytes } from "crypto";
1233
- import { join as join7 } from "path";
1234
1495
  function ensureInternalToken() {
1235
- const path2 = join7(OS_DIR, "internal-token");
1496
+ const path2 = INTERNAL_TOKEN_FILE;
1236
1497
  if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
1237
1498
  mkdirSync7(OS_DIR, { recursive: true, mode: 448 });
1238
1499
  const token = randomBytes(32).toString("hex");
@@ -1265,17 +1526,17 @@ async function internalPost(port, path2, body, token) {
1265
1526
  return { ok: false, data: { error: err2.message } };
1266
1527
  }
1267
1528
  }
1268
- async function bootstrapAdmin(apiPort2, admin, token) {
1269
- const { ok: ok3, data } = await internalPost(apiPort2, "/api/system/bootstrap-admin", admin, token);
1529
+ async function bootstrapAdmin(apiPort, admin, token) {
1530
+ const { ok: ok3, data } = await internalPost(apiPort, "/api/system/bootstrap-admin", admin, token);
1270
1531
  if (ok3) return { ok: true };
1271
1532
  if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
1272
1533
  return { ok: false, message: data?.error || "failed" };
1273
1534
  }
1274
- async function waitHealthy(apiPort2, seconds = 90) {
1535
+ async function waitHealthy(apiPort, seconds = 90) {
1275
1536
  for (let i = 0; i < seconds; i++) {
1276
1537
  await new Promise((r) => setTimeout(r, 1e3));
1277
1538
  try {
1278
- await fetch(`http://127.0.0.1:${apiPort2}/api/health`, { signal: AbortSignal.timeout(2e3) });
1539
+ await fetch(`http://127.0.0.1:${apiPort}/api/health`, { signal: AbortSignal.timeout(2e3) });
1279
1540
  return true;
1280
1541
  } catch {
1281
1542
  }
@@ -1500,7 +1761,8 @@ async function headlessProvision(opts) {
1500
1761
  dashPort,
1501
1762
  localEdge: d.localEdge,
1502
1763
  edgeTakeover: d.edgeTakeover,
1503
- edgeMigrate: d.edgeMigrate
1764
+ edgeMigrate: d.edgeMigrate,
1765
+ ...d.staticRootOverrides ? { staticRootOverrides: d.staticRootOverrides } : {}
1504
1766
  });
1505
1767
  let slug = d.slug;
1506
1768
  let res = await internalPost(port, "/api/system/self-register", freeBody(slug), token);
@@ -1548,8 +1810,8 @@ function normalizePublicUrl(raw) {
1548
1810
  const url = normalizeUrl(raw);
1549
1811
  if (!url) {
1550
1812
  console.error(
1551
- chalk6.red(`
1552
- 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")
1553
1815
  );
1554
1816
  process.exit(1);
1555
1817
  }
@@ -1558,14 +1820,17 @@ function normalizePublicUrl(raw) {
1558
1820
  var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
1559
1821
  var SERVER_DIR = join8(DIST_DIR, "server");
1560
1822
  function ensureAuthSecret() {
1561
- const path2 = join8(OS_DIR, "auth-secret");
1823
+ const path2 = AUTH_SECRET_FILE;
1562
1824
  if (existsSync7(path2)) return readFileSync5(path2, "utf8").trim();
1563
1825
  mkdirSync8(OS_DIR, { recursive: true, mode: 448 });
1564
1826
  const secret = randomBytes2(32).toString("hex");
1565
1827
  writeFileSync7(path2, secret, { mode: 384 });
1566
1828
  return secret;
1567
1829
  }
1568
- var upCommand = new Command4("up").description("Start Openship as a persistent service (boot + auto-restart); --foreground to run attached").option("--port <port>", "API port to listen on", "4000").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port", "3001").option("--no-ui", "Run the API only \u2014 don't download/serve the dashboard").option("--ui-version <tag>", "Dashboard release tag to run (default: this CLI's version)").option("-f, --foreground", "Run attached in this terminal instead of as a background service").option("--dry-run", "Print the service definition that would be installed, then exit").option(
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(
1569
1834
  "--public-url <url>",
1570
1835
  "Serve remotely at this public URL (VPS): binds the dashboard to all interfaces, proxies the API same-origin, and requires login"
1571
1836
  ).option(
@@ -1581,34 +1846,31 @@ var upCommand = new Command4("up").description("Start Openship as a persistent s
1581
1846
  "--no-host-control",
1582
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."
1583
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
+ }
1584
1853
  if (opts.fromSource || opts.source) return runFromSource(opts);
1585
1854
  if (opts.foreground) return runForeground(opts);
1586
1855
  const headless = !!(opts.nonInteractive || opts.yes);
1587
- let method;
1588
- if (opts.bare) {
1589
- method = "bare";
1590
- } else if (opts.compose) {
1591
- if (!await ensureDocker()) {
1592
- console.error(
1593
- chalk6.red("\n --compose needs Docker + docker compose, and they couldn't be installed automatically.") + chalk6.dim(
1594
- "\n Install Docker (https://docs.docker.com/engine/install/) and re-run, or use --bare.\n"
1595
- )
1596
- );
1597
- process.exit(1);
1598
- }
1599
- method = "compose";
1600
- } else if (process.platform === "linux") {
1601
- method = await ensureDocker() ? "compose" : "bare";
1602
- } else {
1603
- 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);
1604
1866
  }
1605
1867
  if (method === "compose") {
1606
1868
  const started2 = await runCompose(opts);
1607
- if (headless && !opts.dryRun) {
1869
+ if (headless) {
1608
1870
  const token = composeInternalToken();
1609
1871
  if (!token) {
1610
1872
  console.warn(
1611
- chalk6.yellow(
1873
+ chalk7.yellow(
1612
1874
  "\n Couldn't read the stack's internal token (compose/.env) \u2014 create the admin from the dashboard.\n"
1613
1875
  )
1614
1876
  );
@@ -1623,7 +1885,7 @@ var upCommand = new Command4("up").description("Start Openship as a persistent s
1623
1885
  return;
1624
1886
  }
1625
1887
  const started = await startService(opts);
1626
- if (headless && !opts.dryRun) await runHeadlessProvision(opts, started, { method: "bare" });
1888
+ if (headless) await runHeadlessProvision(opts, started, { method: "bare" });
1627
1889
  });
1628
1890
  async function runHeadlessProvision(opts, started, extra) {
1629
1891
  let inputs;
@@ -1641,7 +1903,7 @@ async function runHeadlessProvision(opts, started, extra) {
1641
1903
  });
1642
1904
  } catch (err2) {
1643
1905
  if (err2 instanceof HeadlessInputError) {
1644
- console.error(chalk6.red(`
1906
+ console.error(chalk7.red(`
1645
1907
  ${err2.message}
1646
1908
  `));
1647
1909
  process.exit(1);
@@ -1655,13 +1917,13 @@ async function runHeadlessProvision(opts, started, extra) {
1655
1917
  inputs,
1656
1918
  token: extra?.token,
1657
1919
  method: extra?.method,
1658
- onLog: (m) => console.log(chalk6.dim(` ${m}`))
1920
+ onLog: (m) => console.log(chalk7.dim(` ${m}`))
1659
1921
  });
1660
- console.log(chalk6.green(`
1922
+ console.log(chalk7.green(`
1661
1923
  \u2713 Openship provisioned${result.liveUrl ? `: ${result.liveUrl}` : "."}`));
1662
- 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}`));
1663
1925
  } catch (err2) {
1664
- console.error(chalk6.red(`
1926
+ console.error(chalk7.red(`
1665
1927
  Headless provisioning failed: ${err2.message}
1666
1928
  `));
1667
1929
  process.exit(1);
@@ -1671,26 +1933,33 @@ async function runCompose(opts) {
1671
1933
  const headless = !!(opts.nonInteractive || opts.yes);
1672
1934
  if (!hasDockerCompose()) {
1673
1935
  console.error(
1674
- 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")
1675
1937
  );
1676
1938
  process.exit(1);
1677
1939
  }
1678
1940
  if (opts.edge && !EDGE_ACTIONS.includes(opts.edge)) {
1679
1941
  console.error(
1680
- chalk6.red(`
1681
- Invalid --edge value: ${opts.edge}`) + chalk6.dim(`
1942
+ chalk7.red(`
1943
+ Invalid --edge value: ${opts.edge}`) + chalk7.dim(`
1682
1944
  Expected one of: ${EDGE_ACTIONS.join(", ")}
1683
1945
  `)
1684
1946
  );
1685
1947
  process.exit(1);
1686
1948
  }
1687
1949
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1950
+ const ports = await resolveComposePorts({ api: opts.port, dashboard: opts.dashboardPort });
1951
+ const apiPort = String(ports.api);
1952
+ const dashboardPort = String(ports.dashboard);
1953
+ const portNotice = portMoveNotice(ports, [...composeTrustedOriginUrls(), publicUrl]);
1954
+ if (portNotice.length) {
1955
+ console.log("\n" + portNotice.map((l) => chalk7.yellow(` ${l}`)).join("\n"));
1956
+ }
1688
1957
  const prefetch = ora(
1689
1958
  sourceBuildDir() ? "Building images before touching :80/:443\u2026" : "Pulling images before touching :80/:443\u2026"
1690
1959
  ).start();
1691
1960
  const fetched = composePrefetch({
1692
- apiPort: opts.port,
1693
- dashboardPort: opts.dashboardPort,
1961
+ apiPort,
1962
+ dashboardPort,
1694
1963
  publicUrl,
1695
1964
  trustProxy: opts.trustProxy,
1696
1965
  noHostControl: opts.hostControl === false ? true : void 0
@@ -1698,7 +1967,7 @@ async function runCompose(opts) {
1698
1967
  if (!fetched) {
1699
1968
  prefetch.fail("Couldn't fetch the stack's images \u2014 nothing was changed on this box.");
1700
1969
  console.error(
1701
- 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")
1702
1971
  );
1703
1972
  process.exit(1);
1704
1973
  }
@@ -1708,15 +1977,17 @@ async function runCompose(opts) {
1708
1977
  edgePlan = await planAndApplyHostEdge({ edge: opts.edge });
1709
1978
  } catch (e) {
1710
1979
  console.error(
1711
- chalk6.red(`
1980
+ chalk7.red(`
1712
1981
  Edge preflight failed: ${e.message}
1713
- `) + 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")
1714
1983
  );
1715
1984
  process.exit(1);
1716
1985
  }
1717
1986
  if (!edgePlan.proceed) {
1718
1987
  console.log(
1719
- 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")
1720
1991
  );
1721
1992
  process.exit(1);
1722
1993
  }
@@ -1727,8 +1998,8 @@ async function runCompose(opts) {
1727
1998
  const res = await composeUp({
1728
1999
  // Prefetched above, before the preflight stopped anything.
1729
2000
  alreadyFetched: true,
1730
- apiPort: opts.port,
1731
- dashboardPort: opts.dashboardPort,
2001
+ apiPort,
2002
+ dashboardPort,
1732
2003
  publicUrl,
1733
2004
  trustProxy: opts.trustProxy,
1734
2005
  // commander maps `--no-host-control` to hostControl === false. `undefined` when
@@ -1740,7 +2011,7 @@ async function runCompose(opts) {
1740
2011
  spinner5.fail("docker compose failed to start the stack");
1741
2012
  const restored = edgePlan.action ? await rollbackHostEdge() : false;
1742
2013
  console.error(
1743
- 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"))
1744
2015
  );
1745
2016
  process.exit(1);
1746
2017
  }
@@ -1748,16 +2019,16 @@ async function runCompose(opts) {
1748
2019
  if (edgePlan.action && await edgeIsBroken(new LocalExecutor())) {
1749
2020
  const reason = await edgeCrashReason(new LocalExecutor());
1750
2021
  console.error(
1751
- chalk6.red(`
2022
+ chalk7.red(`
1752
2023
  Openship's edge container is not running${reason ? ` \u2014 ${reason}` : "."}`)
1753
2024
  );
1754
2025
  const restored = await rollbackHostEdge();
1755
2026
  console.error(
1756
- restored ? chalk6.yellow(
2027
+ restored ? chalk7.yellow(
1757
2028
  ` Your previous proxy has been restarted \u2014 the box is serving again, on it.
1758
2029
  Nothing was migrated. Fix the cause above, then re-run \`openship up\`.
1759
2030
  `
1760
- ) : chalk6.red(
2031
+ ) : chalk7.red(
1761
2032
  ` AND your previous proxy could NOT be restarted automatically \u2014 the box is
1762
2033
  serving nothing right now. Start it by hand:
1763
2034
  docker stop openship-edge && sudo systemctl enable --now nginx
@@ -1768,68 +2039,47 @@ async function runCompose(opts) {
1768
2039
  }
1769
2040
  let importedOk = true;
1770
2041
  if (edgePlan.action === "migrate" && edgePlan.sites?.length) {
1771
- 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
+ );
1772
2048
  importedOk = outcome.registered.length > 0;
1773
2049
  if (importedOk) markStoppedProxyImported();
1774
2050
  }
1775
2051
  if (edgePlan.action && importedOk) await completeHostEdge();
1776
2052
  const dashboardUrl = publicUrl ?? `http://localhost:${res.dashPort}`;
1777
2053
  console.log(
1778
- chalk6.dim(` Dashboard: ${dashboardUrl} (login required)
1779
- `) + 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.
1780
- (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"))
1781
2057
  );
1782
2058
  return { apiPort: res.apiPort, dashPort: res.dashPort };
1783
2059
  }
1784
2060
  async function runFromSource(opts) {
1785
- 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"));
1786
2062
  console.log(
1787
- 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")
1788
2064
  );
1789
2065
  let src;
1790
2066
  try {
1791
2067
  src = await prepareFromSource({ ref: opts.ref, source: opts.source, repo: opts.repo });
1792
2068
  } catch (e) {
1793
2069
  console.error(
1794
- chalk6.red(`
2070
+ chalk7.red(`
1795
2071
  Build from source failed: ${e.message}
1796
- `) + 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")
1797
2073
  );
1798
2074
  process.exit(1);
1799
2075
  }
1800
- console.log(chalk6.green(`
2076
+ console.log(chalk7.green(`
1801
2077
  Built ${src.ref} (${src.sha}). Starting\u2026
1802
2078
  `));
1803
2079
  await runForeground(opts, src);
1804
2080
  }
1805
2081
  async function startService(opts, runOpts = {}) {
1806
2082
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1807
- if (opts.dryRun) {
1808
- const p = preview({
1809
- port: opts.port,
1810
- dataDir: opts.dataDir,
1811
- dashboardPort: opts.dashboardPort,
1812
- ui: opts.ui,
1813
- uiVersion: opts.uiVersion,
1814
- publicUrl,
1815
- trustProxy: opts.trustProxy || opts.managedEdge,
1816
- host: opts.host,
1817
- managedEdge: opts.managedEdge,
1818
- acmeEmail: opts.acmeEmail
1819
- });
1820
- console.log(
1821
- chalk6.dim(`
1822
- service manager: ${p.kind}
1823
- path: ${p.path}
1824
-
1825
- `) + p.content + "\n"
1826
- );
1827
- return {
1828
- port: String(opts.port || "4000"),
1829
- dashPort: String(opts.dashboardPort || "3001"),
1830
- publicUrl
1831
- };
1832
- }
1833
2083
  const resolved = await resolvePorts({
1834
2084
  api: opts.port ? Number(opts.port) : void 0,
1835
2085
  dashboard: opts.dashboardPort ? Number(opts.dashboardPort) : void 0
@@ -1852,28 +2102,26 @@ async function startService(opts, runOpts = {}) {
1852
2102
  try {
1853
2103
  const res = installAndStart(flags);
1854
2104
  if (!runOpts.quiet) {
1855
- if (resolved.switched.api || resolved.switched.dashboard) {
1856
- console.log(
1857
- chalk6.yellow(`
1858
- A preferred port was busy \u2014 using API ${port}, dashboard ${dashPort}.`)
1859
- );
2105
+ const portNotice = portMoveNotice(resolved, [publicUrl]);
2106
+ if (portNotice.length) {
2107
+ console.log("\n" + portNotice.map((l) => chalk7.yellow(` ${l}`)).join("\n"));
1860
2108
  }
1861
- const dashboardLine = publicUrl ? chalk6.dim(` Dashboard: ${publicUrl} (login required)
1862
- `) : 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)
1863
2111
  `);
1864
2112
  console.log(
1865
- 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
1866
- `)) + chalk6.dim(` ${res.detail}
1867
- `) + 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")
1868
2116
  );
1869
2117
  }
1870
2118
  return { port, dashPort, publicUrl };
1871
2119
  } catch (e) {
1872
2120
  if (runOpts.quiet) throw e;
1873
2121
  console.error(
1874
- chalk6.red(`
2122
+ chalk7.red(`
1875
2123
  Couldn't install the service: ${e.message}
1876
- `) + 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")
1877
2125
  );
1878
2126
  process.exit(1);
1879
2127
  }
@@ -1890,7 +2138,7 @@ async function runForeground(opts, source) {
1890
2138
  const serverEntry = join8(SERVER_DIR, "index.js");
1891
2139
  if (!existsSync7(serverEntry)) {
1892
2140
  console.error(
1893
- 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")
1894
2142
  );
1895
2143
  process.exit(1);
1896
2144
  }
@@ -1904,18 +2152,31 @@ async function runForeground(opts, source) {
1904
2152
  const dashPort = String(resolved.dashboard);
1905
2153
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1906
2154
  const managedEdge = Boolean(opts.managedEdge && publicUrl);
1907
- const dataDir = opts.dataDir || join8(OS_DIR, "data");
2155
+ const dataDir = opts.dataDir || DATA_DIR;
1908
2156
  mkdirSync8(dataDir, { recursive: true });
1909
- const logDir = join8(OS_DIR, "logs");
1910
- mkdirSync8(logDir, { recursive: true });
1911
- const instanceLogPath = join8(logDir, "instance.log");
2157
+ mkdirSync8(LOG_DIR, { recursive: true });
2158
+ const instanceLogPath = join8(LOG_DIR, "instance.log");
1912
2159
  const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
1913
2160
  const env = {
1914
2161
  ...process.env,
1915
2162
  PORT: port,
1916
2163
  NODE_ENV: "production",
1917
- // desktop mode in-process job runner (no Redis).
1918
- DEPLOY_MODE: "desktop",
2164
+ // BARE, not "desktop". This box is a server-host: openship runs ON it and it
2165
+ // is itself a deploy target. Claiming "desktop" (once done here just to get
2166
+ // the in-process job runner) made the API report `isServerHost: false`
2167
+ // (health.routes.ts), so `registerSelfServerReconcile` — gated on
2168
+ // modes:["selfhosted"] — never ran, "This Server" was never registered, and
2169
+ // the deploy wizard offered Openship Cloud as the only target on a box the
2170
+ // operator had just installed on purpose.
2171
+ //
2172
+ // It also mislabelled the security posture: DEPLOY_MODE=desktop relaxes the
2173
+ // zero-auth gate and makes INTERNAL_TOKEN optional. "desktop" belongs to
2174
+ // Electron alone (apps/desktop/src/main/services.ts:361) — the only launcher
2175
+ // that is genuinely a single-user loopback app.
2176
+ //
2177
+ // The job runner is unaffected: it picks BullMQ vs in-process by REDIS
2178
+ // REACHABILITY (app.ts:238), and OPENSHIP_JOB_RUNNER below already pins it.
2179
+ DEPLOY_MODE: "bare",
1919
2180
  OPENSHIP_TARGET: "local",
1920
2181
  OPENSHIP_JOB_RUNNER: "in-process",
1921
2182
  PGLITE_DATA_DIR: dataDir,
@@ -2017,7 +2278,7 @@ async function runForeground(opts, source) {
2017
2278
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
2018
2279
  try {
2019
2280
  const bundle = await ensureDashboard({
2020
- tag: source ? "local" : opts.uiVersion || `v${"0.4.8"}`,
2281
+ tag: source ? "local" : opts.uiVersion || `v${"0.6.0"}`,
2021
2282
  onProgress: (received, total) => {
2022
2283
  if (total) {
2023
2284
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -2092,7 +2353,7 @@ async function runForeground(opts, source) {
2092
2353
  } catch (e) {
2093
2354
  uiSpinner.warn(`Dashboard unavailable: ${e.message}`);
2094
2355
  console.log(
2095
- chalk6.dim(
2356
+ chalk7.dim(
2096
2357
  " The API is still running. Retry `openship up`, pass --no-ui, or use `openship install` for the desktop app.\n"
2097
2358
  )
2098
2359
  );
@@ -2100,16 +2361,16 @@ async function runForeground(opts, source) {
2100
2361
  }
2101
2362
  if (publicUrl) {
2102
2363
  console.log(
2103
- (dashboardUrl ? chalk6.dim(` Dashboard: ${dashboardUrl} (login required)
2104
- `) : "") + chalk6.dim(" API is proxied through the dashboard (bound to loopback, not exposed).\n") + chalk6.dim(` Data: ${dataDir}
2105
- `) + (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"))
2106
2367
  );
2107
2368
  } else {
2108
2369
  console.log(
2109
- chalk6.dim(` API: http://localhost:${port}/api
2110
- `) + (dashboardUrl ? chalk6.dim(` Dashboard: ${dashboardUrl} (login required)
2111
- `) : "") + chalk6.dim(` Data: ${dataDir}
2112
- `) + 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")
2113
2374
  );
2114
2375
  }
2115
2376
  child.stdout.off("data", buffer);
@@ -2123,20 +2384,20 @@ async function runForeground(opts, source) {
2123
2384
 
2124
2385
  // src/commands/stop.ts
2125
2386
  import { Command as Command5 } from "commander";
2126
- import chalk7 from "chalk";
2387
+ import chalk8 from "chalk";
2127
2388
  var stopCommand = new Command5("stop").description("Stop Openship (started by `openship up`) \u2014 it won't restart or return on reboot").action(() => {
2128
2389
  if (readInstallMethod() === "compose") {
2129
2390
  const ok3 = composeDown();
2130
- if (ok3) console.log(chalk7.green("\n \u2714 Openship stopped (docker compose down).\n"));
2131
- 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"));
2132
2393
  process.exit(ok3 ? 0 : 1);
2133
2394
  }
2134
2395
  try {
2135
2396
  const res = stop();
2136
- 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}
2137
2398
  `));
2138
2399
  } catch (e) {
2139
- console.error(chalk7.red(`
2400
+ console.error(chalk8.red(`
2140
2401
  Couldn't stop the service: ${e.message}
2141
2402
  `));
2142
2403
  process.exit(1);
@@ -2145,7 +2406,7 @@ var stopCommand = new Command5("stop").description("Stop Openship (started by `o
2145
2406
 
2146
2407
  // src/commands/uninstall.ts
2147
2408
  import { Command as Command6 } from "commander";
2148
- import chalk8 from "chalk";
2409
+ import chalk9 from "chalk";
2149
2410
  import { confirm, isCancel as isCancel2 } from "@clack/prompts";
2150
2411
  import { existsSync as existsSync8, rmSync as rmSync3 } from "fs";
2151
2412
  var uninstallCommand = new Command6("uninstall").description(
@@ -2161,52 +2422,52 @@ var uninstallCommand = new Command6("uninstall").description(
2161
2422
  !opts.keepImages && method === "compose" ? " \u2022 delete the Openship container images" : null,
2162
2423
  " \u2022 leave your DEPLOYED apps and their data untouched"
2163
2424
  ].filter(Boolean);
2164
- 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");
2165
2426
  const go = await confirm({
2166
2427
  message: destructive ? "Permanently remove Openship and its data?" : "Remove Openship (keeping data)?",
2167
2428
  initialValue: false
2168
2429
  });
2169
2430
  if (isCancel2(go) || !go) {
2170
- console.log(chalk8.dim("\n Cancelled \u2014 nothing was changed.\n"));
2431
+ console.log(chalk9.dim("\n Cancelled \u2014 nothing was changed.\n"));
2171
2432
  return;
2172
2433
  }
2173
2434
  }
2174
2435
  const restored = await rollbackHostEdge().catch(() => false);
2175
- 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."));
2176
2437
  if (method === "compose") {
2177
2438
  const { ok: ok3, removedImages } = composeUninstall({ removeImages: !opts.keepImages });
2178
2439
  if (!ok3) {
2179
2440
  console.log(
2180
- 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.")
2181
2442
  );
2182
2443
  } else {
2183
2444
  console.log(
2184
- chalk8.green(` Stack removed${destructive ? " (including its volumes)" : ""}.`)
2445
+ chalk9.green(` Stack removed${destructive ? " (including its volumes)" : ""}.`)
2185
2446
  );
2186
2447
  }
2187
- 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}`));
2188
2449
  } else {
2189
2450
  try {
2190
2451
  const res = stop();
2191
- console.log(chalk8.green(` Service removed \u2014 ${res.detail}`));
2452
+ console.log(chalk9.green(` Service removed \u2014 ${res.detail}`));
2192
2453
  } catch (err2) {
2193
- console.log(chalk8.yellow(` Couldn't remove the service: ${err2.message}`));
2454
+ console.log(chalk9.yellow(` Couldn't remove the service: ${err2.message}`));
2194
2455
  }
2195
2456
  }
2196
2457
  if (destructive && existsSync8(OS_DIR)) {
2197
2458
  try {
2198
2459
  rmSync3(OS_DIR, { recursive: true, force: true });
2199
- console.log(chalk8.green(` Removed ${OS_DIR}`));
2460
+ console.log(chalk9.green(` Removed ${OS_DIR}`));
2200
2461
  } catch (err2) {
2201
- console.log(chalk8.yellow(` Couldn't remove ${OS_DIR}: ${err2.message}`));
2462
+ console.log(chalk9.yellow(` Couldn't remove ${OS_DIR}: ${err2.message}`));
2202
2463
  }
2203
2464
  }
2204
2465
  console.log(
2205
- 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
2206
2467
  // what to restart — say so rather than leaving :80/:443 quietly dark.
2207
- (restored ? "" : chalk8.dim(
2468
+ (restored ? "" : chalk9.dim(
2208
2469
  " If Openship took over :80/:443 from another proxy, re-enable it:\n sudo systemctl enable --now nginx (or caddy / apache2)\n"
2209
- )) + 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")
2210
2471
  );
2211
2472
  });
2212
2473
 
@@ -2415,36 +2676,27 @@ var contextCommand = new Command9("context").alias("ctx").description("Manage co
2415
2676
 
2416
2677
  // src/commands/status.ts
2417
2678
  import { Command as Command10 } from "commander";
2418
- import chalk9 from "chalk";
2419
- import { readFileSync as readFileSync7 } from "fs";
2420
- import { join as join11 } from "path";
2421
- function readPorts() {
2422
- try {
2423
- return JSON.parse(readFileSync7(join11(OS_DIR, "ports.json"), "utf8"));
2424
- } catch {
2425
- return {};
2426
- }
2427
- }
2679
+ import chalk10 from "chalk";
2428
2680
  var statusCommand = new Command10("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
2429
2681
  const context = getActiveContext();
2430
2682
  const apiUrl = getApiUrl2();
2431
2683
  if (readInstallMethod() === "compose" && !isJsonMode()) {
2432
- console.log(chalk9.bold("\n Openship status (Docker Compose)\n"));
2684
+ console.log(chalk10.bold("\n Openship status (Docker Compose)\n"));
2433
2685
  composePs();
2434
2686
  try {
2435
2687
  const h = await apiRequest("/health", { signal: AbortSignal.timeout(8e3) });
2436
- console.log(chalk9.dim(`
2437
- API: ${apiUrl} \u2014 `) + chalk9.green(h.status ?? "ok") + "\n");
2688
+ console.log(chalk10.dim(`
2689
+ API: ${apiUrl} \u2014 `) + chalk10.green(h.status ?? "ok") + "\n");
2438
2690
  } catch (e) {
2439
2691
  const msg = e instanceof ApiError ? e.message : e.message;
2440
- console.log(chalk9.dim(`
2441
- 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}
2442
2694
  `));
2443
2695
  }
2444
2696
  return;
2445
2697
  }
2446
2698
  const svc = serviceStatus();
2447
- const ports = readPorts();
2699
+ const ports = readStoredPorts();
2448
2700
  let health = null;
2449
2701
  let envInfo = null;
2450
2702
  let reachable = true;
@@ -2460,15 +2712,15 @@ var statusCommand = new Command10("status").description("Show the local Openship
2460
2712
  printJson({ context, apiUrl, service: svc, ports, reachable, health, env: envInfo });
2461
2713
  process.exit(reachable ? 0 : 1);
2462
2714
  }
2463
- 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("-")}
2464
2716
  `;
2465
- const serviceState = svc.running ? chalk9.green("running") : svc.installed ? chalk9.yellow("installed \xB7 stopped") : chalk9.dim("not installed");
2466
- 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);
2467
2719
  if (reachable && health && envInfo) {
2468
- 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) : "");
2469
2721
  } else {
2470
- out += row2("Health", chalk9.red("not reachable")) + chalk9.dim(` ${unreachableMsg}
2471
- `) + 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");
2472
2724
  }
2473
2725
  process.stdout.write(out + "\n");
2474
2726
  if (!reachable) process.exit(1);
@@ -2478,15 +2730,15 @@ var statusCommand = new Command10("status").description("Show the local Openship
2478
2730
  import { Command as Command11 } from "commander";
2479
2731
  import { existsSync as existsSync13 } from "fs";
2480
2732
  import { execFileSync as execFileSync2 } from "child_process";
2481
- import chalk11 from "chalk";
2733
+ import chalk12 from "chalk";
2482
2734
  import open from "open";
2483
2735
  import { intro, outro, note as note2, select as select3, log as log3, spinner as spinner2, confirm as confirm3 } from "@clack/prompts";
2484
2736
 
2485
2737
  // src/lib/repair.ts
2486
2738
  import { spawnSync as spawnSync4 } from "child_process";
2487
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2488
- import { join as join13 } from "path";
2489
- 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";
2490
2742
  import { spinner, log as log2, select as select2, isCancel as isCancel3, cancel } from "@clack/prompts";
2491
2743
 
2492
2744
  // src/lib/heal.ts
@@ -2499,15 +2751,15 @@ import {
2499
2751
  copyFileSync,
2500
2752
  renameSync
2501
2753
  } from "fs";
2502
- import { homedir as homedir2 } from "os";
2503
- import { join as join12 } from "path";
2754
+ import { homedir as homedir3 } from "os";
2755
+ import { join as join11 } from "path";
2504
2756
  import { execFileSync } from "child_process";
2505
2757
  function resolveDataDir() {
2506
2758
  const fromEnv = process.env.PGLITE_DATA_DIR?.trim();
2507
2759
  if (fromEnv) {
2508
- return fromEnv.startsWith("~") ? join12(homedir2(), fromEnv.slice(1).replace(/^[/\\]/, "")) : fromEnv;
2760
+ return fromEnv.startsWith("~") ? join11(homedir3(), fromEnv.slice(1).replace(/^[/\\]/, "")) : fromEnv;
2509
2761
  }
2510
- return join12(OS_DIR, "data");
2762
+ return DATA_DIR;
2511
2763
  }
2512
2764
  function dataDirExists(dataDir = resolveDataDir()) {
2513
2765
  return existsSync11(dataDir);
@@ -2518,8 +2770,8 @@ function stamp() {
2518
2770
  function copyDirRecursive(src, dest) {
2519
2771
  if (!existsSync11(dest)) mkdirSync10(dest, { recursive: true });
2520
2772
  for (const entry of readdirSync(src)) {
2521
- const s = join12(src, entry);
2522
- const d = join12(dest, entry);
2773
+ const s = join11(src, entry);
2774
+ const d = join11(dest, entry);
2523
2775
  if (statSync(s).isDirectory()) copyDirRecursive(s, d);
2524
2776
  else copyFileSync(s, d);
2525
2777
  }
@@ -2531,17 +2783,17 @@ function backupDataDir(dataDir = resolveDataDir()) {
2531
2783
  }
2532
2784
  function healDataDir(dataDir = resolveDataDir()) {
2533
2785
  const result = { removedPostmasterPid: false, trimmedWalSegment: null };
2534
- const pidPath = join12(dataDir, "postmaster.pid");
2786
+ const pidPath = join11(dataDir, "postmaster.pid");
2535
2787
  if (existsSync11(pidPath)) {
2536
2788
  unlinkSync(pidPath);
2537
2789
  result.removedPostmasterPid = true;
2538
2790
  }
2539
- const walDir = join12(dataDir, "pg_wal");
2791
+ const walDir = join11(dataDir, "pg_wal");
2540
2792
  if (existsSync11(walDir)) {
2541
- const segments = readdirSync(walDir).filter((name) => /^[0-9A-F]{24}$/.test(name)).map((name) => ({ name, mtime: statSync(join12(walDir, name)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
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);
2542
2794
  if (segments.length > 0) {
2543
2795
  const newest = segments[0];
2544
- unlinkSync(join12(walDir, newest.name));
2796
+ unlinkSync(join11(walDir, newest.name));
2545
2797
  result.trimmedWalSegment = newest.name;
2546
2798
  }
2547
2799
  }
@@ -2587,7 +2839,6 @@ function deepHeal(dataDir = resolveDataDir()) {
2587
2839
  }
2588
2840
 
2589
2841
  // src/lib/repair.ts
2590
- var LOG_DIR2 = join13(OS_DIR, "logs");
2591
2842
  function ensure(value) {
2592
2843
  if (isCancel3(value)) {
2593
2844
  cancel("Cancelled.");
@@ -2595,23 +2846,9 @@ function ensure(value) {
2595
2846
  }
2596
2847
  return value;
2597
2848
  }
2598
- function storedPorts() {
2599
- try {
2600
- const p = join13(OS_DIR, "ports.json");
2601
- return existsSync12(p) ? JSON.parse(readFileSync8(p, "utf8")) : {};
2602
- } catch {
2603
- return {};
2604
- }
2605
- }
2606
- function apiPort() {
2607
- return storedPorts().api ?? 4e3;
2608
- }
2609
- function dashboardPort() {
2610
- return storedPorts().dashboard ?? 3001;
2611
- }
2612
2849
  async function internalGet2(path2, timeoutMs = 8e3) {
2613
2850
  try {
2614
- const res = await fetch(`http://127.0.0.1:${apiPort()}${path2}`, {
2851
+ const res = await fetch(`http://127.0.0.1:${storedApiPort()}${path2}`, {
2615
2852
  headers: { "X-Internal-Token": ensureInternalToken() },
2616
2853
  signal: AbortSignal.timeout(timeoutMs)
2617
2854
  });
@@ -2623,7 +2860,7 @@ async function internalGet2(path2, timeoutMs = 8e3) {
2623
2860
  }
2624
2861
  async function apiReachable() {
2625
2862
  try {
2626
- const res = await fetch(`http://127.0.0.1:${apiPort()}/api/health`, {
2863
+ const res = await fetch(`http://127.0.0.1:${storedApiPort()}/api/health`, {
2627
2864
  signal: AbortSignal.timeout(2500)
2628
2865
  });
2629
2866
  return res.ok;
@@ -2647,10 +2884,10 @@ async function waitApiDown(seconds = 20) {
2647
2884
  }
2648
2885
  function lastServiceError() {
2649
2886
  for (const name of ["up.err.log", "up.log", "instance.log"]) {
2650
- const p = join13(LOG_DIR2, name);
2887
+ const p = join12(LOG_DIR, name);
2651
2888
  if (!existsSync12(p)) continue;
2652
2889
  try {
2653
- const lines = readFileSync8(p, "utf8").trim().split("\n");
2890
+ const lines = readFileSync7(p, "utf8").trim().split("\n");
2654
2891
  const hit = [...lines].reverse().find((l) => /error|locked|aborted|malformed|corrupt|throw|cannot|EADDRINUSE/i.test(l));
2655
2892
  if (hit) return hit.trim().slice(0, 240);
2656
2893
  } catch {
@@ -2708,11 +2945,11 @@ async function componentChecks(apiUp) {
2708
2945
  checks.push({
2709
2946
  name: "API",
2710
2947
  state: apiUp ? "pass" : "fail",
2711
- detail: apiUp ? `reachable on :${apiPort()}` : `not answering on :${apiPort()}`
2948
+ detail: apiUp ? `reachable on :${storedApiPort()}` : `not answering on :${storedApiPort()}`
2712
2949
  });
2713
2950
  let dashUp = false;
2714
2951
  try {
2715
- const res = await fetch(`http://127.0.0.1:${dashboardPort()}/`, {
2952
+ const res = await fetch(`http://127.0.0.1:${storedDashboardPort()}/`, {
2716
2953
  redirect: "manual",
2717
2954
  signal: AbortSignal.timeout(2500)
2718
2955
  });
@@ -2722,7 +2959,7 @@ async function componentChecks(apiUp) {
2722
2959
  checks.push({
2723
2960
  name: "Dashboard",
2724
2961
  state: dashUp ? "pass" : "warn",
2725
- detail: dashUp ? `serving on :${dashboardPort()}` : `not serving on :${dashboardPort()}`
2962
+ detail: dashUp ? `serving on :${storedDashboardPort()}` : `not serving on :${storedDashboardPort()}`
2726
2963
  });
2727
2964
  const edge = dockerNameRunning("openship-edge");
2728
2965
  checks.push(
@@ -2765,7 +3002,7 @@ async function runRepair() {
2765
3002
  bk.start("Backing up the database");
2766
3003
  try {
2767
3004
  backupDir = backupDataDir();
2768
- bk.stop(`Backup saved \u2192 ${chalk10.dim(backupDir)}`);
3005
+ bk.stop(`Backup saved \u2192 ${chalk11.dim(backupDir)}`);
2769
3006
  } catch (err2) {
2770
3007
  bk.stop("Backup failed \u2014 aborting to protect your data.", 1);
2771
3008
  await bringUp();
@@ -2781,7 +3018,7 @@ async function runRepair() {
2781
3018
  up.start("Restarting and verifying");
2782
3019
  let healthy = await bringUp();
2783
3020
  let verified = healthy && !!(await internalGet2("/api/system/health"))?.db?.ok;
2784
- 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);
2785
3022
  if (verified) {
2786
3023
  return { healed: true, backupDir, detail: "In-place repair succeeded \u2014 your data is intact." };
2787
3024
  }
@@ -2829,7 +3066,7 @@ async function runRepair() {
2829
3066
  const f = spinner();
2830
3067
  f.start("Starting a fresh database");
2831
3068
  const aside = freshStart();
2832
- f.stop(`Corrupt data parked \u2192 ${chalk10.dim(aside)}`);
3069
+ f.stop(`Corrupt data parked \u2192 ${chalk11.dim(aside)}`);
2833
3070
  const ok3 = await bringUp();
2834
3071
  return {
2835
3072
  healed: ok3,
@@ -2842,9 +3079,9 @@ async function runRepair() {
2842
3079
 
2843
3080
  // src/commands/doctor.ts
2844
3081
  var GLYPH = {
2845
- pass: chalk11.green("\u2713"),
2846
- warn: chalk11.yellow("!"),
2847
- fail: chalk11.red("\u2717")
3082
+ pass: chalk12.green("\u2713"),
3083
+ warn: chalk12.yellow("!"),
3084
+ fail: chalk12.red("\u2717")
2848
3085
  };
2849
3086
  function isLocalContext() {
2850
3087
  return /localhost|127\.0\.0\.1|\[::1\]/i.test(getApiUrl());
@@ -2880,7 +3117,7 @@ function servicesSummary(s) {
2880
3117
  };
2881
3118
  }
2882
3119
  function row(name, c) {
2883
- 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)}`;
2884
3121
  }
2885
3122
  function statusPanel(s) {
2886
3123
  const lines = [
@@ -2892,10 +3129,10 @@ function statusPanel(s) {
2892
3129
  if (s.services.length > 0) {
2893
3130
  for (const svc of s.services.slice(0, 8)) {
2894
3131
  const g = svc.state === "running" ? GLYPH.pass : svc.state === "failed" ? GLYPH.fail : GLYPH.warn;
2895
- lines.push(chalk11.dim(` ${g} ${svc.name} \u2014 ${svc.state}`));
3132
+ lines.push(chalk12.dim(` ${g} ${svc.name} \u2014 ${svc.state}`));
2896
3133
  }
2897
3134
  }
2898
- 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}`));
2899
3136
  return lines.join("\n");
2900
3137
  }
2901
3138
  function bunVersion() {
@@ -2929,8 +3166,8 @@ async function remotePreflight() {
2929
3166
  if (isJsonMode()) {
2930
3167
  printJson({ context, apiUrl, reachable, checks });
2931
3168
  } else {
2932
- process.stdout.write(chalk11.bold("\n Openship doctor\n\n"));
2933
- 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}
2934
3171
  `);
2935
3172
  process.stdout.write("\n");
2936
3173
  }
@@ -2954,16 +3191,16 @@ async function reportOnce() {
2954
3191
  lastError: s.lastError
2955
3192
  });
2956
3193
  } else {
2957
- process.stdout.write(chalk11.bold("\n Openship doctor\n\n"));
2958
- 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}
2959
3196
  `);
2960
- 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}
2961
3198
  `);
2962
- 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}
2963
3200
  `);
2964
- 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}
2965
3202
  `);
2966
- if (s.corrupted) process.stdout.write(chalk11.yellow(`
3203
+ if (s.corrupted) process.stdout.write(chalk12.yellow(`
2967
3204
  Database looks corrupted \u2014 run \`openship doctor\` (interactive) or \`openship doctor --fix\`.
2968
3205
  `));
2969
3206
  process.stdout.write("\n");
@@ -2975,11 +3212,11 @@ async function autoFix() {
2975
3212
  const out = (m) => process.stdout.write(` ${m}
2976
3213
  `);
2977
3214
  if (!dataDirExists()) {
2978
- 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.`));
2979
3216
  return;
2980
3217
  }
2981
3218
  if ((await internalGet2("/api/system/health"))?.db?.ok) {
2982
- out(chalk11.green("Database is healthy \u2014 nothing to fix."));
3219
+ out(chalk12.green("Database is healthy \u2014 nothing to fix."));
2983
3220
  return;
2984
3221
  }
2985
3222
  out("Stopping the service\u2026");
@@ -2994,17 +3231,17 @@ async function autoFix() {
2994
3231
  const healthy = await waitApiHealthy(60);
2995
3232
  const verified = healthy && !!(await internalGet2("/api/system/health"))?.db?.ok;
2996
3233
  if (verified) {
2997
- out(chalk11.green("Database is healthy again. Your data is intact."));
3234
+ out(chalk12.green("Database is healthy again. Your data is intact."));
2998
3235
  } else {
2999
- 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.`));
3000
3237
  process.exit(1);
3001
3238
  }
3002
3239
  }
3003
3240
  async function interactiveDoctor() {
3004
- intro(`${chalk11.bgCyan(chalk11.black(" Openship "))}${chalk11.dim(" doctor")}`);
3241
+ intro(`${chalk12.bgCyan(chalk12.black(" Openship "))}${chalk12.dim(" doctor")}`);
3005
3242
  for (; ; ) {
3006
3243
  const s = await gatherStatus();
3007
- note2(statusPanel(s), s.corrupted ? chalk11.red("Database looks corrupted") : "Health");
3244
+ note2(statusPanel(s), s.corrupted ? chalk12.red("Database looks corrupted") : "Health");
3008
3245
  const options = [];
3009
3246
  if (s.corrupted || !s.apiUp && dataDirExists() || s.health?.db && !s.health.db.ok) {
3010
3247
  options.push({ value: "repair", label: "Repair database", hint: "backup \u2192 heal \u2192 verify" });
@@ -3018,7 +3255,7 @@ async function interactiveDoctor() {
3018
3255
  await select3({ message: "What would you like to do?", options, initialValue: options[0]?.value })
3019
3256
  );
3020
3257
  if (action2 === "quit") {
3021
- outro(chalk11.dim("Bye."));
3258
+ outro(chalk12.dim("Bye."));
3022
3259
  return;
3023
3260
  }
3024
3261
  if (action2 === "recheck") continue;
@@ -3037,20 +3274,20 @@ async function interactiveDoctor() {
3037
3274
  sp.start("Restarting");
3038
3275
  const r = restart();
3039
3276
  const up = r.restarted ? await waitApiHealthy(45) : false;
3040
- 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);
3041
3278
  continue;
3042
3279
  }
3043
3280
  if (action2 === "logs") {
3044
3281
  const err2 = lastServiceError();
3045
- 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");
3046
3283
  continue;
3047
3284
  }
3048
3285
  if (action2 === "open") {
3049
3286
  const url = readInstanceUrl();
3050
- const target = url && !/^https?:\/\/localhost/i.test(url) ? url : `http://localhost:${dashboardPort()}`;
3287
+ const target = url && !/^https?:\/\/localhost/i.test(url) ? url : `http://localhost:${storedDashboardPort()}`;
3051
3288
  await open(target).catch(() => {
3052
3289
  });
3053
- log3.message(chalk11.dim(`Opening ${target}`));
3290
+ log3.message(chalk12.dim(`Opening ${target}`));
3054
3291
  continue;
3055
3292
  }
3056
3293
  }
@@ -3077,14 +3314,14 @@ import { execFileSync as execFileSync4 } from "child_process";
3077
3314
  import ora2 from "ora";
3078
3315
 
3079
3316
  // src/lib/project-link.ts
3080
- import { readFileSync as readFileSync9, existsSync as existsSync14 } from "fs";
3081
- import { join as join14, dirname as dirname3, parse } from "path";
3082
- var LINK_REL = join14(".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");
3083
3320
  function findProjectLinkPath(from = process.cwd()) {
3084
3321
  let dir = from;
3085
3322
  const root = parse(dir).root;
3086
3323
  for (; ; ) {
3087
- const candidate = join14(dir, LINK_REL);
3324
+ const candidate = join13(dir, LINK_REL);
3088
3325
  if (existsSync14(candidate)) return candidate;
3089
3326
  if (dir === root) return null;
3090
3327
  dir = dirname3(dir);
@@ -3094,7 +3331,7 @@ function readProjectLink(from) {
3094
3331
  const path2 = findProjectLinkPath(from);
3095
3332
  if (!path2) return null;
3096
3333
  try {
3097
- return JSON.parse(readFileSync9(path2, "utf8"));
3334
+ return JSON.parse(readFileSync8(path2, "utf8"));
3098
3335
  } catch {
3099
3336
  return null;
3100
3337
  }
@@ -3102,21 +3339,21 @@ function readProjectLink(from) {
3102
3339
 
3103
3340
  // src/lib/folder-deploy.ts
3104
3341
  import { execFileSync as execFileSync3 } from "child_process";
3105
- import { readFileSync as readFileSync10, existsSync as existsSync15, rmSync as rmSync4 } from "fs";
3342
+ import { readFileSync as readFileSync9, existsSync as existsSync15, rmSync as rmSync4 } from "fs";
3106
3343
  import { tmpdir } from "os";
3107
- import { join as join15, basename } from "path";
3344
+ import { join as join14, basename } from "path";
3108
3345
  function detectPackageManager(dir) {
3109
- if (existsSync15(join15(dir, "bun.lockb")) || existsSync15(join15(dir, "bun.lock"))) return "bun";
3110
- if (existsSync15(join15(dir, "pnpm-lock.yaml"))) return "pnpm";
3111
- if (existsSync15(join15(dir, "yarn.lock"))) return "yarn";
3112
- if (existsSync15(join15(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";
3113
3350
  return void 0;
3114
3351
  }
3115
3352
  function detectStack(dir) {
3116
- if (existsSync15(join15(dir, "go.mod"))) return "go";
3117
- if (existsSync15(join15(dir, "Cargo.toml"))) return "rust";
3118
- if (existsSync15(join15(dir, "requirements.txt")) || existsSync15(join15(dir, "pyproject.toml"))) return "python";
3119
- if (existsSync15(join15(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";
3120
3357
  return void 0;
3121
3358
  }
3122
3359
  async function deployFolder(opts) {
@@ -3133,7 +3370,7 @@ async function deployFolder(opts) {
3133
3370
  throw new Error(session.error || "Failed to open upload session");
3134
3371
  }
3135
3372
  step("Packaging folder");
3136
- const tarball = join15(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3373
+ const tarball = join14(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3137
3374
  execFileSync3(
3138
3375
  "tar",
3139
3376
  [
@@ -3152,7 +3389,7 @@ async function deployFolder(opts) {
3152
3389
  );
3153
3390
  step("Uploading source");
3154
3391
  try {
3155
- const body = readFileSync10(tarball);
3392
+ const body = readFileSync9(tarball);
3156
3393
  const up = session.upload;
3157
3394
  const method = up.method || "POST";
3158
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 });
@@ -3478,7 +3715,7 @@ function shortSha2(v) {
3478
3715
  async function confirm4(question) {
3479
3716
  if (!process.stdin.isTTY) return true;
3480
3717
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
3481
- 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));
3482
3719
  rl.close();
3483
3720
  return /^y(es)?$/i.test(answer.trim());
3484
3721
  }
@@ -3606,12 +3843,19 @@ var sslStatus = new Command13("status").description("Check SSL certificate statu
3606
3843
  printJson(res);
3607
3844
  })
3608
3845
  );
3609
- var sslRenew = new Command13("renew").description("Renew (issue) an SSL certificate for a domain").argument("<domain>", "Domain to renew").option("--www", "Also include the www subdomain").action(
3846
+ var sslRenew = new Command13("renew").description("Renew (issue) an SSL certificate for a domain").argument("<domain>", "Domain to renew").option("--www", "Also renew the www subdomain (separately \u2014 its own certificate)").action(
3610
3847
  run2(async (domain, opts) => {
3611
3848
  const res = await apiRequest("/deployments/ssl/renew", {
3612
3849
  method: "POST",
3613
3850
  body: JSON.stringify({ domain, includeWww: opts.www === true })
3614
3851
  });
3852
+ if (!isJsonMode() && res.results && res.results.length > 1) {
3853
+ for (const r of res.results) {
3854
+ if (r.success) ok(`${r.domain}: ${r.status}`);
3855
+ else err(`${r.domain}: ${r.message ?? r.status}`);
3856
+ }
3857
+ return;
3858
+ }
3615
3859
  report(res, `SSL renewal requested for ${domain}`);
3616
3860
  })
3617
3861
  );
@@ -3676,7 +3920,7 @@ var logsCommand = new Command14("logs").description("View or stream a deployment
3676
3920
 
3677
3921
  // src/commands/project.ts
3678
3922
  import { Command as Command15 } from "commander";
3679
- import chalk12 from "chalk";
3923
+ import chalk13 from "chalk";
3680
3924
  import { createInterface as createInterface4 } from "readline/promises";
3681
3925
  import { stdin as input3, stdout as output3 } from "process";
3682
3926
  function action(fn) {
@@ -3707,7 +3951,7 @@ function printProject(project) {
3707
3951
  ];
3708
3952
  for (const [k, v] of fields) {
3709
3953
  if (v === null || v === void 0 || v === "") continue;
3710
- process.stdout.write(` ${chalk12.dim(k.padEnd(12))} ${String(v)}
3954
+ process.stdout.write(` ${chalk13.dim(k.padEnd(12))} ${String(v)}
3711
3955
  `);
3712
3956
  }
3713
3957
  }
@@ -3764,7 +4008,7 @@ var deleteCmd = new Command15("delete").alias("rm").description("Delete a projec
3764
4008
  if (!opts.yes) {
3765
4009
  const rl = createInterface4({ input: input3, output: output3 });
3766
4010
  const answer = await rl.question(
3767
- chalk12.yellow(` Delete project ${id}? This cannot be undone. `) + "(y/N) "
4011
+ chalk13.yellow(` Delete project ${id}? This cannot be undone. `) + "(y/N) "
3768
4012
  );
3769
4013
  rl.close();
3770
4014
  if (answer.trim().toLowerCase() !== "y") {
@@ -4107,9 +4351,9 @@ function safeParse(s) {
4107
4351
  }
4108
4352
  }
4109
4353
  function printLogEntry(entry) {
4110
- const ts = entry.timestamp ? chalk12.dim(String(entry.timestamp)) : "";
4354
+ const ts = entry.timestamp ? chalk13.dim(String(entry.timestamp)) : "";
4111
4355
  const level = String(entry.level ?? "info");
4112
- 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;
4113
4357
  const msg = entry.message ?? entry.data ?? "";
4114
4358
  process.stdout.write(` ${ts} ${color(level.padEnd(5))} ${String(msg)}
4115
4359
  `);
@@ -4131,7 +4375,7 @@ projectCommand.addCommand(serverLogsCmd);
4131
4375
 
4132
4376
  // src/commands/service.ts
4133
4377
  import { Command as Command16 } from "commander";
4134
- import chalk13 from "chalk";
4378
+ import chalk14 from "chalk";
4135
4379
  import { spawnSync as spawnSync5 } from "child_process";
4136
4380
  import path from "path";
4137
4381
  import { createInterface as createInterface5 } from "readline/promises";
@@ -4144,7 +4388,7 @@ function requireAuth() {
4144
4388
  }
4145
4389
  function fail(e) {
4146
4390
  if (e instanceof ApiError) {
4147
- err(` ${e.message}` + (e.status ? chalk13.dim(` (HTTP ${e.status})`) : ""));
4391
+ err(` ${e.message}` + (e.status ? chalk14.dim(` (HTTP ${e.status})`) : ""));
4148
4392
  } else {
4149
4393
  err(` ${e instanceof Error ? e.message : String(e)}`);
4150
4394
  }
@@ -4236,7 +4480,7 @@ var listCmd3 = stackCommand("list").description("List the services in a stack").
4236
4480
  image: s.image ?? "\u2014",
4237
4481
  enabled: s.enabled ? "yes" : "no",
4238
4482
  exposed: s.exposed ? "yes" : "no",
4239
- drift: s.drift ? chalk13.yellow("pending") : "\u2014"
4483
+ drift: s.drift ? chalk14.yellow("pending") : "\u2014"
4240
4484
  })),
4241
4485
  ["name", "kind", "image", "enabled", "exposed", "drift"]
4242
4486
  );
@@ -4391,8 +4635,10 @@ function mapComposeService(name, def, baseDir) {
4391
4635
  const volumes = mapVolumes(d.volumes);
4392
4636
  if (volumes.length) svc.volumes = volumes;
4393
4637
  const command = d.command;
4394
- if (typeof command === "string") svc.command = command;
4395
- else if (Array.isArray(command)) svc.command = command.map(String).join(" ");
4638
+ if (command != null) {
4639
+ svc.commandArgv = commandToArgv(command);
4640
+ svc.command = typeof command === "string" ? command : command.map(String).join(" ");
4641
+ }
4396
4642
  if (typeof d.restart === "string") svc.restart = d.restart;
4397
4643
  return svc;
4398
4644
  }
@@ -4604,8 +4850,8 @@ function printLogEntry2(entry) {
4604
4850
  process.stdout.write(JSON.stringify(entry) + "\n");
4605
4851
  return;
4606
4852
  }
4607
- const ts = entry.timestamp ? chalk13.dim(entry.timestamp) : "";
4608
- 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;
4609
4855
  process.stdout.write(`${ts ? ts + " " : ""}${line}
4610
4856
  `);
4611
4857
  }
@@ -4671,14 +4917,14 @@ serviceCommand.addCommand(execCmd);
4671
4917
 
4672
4918
  // src/commands/domain.ts
4673
4919
  import { Command as Command17 } from "commander";
4674
- import chalk14 from "chalk";
4920
+ import chalk15 from "chalk";
4675
4921
  import ora3 from "ora";
4676
4922
  function spin(text2) {
4677
4923
  return isJsonMode() ? null : ora3(text2).start();
4678
4924
  }
4679
4925
  function fail2(e) {
4680
4926
  if (e instanceof ApiError) {
4681
- err(` ${e.message}${e.status ? chalk14.dim(` (${e.status})`) : ""}`);
4927
+ err(` ${e.message}${e.status ? chalk15.dim(` (${e.status})`) : ""}`);
4682
4928
  } else {
4683
4929
  err(` ${e instanceof Error ? e.message : String(e)}`);
4684
4930
  }
@@ -4862,7 +5108,7 @@ var domainCommand = new Command17("domain").description("Manage custom domains,
4862
5108
 
4863
5109
  // src/commands/server.ts
4864
5110
  import { Command as Command18 } from "commander";
4865
- import chalk15 from "chalk";
5111
+ import chalk16 from "chalk";
4866
5112
  import ora4 from "ora";
4867
5113
  var INSTALLABLE = ["docker", "git", "edge", "rsync"];
4868
5114
  function guard(fn) {
@@ -5037,8 +5283,8 @@ server.command("install <serverId>").description("Install components on a server
5037
5283
  printJson({ event: ev.event, ...payload });
5038
5284
  } else if (ev.event === "log") {
5039
5285
  const p = payload;
5040
- const line = ` ${chalk15.dim(`[${p.component}]`)} ${p.message ?? ""}`;
5041
- 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");
5042
5288
  } else if (ev.event === "progress") {
5043
5289
  const p = payload;
5044
5290
  if (p.component) info(` ${p.component}: ${p.status}`);
@@ -5158,7 +5404,7 @@ var serverCommand = server;
5158
5404
  // src/commands/system.ts
5159
5405
  import { Command as Command19 } from "commander";
5160
5406
  import ora5 from "ora";
5161
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
5407
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
5162
5408
  import { createInterface as createInterface6 } from "readline/promises";
5163
5409
  import { stdin as input5, stdout as output5 } from "process";
5164
5410
  async function guarded(fn) {
@@ -5484,7 +5730,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
5484
5730
  }
5485
5731
  let file;
5486
5732
  try {
5487
- file = JSON.parse(readFileSync11(opts.file, "utf8"));
5733
+ file = JSON.parse(readFileSync10(opts.file, "utf8"));
5488
5734
  } catch {
5489
5735
  err(`
5490
5736
  Could not read or parse ${opts.file}.
@@ -5524,8 +5770,11 @@ var systemCommand = new Command19("system").description("Instance settings, onbo
5524
5770
 
5525
5771
  // src/commands/mail.ts
5526
5772
  import { Command as Command20 } from "commander";
5527
- import chalk16 from "chalk";
5773
+ import chalk17 from "chalk";
5528
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";
5529
5778
  import { createInterface as createInterface7 } from "readline/promises";
5530
5779
  import { stdin as input6, stdout as output6 } from "process";
5531
5780
  function guard2(fn) {
@@ -5681,7 +5930,7 @@ var setupCmd = new Command20("setup").description("Start or resume the mail setu
5681
5930
  break;
5682
5931
  case "log": {
5683
5932
  const line = ` ${String(p.message ?? "")}`;
5684
- 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");
5685
5934
  break;
5686
5935
  }
5687
5936
  case "step_done":
@@ -5830,7 +6079,61 @@ postmasterCmd.addCommand(
5830
6079
  })
5831
6080
  )
5832
6081
  );
5833
- 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);
5834
6137
 
5835
6138
  // src/commands/backup.ts
5836
6139
  import { Command as Command21 } from "commander";
@@ -6082,13 +6385,34 @@ restoreCmd.command("apply").description("Apply a staged restore (destructive)").
6082
6385
  );
6083
6386
  restoreCmd.command("cancel").description("Cancel a staged or in-flight restore").argument("<restoreId>", "Restore ID").action(
6084
6387
  (restoreId) => guard3(async () => {
6085
- await apiRequest(
6086
- `/backup-restores/${encodeURIComponent(restoreId)}/cancel`,
6087
- { method: "POST", body: JSON.stringify({}) }
6088
- );
6089
- ok(`
6090
- 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)" : ""}
6091
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.
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
+ }
6092
6416
  })
6093
6417
  );
6094
6418
  restoreCmd.command("get").description("Show one restore (optionally stream it)").argument("<restoreId>", "Restore ID").option("--follow", "Stream progress to completion").action(
@@ -6183,17 +6507,17 @@ var backupCommand = new Command21("backup").description("Manage backups: policie
6183
6507
 
6184
6508
  // src/commands/token.ts
6185
6509
  import { Command as Command22 } from "commander";
6186
- import chalk18 from "chalk";
6510
+ import chalk19 from "chalk";
6187
6511
 
6188
6512
  // src/lib/cmd-helpers.ts
6189
- import chalk17 from "chalk";
6513
+ import chalk18 from "chalk";
6190
6514
  import ora8 from "ora";
6191
6515
  function spin3(text2) {
6192
6516
  return isJsonMode() ? null : ora8(text2).start();
6193
6517
  }
6194
6518
  function fail3(e) {
6195
6519
  if (e instanceof ApiError) {
6196
- err(` ${e.message}${e.status ? chalk17.dim(` (${e.status})`) : ""}`);
6520
+ err(` ${e.message}${e.status ? chalk18.dim(` (${e.status})`) : ""}`);
6197
6521
  } else {
6198
6522
  err(` ${e instanceof Error ? e.message : String(e)}`);
6199
6523
  }
@@ -6240,13 +6564,28 @@ var createCmd3 = new Command22("create").description("Mint a new personal access
6240
6564
  "Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
6241
6565
  collectGrant,
6242
6566
  []
6567
+ ).option(
6568
+ "--full-access",
6569
+ "Mint an UNSCOPED token with your own full access (required when no --grant is given)",
6570
+ false
6243
6571
  ).action(async (name, opts) => {
6244
6572
  const grants = opts.grant ?? [];
6573
+ if (grants.length === 0 && !opts.fullAccess) {
6574
+ fail3(
6575
+ new Error(
6576
+ "Refusing to mint an unscoped token by default. Pass --grant <type:id:perms> to scope it, or --full-access to deliberately create one with your own full access."
6577
+ )
6578
+ );
6579
+ }
6580
+ if (grants.length > 0 && opts.fullAccess) {
6581
+ fail3(new Error("--grant and --full-access are mutually exclusive: pick one."));
6582
+ }
6245
6583
  const body = {
6246
6584
  name,
6247
6585
  readOnly: opts.readOnly || void 0,
6248
6586
  expiresInDays: Number.isFinite(opts.expires) ? opts.expires : void 0,
6249
- grants: grants.length > 0 ? grants : void 0
6587
+ grants: grants.length > 0 ? grants : void 0,
6588
+ fullAccess: grants.length === 0 ? true : void 0
6250
6589
  };
6251
6590
  const sp = spin3(`Creating token "${name}"\u2026`);
6252
6591
  try {
@@ -6260,7 +6599,7 @@ var createCmd3 = new Command22("create").description("Mint a new personal access
6260
6599
  return;
6261
6600
  }
6262
6601
  info(" Copy this token now \u2014 it will not be shown again:");
6263
- process.stdout.write(chalk18.cyan(` ${res.data.token}
6602
+ process.stdout.write(chalk19.cyan(` ${res.data.token}
6264
6603
  `));
6265
6604
  } catch (e) {
6266
6605
  sp?.fail("Create failed");
@@ -6318,19 +6657,10 @@ var apiCommand = new Command23("api").description("Make an authenticated request
6318
6657
 
6319
6658
  // src/commands/reset-admin.ts
6320
6659
  import { Command as Command24 } from "commander";
6321
- import chalk19 from "chalk";
6660
+ import chalk20 from "chalk";
6322
6661
  import { intro as intro2, outro as outro2, password as passwordPrompt, isCancel as isCancel4, cancel as cancel3, log as log4 } from "@clack/prompts";
6323
- import { readFileSync as readFileSync13 } from "fs";
6324
- import { join as join16 } from "path";
6325
- function resolvedApiPort() {
6326
- try {
6327
- return JSON.parse(readFileSync13(join16(OS_DIR, "ports.json"), "utf8")).api;
6328
- } catch {
6329
- return void 0;
6330
- }
6331
- }
6332
- 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 resolved port from ~/.openship/ports.json, 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) => {
6333
- intro2(chalk19.cyan("Reset Openship admin password"));
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) => {
6663
+ intro2(chalk20.cyan("Reset Openship admin password"));
6334
6664
  let pw = opts.password;
6335
6665
  if (!pw) {
6336
6666
  if (!process.stdin.isTTY) {
@@ -6355,7 +6685,7 @@ var resetAdminCommand = new Command24("reset-admin-password").description("Reset
6355
6685
  }
6356
6686
  pw = entered;
6357
6687
  }
6358
- const port = String(opts.port || resolvedApiPort() || 4e3);
6688
+ const port = String(opts.port || storedApiPort());
6359
6689
  let res;
6360
6690
  try {
6361
6691
  res = await fetch(`http://127.0.0.1:${port}/api/system/reset-admin-password`, {
@@ -6373,15 +6703,15 @@ var resetAdminCommand = new Command24("reset-admin-password").description("Reset
6373
6703
  log4.error(`Reset failed: ${data.error || res.statusText}`);
6374
6704
  process.exit(1);
6375
6705
  }
6376
- 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.`));
6377
6707
  });
6378
6708
 
6379
6709
  // src/commands/install.ts
6380
6710
  import { Command as Command25 } from "commander";
6381
- import { chmodSync as chmodSync2, existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11 } from "fs";
6382
- import { spawn as spawn2, spawnSync as spawnSync6 } from "child_process";
6383
- import { homedir as homedir3 } from "os";
6384
- import { join as join17 } 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";
6385
6715
  import ora9 from "ora";
6386
6716
  function assetForPlatform() {
6387
6717
  const { platform, arch } = process;
@@ -6395,33 +6725,33 @@ function assetForPlatform() {
6395
6725
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
6396
6726
  }
6397
6727
  function installDmg(dmg) {
6398
- const homeApps = join17(homedir3(), "Applications");
6728
+ const homeApps = join16(homedir4(), "Applications");
6399
6729
  let dest = homeApps;
6400
6730
  try {
6401
6731
  mkdirSync11(homeApps, { recursive: true });
6402
6732
  } catch {
6403
6733
  dest = "/Applications";
6404
6734
  }
6405
- const attach = spawnSync6("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6735
+ const attach = spawnSync7("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6406
6736
  encoding: "utf8"
6407
6737
  });
6408
6738
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
6409
6739
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
6410
6740
  if (!mount) throw new Error("Could not determine the mounted volume");
6411
- let target = join17(dest, "Openship.app");
6741
+ let target = join16(dest, "Openship.app");
6412
6742
  try {
6413
- const appInDmg = join17(mount, "Openship.app");
6414
- if (!existsSync16(appInDmg)) throw new Error("Openship.app not found in the disk image");
6415
- spawnSync6("rm", ["-rf", target]);
6416
- 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" });
6417
6747
  if (copy.status !== 0 && dest === homeApps) {
6418
- target = join17("/Applications", "Openship.app");
6419
- spawnSync6("rm", ["-rf", target]);
6420
- 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" });
6421
6751
  }
6422
6752
  if (copy.status !== 0) throw new Error(`ditto copy failed: ${copy.stderr?.trim()}`);
6423
6753
  } finally {
6424
- spawnSync6("hdiutil", ["detach", mount, "-quiet"]);
6754
+ spawnSync7("hdiutil", ["detach", mount, "-quiet"]);
6425
6755
  }
6426
6756
  return target;
6427
6757
  }
@@ -6430,10 +6760,10 @@ function installAppImage(appImage) {
6430
6760
  return appImage;
6431
6761
  }
6432
6762
  function installZip(zip) {
6433
- const localAppData = process.env.LOCALAPPDATA || join17(homedir3(), "AppData", "Local");
6434
- const target = join17(localAppData, "Programs", "Openship");
6763
+ const localAppData = process.env.LOCALAPPDATA || join16(homedir4(), "AppData", "Local");
6764
+ const target = join16(localAppData, "Programs", "Openship");
6435
6765
  mkdirSync11(target, { recursive: true });
6436
- const expand = spawnSync6(
6766
+ const expand = spawnSync7(
6437
6767
  "powershell",
6438
6768
  [
6439
6769
  "-NoProfile",
@@ -6448,7 +6778,7 @@ function installZip(zip) {
6448
6778
  }
6449
6779
  function launch(kind, target) {
6450
6780
  if (kind === "dmg") {
6451
- spawnSync6("open", [target]);
6781
+ spawnSync7("open", [target]);
6452
6782
  return;
6453
6783
  }
6454
6784
  if (kind === "appimage") {
@@ -6459,9 +6789,9 @@ function launch(kind, target) {
6459
6789
  child.unref();
6460
6790
  return;
6461
6791
  }
6462
- const exe = join17(target, "Openship.exe");
6463
- const path2 = existsSync16(exe) ? exe : target;
6464
- 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]);
6465
6795
  }
6466
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) => {
6467
6797
  let asset;
@@ -6486,13 +6816,13 @@ var installCommand = new Command25("install").description("Download and install
6486
6816
  process.exit(1);
6487
6817
  }
6488
6818
  const dir = releaseDir(tag);
6489
- const assetPath = join17(dir, asset.name);
6819
+ const assetPath = join16(dir, asset.name);
6490
6820
  const sidecarPath = `${assetPath}.sha256`;
6491
6821
  const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
6492
6822
  const sidecarUrl = `${assetUrl2}.sha256`;
6493
6823
  let downloaded = false;
6494
6824
  let sha;
6495
- const cachedUsable = !opts.force && existsSync16(assetPath) && (existsSync16(sidecarPath) || opts.verify === false);
6825
+ const cachedUsable = !opts.force && existsSync17(assetPath) && (existsSync17(sidecarPath) || opts.verify === false);
6496
6826
  if (cachedUsable) {
6497
6827
  info(` Using cached ${asset.name} (${tag}).`);
6498
6828
  } else {
@@ -6514,8 +6844,8 @@ var installCommand = new Command25("install").description("Download and install
6514
6844
  const s2 = spin4("Verifying checksum\u2026");
6515
6845
  try {
6516
6846
  let sidecarBody;
6517
- if (existsSync16(sidecarPath) && !downloaded) {
6518
- sidecarBody = readFileSync14(sidecarPath, "utf8");
6847
+ if (existsSync17(sidecarPath) && !downloaded) {
6848
+ sidecarBody = readFileSync13(sidecarPath, "utf8");
6519
6849
  } else {
6520
6850
  sidecarBody = await fetchSidecar(sidecarUrl);
6521
6851
  }
@@ -6579,7 +6909,222 @@ var installCommand = new Command25("install").description("Download and install
6579
6909
 
6580
6910
  // src/commands/update.ts
6581
6911
  import { Command as Command26 } from "commander";
6582
- import { spawnSync as spawnSync7 } from "child_process";
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/commands/update.ts
6583
7128
  async function runSourceUpdate(source, opts) {
6584
7129
  const current = shortSha(source.dir);
6585
7130
  const remote = remoteSha(source.repo, source.ref);
@@ -6618,15 +7163,81 @@ async function runSourceUpdate(source, opts) {
6618
7163
  ok(`Rebuilt from source (${source.ref} @ ${sha}). Restart to run it: openship up`);
6619
7164
  }
6620
7165
  }
7166
+ var REINSTALL_HINT = "curl -fsSL https://get.openship.io | sh";
7167
+ async function runTarballUpdate(install, opts) {
7168
+ const current = "0.6.0";
7169
+ let latest;
7170
+ try {
7171
+ latest = (await resolveLatestTag()).replace(/^v/, "");
7172
+ } catch {
7173
+ err(`Could not reach GitHub to check for updates. Try again, or reinstall: ${REINSTALL_HINT}`);
7174
+ process.exitCode = 1;
7175
+ return;
7176
+ }
7177
+ const plan = resolveCliUpdatePlan(current, latest);
7178
+ if (opts.check) {
7179
+ if (isJsonMode()) {
7180
+ printJson({ current, latest, updateAvailable: plan.action === "install", method: "tarball" });
7181
+ } else if (plan.action === "install") {
7182
+ info(`Update available: v${current} \u2192 v${latest}. Run \`openship update\`.`);
7183
+ } else {
7184
+ ok(`Up to date (v${current}).`);
7185
+ }
7186
+ return;
7187
+ }
7188
+ if (plan.action === "up-to-date") {
7189
+ ok(`Already on the latest version (v${current}).`);
7190
+ return;
7191
+ }
7192
+ const tag = `v${latest}`;
7193
+ info(`Updating v${current} \u2192 v${latest} (verified payload)\u2026`);
7194
+ let runtime = install.runtime;
7195
+ try {
7196
+ runtime = (await ensureNodeRuntime((m) => info(m))).source;
7197
+ } catch (e) {
7198
+ info(`Keeping the current Node runtime (couldn't refresh it: ${e.message}).`);
7199
+ }
7200
+ try {
7201
+ await ensureCliPayload(tag);
7202
+ if (process.platform !== "win32") writeLauncher();
7203
+ } catch (e) {
7204
+ err(`Update failed: ${e.message} Reinstall: ${REINSTALL_HINT}`);
7205
+ process.exitCode = 1;
7206
+ return;
7207
+ }
7208
+ writeCliInstall({ ...install, tag, runtime });
7209
+ if (readInstallMethod() === "compose") {
7210
+ const applied = await composeUpdate(latest);
7211
+ if (isJsonMode()) {
7212
+ printJson({ updated: true, from: current, to: latest, method: "compose", applied });
7213
+ } else if (applied) {
7214
+ ok(`Updated to v${latest} \u2014 compose stack regenerated, images pulled, services recreated.`);
7215
+ } else {
7216
+ err(`Updated the CLI to v${latest}, but bringing the compose stack up failed. Re-run \`openship update\`, or \`openship up\` to retry.`);
7217
+ process.exitCode = 1;
7218
+ }
7219
+ return;
7220
+ }
7221
+ const { restarted } = restart();
7222
+ if (isJsonMode()) {
7223
+ printJson({ updated: true, from: current, to: latest, method: "tarball", restarted });
7224
+ } else if (restarted) {
7225
+ ok(`Updated to v${latest} and restarted the service \u2014 you're on the new version.`);
7226
+ } else {
7227
+ ok(`Updated to v${latest}. Restart the server to run the new version: openship up`);
7228
+ }
7229
+ }
6621
7230
  function detectPackageManager2(override) {
6622
7231
  if (override === "bun" || override === "npm") return override;
6623
- const hasBun = spawnSync7("bun", ["--version"], { stdio: "ignore" }).status === 0;
7232
+ const hasBun = spawnSync10("bun", ["--version"], { stdio: "ignore" }).status === 0;
6624
7233
  return hasBun ? "bun" : "npm";
6625
7234
  }
6626
7235
  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) => {
6627
7236
  const source = readSourceInstall();
6628
7237
  if (source) return runSourceUpdate(source, opts);
6629
- const current = "0.4.8";
7238
+ const cliInstall = readCliInstall();
7239
+ if (cliInstall?.method === "tarball") return runTarballUpdate(cliInstall, opts);
7240
+ const current = "0.6.0";
6630
7241
  let latest;
6631
7242
  try {
6632
7243
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -6654,7 +7265,7 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
6654
7265
  const ref = `openship@${latest}`;
6655
7266
  const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
6656
7267
  info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
6657
- const res = spawnSync7(pm, argv, { stdio: "inherit", shell: process.platform === "win32" });
7268
+ const res = spawnSync10(pm, argv, { stdio: "inherit", shell: process.platform === "win32" });
6658
7269
  if (res.status !== 0) {
6659
7270
  err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
6660
7271
  process.exitCode = 1;
@@ -6684,17 +7295,17 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
6684
7295
 
6685
7296
  // src/commands/cache.ts
6686
7297
  import { Command as Command27 } from "commander";
6687
- import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync15, rmSync as rmSync5, statSync as statSync2 } from "fs";
6688
- import { join as join18 } from "path";
7298
+ import { existsSync as existsSync20, readdirSync as readdirSync3, readFileSync as readFileSync14, rmSync as rmSync7, statSync as statSync2 } from "fs";
7299
+ import { join as join19 } from "path";
6689
7300
  function listAssets() {
6690
- if (!existsSync17(RELEASES_DIR)) return [];
7301
+ if (!existsSync20(RELEASES_DIR)) return [];
6691
7302
  const out = [];
6692
- for (const tag of readdirSync2(RELEASES_DIR)) {
7303
+ for (const tag of readdirSync3(RELEASES_DIR)) {
6693
7304
  const dir = releaseDir(tag);
6694
7305
  if (!statSync2(dir).isDirectory()) continue;
6695
- for (const name of readdirSync2(dir)) {
7306
+ for (const name of readdirSync3(dir)) {
6696
7307
  if (name.endsWith(".sha256")) continue;
6697
- const path2 = join18(dir, name);
7308
+ const path2 = join19(dir, name);
6698
7309
  const st = statSync2(path2);
6699
7310
  if (!st.isFile()) continue;
6700
7311
  out.push({
@@ -6702,7 +7313,7 @@ function listAssets() {
6702
7313
  name,
6703
7314
  path: path2,
6704
7315
  size: st.size,
6705
- hasSidecar: existsSync17(`${path2}.sha256`)
7316
+ hasSidecar: existsSync20(`${path2}.sha256`)
6706
7317
  });
6707
7318
  }
6708
7319
  }
@@ -6733,7 +7344,7 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
6733
7344
  results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
6734
7345
  continue;
6735
7346
  }
6736
- const expected = parseSha256(readFileSync15(`${a.path}.sha256`, "utf8"));
7347
+ const expected = parseSha256(readFileSync14(`${a.path}.sha256`, "utf8"));
6737
7348
  const actual = await hashFile(a.path);
6738
7349
  const okMatch = expected !== null && expected === actual;
6739
7350
  if (!okMatch) bad += 1;
@@ -6749,12 +7360,12 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
6749
7360
  });
6750
7361
  var cleanCmd = new Command27("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
6751
7362
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
6752
- if (!existsSync17(target)) {
7363
+ if (!existsSync20(target)) {
6753
7364
  if (isJsonMode()) printJson({ removed: false, path: target });
6754
7365
  else info(` Nothing to clean (${target}).`);
6755
7366
  return;
6756
7367
  }
6757
- rmSync5(target, { recursive: true, force: true });
7368
+ rmSync7(target, { recursive: true, force: true });
6758
7369
  if (isJsonMode()) printJson({ removed: true, path: target });
6759
7370
  else ok(`
6760
7371
  Removed ${target}
@@ -6766,11 +7377,11 @@ var cacheCommand = new Command27("cache").description("Manage the local download
6766
7377
  }).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
6767
7378
 
6768
7379
  // src/commands/wizard.ts
6769
- import chalk20 from "chalk";
7380
+ import chalk21 from "chalk";
6770
7381
  import open2 from "open";
6771
7382
  import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
6772
- import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync16, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "fs";
6773
- import { join as join19 } from "path";
7383
+ import { existsSync as existsSync21, mkdirSync as mkdirSync14, rmSync as rmSync8, writeFileSync as writeFileSync14 } from "fs";
7384
+ import { join as join20 } from "path";
6774
7385
  import {
6775
7386
  intro as intro3,
6776
7387
  outro as outro3,
@@ -6811,7 +7422,7 @@ async function connectOpenshipCloud(port, token) {
6811
7422
  const handoff = `${apiBase}/api/cloud/connect-handoff?state=${encodeURIComponent(state)}&code_challenge=${challenge}&mode=device`;
6812
7423
  const overSsh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT);
6813
7424
  log5.step("Open this URL in your browser to authorize, then click Authorize:");
6814
- console.log("\n" + chalk20.cyan.underline(handoff) + "\n");
7425
+ console.log("\n" + chalk21.cyan.underline(handoff) + "\n");
6815
7426
  if (!overSsh) void open2(handoff).catch(() => {
6816
7427
  });
6817
7428
  const s = spinner4();
@@ -6916,16 +7527,16 @@ async function streamProvision(port, sessionId, s) {
6916
7527
  }
6917
7528
  return { ok: ok3, detail };
6918
7529
  }
6919
- var SETUP_LOCK = join19(OS_DIR, "setup-in-progress");
7530
+ var SETUP_LOCK = join20(OS_DIR, "setup-in-progress");
6920
7531
  function isSetupInProgress() {
6921
- return existsSync18(SETUP_LOCK);
7532
+ return existsSync21(SETUP_LOCK);
6922
7533
  }
6923
7534
  function markSetupStarted() {
6924
- mkdirSync12(OS_DIR, { recursive: true });
6925
- writeFileSync12(SETUP_LOCK, "1");
7535
+ mkdirSync14(OS_DIR, { recursive: true });
7536
+ writeFileSync14(SETUP_LOCK, "1");
6926
7537
  }
6927
7538
  function markSetupDone() {
6928
- rmSync6(SETUP_LOCK, { force: true });
7539
+ rmSync8(SETUP_LOCK, { force: true });
6929
7540
  }
6930
7541
  function wizardInputs(admin, plan) {
6931
7542
  switch (plan.type) {
@@ -6942,20 +7553,20 @@ function wizardInputs(admin, plan) {
6942
7553
  function finishSetup(opts) {
6943
7554
  saveInstanceUrl(opts.liveUrl);
6944
7555
  markSetupDone();
6945
- const pad = (label) => chalk20.dim(label.padEnd(11));
6946
- log5.success(chalk20.bold("Openship is live"));
7556
+ const pad = (label) => chalk21.dim(label.padEnd(11));
7557
+ log5.success(chalk21.bold("Openship is live"));
6947
7558
  log5.message(
6948
- `${pad("URL")}${chalk20.bold(opts.liveUrl)}
7559
+ `${pad("URL")}${chalk21.bold(opts.liveUrl)}
6949
7560
  ${pad("Dashboard")}http://localhost:${opts.dashPort}
6950
7561
  ${pad("API")}http://localhost:${opts.apiPort}
6951
- ${pad("Login")}${opts.adminEmail} ${chalk20.dim("(email + password you set)")}
6952
- ` + (opts.cloudEmail ? `${pad("Cloud")}${chalk20.dim("connected as ")}${opts.cloudEmail}${chalk20.dim(" \u2014 free domain + mail only")}
6953
- ` : "") + `${pad("Status")}${chalk20.green("running")} ${chalk20.dim(opts.method === "compose" ? "\xB7 Docker Compose stack (restarts on boot)" : "\xB7 service (restarts on boot)")}`
7562
+ ${pad("Login")}${opts.adminEmail} ${chalk21.dim("(email + password you set)")}
7563
+ ` + (opts.cloudEmail ? `${pad("Cloud")}${chalk21.dim("connected as ")}${opts.cloudEmail}${chalk21.dim(" \u2014 free domain + mail only")}
7564
+ ` : "") + `${pad("Status")}${chalk21.green("running")} ${chalk21.dim(opts.method === "compose" ? "\xB7 Docker Compose stack (restarts on boot)" : "\xB7 service (restarts on boot)")}`
6954
7565
  );
6955
7566
  log5.message(
6956
- 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.")}`)
7567
+ 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.")}`)
6957
7568
  );
6958
- outro3(opts.byo ? chalk20.dim("Point your reverse proxy at the dashboard port above.") : chalk20.green("Happy shipping."));
7569
+ outro3(opts.byo ? chalk21.dim("Point your reverse proxy at the dashboard port above.") : chalk21.green("Happy shipping."));
6959
7570
  }
6960
7571
  async function promptLocalEdgeTakeover(port) {
6961
7572
  const pf = await internalPost(port, "/api/system/self-edge/preflight", {});
@@ -6970,22 +7581,36 @@ async function promptLocalEdgeTakeover(port) {
6970
7581
  renderEdgeConflict({ owner, sites, warnings });
6971
7582
  const action2 = await confirmEdgeAction({ owner, known, importable: sites.length });
6972
7583
  if (action2 === "cancel") return { proceed: false, edgeMigrate: false, edgeTakeover: false };
6973
- return { proceed: true, edgeMigrate: action2 === "migrate", edgeTakeover: action2 === "takeover" };
7584
+ let staticRootOverrides;
7585
+ if (action2 === "migrate") {
7586
+ const unreachable = pf.ok && Array.isArray(pf.data?.unreachableStaticRoots) ? pf.data.unreachableStaticRoots : [];
7587
+ staticRootOverrides = await remediateUnreachableStaticRoots({
7588
+ unreachable,
7589
+ executor: new LocalExecutor(),
7590
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
7591
+ });
7592
+ }
7593
+ return {
7594
+ proceed: true,
7595
+ edgeMigrate: action2 === "migrate",
7596
+ edgeTakeover: action2 === "takeover",
7597
+ ...staticRootOverrides ? { staticRootOverrides } : {}
7598
+ };
6974
7599
  }
6975
7600
  async function runWizard() {
6976
7601
  const resuming = isSetupInProgress();
6977
- intro3(`${chalk20.bgCyan(chalk20.black(" Openship "))}${chalk20.dim(" setup")}`);
7602
+ intro3(`${chalk21.bgCyan(chalk21.black(" Openship "))}${chalk21.dim(" setup")}`);
6978
7603
  if (resuming) {
6979
7604
  log5.warn(
6980
7605
  "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)."
6981
7606
  );
6982
7607
  }
6983
7608
  log5.message(
6984
- chalk20.dim(
7609
+ chalk21.dim(
6985
7610
  "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."
6986
7611
  )
6987
7612
  );
6988
- 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."));
7613
+ 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."));
6989
7614
  const admin = await promptLocalAdmin();
6990
7615
  let cloudEmail = null;
6991
7616
  let publicUrl;
@@ -7004,7 +7629,7 @@ async function runWizard() {
7004
7629
  sp.start("Detecting this server's public IP");
7005
7630
  const detected = await detectPublicIp();
7006
7631
  if (detected) {
7007
- sp.stop(`Public IP: ${chalk20.bold(detected)}`);
7632
+ sp.stop(`Public IP: ${chalk21.bold(detected)}`);
7008
7633
  publicHost = detected;
7009
7634
  return detected;
7010
7635
  }
@@ -7019,7 +7644,7 @@ async function runWizard() {
7019
7644
  return publicHost;
7020
7645
  }
7021
7646
  let stage = "reach";
7022
- log5.message(chalk20.dim("These are just starting choices \u2014 domain, Cloud, team, and the rest are all editable later in Settings."));
7647
+ log5.message(chalk21.dim("These are just starting choices \u2014 domain, Cloud, team, and the rest are all editable later in Settings."));
7023
7648
  planning: while (true) {
7024
7649
  if (stage === "reach") {
7025
7650
  const reach = ensure2(
@@ -7075,11 +7700,11 @@ async function runWizard() {
7075
7700
  ).trim().toLowerCase();
7076
7701
  const host = await resolvePublicHost();
7077
7702
  note3(
7078
- `${chalk20.cyan(`https://${slug}.opsh.io`)}
7703
+ `${chalk21.cyan(`https://${slug}.opsh.io`)}
7079
7704
 
7080
- ${chalk20.dim("served via")} Openship Cloud edge ${chalk20.dim("\u2192")} ${chalk20.cyan(host)}
7705
+ ${chalk21.dim("served via")} Openship Cloud edge ${chalk21.dim("\u2192")} ${chalk21.cyan(host)}
7081
7706
 
7082
- ` + chalk20.dim("Openship Cloud terminates HTTPS and forwards to this server."),
7707
+ ` + chalk21.dim("Openship Cloud terminates HTTPS and forwards to this server."),
7083
7708
  "Confirm free domain"
7084
7709
  );
7085
7710
  const go2 = ensure2(
@@ -7117,11 +7742,11 @@ async function runWizard() {
7117
7742
  }
7118
7743
  const host = await resolvePublicHost();
7119
7744
  note3(
7120
- `Add a DNS ${chalk20.bold("A record")}:
7745
+ `Add a DNS ${chalk21.bold("A record")}:
7121
7746
 
7122
- ${chalk20.cyan(hostname2)} \u2192 ${chalk20.cyan(host)}
7747
+ ${chalk21.cyan(hostname2)} \u2192 ${chalk21.cyan(host)}
7123
7748
 
7124
- ` + chalk20.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
7749
+ ` + chalk21.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
7125
7750
  "DNS"
7126
7751
  );
7127
7752
  const go2 = ensure2(
@@ -7158,9 +7783,9 @@ async function runWizard() {
7158
7783
  log5.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
7159
7784
  }
7160
7785
  note3(
7161
- `${chalk20.cyan(url)}
7786
+ `${chalk21.cyan(url)}
7162
7787
 
7163
- ` + chalk20.dim("Point your reverse proxy at the dashboard port shown at the end."),
7788
+ ` + chalk21.dim("Point your reverse proxy at the dashboard port shown at the end."),
7164
7789
  "Confirm"
7165
7790
  );
7166
7791
  const go = ensure2(
@@ -7183,17 +7808,22 @@ async function runWizard() {
7183
7808
  }
7184
7809
  let method = "bare";
7185
7810
  if (process.platform === "linux") {
7186
- if (hasDockerCompose()) {
7811
+ const gap = dockerGap();
7812
+ if (!gap) {
7187
7813
  method = "compose";
7814
+ } else if (!gap.installable) {
7815
+ log5.warn(`${gap.summary} \u2014 using the bare process service instead.`);
7816
+ if (gap.hint) log5.info(gap.hint);
7817
+ method = "bare";
7188
7818
  } else {
7189
- log5.step("Docker isn't installed \u2014 installing it now (get.docker.com)\u2026");
7190
- method = await ensureDocker() ? "compose" : "bare";
7819
+ log5.step(`${gap.summary} \u2014 installing via get.docker.com\u2026`);
7820
+ method = await ensureDocker({ onNotice: (line) => log5.info(line) }) ? "compose" : "bare";
7191
7821
  if (method === "bare") {
7192
7822
  log5.warn("Couldn't install Docker automatically \u2014 falling back to the bare process service.");
7193
7823
  }
7194
7824
  }
7195
7825
  }
7196
- const uiTag = `v${"0.4.8"}`;
7826
+ const uiTag = `v${"0.6.0"}`;
7197
7827
  if (method === "bare") {
7198
7828
  const dl = spinner4();
7199
7829
  dl.start("Pulling the Openship dist from GitHub");
@@ -7218,6 +7848,7 @@ async function runWizard() {
7218
7848
  let edgeAction;
7219
7849
  let migratedSites;
7220
7850
  let migratedCertPems;
7851
+ let migratedStaticRootOverrides;
7221
7852
  let allowHostControl = true;
7222
7853
  if (method === "compose") {
7223
7854
  allowHostControl = ensure2(
@@ -7240,20 +7871,27 @@ async function runWizard() {
7240
7871
  ) === "allow";
7241
7872
  if (!allowHostControl) {
7242
7873
  log5.message(
7243
- chalk20.dim(
7874
+ chalk21.dim(
7244
7875
  " 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."
7245
7876
  )
7246
7877
  );
7247
7878
  }
7248
7879
  }
7249
7880
  if (method === "compose") {
7881
+ const ports = await resolveComposePorts({});
7882
+ const apiPort = String(ports.api);
7883
+ const dashboardPort = String(ports.dashboard);
7884
+ const moved = portMoveNotice(ports, composeTrustedOriginUrls());
7885
+ if (moved.length) log5.info(moved.join("\n"));
7250
7886
  log5.step(
7251
7887
  sourceBuildDir() ? "Building the Openship images before touching :80/:443 (first run takes a few minutes)\u2026" : "Pulling images before touching :80/:443\u2026"
7252
7888
  );
7253
7889
  if (!composePrefetch({
7890
+ apiPort,
7891
+ dashboardPort,
7254
7892
  publicUrl,
7255
7893
  trustProxy: behindProxy,
7256
- version: "0.4.8",
7894
+ version: "0.6.0",
7257
7895
  noHostControl: !allowHostControl
7258
7896
  })) {
7259
7897
  cancel4("Couldn't fetch the Openship images \u2014 nothing on this box was changed.");
@@ -7261,19 +7899,24 @@ async function runWizard() {
7261
7899
  }
7262
7900
  const edgePlan = await planAndApplyHostEdge({});
7263
7901
  if (!edgePlan.proceed) {
7264
- cancel4("Left the existing proxy on 80/443 running \u2014 re-run and choose migrate/takeover when ready.");
7265
- process.exit(0);
7902
+ cancel4(
7903
+ 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."
7904
+ );
7905
+ process.exit(edgePlan.blockedBy ? 1 : 0);
7266
7906
  }
7267
7907
  edgeAction = edgePlan.action;
7268
7908
  migratedSites = edgePlan.sites;
7269
7909
  migratedCertPems = edgePlan.certPems;
7910
+ migratedStaticRootOverrides = edgePlan.staticRootOverrides;
7270
7911
  log5.step("Starting the Docker Compose stack\u2026");
7271
7912
  const up = await composeUp({
7272
7913
  // Prefetched above, before the preflight stopped anything.
7273
7914
  alreadyFetched: true,
7915
+ apiPort,
7916
+ dashboardPort,
7274
7917
  publicUrl,
7275
7918
  trustProxy: behindProxy,
7276
- version: "0.4.8",
7919
+ version: "0.6.0",
7277
7920
  noHostControl: !allowHostControl
7278
7921
  });
7279
7922
  if (!up.ok) {
@@ -7322,7 +7965,12 @@ async function runWizard() {
7322
7965
  await waitDashboard(started.dashPort);
7323
7966
  s.stop("Deployed.");
7324
7967
  if (edgeAction === "migrate" && migratedSites?.length) {
7325
- const imported = await importMigratedSites(started.port, migratedSites, migratedCertPems);
7968
+ const imported = await importMigratedSites(
7969
+ started.port,
7970
+ migratedSites,
7971
+ migratedCertPems,
7972
+ migratedStaticRootOverrides
7973
+ );
7326
7974
  if (!imported.ok) {
7327
7975
  log5.warn(
7328
7976
  `Your ${migratedSites.length} existing site${migratedSites.length === 1 ? "" : "s"} aren't served yet \u2014 re-run \`openship up\` to retry the import.`
@@ -7345,7 +7993,7 @@ async function runWizard() {
7345
7993
  inputs: wizardInputs(admin, domainPlan),
7346
7994
  token: provisionToken,
7347
7995
  method: "compose",
7348
- onLog: (msg) => log5.message(chalk20.dim(msg)),
7996
+ onLog: (msg) => log5.message(chalk21.dim(msg)),
7349
7997
  // .opsh.io is a SHARED zone, and Cloud only connects after the stack is up —
7350
7998
  // so a taken subdomain can't be detected at prompt time. Recover here, where
7351
7999
  // we still have a TTY, instead of ending the run with no domain.
@@ -7431,10 +8079,11 @@ async function runWizard() {
7431
8079
  // the operator declined the takeover (their proxy keeps 80/443).
7432
8080
  localEdge: edge.proceed,
7433
8081
  edgeTakeover: edge.edgeTakeover,
7434
- edgeMigrate: edge.edgeMigrate
8082
+ edgeMigrate: edge.edgeMigrate,
8083
+ ...edge.staticRootOverrides ? { staticRootOverrides: edge.staticRootOverrides } : {}
7435
8084
  }
7436
8085
  },
7437
- onLog: (msg) => log5.message(chalk20.dim(msg)),
8086
+ onLog: (msg) => log5.message(chalk21.dim(msg)),
7438
8087
  onSlugTaken: async (taken) => {
7439
8088
  log5.warn(`"${taken}.opsh.io" is already taken.`);
7440
8089
  const next = await text({
@@ -7469,7 +8118,8 @@ async function runWizard() {
7469
8118
  dashPort: Number(started.dashPort),
7470
8119
  acmeEmail: admin?.email,
7471
8120
  edgeTakeover,
7472
- edgeMigrate
8121
+ edgeMigrate,
8122
+ ...edge.staticRootOverrides ? { staticRootOverrides: edge.staticRootOverrides } : {}
7473
8123
  });
7474
8124
  if (res.ok && res.data?.sessionId) {
7475
8125
  const s2 = spinner4();
@@ -7504,36 +8154,28 @@ async function runWizard() {
7504
8154
  byo: domainPlan.type === "byo"
7505
8155
  });
7506
8156
  }
7507
- function storedPorts2() {
7508
- const p = join19(OS_DIR, "ports.json");
7509
- try {
7510
- return existsSync18(p) ? JSON.parse(readFileSync16(p, "utf8")) : {};
7511
- } catch {
7512
- return {};
7513
- }
7514
- }
7515
8157
  async function runControl() {
7516
8158
  const svc = serviceStatus();
7517
8159
  const isCompose = readInstallMethod() === "compose";
7518
8160
  const running = isCompose ? composeRunning() : svc.running;
7519
8161
  const managerLabel = isCompose ? "docker compose" : svc.kind === "unsupported" ? "none" : svc.kind;
7520
- const ports = storedPorts2();
7521
- const apiPort2 = String(ports.api ?? 4e3);
7522
- const dashUrl = `http://localhost:${ports.dashboard ?? 3001}`;
8162
+ const ports = readStoredPorts();
8163
+ const apiPort = String(storedApiPort());
8164
+ const dashUrl = `http://localhost:${storedDashboardPort()}`;
7523
8165
  const publicUrl = readInstanceUrl();
7524
8166
  const primaryUrl = publicUrl && !/^https?:\/\/localhost/i.test(publicUrl) ? publicUrl : dashUrl;
7525
- intro3(`${chalk20.bgCyan(chalk20.black(" Openship "))}${chalk20.dim(" control")}`);
8167
+ intro3(`${chalk21.bgCyan(chalk21.black(" Openship "))}${chalk21.dim(" control")}`);
7526
8168
  note3(
7527
- `${chalk20.dim("URL".padEnd(11))}${chalk20.bold(primaryUrl)}
7528
- ${chalk20.dim("Service".padEnd(11))}${running ? chalk20.green("running") : chalk20.yellow("stopped")}
7529
- ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7530
- ` + (ports.api ? `${chalk20.dim("API".padEnd(11))}http://localhost:${ports.api}
7531
- ` : "") + `${chalk20.dim("Manager".padEnd(11))}${managerLabel}`,
8169
+ `${chalk21.dim("URL".padEnd(11))}${chalk21.bold(primaryUrl)}
8170
+ ${chalk21.dim("Service".padEnd(11))}${running ? chalk21.green("running") : chalk21.yellow("stopped")}
8171
+ ${chalk21.dim("Dashboard".padEnd(11))}${dashUrl}
8172
+ ` + (ports.api ? `${chalk21.dim("API".padEnd(11))}http://localhost:${ports.api}
8173
+ ` : "") + `${chalk21.dim("Manager".padEnd(11))}${managerLabel}`,
7532
8174
  "Openship is already set up"
7533
8175
  );
7534
8176
  const corrupted = looksCorrupted();
7535
8177
  if (corrupted) {
7536
- note3(chalk20.red("The service is installed but keeps failing to start \u2014 the database looks corrupted."), "Needs repair");
8178
+ note3(chalk21.red("The service is installed but keeps failing to start \u2014 the database looks corrupted."), "Needs repair");
7537
8179
  }
7538
8180
  const action2 = ensure2(
7539
8181
  await select4({
@@ -7553,18 +8195,18 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7553
8195
  switch (action2) {
7554
8196
  case "repair": {
7555
8197
  const res = await runRepair();
7556
- outro3(res.healed ? chalk20.green(res.detail) : chalk20.yellow(res.detail));
8198
+ outro3(res.healed ? chalk21.green(res.detail) : chalk21.yellow(res.detail));
7557
8199
  return;
7558
8200
  }
7559
8201
  case "open":
7560
8202
  await open2(primaryUrl).catch(() => {
7561
8203
  });
7562
- outro3(chalk20.dim(`Opening ${primaryUrl}`));
8204
+ outro3(chalk21.dim(`Opening ${primaryUrl}`));
7563
8205
  return;
7564
8206
  case "start": {
7565
8207
  if (isCompose) {
7566
8208
  const ok3 = composeStart();
7567
- outro3(ok3 ? chalk20.green("Started.") : chalk20.yellow("Couldn't start the stack \u2014 run `openship up` to see the error."));
8209
+ outro3(ok3 ? chalk21.green("Started.") : chalk21.yellow("Couldn't start the stack \u2014 run `openship up` to see the error."));
7568
8210
  return;
7569
8211
  }
7570
8212
  await startService({});
@@ -7573,30 +8215,30 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7573
8215
  case "restart": {
7574
8216
  if (isCompose) {
7575
8217
  const ok3 = composeRestart();
7576
- outro3(ok3 ? chalk20.green("Restarted.") : chalk20.yellow("Couldn't restart the stack."));
8218
+ outro3(ok3 ? chalk21.green("Restarted.") : chalk21.yellow("Couldn't restart the stack."));
7577
8219
  return;
7578
8220
  }
7579
8221
  const r = restart();
7580
- outro3(r.restarted ? chalk20.green("Restarted.") : chalk20.yellow(r.detail));
8222
+ outro3(r.restarted ? chalk21.green("Restarted.") : chalk21.yellow(r.detail));
7581
8223
  return;
7582
8224
  }
7583
8225
  case "stop": {
7584
8226
  if (isCompose) {
7585
8227
  const ok3 = composeDown();
7586
- outro3(ok3 ? chalk20.green("Stopped. Won't restart on boot.") : chalk20.yellow("Couldn't stop the stack."));
8228
+ outro3(ok3 ? chalk21.green("Stopped. Won't restart on boot.") : chalk21.yellow("Couldn't stop the stack."));
7587
8229
  return;
7588
8230
  }
7589
8231
  const r = stop();
7590
- outro3(chalk20.green(`Stopped. ${chalk20.dim(r.detail)}`));
8232
+ outro3(chalk21.green(`Stopped. ${chalk21.dim(r.detail)}`));
7591
8233
  return;
7592
8234
  }
7593
8235
  case "reset": {
7594
8236
  const pw = ensure2(
7595
8237
  await password({ message: "New admin password", validate: (v) => v && v.length >= 8 ? void 0 : "At least 8 characters" })
7596
8238
  );
7597
- const rr = await internalPost(apiPort2, "/api/system/reset-admin-password", { password: pw });
8239
+ const rr = await internalPost(apiPort, "/api/system/reset-admin-password", { password: pw });
7598
8240
  outro3(
7599
- rr.ok ? chalk20.green(`Password reset. Sign in at ${dashUrl} with your email + new password.`) : chalk20.red(`Couldn't reset: ${rr.data?.error || "failed"}`)
8241
+ rr.ok ? chalk21.green(`Password reset. Sign in at ${dashUrl} with your email + new password.`) : chalk21.red(`Couldn't reset: ${rr.data?.error || "failed"}`)
7600
8242
  );
7601
8243
  return;
7602
8244
  }
@@ -7604,13 +8246,19 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7604
8246
  await runWizard();
7605
8247
  return;
7606
8248
  default:
7607
- outro3(chalk20.dim("Nothing changed."));
8249
+ outro3(chalk21.dim("Nothing changed."));
7608
8250
  }
7609
8251
  }
7610
8252
 
8253
+ // src/commands/completion.ts
8254
+ import tab from "@bomb.sh/tab/commander";
8255
+ function attachCompletion(program2) {
8256
+ tab(program2, { completionCommandName: "completion" });
8257
+ }
8258
+
7611
8259
  // src/index.ts
7612
8260
  var program = new Command28();
7613
- program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.4.8").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
8261
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.6.0").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7614
8262
  if (thisCommand.opts().json) setJsonMode(true);
7615
8263
  }).action(async () => {
7616
8264
  const installed = serviceStatus().installed || readInstallMethod() === "compose";
@@ -7644,4 +8292,5 @@ program.addCommand(tokenCommand);
7644
8292
  program.addCommand(apiCommand);
7645
8293
  program.addCommand(resetAdminCommand);
7646
8294
  installCommand.addCommand(cacheCommand);
8295
+ attachCompletion(program);
7647
8296
  program.parse();