openship 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/dist/{bare-JY5Y5ELU.js → bare-VRGE5MXH.js} +7 -7
  3. package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
  4. package/dist/{chunk-2D3I5G6P.js → chunk-6PIYRG5I.js} +23 -3
  5. package/dist/{chunk-BBHX7T4R.js → chunk-BS2AXYZX.js} +401 -76
  6. package/dist/{chunk-SJIFLLX4.js → chunk-CUJXGK7R.js} +321 -85
  7. package/dist/{chunk-VFINNTUL.js → chunk-EAAGSQXN.js} +23 -8
  8. package/dist/{chunk-4Z5QO7N6.js → chunk-I6PSTQIR.js} +2 -2
  9. package/dist/{chunk-5NYUCONN.js → chunk-JLKKYUZA.js} +31 -49
  10. package/dist/{chunk-XPPZ7B2J.js → chunk-K2RPVGPN.js} +2 -2
  11. package/dist/{chunk-MZ2WEHHY.js → chunk-KNTJ7PSF.js} +1 -1
  12. package/dist/{chunk-L2VOGB2X.js → chunk-KZOBYJUW.js} +1 -1
  13. package/dist/{chunk-GKHUSZCD.js → chunk-NNPEG6HM.js} +1 -1
  14. package/dist/{chunk-SLTYVHU7.js → chunk-NXFIRK2Z.js} +381 -72
  15. package/dist/{chunk-FXTBHTWY.js → chunk-RB35YN3J.js} +271 -35
  16. package/dist/{chunk-6APULWSU.js → chunk-UMQP7SYT.js} +429 -66
  17. package/dist/{chunk-V6AN6XLZ.js → chunk-V7BZMGOT.js} +123 -45
  18. package/dist/{chunk-VDMQJHPI.js → chunk-YWMODKWL.js} +67 -4
  19. package/dist/{chunk-OXWDY22G.js → chunk-ZCOQJCZO.js} +1 -1
  20. package/dist/{cloud-KYWY3RP4.js → cloud-3M2WZ624.js} +12 -12
  21. package/dist/{detect-AZLOHUFX.js → detect-BAJMY6KM.js} +3 -3
  22. package/dist/{docker-LODK5Y2S.js → docker-2NGBRJFJ.js} +9 -7
  23. package/dist/{docker-edge-executor-VHW3JPJS.js → docker-edge-executor-5I4M5QT7.js} +1 -1
  24. package/dist/{edge-import-ABUDKKJI.js → edge-import-RIWNEIXQ.js} +21 -19
  25. package/dist/{ensure-container-edge-SW6HENSG.js → ensure-container-edge-3NSYKHRX.js} +6 -6
  26. package/dist/{executor-XJ6RP4Q2.js → executor-LOYMPU67.js} +3 -3
  27. package/dist/index.js +269 -331
  28. package/dist/{nginx-3L4SIBJA.js → nginx-QE4DOM5J.js} +4 -4
  29. package/dist/{openresty-lua-4WLGDSER.js → openresty-lua-BTAKECCF.js} +2 -2
  30. package/dist/server/index.js +51643 -45322
  31. package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
  32. package/dist/server/migrations/0074_project_compose_path.sql +1 -0
  33. package/dist/server/migrations/0075_service_command_argv.sql +1 -0
  34. package/dist/server/migrations/0076_rollback_retention.sql +17 -0
  35. package/dist/server/migrations/0077_domain_redirect.sql +13 -0
  36. package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
  37. package/dist/server/migrations/0079_project_readiness.sql +18 -0
  38. package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
  39. package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
  40. package/dist/server/migrations/meta/_journal.json +63 -0
  41. package/dist/{setup-NU6T2PDU.js → setup-KELALK7H.js} +7 -7
  42. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
+ DEFAULT_API_PORT,
7
+ DEFAULT_DASHBOARD_PORT,
6
8
  IS_ALT_HOME,
7
9
  OS_DIR,
8
10
  composeDown,
@@ -13,20 +15,30 @@ import {
13
15
  composeRestart,
14
16
  composeRunning,
15
17
  composeStart,
18
+ composeTrustedOriginUrls,
16
19
  composeUninstall,
17
20
  composeUp,
18
21
  composeUpdate,
22
+ dockerGap,
19
23
  ensureDocker,
20
24
  hasDockerCompose,
21
25
  importMigratedSites,
26
+ portMoveNotice,
22
27
  prepareFromSource,
23
28
  readInstallMethod,
29
+ readInstanceUrl,
24
30
  readSourceInstall,
31
+ readStoredPorts,
25
32
  rebuildFromSource,
26
33
  remoteSha,
34
+ resolveComposePorts,
35
+ resolvePorts,
36
+ saveInstanceUrl,
27
37
  shortSha,
28
- sourceBuildDir
29
- } from "./chunk-BBHX7T4R.js";
38
+ sourceBuildDir,
39
+ storedApiPort,
40
+ storedDashboardPort
41
+ } from "./chunk-BS2AXYZX.js";
30
42
  import "./chunk-FRNAR33O.js";
31
43
  import "./chunk-EI575GHN.js";
32
44
  import "./chunk-6APIPIJY.js";
@@ -34,12 +46,12 @@ import "./chunk-IXCHURVK.js";
34
46
  import "./chunk-OWED2GRM.js";
35
47
  import "./chunk-VWPEKOH6.js";
36
48
  import "./chunk-W73X43R7.js";
37
- import "./chunk-TGIINPVM.js";
38
- import "./chunk-5NYUCONN.js";
49
+ import "./chunk-6LKHXB6T.js";
50
+ import "./chunk-JLKKYUZA.js";
39
51
  import "./chunk-AVIB7CT7.js";
40
52
  import "./chunk-5MZ624VX.js";
41
53
  import "./chunk-Q2CIRAOK.js";
42
- import "./chunk-GKHUSZCD.js";
54
+ import "./chunk-NNPEG6HM.js";
43
55
  import {
44
56
  beginEdgeTakeover,
45
57
  collectProxyCerts,
@@ -52,39 +64,40 @@ import {
52
64
  recoverInterruptedTakeover,
53
65
  rollbackEdgeTakeover,
54
66
  scanImportableSites
55
- } from "./chunk-SLTYVHU7.js";
67
+ } from "./chunk-NXFIRK2Z.js";
56
68
  import "./chunk-O6ZZF52V.js";
57
69
  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";
70
+ import "./chunk-CUJXGK7R.js";
71
+ import "./chunk-V7BZMGOT.js";
72
+ import "./chunk-YWMODKWL.js";
73
+ import "./chunk-KNTJ7PSF.js";
74
+ import "./chunk-KZOBYJUW.js";
75
+ import "./chunk-I6PSTQIR.js";
76
+ import "./chunk-K2RPVGPN.js";
77
+ import "./chunk-ZCOQJCZO.js";
66
78
  import "./chunk-RPHRPFEH.js";
67
79
  import {
68
80
  LocalExecutor
69
81
  } from "./chunk-TYVME3XD.js";
70
- import "./chunk-FXTBHTWY.js";
82
+ import "./chunk-RB35YN3J.js";
71
83
  import "./chunk-FPRHYBY2.js";
72
84
  import {
73
85
  edgeCrashReason,
74
86
  edgeIsBroken,
75
87
  ourEdgeContainerRunning
76
- } from "./chunk-2D3I5G6P.js";
88
+ } from "./chunk-6PIYRG5I.js";
77
89
  import "./chunk-ILFETZOM.js";
78
- import "./chunk-VFINNTUL.js";
90
+ import "./chunk-EAAGSQXN.js";
79
91
  import {
80
92
  CLOUD_DASHBOARD_URL,
81
93
  LOCAL_API_URL,
82
94
  LOCAL_DASHBOARD_URL,
83
95
  cliInstallCommand,
96
+ commandToArgv,
84
97
  isValidEmail,
85
98
  parseOpenshipConfigJson,
86
99
  resolveCliUpdatePlan
87
- } from "./chunk-6APULWSU.js";
100
+ } from "./chunk-UMQP7SYT.js";
88
101
  import "./chunk-KI2EY3WB.js";
89
102
 
90
103
  // src/index.ts
@@ -487,8 +500,8 @@ import chalk6 from "chalk";
487
500
  import ora from "ora";
488
501
  import { spawn } from "child_process";
489
502
  import { randomBytes as randomBytes2 } from "crypto";
490
- import { createWriteStream as createWriteStream2, existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
491
- import { dirname as dirname2, join as join8 } from "path";
503
+ import { createWriteStream as createWriteStream2, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync6 } from "fs";
504
+ import { dirname as dirname2, join as join7 } from "path";
492
505
  import { fileURLToPath } from "url";
493
506
 
494
507
  // src/lib/dashboard.ts
@@ -658,7 +671,7 @@ async function fetchBundle(tag, onProgress) {
658
671
 
659
672
  // src/lib/service.ts
660
673
  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";
674
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
662
675
  import { homedir } from "os";
663
676
  import { join as join4, resolve } from "path";
664
677
  var HOME = homedir();
@@ -876,14 +889,9 @@ function serviceStatus() {
876
889
  }
877
890
  function sweepOrphanPorts() {
878
891
  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
- }
892
+ const stored = readStoredPorts();
893
+ const ports = [stored.api, stored.dashboard].filter((n) => typeof n === "number");
894
+ if (ports.length === 0) return;
887
895
  const self = process.pid;
888
896
  for (const port of ports) {
889
897
  const q = run("lsof", ["-ti", `tcp:${port}`]);
@@ -924,97 +932,10 @@ function stop() {
924
932
  return result;
925
933
  }
926
934
 
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
- }
1013
-
1014
935
  // src/lib/edge-preflight.ts
1015
936
  import { spawnSync as spawnSync3 } from "child_process";
1016
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
1017
- import { join as join6 } from "path";
937
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
938
+ import { join as join5 } from "path";
1018
939
  import chalk5 from "chalk";
1019
940
  import { isCancel, log, note, select } from "@clack/prompts";
1020
941
  async function planAndApplyHostEdge(opts, overrides = {}) {
@@ -1053,7 +974,7 @@ async function rollbackHostEdge() {
1053
974
  return false;
1054
975
  }
1055
976
  }
1056
- var IMPORTED_MARKER = join6(OS_DIR, "imported-proxy");
977
+ var IMPORTED_MARKER = join5(OS_DIR, "imported-proxy");
1057
978
  function edgeServedHostnames() {
1058
979
  const served = /* @__PURE__ */ new Set();
1059
980
  const r = spawnSync3(
@@ -1077,13 +998,13 @@ function edgeServedHostnames() {
1077
998
  }
1078
999
  function markStoppedProxyImported() {
1079
1000
  try {
1080
- mkdirSync6(OS_DIR, { recursive: true, mode: 448 });
1081
- writeFileSync5(IMPORTED_MARKER, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
1001
+ mkdirSync5(OS_DIR, { recursive: true, mode: 448 });
1002
+ writeFileSync4(IMPORTED_MARKER, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
1082
1003
  } catch {
1083
1004
  }
1084
1005
  }
1085
1006
  async function offerStoppedProxyImport(executor, deps) {
1086
- if (!deps.interactive || existsSync5(IMPORTED_MARKER)) return { proceed: true };
1007
+ if (!deps.interactive || existsSync4(IMPORTED_MARKER)) return { proceed: true };
1087
1008
  let proxy = null;
1088
1009
  try {
1089
1010
  proxy = await deps.detectInstalledProxy(executor);
@@ -1228,15 +1149,15 @@ function defaultDeps() {
1228
1149
  }
1229
1150
 
1230
1151
  // src/lib/loopback-api.ts
1231
- import { existsSync as existsSync6, readFileSync as readFileSync4, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
1152
+ import { existsSync as existsSync5, readFileSync as readFileSync3, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
1232
1153
  import { randomBytes } from "crypto";
1233
- import { join as join7 } from "path";
1154
+ import { join as join6 } from "path";
1234
1155
  function ensureInternalToken() {
1235
- const path2 = join7(OS_DIR, "internal-token");
1236
- if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
1237
- mkdirSync7(OS_DIR, { recursive: true, mode: 448 });
1156
+ const path2 = join6(OS_DIR, "internal-token");
1157
+ if (existsSync5(path2)) return readFileSync3(path2, "utf8").trim();
1158
+ mkdirSync6(OS_DIR, { recursive: true, mode: 448 });
1238
1159
  const token = randomBytes(32).toString("hex");
1239
- writeFileSync6(path2, token, { mode: 384 });
1160
+ writeFileSync5(path2, token, { mode: 384 });
1240
1161
  return token;
1241
1162
  }
1242
1163
  async function internalGet(port, path2, token) {
@@ -1265,17 +1186,17 @@ async function internalPost(port, path2, body, token) {
1265
1186
  return { ok: false, data: { error: err2.message } };
1266
1187
  }
1267
1188
  }
1268
- async function bootstrapAdmin(apiPort2, admin, token) {
1269
- const { ok: ok3, data } = await internalPost(apiPort2, "/api/system/bootstrap-admin", admin, token);
1189
+ async function bootstrapAdmin(apiPort, admin, token) {
1190
+ const { ok: ok3, data } = await internalPost(apiPort, "/api/system/bootstrap-admin", admin, token);
1270
1191
  if (ok3) return { ok: true };
1271
1192
  if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
1272
1193
  return { ok: false, message: data?.error || "failed" };
1273
1194
  }
1274
- async function waitHealthy(apiPort2, seconds = 90) {
1195
+ async function waitHealthy(apiPort, seconds = 90) {
1275
1196
  for (let i = 0; i < seconds; i++) {
1276
1197
  await new Promise((r) => setTimeout(r, 1e3));
1277
1198
  try {
1278
- await fetch(`http://127.0.0.1:${apiPort2}/api/health`, { signal: AbortSignal.timeout(2e3) });
1199
+ await fetch(`http://127.0.0.1:${apiPort}/api/health`, { signal: AbortSignal.timeout(2e3) });
1279
1200
  return true;
1280
1201
  } catch {
1281
1202
  }
@@ -1556,16 +1477,16 @@ function normalizePublicUrl(raw) {
1556
1477
  return url;
1557
1478
  }
1558
1479
  var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
1559
- var SERVER_DIR = join8(DIST_DIR, "server");
1480
+ var SERVER_DIR = join7(DIST_DIR, "server");
1560
1481
  function ensureAuthSecret() {
1561
- const path2 = join8(OS_DIR, "auth-secret");
1562
- if (existsSync7(path2)) return readFileSync5(path2, "utf8").trim();
1563
- mkdirSync8(OS_DIR, { recursive: true, mode: 448 });
1482
+ const path2 = join7(OS_DIR, "auth-secret");
1483
+ if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
1484
+ mkdirSync7(OS_DIR, { recursive: true, mode: 448 });
1564
1485
  const secret = randomBytes2(32).toString("hex");
1565
- writeFileSync7(path2, secret, { mode: 384 });
1486
+ writeFileSync6(path2, secret, { mode: 384 });
1566
1487
  return secret;
1567
1488
  }
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(
1489
+ var upCommand = new Command4("up").description("Start Openship as a persistent service (boot + auto-restart); --foreground to run attached").option("--port <port>", "API port to listen on (default: 4000, or the next free port if it's taken)").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port (default: 3001, or the next free port if it's taken)").option("--no-ui", "Run the API only \u2014 don't download/serve the dashboard").option("--ui-version <tag>", "Dashboard release tag to run (default: this CLI's version)").option("-f, --foreground", "Run attached in this terminal instead of as a background service").option("--dry-run", "Print the service definition that would be installed, then exit").option(
1569
1490
  "--public-url <url>",
1570
1491
  "Serve remotely at this public URL (VPS): binds the dashboard to all interfaces, proxies the API same-origin, and requires login"
1571
1492
  ).option(
@@ -1685,12 +1606,19 @@ async function runCompose(opts) {
1685
1606
  process.exit(1);
1686
1607
  }
1687
1608
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1609
+ const ports = await resolveComposePorts({ api: opts.port, dashboard: opts.dashboardPort });
1610
+ const apiPort = String(ports.api);
1611
+ const dashboardPort = String(ports.dashboard);
1612
+ const portNotice = portMoveNotice(ports, [...composeTrustedOriginUrls(), publicUrl]);
1613
+ if (portNotice.length) {
1614
+ console.log("\n" + portNotice.map((l) => chalk6.yellow(` ${l}`)).join("\n"));
1615
+ }
1688
1616
  const prefetch = ora(
1689
1617
  sourceBuildDir() ? "Building images before touching :80/:443\u2026" : "Pulling images before touching :80/:443\u2026"
1690
1618
  ).start();
1691
1619
  const fetched = composePrefetch({
1692
- apiPort: opts.port,
1693
- dashboardPort: opts.dashboardPort,
1620
+ apiPort,
1621
+ dashboardPort,
1694
1622
  publicUrl,
1695
1623
  trustProxy: opts.trustProxy,
1696
1624
  noHostControl: opts.hostControl === false ? true : void 0
@@ -1727,8 +1655,8 @@ async function runCompose(opts) {
1727
1655
  const res = await composeUp({
1728
1656
  // Prefetched above, before the preflight stopped anything.
1729
1657
  alreadyFetched: true,
1730
- apiPort: opts.port,
1731
- dashboardPort: opts.dashboardPort,
1658
+ apiPort,
1659
+ dashboardPort,
1732
1660
  publicUrl,
1733
1661
  trustProxy: opts.trustProxy,
1734
1662
  // commander maps `--no-host-control` to hostControl === false. `undefined` when
@@ -1825,8 +1753,8 @@ async function startService(opts, runOpts = {}) {
1825
1753
  `) + p.content + "\n"
1826
1754
  );
1827
1755
  return {
1828
- port: String(opts.port || "4000"),
1829
- dashPort: String(opts.dashboardPort || "3001"),
1756
+ port: String(opts.port || DEFAULT_API_PORT),
1757
+ dashPort: String(opts.dashboardPort || DEFAULT_DASHBOARD_PORT),
1830
1758
  publicUrl
1831
1759
  };
1832
1760
  }
@@ -1852,11 +1780,9 @@ async function startService(opts, runOpts = {}) {
1852
1780
  try {
1853
1781
  const res = installAndStart(flags);
1854
1782
  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
- );
1783
+ const portNotice = portMoveNotice(resolved, [publicUrl]);
1784
+ if (portNotice.length) {
1785
+ console.log("\n" + portNotice.map((l) => chalk6.yellow(` ${l}`)).join("\n"));
1860
1786
  }
1861
1787
  const dashboardLine = publicUrl ? chalk6.dim(` Dashboard: ${publicUrl} (login required)
1862
1788
  `) : chalk6.dim(` Dashboard: http://localhost:${dashPort} (login required)
@@ -1887,8 +1813,8 @@ async function runForeground(opts, source) {
1887
1813
  apiArgs = ["run", "src/index.ts"];
1888
1814
  apiCwd = source.apiDir;
1889
1815
  } else {
1890
- const serverEntry = join8(SERVER_DIR, "index.js");
1891
- if (!existsSync7(serverEntry)) {
1816
+ const serverEntry = join7(SERVER_DIR, "index.js");
1817
+ if (!existsSync6(serverEntry)) {
1892
1818
  console.error(
1893
1819
  chalk6.red("\n Bundled server not found in this install.") + chalk6.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
1894
1820
  );
@@ -1904,28 +1830,42 @@ async function runForeground(opts, source) {
1904
1830
  const dashPort = String(resolved.dashboard);
1905
1831
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1906
1832
  const managedEdge = Boolean(opts.managedEdge && publicUrl);
1907
- const dataDir = opts.dataDir || join8(OS_DIR, "data");
1908
- mkdirSync8(dataDir, { recursive: true });
1909
- const logDir = join8(OS_DIR, "logs");
1910
- mkdirSync8(logDir, { recursive: true });
1911
- const instanceLogPath = join8(logDir, "instance.log");
1833
+ const dataDir = opts.dataDir || join7(OS_DIR, "data");
1834
+ mkdirSync7(dataDir, { recursive: true });
1835
+ const logDir = join7(OS_DIR, "logs");
1836
+ mkdirSync7(logDir, { recursive: true });
1837
+ const instanceLogPath = join7(logDir, "instance.log");
1912
1838
  const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
1913
1839
  const env = {
1914
1840
  ...process.env,
1915
1841
  PORT: port,
1916
1842
  NODE_ENV: "production",
1917
- // desktop mode in-process job runner (no Redis).
1918
- DEPLOY_MODE: "desktop",
1843
+ // BARE, not "desktop". This box is a server-host: openship runs ON it and it
1844
+ // is itself a deploy target. Claiming "desktop" (once done here just to get
1845
+ // the in-process job runner) made the API report `isServerHost: false`
1846
+ // (health.routes.ts), so `registerSelfServerReconcile` — gated on
1847
+ // modes:["selfhosted"] — never ran, "This Server" was never registered, and
1848
+ // the deploy wizard offered Openship Cloud as the only target on a box the
1849
+ // operator had just installed on purpose.
1850
+ //
1851
+ // It also mislabelled the security posture: DEPLOY_MODE=desktop relaxes the
1852
+ // zero-auth gate and makes INTERNAL_TOKEN optional. "desktop" belongs to
1853
+ // Electron alone (apps/desktop/src/main/services.ts:361) — the only launcher
1854
+ // that is genuinely a single-user loopback app.
1855
+ //
1856
+ // The job runner is unaffected: it picks BullMQ vs in-process by REDIS
1857
+ // REACHABILITY (app.ts:238), and OPENSHIP_JOB_RUNNER below already pins it.
1858
+ DEPLOY_MODE: "bare",
1919
1859
  OPENSHIP_TARGET: "local",
1920
1860
  OPENSHIP_JOB_RUNNER: "in-process",
1921
1861
  PGLITE_DATA_DIR: dataDir,
1922
1862
  BETTER_AUTH_SECRET: ensureAuthSecret()
1923
1863
  };
1924
1864
  if (!source) {
1925
- env.OPENSHIP_MIGRATIONS_DIR = join8(SERVER_DIR, "migrations");
1926
- env.OPENSHIP_PGLITE_ASSETS_DIR = join8(SERVER_DIR, "pglite");
1927
- const engineDir = join8(SERVER_DIR, "engine");
1928
- if (existsSync7(engineDir)) env.MAIL_SERVER_ENGINE_DIR = engineDir;
1865
+ env.OPENSHIP_MIGRATIONS_DIR = join7(SERVER_DIR, "migrations");
1866
+ env.OPENSHIP_PGLITE_ASSETS_DIR = join7(SERVER_DIR, "pglite");
1867
+ const engineDir = join7(SERVER_DIR, "engine");
1868
+ if (existsSync6(engineDir)) env.MAIL_SERVER_ENGINE_DIR = engineDir;
1929
1869
  }
1930
1870
  env.OPENSHIP_REQUIRE_AUTH = "true";
1931
1871
  env.INTERNAL_TOKEN = ensureInternalToken();
@@ -2017,7 +1957,7 @@ async function runForeground(opts, source) {
2017
1957
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
2018
1958
  try {
2019
1959
  const bundle = await ensureDashboard({
2020
- tag: source ? "local" : opts.uiVersion || `v${"0.4.8"}`,
1960
+ tag: source ? "local" : opts.uiVersion || `v${"0.5.0"}`,
2021
1961
  onProgress: (received, total) => {
2022
1962
  if (total) {
2023
1963
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -2147,7 +2087,7 @@ var stopCommand = new Command5("stop").description("Stop Openship (started by `o
2147
2087
  import { Command as Command6 } from "commander";
2148
2088
  import chalk8 from "chalk";
2149
2089
  import { confirm, isCancel as isCancel2 } from "@clack/prompts";
2150
- import { existsSync as existsSync8, rmSync as rmSync3 } from "fs";
2090
+ import { existsSync as existsSync7, rmSync as rmSync3 } from "fs";
2151
2091
  var uninstallCommand = new Command6("uninstall").description(
2152
2092
  "Remove Openship from this machine: stop it, delete its data (database, certs, edge config), its images, and ~/.openship. Deployed apps are left running."
2153
2093
  ).option("-y, --yes", "Skip the confirmation prompt (for scripts)").option("--keep-data", "Keep the database/certs volumes and ~/.openship \u2014 remove only the running stack").option("--keep-images", "Don't delete the Openship container images").action(async (opts) => {
@@ -2193,7 +2133,7 @@ var uninstallCommand = new Command6("uninstall").description(
2193
2133
  console.log(chalk8.yellow(` Couldn't remove the service: ${err2.message}`));
2194
2134
  }
2195
2135
  }
2196
- if (destructive && existsSync8(OS_DIR)) {
2136
+ if (destructive && existsSync7(OS_DIR)) {
2197
2137
  try {
2198
2138
  rmSync3(OS_DIR, { recursive: true, force: true });
2199
2139
  console.log(chalk8.green(` Removed ${OS_DIR}`));
@@ -2212,15 +2152,15 @@ var uninstallCommand = new Command6("uninstall").description(
2212
2152
 
2213
2153
  // src/commands/init.ts
2214
2154
  import { Command as Command7 } from "commander";
2215
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
2155
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
2216
2156
  import { createInterface as createInterface2 } from "readline/promises";
2217
2157
  import { stdin as input2, stdout as output2 } from "process";
2218
- import { join as join9 } from "path";
2158
+ import { join as join8 } from "path";
2219
2159
  var initCommand = new Command7("init").description("Link the current directory to an Openship project (.openship/project.json)").option("--project <id>", "Project id to link (skips the picker)").option("--environment <name>", "Default deploy environment", "production").option("--dir <path>", "Directory to initialize", process.cwd()).option("--force", "Overwrite an existing project link").option("-y, --yes", "Non-interactive: fail instead of prompting").action(async (opts) => {
2220
2160
  const root = opts.dir || process.cwd();
2221
- const linkDir = join9(root, ".openship");
2222
- const linkPath = join9(linkDir, "project.json");
2223
- if (existsSync9(linkPath) && !opts.force) {
2161
+ const linkDir = join8(root, ".openship");
2162
+ const linkPath = join8(linkDir, "project.json");
2163
+ if (existsSync8(linkPath) && !opts.force) {
2224
2164
  err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
2225
2165
  process.exit(1);
2226
2166
  }
@@ -2273,8 +2213,8 @@ var initCommand = new Command7("init").description("Link the current directory t
2273
2213
  context: getActiveContext(),
2274
2214
  defaults: { environment: opts.environment || "production" }
2275
2215
  };
2276
- mkdirSync9(linkDir, { recursive: true });
2277
- writeFileSync8(linkPath, JSON.stringify(link, null, 2) + "\n");
2216
+ mkdirSync8(linkDir, { recursive: true });
2217
+ writeFileSync7(linkPath, JSON.stringify(link, null, 2) + "\n");
2278
2218
  if (isJsonMode()) {
2279
2219
  printJson({ path: linkPath, link });
2280
2220
  return;
@@ -2286,8 +2226,8 @@ var initCommand = new Command7("init").description("Link the current directory t
2286
2226
 
2287
2227
  // src/commands/config.ts
2288
2228
  import { Command as Command8 } from "commander";
2289
- import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync9 } from "fs";
2290
- import { join as join10 } from "path";
2229
+ import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync8 } from "fs";
2230
+ import { join as join9 } from "path";
2291
2231
  var SCHEMA_URL = "https://openship.io/openship.schema.json";
2292
2232
  var CONFIG_FILE = "openship.json";
2293
2233
  function detectHints(dir) {
@@ -2299,15 +2239,15 @@ function detectHints(dir) {
2299
2239
  ["package-lock.json", "npm"]
2300
2240
  ];
2301
2241
  for (const [file, pm] of lock) {
2302
- if (existsSync10(join10(dir, file))) {
2242
+ if (existsSync9(join9(dir, file))) {
2303
2243
  hints.packageManager = pm;
2304
2244
  break;
2305
2245
  }
2306
2246
  }
2307
- const pkgPath = join10(dir, "package.json");
2308
- if (existsSync10(pkgPath)) {
2247
+ const pkgPath = join9(dir, "package.json");
2248
+ if (existsSync9(pkgPath)) {
2309
2249
  try {
2310
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
2250
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
2311
2251
  const runner = hints.packageManager ?? "npm";
2312
2252
  const exec = runner === "npm" ? "npm run" : runner;
2313
2253
  if (pkg.scripts?.build) hints.buildCommand = `${exec} build`;
@@ -2319,15 +2259,15 @@ function detectHints(dir) {
2319
2259
  }
2320
2260
  var initCmd = new Command8("init").description("Scaffold an openship.json in the current directory").option("--dir <path>", "Directory to write into", process.cwd()).option("--force", "Overwrite an existing openship.json").action((opts) => {
2321
2261
  const dir = opts.dir || process.cwd();
2322
- const path2 = join10(dir, CONFIG_FILE);
2323
- if (existsSync10(path2) && !opts.force) {
2262
+ const path2 = join9(dir, CONFIG_FILE);
2263
+ if (existsSync9(path2) && !opts.force) {
2324
2264
  err(`${CONFIG_FILE} already exists. Re-run with --force to overwrite.`);
2325
2265
  process.exit(1);
2326
2266
  }
2327
2267
  const hints = detectHints(dir);
2328
2268
  const scaffold = { $schema: SCHEMA_URL, ...hints };
2329
2269
  const text2 = JSON.stringify(scaffold, null, 2) + "\n";
2330
- writeFileSync9(path2, text2);
2270
+ writeFileSync8(path2, text2);
2331
2271
  if (isJsonMode()) {
2332
2272
  printJson({ path: path2, config: scaffold });
2333
2273
  return;
@@ -2337,13 +2277,13 @@ var initCmd = new Command8("init").description("Scaffold an openship.json in the
2337
2277
  info(" Edit it to declare framework, env, domains, resources, services\u2026 then `openship config validate`.\n");
2338
2278
  });
2339
2279
  var validateCmd = new Command8("validate").description("Validate an openship.json against the deploy schema").argument("[file]", "Path to the config file", CONFIG_FILE).action((file) => {
2340
- const path2 = file === CONFIG_FILE ? join10(process.cwd(), CONFIG_FILE) : file;
2341
- if (!existsSync10(path2)) {
2280
+ const path2 = file === CONFIG_FILE ? join9(process.cwd(), CONFIG_FILE) : file;
2281
+ if (!existsSync9(path2)) {
2342
2282
  if (isJsonMode()) printJson({ valid: false, errors: [`${path2} not found`], warnings: [] });
2343
2283
  else err(`Not found: ${path2}`);
2344
2284
  process.exit(1);
2345
2285
  }
2346
- const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync6(path2, "utf8"));
2286
+ const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync5(path2, "utf8"));
2347
2287
  const valid = errors.length === 0 && config !== null;
2348
2288
  if (isJsonMode()) {
2349
2289
  printJson({ valid, errors, warnings });
@@ -2416,15 +2356,6 @@ var contextCommand = new Command9("context").alias("ctx").description("Manage co
2416
2356
  // src/commands/status.ts
2417
2357
  import { Command as Command10 } from "commander";
2418
2358
  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
- }
2428
2359
  var statusCommand = new Command10("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
2429
2360
  const context = getActiveContext();
2430
2361
  const apiUrl = getApiUrl2();
@@ -2444,7 +2375,7 @@ var statusCommand = new Command10("status").description("Show the local Openship
2444
2375
  return;
2445
2376
  }
2446
2377
  const svc = serviceStatus();
2447
- const ports = readPorts();
2378
+ const ports = readStoredPorts();
2448
2379
  let health = null;
2449
2380
  let envInfo = null;
2450
2381
  let reachable = true;
@@ -2476,7 +2407,7 @@ var statusCommand = new Command10("status").description("Show the local Openship
2476
2407
 
2477
2408
  // src/commands/doctor.ts
2478
2409
  import { Command as Command11 } from "commander";
2479
- import { existsSync as existsSync13 } from "fs";
2410
+ import { existsSync as existsSync12 } from "fs";
2480
2411
  import { execFileSync as execFileSync2 } from "child_process";
2481
2412
  import chalk11 from "chalk";
2482
2413
  import open from "open";
@@ -2484,42 +2415,42 @@ import { intro, outro, note as note2, select as select3, log as log3, spinner as
2484
2415
 
2485
2416
  // src/lib/repair.ts
2486
2417
  import { spawnSync as spawnSync4 } from "child_process";
2487
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2488
- import { join as join13 } from "path";
2418
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
2419
+ import { join as join11 } from "path";
2489
2420
  import chalk10 from "chalk";
2490
2421
  import { spinner, log as log2, select as select2, isCancel as isCancel3, cancel } from "@clack/prompts";
2491
2422
 
2492
2423
  // src/lib/heal.ts
2493
2424
  import {
2494
- existsSync as existsSync11,
2425
+ existsSync as existsSync10,
2495
2426
  readdirSync,
2496
2427
  statSync,
2497
2428
  unlinkSync,
2498
- mkdirSync as mkdirSync10,
2429
+ mkdirSync as mkdirSync9,
2499
2430
  copyFileSync,
2500
2431
  renameSync
2501
2432
  } from "fs";
2502
2433
  import { homedir as homedir2 } from "os";
2503
- import { join as join12 } from "path";
2434
+ import { join as join10 } from "path";
2504
2435
  import { execFileSync } from "child_process";
2505
2436
  function resolveDataDir() {
2506
2437
  const fromEnv = process.env.PGLITE_DATA_DIR?.trim();
2507
2438
  if (fromEnv) {
2508
- return fromEnv.startsWith("~") ? join12(homedir2(), fromEnv.slice(1).replace(/^[/\\]/, "")) : fromEnv;
2439
+ return fromEnv.startsWith("~") ? join10(homedir2(), fromEnv.slice(1).replace(/^[/\\]/, "")) : fromEnv;
2509
2440
  }
2510
- return join12(OS_DIR, "data");
2441
+ return join10(OS_DIR, "data");
2511
2442
  }
2512
2443
  function dataDirExists(dataDir = resolveDataDir()) {
2513
- return existsSync11(dataDir);
2444
+ return existsSync10(dataDir);
2514
2445
  }
2515
2446
  function stamp() {
2516
2447
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2517
2448
  }
2518
2449
  function copyDirRecursive(src, dest) {
2519
- if (!existsSync11(dest)) mkdirSync10(dest, { recursive: true });
2450
+ if (!existsSync10(dest)) mkdirSync9(dest, { recursive: true });
2520
2451
  for (const entry of readdirSync(src)) {
2521
- const s = join12(src, entry);
2522
- const d = join12(dest, entry);
2452
+ const s = join10(src, entry);
2453
+ const d = join10(dest, entry);
2523
2454
  if (statSync(s).isDirectory()) copyDirRecursive(s, d);
2524
2455
  else copyFileSync(s, d);
2525
2456
  }
@@ -2531,30 +2462,30 @@ function backupDataDir(dataDir = resolveDataDir()) {
2531
2462
  }
2532
2463
  function healDataDir(dataDir = resolveDataDir()) {
2533
2464
  const result = { removedPostmasterPid: false, trimmedWalSegment: null };
2534
- const pidPath = join12(dataDir, "postmaster.pid");
2535
- if (existsSync11(pidPath)) {
2465
+ const pidPath = join10(dataDir, "postmaster.pid");
2466
+ if (existsSync10(pidPath)) {
2536
2467
  unlinkSync(pidPath);
2537
2468
  result.removedPostmasterPid = true;
2538
2469
  }
2539
- const walDir = join12(dataDir, "pg_wal");
2540
- 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);
2470
+ const walDir = join10(dataDir, "pg_wal");
2471
+ if (existsSync10(walDir)) {
2472
+ const segments = readdirSync(walDir).filter((name) => /^[0-9A-F]{24}$/.test(name)).map((name) => ({ name, mtime: statSync(join10(walDir, name)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
2542
2473
  if (segments.length > 0) {
2543
2474
  const newest = segments[0];
2544
- unlinkSync(join12(walDir, newest.name));
2475
+ unlinkSync(join10(walDir, newest.name));
2545
2476
  result.trimmedWalSegment = newest.name;
2546
2477
  }
2547
2478
  }
2548
2479
  return result;
2549
2480
  }
2550
2481
  function restoreBackup(backupDir, dataDir = resolveDataDir()) {
2551
- if (!existsSync11(backupDir)) throw new Error(`Backup not found: ${backupDir}`);
2552
- if (existsSync11(dataDir)) renameSync(dataDir, `${dataDir}.discarded-${stamp()}`);
2482
+ if (!existsSync10(backupDir)) throw new Error(`Backup not found: ${backupDir}`);
2483
+ if (existsSync10(dataDir)) renameSync(dataDir, `${dataDir}.discarded-${stamp()}`);
2553
2484
  copyDirRecursive(backupDir, dataDir);
2554
2485
  }
2555
2486
  function freshStart(dataDir = resolveDataDir()) {
2556
2487
  const aside = `${dataDir}.corrupt-${stamp()}`;
2557
- if (existsSync11(dataDir)) renameSync(dataDir, aside);
2488
+ if (existsSync10(dataDir)) renameSync(dataDir, aside);
2558
2489
  return aside;
2559
2490
  }
2560
2491
  function findResetwal() {
@@ -2566,7 +2497,7 @@ function findResetwal() {
2566
2497
  "/usr/lib/postgresql/16/bin/pg_resetwal",
2567
2498
  "/usr/bin/pg_resetwal"
2568
2499
  ];
2569
- for (const c of candidates) if (existsSync11(c)) return c;
2500
+ for (const c of candidates) if (existsSync10(c)) return c;
2570
2501
  try {
2571
2502
  const found = execFileSync("bash", ["-lc", "command -v pg_resetwal"], {
2572
2503
  encoding: "utf8",
@@ -2587,7 +2518,7 @@ function deepHeal(dataDir = resolveDataDir()) {
2587
2518
  }
2588
2519
 
2589
2520
  // src/lib/repair.ts
2590
- var LOG_DIR2 = join13(OS_DIR, "logs");
2521
+ var LOG_DIR2 = join11(OS_DIR, "logs");
2591
2522
  function ensure(value) {
2592
2523
  if (isCancel3(value)) {
2593
2524
  cancel("Cancelled.");
@@ -2595,23 +2526,9 @@ function ensure(value) {
2595
2526
  }
2596
2527
  return value;
2597
2528
  }
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
2529
  async function internalGet2(path2, timeoutMs = 8e3) {
2613
2530
  try {
2614
- const res = await fetch(`http://127.0.0.1:${apiPort()}${path2}`, {
2531
+ const res = await fetch(`http://127.0.0.1:${storedApiPort()}${path2}`, {
2615
2532
  headers: { "X-Internal-Token": ensureInternalToken() },
2616
2533
  signal: AbortSignal.timeout(timeoutMs)
2617
2534
  });
@@ -2623,7 +2540,7 @@ async function internalGet2(path2, timeoutMs = 8e3) {
2623
2540
  }
2624
2541
  async function apiReachable() {
2625
2542
  try {
2626
- const res = await fetch(`http://127.0.0.1:${apiPort()}/api/health`, {
2543
+ const res = await fetch(`http://127.0.0.1:${storedApiPort()}/api/health`, {
2627
2544
  signal: AbortSignal.timeout(2500)
2628
2545
  });
2629
2546
  return res.ok;
@@ -2647,10 +2564,10 @@ async function waitApiDown(seconds = 20) {
2647
2564
  }
2648
2565
  function lastServiceError() {
2649
2566
  for (const name of ["up.err.log", "up.log", "instance.log"]) {
2650
- const p = join13(LOG_DIR2, name);
2651
- if (!existsSync12(p)) continue;
2567
+ const p = join11(LOG_DIR2, name);
2568
+ if (!existsSync11(p)) continue;
2652
2569
  try {
2653
- const lines = readFileSync8(p, "utf8").trim().split("\n");
2570
+ const lines = readFileSync6(p, "utf8").trim().split("\n");
2654
2571
  const hit = [...lines].reverse().find((l) => /error|locked|aborted|malformed|corrupt|throw|cannot|EADDRINUSE/i.test(l));
2655
2572
  if (hit) return hit.trim().slice(0, 240);
2656
2573
  } catch {
@@ -2708,11 +2625,11 @@ async function componentChecks(apiUp) {
2708
2625
  checks.push({
2709
2626
  name: "API",
2710
2627
  state: apiUp ? "pass" : "fail",
2711
- detail: apiUp ? `reachable on :${apiPort()}` : `not answering on :${apiPort()}`
2628
+ detail: apiUp ? `reachable on :${storedApiPort()}` : `not answering on :${storedApiPort()}`
2712
2629
  });
2713
2630
  let dashUp = false;
2714
2631
  try {
2715
- const res = await fetch(`http://127.0.0.1:${dashboardPort()}/`, {
2632
+ const res = await fetch(`http://127.0.0.1:${storedDashboardPort()}/`, {
2716
2633
  redirect: "manual",
2717
2634
  signal: AbortSignal.timeout(2500)
2718
2635
  });
@@ -2722,7 +2639,7 @@ async function componentChecks(apiUp) {
2722
2639
  checks.push({
2723
2640
  name: "Dashboard",
2724
2641
  state: dashUp ? "pass" : "warn",
2725
- detail: dashUp ? `serving on :${dashboardPort()}` : `not serving on :${dashboardPort()}`
2642
+ detail: dashUp ? `serving on :${storedDashboardPort()}` : `not serving on :${storedDashboardPort()}`
2726
2643
  });
2727
2644
  const edge = dockerNameRunning("openship-edge");
2728
2645
  checks.push(
@@ -2909,7 +2826,7 @@ function bunVersion() {
2909
2826
  }
2910
2827
  async function remotePreflight() {
2911
2828
  const checks = [];
2912
- const hasConfig = existsSync13(CONFIG_PATH);
2829
+ const hasConfig = existsSync12(CONFIG_PATH);
2913
2830
  checks.push({ name: "config", status: hasConfig ? "pass" : "warn", detail: hasConfig ? CONFIG_PATH : `not found; run \`openship login\`` });
2914
2831
  const context = getActiveContext();
2915
2832
  const apiUrl = getApiUrl();
@@ -3047,7 +2964,7 @@ async function interactiveDoctor() {
3047
2964
  }
3048
2965
  if (action2 === "open") {
3049
2966
  const url = readInstanceUrl();
3050
- const target = url && !/^https?:\/\/localhost/i.test(url) ? url : `http://localhost:${dashboardPort()}`;
2967
+ const target = url && !/^https?:\/\/localhost/i.test(url) ? url : `http://localhost:${storedDashboardPort()}`;
3051
2968
  await open(target).catch(() => {
3052
2969
  });
3053
2970
  log3.message(chalk11.dim(`Opening ${target}`));
@@ -3077,15 +2994,15 @@ import { execFileSync as execFileSync4 } from "child_process";
3077
2994
  import ora2 from "ora";
3078
2995
 
3079
2996
  // 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");
2997
+ import { readFileSync as readFileSync7, existsSync as existsSync13 } from "fs";
2998
+ import { join as join12, dirname as dirname3, parse } from "path";
2999
+ var LINK_REL = join12(".openship", "project.json");
3083
3000
  function findProjectLinkPath(from = process.cwd()) {
3084
3001
  let dir = from;
3085
3002
  const root = parse(dir).root;
3086
3003
  for (; ; ) {
3087
- const candidate = join14(dir, LINK_REL);
3088
- if (existsSync14(candidate)) return candidate;
3004
+ const candidate = join12(dir, LINK_REL);
3005
+ if (existsSync13(candidate)) return candidate;
3089
3006
  if (dir === root) return null;
3090
3007
  dir = dirname3(dir);
3091
3008
  }
@@ -3094,7 +3011,7 @@ function readProjectLink(from) {
3094
3011
  const path2 = findProjectLinkPath(from);
3095
3012
  if (!path2) return null;
3096
3013
  try {
3097
- return JSON.parse(readFileSync9(path2, "utf8"));
3014
+ return JSON.parse(readFileSync7(path2, "utf8"));
3098
3015
  } catch {
3099
3016
  return null;
3100
3017
  }
@@ -3102,21 +3019,21 @@ function readProjectLink(from) {
3102
3019
 
3103
3020
  // src/lib/folder-deploy.ts
3104
3021
  import { execFileSync as execFileSync3 } from "child_process";
3105
- import { readFileSync as readFileSync10, existsSync as existsSync15, rmSync as rmSync4 } from "fs";
3022
+ import { readFileSync as readFileSync8, existsSync as existsSync14, rmSync as rmSync4 } from "fs";
3106
3023
  import { tmpdir } from "os";
3107
- import { join as join15, basename } from "path";
3024
+ import { join as join13, basename } from "path";
3108
3025
  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";
3026
+ if (existsSync14(join13(dir, "bun.lockb")) || existsSync14(join13(dir, "bun.lock"))) return "bun";
3027
+ if (existsSync14(join13(dir, "pnpm-lock.yaml"))) return "pnpm";
3028
+ if (existsSync14(join13(dir, "yarn.lock"))) return "yarn";
3029
+ if (existsSync14(join13(dir, "package.json"))) return "npm";
3113
3030
  return void 0;
3114
3031
  }
3115
3032
  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";
3033
+ if (existsSync14(join13(dir, "go.mod"))) return "go";
3034
+ if (existsSync14(join13(dir, "Cargo.toml"))) return "rust";
3035
+ if (existsSync14(join13(dir, "requirements.txt")) || existsSync14(join13(dir, "pyproject.toml"))) return "python";
3036
+ if (existsSync14(join13(dir, "package.json"))) return "node";
3120
3037
  return void 0;
3121
3038
  }
3122
3039
  async function deployFolder(opts) {
@@ -3133,7 +3050,7 @@ async function deployFolder(opts) {
3133
3050
  throw new Error(session.error || "Failed to open upload session");
3134
3051
  }
3135
3052
  step("Packaging folder");
3136
- const tarball = join15(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3053
+ const tarball = join13(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3137
3054
  execFileSync3(
3138
3055
  "tar",
3139
3056
  [
@@ -3152,7 +3069,7 @@ async function deployFolder(opts) {
3152
3069
  );
3153
3070
  step("Uploading source");
3154
3071
  try {
3155
- const body = readFileSync10(tarball);
3072
+ const body = readFileSync8(tarball);
3156
3073
  const up = session.upload;
3157
3074
  const method = up.method || "POST";
3158
3075
  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 });
@@ -3606,12 +3523,19 @@ var sslStatus = new Command13("status").description("Check SSL certificate statu
3606
3523
  printJson(res);
3607
3524
  })
3608
3525
  );
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(
3526
+ 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
3527
  run2(async (domain, opts) => {
3611
3528
  const res = await apiRequest("/deployments/ssl/renew", {
3612
3529
  method: "POST",
3613
3530
  body: JSON.stringify({ domain, includeWww: opts.www === true })
3614
3531
  });
3532
+ if (!isJsonMode() && res.results && res.results.length > 1) {
3533
+ for (const r of res.results) {
3534
+ if (r.success) ok(`${r.domain}: ${r.status}`);
3535
+ else err(`${r.domain}: ${r.message ?? r.status}`);
3536
+ }
3537
+ return;
3538
+ }
3615
3539
  report(res, `SSL renewal requested for ${domain}`);
3616
3540
  })
3617
3541
  );
@@ -4391,8 +4315,10 @@ function mapComposeService(name, def, baseDir) {
4391
4315
  const volumes = mapVolumes(d.volumes);
4392
4316
  if (volumes.length) svc.volumes = volumes;
4393
4317
  const command = d.command;
4394
- if (typeof command === "string") svc.command = command;
4395
- else if (Array.isArray(command)) svc.command = command.map(String).join(" ");
4318
+ if (command != null) {
4319
+ svc.commandArgv = commandToArgv(command);
4320
+ svc.command = typeof command === "string" ? command : command.map(String).join(" ");
4321
+ }
4396
4322
  if (typeof d.restart === "string") svc.restart = d.restart;
4397
4323
  return svc;
4398
4324
  }
@@ -5158,7 +5084,7 @@ var serverCommand = server;
5158
5084
  // src/commands/system.ts
5159
5085
  import { Command as Command19 } from "commander";
5160
5086
  import ora5 from "ora";
5161
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
5087
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
5162
5088
  import { createInterface as createInterface6 } from "readline/promises";
5163
5089
  import { stdin as input5, stdout as output5 } from "process";
5164
5090
  async function guarded(fn) {
@@ -5458,7 +5384,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
5458
5384
  );
5459
5385
  spin4?.succeed("Export ready.");
5460
5386
  if (opts.out) {
5461
- writeFileSync10(opts.out, JSON.stringify(file));
5387
+ writeFileSync9(opts.out, JSON.stringify(file));
5462
5388
  const tables = Object.keys(file.dump?.tables ?? {}).length;
5463
5389
  report2(
5464
5390
  { out: opts.out, tables },
@@ -5484,7 +5410,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
5484
5410
  }
5485
5411
  let file;
5486
5412
  try {
5487
- file = JSON.parse(readFileSync11(opts.file, "utf8"));
5413
+ file = JSON.parse(readFileSync9(opts.file, "utf8"));
5488
5414
  } catch {
5489
5415
  err(`
5490
5416
  Could not read or parse ${opts.file}.
@@ -5835,7 +5761,7 @@ var mailCommand = new Command20("mail").description("Self-hosted mail server (iR
5835
5761
  // src/commands/backup.ts
5836
5762
  import { Command as Command21 } from "commander";
5837
5763
  import ora7 from "ora";
5838
- import { readFileSync as readFileSync12 } from "fs";
5764
+ import { readFileSync as readFileSync10 } from "fs";
5839
5765
  async function guard3(fn) {
5840
5766
  try {
5841
5767
  await fn();
@@ -6128,7 +6054,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
6128
6054
  let sftpPrivateKey = opts.sftpPrivateKey;
6129
6055
  if (opts.sftpPrivateKeyFile) {
6130
6056
  try {
6131
- sftpPrivateKey = readFileSync12(opts.sftpPrivateKeyFile, "utf8");
6057
+ sftpPrivateKey = readFileSync10(opts.sftpPrivateKeyFile, "utf8");
6132
6058
  } catch {
6133
6059
  throw new Error(`Cannot read key file: ${opts.sftpPrivateKeyFile}`);
6134
6060
  }
@@ -6240,13 +6166,28 @@ var createCmd3 = new Command22("create").description("Mint a new personal access
6240
6166
  "Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
6241
6167
  collectGrant,
6242
6168
  []
6169
+ ).option(
6170
+ "--full-access",
6171
+ "Mint an UNSCOPED token with your own full access (required when no --grant is given)",
6172
+ false
6243
6173
  ).action(async (name, opts) => {
6244
6174
  const grants = opts.grant ?? [];
6175
+ if (grants.length === 0 && !opts.fullAccess) {
6176
+ fail3(
6177
+ new Error(
6178
+ "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."
6179
+ )
6180
+ );
6181
+ }
6182
+ if (grants.length > 0 && opts.fullAccess) {
6183
+ fail3(new Error("--grant and --full-access are mutually exclusive: pick one."));
6184
+ }
6245
6185
  const body = {
6246
6186
  name,
6247
6187
  readOnly: opts.readOnly || void 0,
6248
6188
  expiresInDays: Number.isFinite(opts.expires) ? opts.expires : void 0,
6249
- grants: grants.length > 0 ? grants : void 0
6189
+ grants: grants.length > 0 ? grants : void 0,
6190
+ fullAccess: grants.length === 0 ? true : void 0
6250
6191
  };
6251
6192
  const sp = spin3(`Creating token "${name}"\u2026`);
6252
6193
  try {
@@ -6320,16 +6261,7 @@ var apiCommand = new Command23("api").description("Make an authenticated request
6320
6261
  import { Command as Command24 } from "commander";
6321
6262
  import chalk19 from "chalk";
6322
6263
  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) => {
6264
+ 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) => {
6333
6265
  intro2(chalk19.cyan("Reset Openship admin password"));
6334
6266
  let pw = opts.password;
6335
6267
  if (!pw) {
@@ -6355,7 +6287,7 @@ var resetAdminCommand = new Command24("reset-admin-password").description("Reset
6355
6287
  }
6356
6288
  pw = entered;
6357
6289
  }
6358
- const port = String(opts.port || resolvedApiPort() || 4e3);
6290
+ const port = String(opts.port || storedApiPort());
6359
6291
  let res;
6360
6292
  try {
6361
6293
  res = await fetch(`http://127.0.0.1:${port}/api/system/reset-admin-password`, {
@@ -6378,10 +6310,10 @@ var resetAdminCommand = new Command24("reset-admin-password").description("Reset
6378
6310
 
6379
6311
  // src/commands/install.ts
6380
6312
  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";
6313
+ import { chmodSync as chmodSync2, existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
6382
6314
  import { spawn as spawn2, spawnSync as spawnSync6 } from "child_process";
6383
6315
  import { homedir as homedir3 } from "os";
6384
- import { join as join17 } from "path";
6316
+ import { join as join14 } from "path";
6385
6317
  import ora9 from "ora";
6386
6318
  function assetForPlatform() {
6387
6319
  const { platform, arch } = process;
@@ -6395,10 +6327,10 @@ function assetForPlatform() {
6395
6327
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
6396
6328
  }
6397
6329
  function installDmg(dmg) {
6398
- const homeApps = join17(homedir3(), "Applications");
6330
+ const homeApps = join14(homedir3(), "Applications");
6399
6331
  let dest = homeApps;
6400
6332
  try {
6401
- mkdirSync11(homeApps, { recursive: true });
6333
+ mkdirSync10(homeApps, { recursive: true });
6402
6334
  } catch {
6403
6335
  dest = "/Applications";
6404
6336
  }
@@ -6408,14 +6340,14 @@ function installDmg(dmg) {
6408
6340
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
6409
6341
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
6410
6342
  if (!mount) throw new Error("Could not determine the mounted volume");
6411
- let target = join17(dest, "Openship.app");
6343
+ let target = join14(dest, "Openship.app");
6412
6344
  try {
6413
- const appInDmg = join17(mount, "Openship.app");
6414
- if (!existsSync16(appInDmg)) throw new Error("Openship.app not found in the disk image");
6345
+ const appInDmg = join14(mount, "Openship.app");
6346
+ if (!existsSync15(appInDmg)) throw new Error("Openship.app not found in the disk image");
6415
6347
  spawnSync6("rm", ["-rf", target]);
6416
6348
  let copy = spawnSync6("ditto", [appInDmg, target], { encoding: "utf8" });
6417
6349
  if (copy.status !== 0 && dest === homeApps) {
6418
- target = join17("/Applications", "Openship.app");
6350
+ target = join14("/Applications", "Openship.app");
6419
6351
  spawnSync6("rm", ["-rf", target]);
6420
6352
  copy = spawnSync6("ditto", [appInDmg, target], { encoding: "utf8" });
6421
6353
  }
@@ -6430,9 +6362,9 @@ function installAppImage(appImage) {
6430
6362
  return appImage;
6431
6363
  }
6432
6364
  function installZip(zip) {
6433
- const localAppData = process.env.LOCALAPPDATA || join17(homedir3(), "AppData", "Local");
6434
- const target = join17(localAppData, "Programs", "Openship");
6435
- mkdirSync11(target, { recursive: true });
6365
+ const localAppData = process.env.LOCALAPPDATA || join14(homedir3(), "AppData", "Local");
6366
+ const target = join14(localAppData, "Programs", "Openship");
6367
+ mkdirSync10(target, { recursive: true });
6436
6368
  const expand = spawnSync6(
6437
6369
  "powershell",
6438
6370
  [
@@ -6459,8 +6391,8 @@ function launch(kind, target) {
6459
6391
  child.unref();
6460
6392
  return;
6461
6393
  }
6462
- const exe = join17(target, "Openship.exe");
6463
- const path2 = existsSync16(exe) ? exe : target;
6394
+ const exe = join14(target, "Openship.exe");
6395
+ const path2 = existsSync15(exe) ? exe : target;
6464
6396
  spawnSync6("cmd", ["/c", "start", "", path2]);
6465
6397
  }
6466
6398
  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) => {
@@ -6486,13 +6418,13 @@ var installCommand = new Command25("install").description("Download and install
6486
6418
  process.exit(1);
6487
6419
  }
6488
6420
  const dir = releaseDir(tag);
6489
- const assetPath = join17(dir, asset.name);
6421
+ const assetPath = join14(dir, asset.name);
6490
6422
  const sidecarPath = `${assetPath}.sha256`;
6491
6423
  const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
6492
6424
  const sidecarUrl = `${assetUrl2}.sha256`;
6493
6425
  let downloaded = false;
6494
6426
  let sha;
6495
- const cachedUsable = !opts.force && existsSync16(assetPath) && (existsSync16(sidecarPath) || opts.verify === false);
6427
+ const cachedUsable = !opts.force && existsSync15(assetPath) && (existsSync15(sidecarPath) || opts.verify === false);
6496
6428
  if (cachedUsable) {
6497
6429
  info(` Using cached ${asset.name} (${tag}).`);
6498
6430
  } else {
@@ -6514,8 +6446,8 @@ var installCommand = new Command25("install").description("Download and install
6514
6446
  const s2 = spin4("Verifying checksum\u2026");
6515
6447
  try {
6516
6448
  let sidecarBody;
6517
- if (existsSync16(sidecarPath) && !downloaded) {
6518
- sidecarBody = readFileSync14(sidecarPath, "utf8");
6449
+ if (existsSync15(sidecarPath) && !downloaded) {
6450
+ sidecarBody = readFileSync11(sidecarPath, "utf8");
6519
6451
  } else {
6520
6452
  sidecarBody = await fetchSidecar(sidecarUrl);
6521
6453
  }
@@ -6538,8 +6470,8 @@ var installCommand = new Command25("install").description("Download and install
6538
6470
  err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
6539
6471
  process.exit(1);
6540
6472
  }
6541
- mkdirSync11(dir, { recursive: true });
6542
- writeFileSync11(sidecarPath, sidecarBody);
6473
+ mkdirSync10(dir, { recursive: true });
6474
+ writeFileSync10(sidecarPath, sidecarBody);
6543
6475
  s2?.succeed("Checksum verified");
6544
6476
  } catch (e) {
6545
6477
  s2?.fail("Verification failed");
@@ -6626,7 +6558,7 @@ function detectPackageManager2(override) {
6626
6558
  var updateCommand = new Command26("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").option("--rebuild", "From-source installs: rebuild even if already at the remote tip").action(async (opts) => {
6627
6559
  const source = readSourceInstall();
6628
6560
  if (source) return runSourceUpdate(source, opts);
6629
- const current = "0.4.8";
6561
+ const current = "0.5.0";
6630
6562
  let latest;
6631
6563
  try {
6632
6564
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -6684,17 +6616,17 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
6684
6616
 
6685
6617
  // src/commands/cache.ts
6686
6618
  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";
6619
+ import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync2 } from "fs";
6620
+ import { join as join15 } from "path";
6689
6621
  function listAssets() {
6690
- if (!existsSync17(RELEASES_DIR)) return [];
6622
+ if (!existsSync16(RELEASES_DIR)) return [];
6691
6623
  const out = [];
6692
6624
  for (const tag of readdirSync2(RELEASES_DIR)) {
6693
6625
  const dir = releaseDir(tag);
6694
6626
  if (!statSync2(dir).isDirectory()) continue;
6695
6627
  for (const name of readdirSync2(dir)) {
6696
6628
  if (name.endsWith(".sha256")) continue;
6697
- const path2 = join18(dir, name);
6629
+ const path2 = join15(dir, name);
6698
6630
  const st = statSync2(path2);
6699
6631
  if (!st.isFile()) continue;
6700
6632
  out.push({
@@ -6702,7 +6634,7 @@ function listAssets() {
6702
6634
  name,
6703
6635
  path: path2,
6704
6636
  size: st.size,
6705
- hasSidecar: existsSync17(`${path2}.sha256`)
6637
+ hasSidecar: existsSync16(`${path2}.sha256`)
6706
6638
  });
6707
6639
  }
6708
6640
  }
@@ -6733,7 +6665,7 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
6733
6665
  results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
6734
6666
  continue;
6735
6667
  }
6736
- const expected = parseSha256(readFileSync15(`${a.path}.sha256`, "utf8"));
6668
+ const expected = parseSha256(readFileSync12(`${a.path}.sha256`, "utf8"));
6737
6669
  const actual = await hashFile(a.path);
6738
6670
  const okMatch = expected !== null && expected === actual;
6739
6671
  if (!okMatch) bad += 1;
@@ -6749,7 +6681,7 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
6749
6681
  });
6750
6682
  var cleanCmd = new Command27("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
6751
6683
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
6752
- if (!existsSync17(target)) {
6684
+ if (!existsSync16(target)) {
6753
6685
  if (isJsonMode()) printJson({ removed: false, path: target });
6754
6686
  else info(` Nothing to clean (${target}).`);
6755
6687
  return;
@@ -6769,8 +6701,8 @@ var cacheCommand = new Command27("cache").description("Manage the local download
6769
6701
  import chalk20 from "chalk";
6770
6702
  import open2 from "open";
6771
6703
  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";
6704
+ import { existsSync as existsSync17, mkdirSync as mkdirSync11, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
6705
+ import { join as join16 } from "path";
6774
6706
  import {
6775
6707
  intro as intro3,
6776
6708
  outro as outro3,
@@ -6916,13 +6848,13 @@ async function streamProvision(port, sessionId, s) {
6916
6848
  }
6917
6849
  return { ok: ok3, detail };
6918
6850
  }
6919
- var SETUP_LOCK = join19(OS_DIR, "setup-in-progress");
6851
+ var SETUP_LOCK = join16(OS_DIR, "setup-in-progress");
6920
6852
  function isSetupInProgress() {
6921
- return existsSync18(SETUP_LOCK);
6853
+ return existsSync17(SETUP_LOCK);
6922
6854
  }
6923
6855
  function markSetupStarted() {
6924
- mkdirSync12(OS_DIR, { recursive: true });
6925
- writeFileSync12(SETUP_LOCK, "1");
6856
+ mkdirSync11(OS_DIR, { recursive: true });
6857
+ writeFileSync11(SETUP_LOCK, "1");
6926
6858
  }
6927
6859
  function markSetupDone() {
6928
6860
  rmSync6(SETUP_LOCK, { force: true });
@@ -7183,17 +7115,22 @@ async function runWizard() {
7183
7115
  }
7184
7116
  let method = "bare";
7185
7117
  if (process.platform === "linux") {
7186
- if (hasDockerCompose()) {
7118
+ const gap = dockerGap();
7119
+ if (!gap) {
7187
7120
  method = "compose";
7121
+ } else if (!gap.installable) {
7122
+ log5.warn(`${gap.summary} \u2014 using the bare process service instead.`);
7123
+ if (gap.hint) log5.info(gap.hint);
7124
+ method = "bare";
7188
7125
  } else {
7189
- log5.step("Docker isn't installed \u2014 installing it now (get.docker.com)\u2026");
7190
- method = await ensureDocker() ? "compose" : "bare";
7126
+ log5.step(`${gap.summary} \u2014 installing via get.docker.com\u2026`);
7127
+ method = await ensureDocker({ onNotice: (line) => log5.info(line) }) ? "compose" : "bare";
7191
7128
  if (method === "bare") {
7192
7129
  log5.warn("Couldn't install Docker automatically \u2014 falling back to the bare process service.");
7193
7130
  }
7194
7131
  }
7195
7132
  }
7196
- const uiTag = `v${"0.4.8"}`;
7133
+ const uiTag = `v${"0.5.0"}`;
7197
7134
  if (method === "bare") {
7198
7135
  const dl = spinner4();
7199
7136
  dl.start("Pulling the Openship dist from GitHub");
@@ -7247,13 +7184,20 @@ async function runWizard() {
7247
7184
  }
7248
7185
  }
7249
7186
  if (method === "compose") {
7187
+ const ports = await resolveComposePorts({});
7188
+ const apiPort = String(ports.api);
7189
+ const dashboardPort = String(ports.dashboard);
7190
+ const moved = portMoveNotice(ports, composeTrustedOriginUrls());
7191
+ if (moved.length) log5.info(moved.join("\n"));
7250
7192
  log5.step(
7251
7193
  sourceBuildDir() ? "Building the Openship images before touching :80/:443 (first run takes a few minutes)\u2026" : "Pulling images before touching :80/:443\u2026"
7252
7194
  );
7253
7195
  if (!composePrefetch({
7196
+ apiPort,
7197
+ dashboardPort,
7254
7198
  publicUrl,
7255
7199
  trustProxy: behindProxy,
7256
- version: "0.4.8",
7200
+ version: "0.5.0",
7257
7201
  noHostControl: !allowHostControl
7258
7202
  })) {
7259
7203
  cancel4("Couldn't fetch the Openship images \u2014 nothing on this box was changed.");
@@ -7271,9 +7215,11 @@ async function runWizard() {
7271
7215
  const up = await composeUp({
7272
7216
  // Prefetched above, before the preflight stopped anything.
7273
7217
  alreadyFetched: true,
7218
+ apiPort,
7219
+ dashboardPort,
7274
7220
  publicUrl,
7275
7221
  trustProxy: behindProxy,
7276
- version: "0.4.8",
7222
+ version: "0.5.0",
7277
7223
  noHostControl: !allowHostControl
7278
7224
  });
7279
7225
  if (!up.ok) {
@@ -7504,22 +7450,14 @@ async function runWizard() {
7504
7450
  byo: domainPlan.type === "byo"
7505
7451
  });
7506
7452
  }
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
7453
  async function runControl() {
7516
7454
  const svc = serviceStatus();
7517
7455
  const isCompose = readInstallMethod() === "compose";
7518
7456
  const running = isCompose ? composeRunning() : svc.running;
7519
7457
  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}`;
7458
+ const ports = readStoredPorts();
7459
+ const apiPort = String(storedApiPort());
7460
+ const dashUrl = `http://localhost:${storedDashboardPort()}`;
7523
7461
  const publicUrl = readInstanceUrl();
7524
7462
  const primaryUrl = publicUrl && !/^https?:\/\/localhost/i.test(publicUrl) ? publicUrl : dashUrl;
7525
7463
  intro3(`${chalk20.bgCyan(chalk20.black(" Openship "))}${chalk20.dim(" control")}`);
@@ -7594,7 +7532,7 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7594
7532
  const pw = ensure2(
7595
7533
  await password({ message: "New admin password", validate: (v) => v && v.length >= 8 ? void 0 : "At least 8 characters" })
7596
7534
  );
7597
- const rr = await internalPost(apiPort2, "/api/system/reset-admin-password", { password: pw });
7535
+ const rr = await internalPost(apiPort, "/api/system/reset-admin-password", { password: pw });
7598
7536
  outro3(
7599
7537
  rr.ok ? chalk20.green(`Password reset. Sign in at ${dashUrl} with your email + new password.`) : chalk20.red(`Couldn't reset: ${rr.data?.error || "failed"}`)
7600
7538
  );
@@ -7610,7 +7548,7 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7610
7548
 
7611
7549
  // src/index.ts
7612
7550
  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) => {
7551
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.5.0").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7614
7552
  if (thisCommand.opts().json) setJsonMode(true);
7615
7553
  }).action(async () => {
7616
7554
  const installed = serviceStatus().installed || readInstallMethod() === "compose";