openship 0.4.7 → 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-2HYDXQEH.js → bare-VRGE5MXH.js} +7 -7
  3. package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
  4. package/dist/{chunk-R5BHFFAV.js → chunk-6PIYRG5I.js} +56 -17
  5. package/dist/{chunk-IMBD5PZ5.js → chunk-BS2AXYZX.js} +430 -86
  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-XFCDYZUQ.js → chunk-JLKKYUZA.js} +31 -49
  10. package/dist/{chunk-4TJC4JZQ.js → chunk-K2RPVGPN.js} +23 -4
  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-IPYPMI3S.js → chunk-NNPEG6HM.js} +1 -1
  14. package/dist/{chunk-UXWAVM4C.js → chunk-NXFIRK2Z.js} +398 -112
  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-2KXREXMO.js → cloud-3M2WZ624.js} +12 -12
  21. package/dist/{detect-2XSCFFTF.js → detect-BAJMY6KM.js} +7 -5
  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-ZQ4Y5HEA.js → edge-import-RIWNEIXQ.js} +21 -19
  25. package/dist/{ensure-container-edge-ED25XL3Z.js → ensure-container-edge-3NSYKHRX.js} +6 -6
  26. package/dist/{executor-YPT6RH6O.js → executor-LOYMPU67.js} +3 -3
  27. package/dist/index.js +309 -338
  28. package/dist/{nginx-XWJAOGS4.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 +51705 -45369
  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-2DMFIA74.js → setup-KELALK7H.js} +7 -7
  42. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,29 +3,42 @@
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,
9
11
  composeInternalToken,
10
12
  composeIsViableDefault,
13
+ composePrefetch,
11
14
  composePs,
12
15
  composeRestart,
13
16
  composeRunning,
14
17
  composeStart,
18
+ composeTrustedOriginUrls,
15
19
  composeUninstall,
16
20
  composeUp,
17
21
  composeUpdate,
22
+ dockerGap,
18
23
  ensureDocker,
19
24
  hasDockerCompose,
20
25
  importMigratedSites,
26
+ portMoveNotice,
21
27
  prepareFromSource,
22
28
  readInstallMethod,
29
+ readInstanceUrl,
23
30
  readSourceInstall,
31
+ readStoredPorts,
24
32
  rebuildFromSource,
25
33
  remoteSha,
34
+ resolveComposePorts,
35
+ resolvePorts,
36
+ saveInstanceUrl,
26
37
  shortSha,
27
- sourceBuildDir
28
- } from "./chunk-IMBD5PZ5.js";
38
+ sourceBuildDir,
39
+ storedApiPort,
40
+ storedDashboardPort
41
+ } from "./chunk-BS2AXYZX.js";
29
42
  import "./chunk-FRNAR33O.js";
30
43
  import "./chunk-EI575GHN.js";
31
44
  import "./chunk-6APIPIJY.js";
@@ -33,12 +46,12 @@ import "./chunk-IXCHURVK.js";
33
46
  import "./chunk-OWED2GRM.js";
34
47
  import "./chunk-VWPEKOH6.js";
35
48
  import "./chunk-W73X43R7.js";
36
- import "./chunk-TGIINPVM.js";
37
- import "./chunk-XFCDYZUQ.js";
49
+ import "./chunk-6LKHXB6T.js";
50
+ import "./chunk-JLKKYUZA.js";
38
51
  import "./chunk-AVIB7CT7.js";
39
52
  import "./chunk-5MZ624VX.js";
40
53
  import "./chunk-Q2CIRAOK.js";
41
- import "./chunk-IPYPMI3S.js";
54
+ import "./chunk-NNPEG6HM.js";
42
55
  import {
43
56
  beginEdgeTakeover,
44
57
  collectProxyCerts,
@@ -51,39 +64,40 @@ import {
51
64
  recoverInterruptedTakeover,
52
65
  rollbackEdgeTakeover,
53
66
  scanImportableSites
54
- } from "./chunk-UXWAVM4C.js";
67
+ } from "./chunk-NXFIRK2Z.js";
55
68
  import "./chunk-O6ZZF52V.js";
56
69
  import "./chunk-JYGD5WSN.js";
57
- import "./chunk-SJIFLLX4.js";
58
- import "./chunk-V6AN6XLZ.js";
59
- import "./chunk-VDMQJHPI.js";
60
- import "./chunk-MZ2WEHHY.js";
61
- import "./chunk-L2VOGB2X.js";
62
- import "./chunk-4Z5QO7N6.js";
63
- import "./chunk-4TJC4JZQ.js";
64
- 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";
65
78
  import "./chunk-RPHRPFEH.js";
66
79
  import {
67
80
  LocalExecutor
68
81
  } from "./chunk-TYVME3XD.js";
69
- import "./chunk-FXTBHTWY.js";
82
+ import "./chunk-RB35YN3J.js";
70
83
  import "./chunk-FPRHYBY2.js";
71
84
  import {
72
85
  edgeCrashReason,
73
- edgeIsServing,
86
+ edgeIsBroken,
74
87
  ourEdgeContainerRunning
75
- } from "./chunk-R5BHFFAV.js";
88
+ } from "./chunk-6PIYRG5I.js";
76
89
  import "./chunk-ILFETZOM.js";
77
- import "./chunk-VFINNTUL.js";
90
+ import "./chunk-EAAGSQXN.js";
78
91
  import {
79
92
  CLOUD_DASHBOARD_URL,
80
93
  LOCAL_API_URL,
81
94
  LOCAL_DASHBOARD_URL,
82
95
  cliInstallCommand,
96
+ commandToArgv,
83
97
  isValidEmail,
84
98
  parseOpenshipConfigJson,
85
99
  resolveCliUpdatePlan
86
- } from "./chunk-6APULWSU.js";
100
+ } from "./chunk-UMQP7SYT.js";
87
101
  import "./chunk-KI2EY3WB.js";
88
102
 
89
103
  // src/index.ts
@@ -486,8 +500,8 @@ import chalk6 from "chalk";
486
500
  import ora from "ora";
487
501
  import { spawn } from "child_process";
488
502
  import { randomBytes as randomBytes2 } from "crypto";
489
- import { createWriteStream as createWriteStream2, existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
490
- 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";
491
505
  import { fileURLToPath } from "url";
492
506
 
493
507
  // src/lib/dashboard.ts
@@ -657,7 +671,7 @@ async function fetchBundle(tag, onProgress) {
657
671
 
658
672
  // src/lib/service.ts
659
673
  import { spawnSync as spawnSync2 } from "child_process";
660
- 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";
661
675
  import { homedir } from "os";
662
676
  import { join as join4, resolve } from "path";
663
677
  var HOME = homedir();
@@ -875,14 +889,9 @@ function serviceStatus() {
875
889
  }
876
890
  function sweepOrphanPorts() {
877
891
  if (process.platform === "win32") return;
878
- let ports = [];
879
- try {
880
- const raw = readFileSync2(join4(HOME, ".openship", "ports.json"), "utf8");
881
- const p = JSON.parse(raw);
882
- ports = [p.api, p.dashboard].filter((n) => typeof n === "number");
883
- } catch {
884
- return;
885
- }
892
+ const stored = readStoredPorts();
893
+ const ports = [stored.api, stored.dashboard].filter((n) => typeof n === "number");
894
+ if (ports.length === 0) return;
886
895
  const self = process.pid;
887
896
  for (const port of ports) {
888
897
  const q = run("lsof", ["-ti", `tcp:${port}`]);
@@ -923,97 +932,10 @@ function stop() {
923
932
  return result;
924
933
  }
925
934
 
926
- // src/lib/ports.ts
927
- import { createServer } from "net";
928
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
929
- import { join as join5 } from "path";
930
- var PORTS_FILE = join5(OS_DIR, "ports.json");
931
- var INSTANCE_FILE = join5(OS_DIR, "instance.json");
932
- function saveInstanceUrl(publicUrl) {
933
- try {
934
- if (!existsSync4(OS_DIR)) mkdirSync5(OS_DIR, { recursive: true, mode: 448 });
935
- writeFileSync4(INSTANCE_FILE, JSON.stringify({ publicUrl: publicUrl ?? null }));
936
- } catch {
937
- }
938
- }
939
- function readInstanceUrl() {
940
- try {
941
- return JSON.parse(readFileSync3(INSTANCE_FILE, "utf8")).publicUrl ?? null;
942
- } catch {
943
- return null;
944
- }
945
- }
946
- var DEFAULT_API = 4e3;
947
- var DEFAULT_DASHBOARD = 3001;
948
- function isPortFree(port) {
949
- return new Promise((resolve2) => {
950
- const srv = createServer();
951
- srv.once("error", () => resolve2(false));
952
- srv.listen(port, "127.0.0.1", () => srv.close(() => resolve2(true)));
953
- });
954
- }
955
- async function waitPortFree(port, opts = {}) {
956
- const timeoutMs = opts.timeoutMs ?? 6e3;
957
- const intervalMs = opts.intervalMs ?? 250;
958
- const deadline = Date.now() + timeoutMs;
959
- for (; ; ) {
960
- if (await isPortFree(port)) return true;
961
- if (Date.now() >= deadline) return false;
962
- await new Promise((r) => setTimeout(r, intervalMs));
963
- }
964
- }
965
- function getFreePort() {
966
- return new Promise((resolve2, reject2) => {
967
- const srv = createServer();
968
- srv.once("error", reject2);
969
- srv.listen(0, "127.0.0.1", () => {
970
- const addr = srv.address();
971
- const port = addr && typeof addr === "object" ? addr.port : 0;
972
- srv.close(() => port ? resolve2(port) : reject2(new Error("no free port")));
973
- });
974
- });
975
- }
976
- function loadStoredPorts() {
977
- try {
978
- return JSON.parse(readFileSync3(PORTS_FILE, "utf-8"));
979
- } catch {
980
- return {};
981
- }
982
- }
983
- function saveStoredPorts(api, dashboard) {
984
- try {
985
- if (!existsSync4(OS_DIR)) mkdirSync5(OS_DIR, { recursive: true, mode: 448 });
986
- writeFileSync4(PORTS_FILE, JSON.stringify({ api, dashboard }));
987
- } catch {
988
- }
989
- }
990
- async function resolvePorts(prefs) {
991
- const stored = loadStoredPorts();
992
- const apiPref = prefs.api ?? stored.api ?? DEFAULT_API;
993
- const dashPref = prefs.dashboard ?? stored.dashboard ?? DEFAULT_DASHBOARD;
994
- const apiRemembered = prefs.api === void 0 && stored.api === apiPref;
995
- const dashRemembered = prefs.dashboard === void 0 && stored.dashboard === dashPref;
996
- let api;
997
- if (await isPortFree(apiPref)) api = apiPref;
998
- else if (apiRemembered && await waitPortFree(apiPref)) api = apiPref;
999
- else api = await getFreePort();
1000
- let dashboard;
1001
- if (dashPref !== api && await isPortFree(dashPref)) dashboard = dashPref;
1002
- else if (dashPref !== api && dashRemembered && await waitPortFree(dashPref)) dashboard = dashPref;
1003
- else dashboard = await getFreePort();
1004
- if (dashboard === api) dashboard = await getFreePort();
1005
- saveStoredPorts(api, dashboard);
1006
- return {
1007
- api,
1008
- dashboard,
1009
- switched: { api: api !== apiPref, dashboard: dashboard !== dashPref }
1010
- };
1011
- }
1012
-
1013
935
  // src/lib/edge-preflight.ts
1014
936
  import { spawnSync as spawnSync3 } from "child_process";
1015
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
1016
- 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";
1017
939
  import chalk5 from "chalk";
1018
940
  import { isCancel, log, note, select } from "@clack/prompts";
1019
941
  async function planAndApplyHostEdge(opts, overrides = {}) {
@@ -1052,7 +974,7 @@ async function rollbackHostEdge() {
1052
974
  return false;
1053
975
  }
1054
976
  }
1055
- var IMPORTED_MARKER = join6(OS_DIR, "imported-proxy");
977
+ var IMPORTED_MARKER = join5(OS_DIR, "imported-proxy");
1056
978
  function edgeServedHostnames() {
1057
979
  const served = /* @__PURE__ */ new Set();
1058
980
  const r = spawnSync3(
@@ -1076,13 +998,13 @@ function edgeServedHostnames() {
1076
998
  }
1077
999
  function markStoppedProxyImported() {
1078
1000
  try {
1079
- mkdirSync6(OS_DIR, { recursive: true, mode: 448 });
1080
- 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 });
1081
1003
  } catch {
1082
1004
  }
1083
1005
  }
1084
1006
  async function offerStoppedProxyImport(executor, deps) {
1085
- if (!deps.interactive || existsSync5(IMPORTED_MARKER)) return { proceed: true };
1007
+ if (!deps.interactive || existsSync4(IMPORTED_MARKER)) return { proceed: true };
1086
1008
  let proxy = null;
1087
1009
  try {
1088
1010
  proxy = await deps.detectInstalledProxy(executor);
@@ -1227,15 +1149,15 @@ function defaultDeps() {
1227
1149
  }
1228
1150
 
1229
1151
  // src/lib/loopback-api.ts
1230
- 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";
1231
1153
  import { randomBytes } from "crypto";
1232
- import { join as join7 } from "path";
1154
+ import { join as join6 } from "path";
1233
1155
  function ensureInternalToken() {
1234
- const path2 = join7(OS_DIR, "internal-token");
1235
- if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
1236
- 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 });
1237
1159
  const token = randomBytes(32).toString("hex");
1238
- writeFileSync6(path2, token, { mode: 384 });
1160
+ writeFileSync5(path2, token, { mode: 384 });
1239
1161
  return token;
1240
1162
  }
1241
1163
  async function internalGet(port, path2, token) {
@@ -1264,17 +1186,17 @@ async function internalPost(port, path2, body, token) {
1264
1186
  return { ok: false, data: { error: err2.message } };
1265
1187
  }
1266
1188
  }
1267
- async function bootstrapAdmin(apiPort2, admin, token) {
1268
- 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);
1269
1191
  if (ok3) return { ok: true };
1270
1192
  if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
1271
1193
  return { ok: false, message: data?.error || "failed" };
1272
1194
  }
1273
- async function waitHealthy(apiPort2, seconds = 90) {
1195
+ async function waitHealthy(apiPort, seconds = 90) {
1274
1196
  for (let i = 0; i < seconds; i++) {
1275
1197
  await new Promise((r) => setTimeout(r, 1e3));
1276
1198
  try {
1277
- 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) });
1278
1200
  return true;
1279
1201
  } catch {
1280
1202
  }
@@ -1555,16 +1477,16 @@ function normalizePublicUrl(raw) {
1555
1477
  return url;
1556
1478
  }
1557
1479
  var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
1558
- var SERVER_DIR = join8(DIST_DIR, "server");
1480
+ var SERVER_DIR = join7(DIST_DIR, "server");
1559
1481
  function ensureAuthSecret() {
1560
- const path2 = join8(OS_DIR, "auth-secret");
1561
- if (existsSync7(path2)) return readFileSync5(path2, "utf8").trim();
1562
- 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 });
1563
1485
  const secret = randomBytes2(32).toString("hex");
1564
- writeFileSync7(path2, secret, { mode: 384 });
1486
+ writeFileSync6(path2, secret, { mode: 384 });
1565
1487
  return secret;
1566
1488
  }
1567
- 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(
1568
1490
  "--public-url <url>",
1569
1491
  "Serve remotely at this public URL (VPS): binds the dashboard to all interfaces, proxies the API same-origin, and requires login"
1570
1492
  ).option(
@@ -1683,6 +1605,32 @@ async function runCompose(opts) {
1683
1605
  );
1684
1606
  process.exit(1);
1685
1607
  }
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
+ }
1616
+ const prefetch = ora(
1617
+ sourceBuildDir() ? "Building images before touching :80/:443\u2026" : "Pulling images before touching :80/:443\u2026"
1618
+ ).start();
1619
+ const fetched = composePrefetch({
1620
+ apiPort,
1621
+ dashboardPort,
1622
+ publicUrl,
1623
+ trustProxy: opts.trustProxy,
1624
+ noHostControl: opts.hostControl === false ? true : void 0
1625
+ });
1626
+ if (!fetched) {
1627
+ prefetch.fail("Couldn't fetch the stack's images \u2014 nothing was changed on this box.");
1628
+ console.error(
1629
+ chalk6.dim("\n Your current proxy is untouched and still serving. Fix the pull/build error above and re-run.\n")
1630
+ );
1631
+ process.exit(1);
1632
+ }
1633
+ prefetch.succeed("Images ready \u2014 nothing on :80/:443 has changed yet.");
1686
1634
  let edgePlan;
1687
1635
  try {
1688
1636
  edgePlan = await planAndApplyHostEdge({ edge: opts.edge });
@@ -1700,14 +1648,15 @@ async function runCompose(opts) {
1700
1648
  );
1701
1649
  process.exit(1);
1702
1650
  }
1703
- const publicUrl = effectivePublicUrl(opts.publicUrl);
1704
1651
  const fromSource = sourceBuildDir();
1705
1652
  const spinner5 = ora(
1706
1653
  fromSource ? `Building Openship from ${fromSource} and starting the stack\u2026` : "Starting Openship via Docker Compose\u2026"
1707
1654
  ).start();
1708
- const res = composeUp({
1709
- apiPort: opts.port,
1710
- dashboardPort: opts.dashboardPort,
1655
+ const res = await composeUp({
1656
+ // Prefetched above, before the preflight stopped anything.
1657
+ alreadyFetched: true,
1658
+ apiPort,
1659
+ dashboardPort,
1711
1660
  publicUrl,
1712
1661
  trustProxy: opts.trustProxy,
1713
1662
  // commander maps `--no-host-control` to hostControl === false. `undefined` when
@@ -1724,11 +1673,11 @@ async function runCompose(opts) {
1724
1673
  process.exit(1);
1725
1674
  }
1726
1675
  spinner5.succeed("Openship is running via Docker Compose.");
1727
- if (edgePlan.action && !await edgeIsServing(new LocalExecutor())) {
1676
+ if (edgePlan.action && await edgeIsBroken(new LocalExecutor())) {
1728
1677
  const reason = await edgeCrashReason(new LocalExecutor());
1729
1678
  console.error(
1730
1679
  chalk6.red(`
1731
- Openship's edge is not serving :80${reason ? ` \u2014 ${reason}` : "."}`)
1680
+ Openship's edge container is not running${reason ? ` \u2014 ${reason}` : "."}`)
1732
1681
  );
1733
1682
  const restored = await rollbackHostEdge();
1734
1683
  console.error(
@@ -1804,8 +1753,8 @@ async function startService(opts, runOpts = {}) {
1804
1753
  `) + p.content + "\n"
1805
1754
  );
1806
1755
  return {
1807
- port: String(opts.port || "4000"),
1808
- dashPort: String(opts.dashboardPort || "3001"),
1756
+ port: String(opts.port || DEFAULT_API_PORT),
1757
+ dashPort: String(opts.dashboardPort || DEFAULT_DASHBOARD_PORT),
1809
1758
  publicUrl
1810
1759
  };
1811
1760
  }
@@ -1831,11 +1780,9 @@ async function startService(opts, runOpts = {}) {
1831
1780
  try {
1832
1781
  const res = installAndStart(flags);
1833
1782
  if (!runOpts.quiet) {
1834
- if (resolved.switched.api || resolved.switched.dashboard) {
1835
- console.log(
1836
- chalk6.yellow(`
1837
- A preferred port was busy \u2014 using API ${port}, dashboard ${dashPort}.`)
1838
- );
1783
+ const portNotice = portMoveNotice(resolved, [publicUrl]);
1784
+ if (portNotice.length) {
1785
+ console.log("\n" + portNotice.map((l) => chalk6.yellow(` ${l}`)).join("\n"));
1839
1786
  }
1840
1787
  const dashboardLine = publicUrl ? chalk6.dim(` Dashboard: ${publicUrl} (login required)
1841
1788
  `) : chalk6.dim(` Dashboard: http://localhost:${dashPort} (login required)
@@ -1866,8 +1813,8 @@ async function runForeground(opts, source) {
1866
1813
  apiArgs = ["run", "src/index.ts"];
1867
1814
  apiCwd = source.apiDir;
1868
1815
  } else {
1869
- const serverEntry = join8(SERVER_DIR, "index.js");
1870
- if (!existsSync7(serverEntry)) {
1816
+ const serverEntry = join7(SERVER_DIR, "index.js");
1817
+ if (!existsSync6(serverEntry)) {
1871
1818
  console.error(
1872
1819
  chalk6.red("\n Bundled server not found in this install.") + chalk6.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
1873
1820
  );
@@ -1883,28 +1830,42 @@ async function runForeground(opts, source) {
1883
1830
  const dashPort = String(resolved.dashboard);
1884
1831
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1885
1832
  const managedEdge = Boolean(opts.managedEdge && publicUrl);
1886
- const dataDir = opts.dataDir || join8(OS_DIR, "data");
1887
- mkdirSync8(dataDir, { recursive: true });
1888
- const logDir = join8(OS_DIR, "logs");
1889
- mkdirSync8(logDir, { recursive: true });
1890
- 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");
1891
1838
  const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
1892
1839
  const env = {
1893
1840
  ...process.env,
1894
1841
  PORT: port,
1895
1842
  NODE_ENV: "production",
1896
- // desktop mode in-process job runner (no Redis).
1897
- 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",
1898
1859
  OPENSHIP_TARGET: "local",
1899
1860
  OPENSHIP_JOB_RUNNER: "in-process",
1900
1861
  PGLITE_DATA_DIR: dataDir,
1901
1862
  BETTER_AUTH_SECRET: ensureAuthSecret()
1902
1863
  };
1903
1864
  if (!source) {
1904
- env.OPENSHIP_MIGRATIONS_DIR = join8(SERVER_DIR, "migrations");
1905
- env.OPENSHIP_PGLITE_ASSETS_DIR = join8(SERVER_DIR, "pglite");
1906
- const engineDir = join8(SERVER_DIR, "engine");
1907
- 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;
1908
1869
  }
1909
1870
  env.OPENSHIP_REQUIRE_AUTH = "true";
1910
1871
  env.INTERNAL_TOKEN = ensureInternalToken();
@@ -1996,7 +1957,7 @@ async function runForeground(opts, source) {
1996
1957
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
1997
1958
  try {
1998
1959
  const bundle = await ensureDashboard({
1999
- tag: source ? "local" : opts.uiVersion || `v${"0.4.7"}`,
1960
+ tag: source ? "local" : opts.uiVersion || `v${"0.5.0"}`,
2000
1961
  onProgress: (received, total) => {
2001
1962
  if (total) {
2002
1963
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -2126,7 +2087,7 @@ var stopCommand = new Command5("stop").description("Stop Openship (started by `o
2126
2087
  import { Command as Command6 } from "commander";
2127
2088
  import chalk8 from "chalk";
2128
2089
  import { confirm, isCancel as isCancel2 } from "@clack/prompts";
2129
- import { existsSync as existsSync8, rmSync as rmSync3 } from "fs";
2090
+ import { existsSync as existsSync7, rmSync as rmSync3 } from "fs";
2130
2091
  var uninstallCommand = new Command6("uninstall").description(
2131
2092
  "Remove Openship from this machine: stop it, delete its data (database, certs, edge config), its images, and ~/.openship. Deployed apps are left running."
2132
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) => {
@@ -2172,7 +2133,7 @@ var uninstallCommand = new Command6("uninstall").description(
2172
2133
  console.log(chalk8.yellow(` Couldn't remove the service: ${err2.message}`));
2173
2134
  }
2174
2135
  }
2175
- if (destructive && existsSync8(OS_DIR)) {
2136
+ if (destructive && existsSync7(OS_DIR)) {
2176
2137
  try {
2177
2138
  rmSync3(OS_DIR, { recursive: true, force: true });
2178
2139
  console.log(chalk8.green(` Removed ${OS_DIR}`));
@@ -2191,15 +2152,15 @@ var uninstallCommand = new Command6("uninstall").description(
2191
2152
 
2192
2153
  // src/commands/init.ts
2193
2154
  import { Command as Command7 } from "commander";
2194
- 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";
2195
2156
  import { createInterface as createInterface2 } from "readline/promises";
2196
2157
  import { stdin as input2, stdout as output2 } from "process";
2197
- import { join as join9 } from "path";
2158
+ import { join as join8 } from "path";
2198
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) => {
2199
2160
  const root = opts.dir || process.cwd();
2200
- const linkDir = join9(root, ".openship");
2201
- const linkPath = join9(linkDir, "project.json");
2202
- if (existsSync9(linkPath) && !opts.force) {
2161
+ const linkDir = join8(root, ".openship");
2162
+ const linkPath = join8(linkDir, "project.json");
2163
+ if (existsSync8(linkPath) && !opts.force) {
2203
2164
  err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
2204
2165
  process.exit(1);
2205
2166
  }
@@ -2252,8 +2213,8 @@ var initCommand = new Command7("init").description("Link the current directory t
2252
2213
  context: getActiveContext(),
2253
2214
  defaults: { environment: opts.environment || "production" }
2254
2215
  };
2255
- mkdirSync9(linkDir, { recursive: true });
2256
- writeFileSync8(linkPath, JSON.stringify(link, null, 2) + "\n");
2216
+ mkdirSync8(linkDir, { recursive: true });
2217
+ writeFileSync7(linkPath, JSON.stringify(link, null, 2) + "\n");
2257
2218
  if (isJsonMode()) {
2258
2219
  printJson({ path: linkPath, link });
2259
2220
  return;
@@ -2265,8 +2226,8 @@ var initCommand = new Command7("init").description("Link the current directory t
2265
2226
 
2266
2227
  // src/commands/config.ts
2267
2228
  import { Command as Command8 } from "commander";
2268
- import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync9 } from "fs";
2269
- 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";
2270
2231
  var SCHEMA_URL = "https://openship.io/openship.schema.json";
2271
2232
  var CONFIG_FILE = "openship.json";
2272
2233
  function detectHints(dir) {
@@ -2278,15 +2239,15 @@ function detectHints(dir) {
2278
2239
  ["package-lock.json", "npm"]
2279
2240
  ];
2280
2241
  for (const [file, pm] of lock) {
2281
- if (existsSync10(join10(dir, file))) {
2242
+ if (existsSync9(join9(dir, file))) {
2282
2243
  hints.packageManager = pm;
2283
2244
  break;
2284
2245
  }
2285
2246
  }
2286
- const pkgPath = join10(dir, "package.json");
2287
- if (existsSync10(pkgPath)) {
2247
+ const pkgPath = join9(dir, "package.json");
2248
+ if (existsSync9(pkgPath)) {
2288
2249
  try {
2289
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
2250
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
2290
2251
  const runner = hints.packageManager ?? "npm";
2291
2252
  const exec = runner === "npm" ? "npm run" : runner;
2292
2253
  if (pkg.scripts?.build) hints.buildCommand = `${exec} build`;
@@ -2298,15 +2259,15 @@ function detectHints(dir) {
2298
2259
  }
2299
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) => {
2300
2261
  const dir = opts.dir || process.cwd();
2301
- const path2 = join10(dir, CONFIG_FILE);
2302
- if (existsSync10(path2) && !opts.force) {
2262
+ const path2 = join9(dir, CONFIG_FILE);
2263
+ if (existsSync9(path2) && !opts.force) {
2303
2264
  err(`${CONFIG_FILE} already exists. Re-run with --force to overwrite.`);
2304
2265
  process.exit(1);
2305
2266
  }
2306
2267
  const hints = detectHints(dir);
2307
2268
  const scaffold = { $schema: SCHEMA_URL, ...hints };
2308
2269
  const text2 = JSON.stringify(scaffold, null, 2) + "\n";
2309
- writeFileSync9(path2, text2);
2270
+ writeFileSync8(path2, text2);
2310
2271
  if (isJsonMode()) {
2311
2272
  printJson({ path: path2, config: scaffold });
2312
2273
  return;
@@ -2316,13 +2277,13 @@ var initCmd = new Command8("init").description("Scaffold an openship.json in the
2316
2277
  info(" Edit it to declare framework, env, domains, resources, services\u2026 then `openship config validate`.\n");
2317
2278
  });
2318
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) => {
2319
- const path2 = file === CONFIG_FILE ? join10(process.cwd(), CONFIG_FILE) : file;
2320
- if (!existsSync10(path2)) {
2280
+ const path2 = file === CONFIG_FILE ? join9(process.cwd(), CONFIG_FILE) : file;
2281
+ if (!existsSync9(path2)) {
2321
2282
  if (isJsonMode()) printJson({ valid: false, errors: [`${path2} not found`], warnings: [] });
2322
2283
  else err(`Not found: ${path2}`);
2323
2284
  process.exit(1);
2324
2285
  }
2325
- const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync6(path2, "utf8"));
2286
+ const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync5(path2, "utf8"));
2326
2287
  const valid = errors.length === 0 && config !== null;
2327
2288
  if (isJsonMode()) {
2328
2289
  printJson({ valid, errors, warnings });
@@ -2395,15 +2356,6 @@ var contextCommand = new Command9("context").alias("ctx").description("Manage co
2395
2356
  // src/commands/status.ts
2396
2357
  import { Command as Command10 } from "commander";
2397
2358
  import chalk9 from "chalk";
2398
- import { readFileSync as readFileSync7 } from "fs";
2399
- import { join as join11 } from "path";
2400
- function readPorts() {
2401
- try {
2402
- return JSON.parse(readFileSync7(join11(OS_DIR, "ports.json"), "utf8"));
2403
- } catch {
2404
- return {};
2405
- }
2406
- }
2407
2359
  var statusCommand = new Command10("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
2408
2360
  const context = getActiveContext();
2409
2361
  const apiUrl = getApiUrl2();
@@ -2423,7 +2375,7 @@ var statusCommand = new Command10("status").description("Show the local Openship
2423
2375
  return;
2424
2376
  }
2425
2377
  const svc = serviceStatus();
2426
- const ports = readPorts();
2378
+ const ports = readStoredPorts();
2427
2379
  let health = null;
2428
2380
  let envInfo = null;
2429
2381
  let reachable = true;
@@ -2455,7 +2407,7 @@ var statusCommand = new Command10("status").description("Show the local Openship
2455
2407
 
2456
2408
  // src/commands/doctor.ts
2457
2409
  import { Command as Command11 } from "commander";
2458
- import { existsSync as existsSync13 } from "fs";
2410
+ import { existsSync as existsSync12 } from "fs";
2459
2411
  import { execFileSync as execFileSync2 } from "child_process";
2460
2412
  import chalk11 from "chalk";
2461
2413
  import open from "open";
@@ -2463,42 +2415,42 @@ import { intro, outro, note as note2, select as select3, log as log3, spinner as
2463
2415
 
2464
2416
  // src/lib/repair.ts
2465
2417
  import { spawnSync as spawnSync4 } from "child_process";
2466
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2467
- import { join as join13 } from "path";
2418
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
2419
+ import { join as join11 } from "path";
2468
2420
  import chalk10 from "chalk";
2469
2421
  import { spinner, log as log2, select as select2, isCancel as isCancel3, cancel } from "@clack/prompts";
2470
2422
 
2471
2423
  // src/lib/heal.ts
2472
2424
  import {
2473
- existsSync as existsSync11,
2425
+ existsSync as existsSync10,
2474
2426
  readdirSync,
2475
2427
  statSync,
2476
2428
  unlinkSync,
2477
- mkdirSync as mkdirSync10,
2429
+ mkdirSync as mkdirSync9,
2478
2430
  copyFileSync,
2479
2431
  renameSync
2480
2432
  } from "fs";
2481
2433
  import { homedir as homedir2 } from "os";
2482
- import { join as join12 } from "path";
2434
+ import { join as join10 } from "path";
2483
2435
  import { execFileSync } from "child_process";
2484
2436
  function resolveDataDir() {
2485
2437
  const fromEnv = process.env.PGLITE_DATA_DIR?.trim();
2486
2438
  if (fromEnv) {
2487
- return fromEnv.startsWith("~") ? join12(homedir2(), fromEnv.slice(1).replace(/^[/\\]/, "")) : fromEnv;
2439
+ return fromEnv.startsWith("~") ? join10(homedir2(), fromEnv.slice(1).replace(/^[/\\]/, "")) : fromEnv;
2488
2440
  }
2489
- return join12(OS_DIR, "data");
2441
+ return join10(OS_DIR, "data");
2490
2442
  }
2491
2443
  function dataDirExists(dataDir = resolveDataDir()) {
2492
- return existsSync11(dataDir);
2444
+ return existsSync10(dataDir);
2493
2445
  }
2494
2446
  function stamp() {
2495
2447
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2496
2448
  }
2497
2449
  function copyDirRecursive(src, dest) {
2498
- if (!existsSync11(dest)) mkdirSync10(dest, { recursive: true });
2450
+ if (!existsSync10(dest)) mkdirSync9(dest, { recursive: true });
2499
2451
  for (const entry of readdirSync(src)) {
2500
- const s = join12(src, entry);
2501
- const d = join12(dest, entry);
2452
+ const s = join10(src, entry);
2453
+ const d = join10(dest, entry);
2502
2454
  if (statSync(s).isDirectory()) copyDirRecursive(s, d);
2503
2455
  else copyFileSync(s, d);
2504
2456
  }
@@ -2510,30 +2462,30 @@ function backupDataDir(dataDir = resolveDataDir()) {
2510
2462
  }
2511
2463
  function healDataDir(dataDir = resolveDataDir()) {
2512
2464
  const result = { removedPostmasterPid: false, trimmedWalSegment: null };
2513
- const pidPath = join12(dataDir, "postmaster.pid");
2514
- if (existsSync11(pidPath)) {
2465
+ const pidPath = join10(dataDir, "postmaster.pid");
2466
+ if (existsSync10(pidPath)) {
2515
2467
  unlinkSync(pidPath);
2516
2468
  result.removedPostmasterPid = true;
2517
2469
  }
2518
- const walDir = join12(dataDir, "pg_wal");
2519
- if (existsSync11(walDir)) {
2520
- 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);
2521
2473
  if (segments.length > 0) {
2522
2474
  const newest = segments[0];
2523
- unlinkSync(join12(walDir, newest.name));
2475
+ unlinkSync(join10(walDir, newest.name));
2524
2476
  result.trimmedWalSegment = newest.name;
2525
2477
  }
2526
2478
  }
2527
2479
  return result;
2528
2480
  }
2529
2481
  function restoreBackup(backupDir, dataDir = resolveDataDir()) {
2530
- if (!existsSync11(backupDir)) throw new Error(`Backup not found: ${backupDir}`);
2531
- 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()}`);
2532
2484
  copyDirRecursive(backupDir, dataDir);
2533
2485
  }
2534
2486
  function freshStart(dataDir = resolveDataDir()) {
2535
2487
  const aside = `${dataDir}.corrupt-${stamp()}`;
2536
- if (existsSync11(dataDir)) renameSync(dataDir, aside);
2488
+ if (existsSync10(dataDir)) renameSync(dataDir, aside);
2537
2489
  return aside;
2538
2490
  }
2539
2491
  function findResetwal() {
@@ -2545,7 +2497,7 @@ function findResetwal() {
2545
2497
  "/usr/lib/postgresql/16/bin/pg_resetwal",
2546
2498
  "/usr/bin/pg_resetwal"
2547
2499
  ];
2548
- for (const c of candidates) if (existsSync11(c)) return c;
2500
+ for (const c of candidates) if (existsSync10(c)) return c;
2549
2501
  try {
2550
2502
  const found = execFileSync("bash", ["-lc", "command -v pg_resetwal"], {
2551
2503
  encoding: "utf8",
@@ -2566,7 +2518,7 @@ function deepHeal(dataDir = resolveDataDir()) {
2566
2518
  }
2567
2519
 
2568
2520
  // src/lib/repair.ts
2569
- var LOG_DIR2 = join13(OS_DIR, "logs");
2521
+ var LOG_DIR2 = join11(OS_DIR, "logs");
2570
2522
  function ensure(value) {
2571
2523
  if (isCancel3(value)) {
2572
2524
  cancel("Cancelled.");
@@ -2574,23 +2526,9 @@ function ensure(value) {
2574
2526
  }
2575
2527
  return value;
2576
2528
  }
2577
- function storedPorts() {
2578
- try {
2579
- const p = join13(OS_DIR, "ports.json");
2580
- return existsSync12(p) ? JSON.parse(readFileSync8(p, "utf8")) : {};
2581
- } catch {
2582
- return {};
2583
- }
2584
- }
2585
- function apiPort() {
2586
- return storedPorts().api ?? 4e3;
2587
- }
2588
- function dashboardPort() {
2589
- return storedPorts().dashboard ?? 3001;
2590
- }
2591
2529
  async function internalGet2(path2, timeoutMs = 8e3) {
2592
2530
  try {
2593
- const res = await fetch(`http://127.0.0.1:${apiPort()}${path2}`, {
2531
+ const res = await fetch(`http://127.0.0.1:${storedApiPort()}${path2}`, {
2594
2532
  headers: { "X-Internal-Token": ensureInternalToken() },
2595
2533
  signal: AbortSignal.timeout(timeoutMs)
2596
2534
  });
@@ -2602,7 +2540,7 @@ async function internalGet2(path2, timeoutMs = 8e3) {
2602
2540
  }
2603
2541
  async function apiReachable() {
2604
2542
  try {
2605
- 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`, {
2606
2544
  signal: AbortSignal.timeout(2500)
2607
2545
  });
2608
2546
  return res.ok;
@@ -2626,10 +2564,10 @@ async function waitApiDown(seconds = 20) {
2626
2564
  }
2627
2565
  function lastServiceError() {
2628
2566
  for (const name of ["up.err.log", "up.log", "instance.log"]) {
2629
- const p = join13(LOG_DIR2, name);
2630
- if (!existsSync12(p)) continue;
2567
+ const p = join11(LOG_DIR2, name);
2568
+ if (!existsSync11(p)) continue;
2631
2569
  try {
2632
- const lines = readFileSync8(p, "utf8").trim().split("\n");
2570
+ const lines = readFileSync6(p, "utf8").trim().split("\n");
2633
2571
  const hit = [...lines].reverse().find((l) => /error|locked|aborted|malformed|corrupt|throw|cannot|EADDRINUSE/i.test(l));
2634
2572
  if (hit) return hit.trim().slice(0, 240);
2635
2573
  } catch {
@@ -2687,11 +2625,11 @@ async function componentChecks(apiUp) {
2687
2625
  checks.push({
2688
2626
  name: "API",
2689
2627
  state: apiUp ? "pass" : "fail",
2690
- detail: apiUp ? `reachable on :${apiPort()}` : `not answering on :${apiPort()}`
2628
+ detail: apiUp ? `reachable on :${storedApiPort()}` : `not answering on :${storedApiPort()}`
2691
2629
  });
2692
2630
  let dashUp = false;
2693
2631
  try {
2694
- const res = await fetch(`http://127.0.0.1:${dashboardPort()}/`, {
2632
+ const res = await fetch(`http://127.0.0.1:${storedDashboardPort()}/`, {
2695
2633
  redirect: "manual",
2696
2634
  signal: AbortSignal.timeout(2500)
2697
2635
  });
@@ -2701,7 +2639,7 @@ async function componentChecks(apiUp) {
2701
2639
  checks.push({
2702
2640
  name: "Dashboard",
2703
2641
  state: dashUp ? "pass" : "warn",
2704
- detail: dashUp ? `serving on :${dashboardPort()}` : `not serving on :${dashboardPort()}`
2642
+ detail: dashUp ? `serving on :${storedDashboardPort()}` : `not serving on :${storedDashboardPort()}`
2705
2643
  });
2706
2644
  const edge = dockerNameRunning("openship-edge");
2707
2645
  checks.push(
@@ -2888,7 +2826,7 @@ function bunVersion() {
2888
2826
  }
2889
2827
  async function remotePreflight() {
2890
2828
  const checks = [];
2891
- const hasConfig = existsSync13(CONFIG_PATH);
2829
+ const hasConfig = existsSync12(CONFIG_PATH);
2892
2830
  checks.push({ name: "config", status: hasConfig ? "pass" : "warn", detail: hasConfig ? CONFIG_PATH : `not found; run \`openship login\`` });
2893
2831
  const context = getActiveContext();
2894
2832
  const apiUrl = getApiUrl();
@@ -3026,7 +2964,7 @@ async function interactiveDoctor() {
3026
2964
  }
3027
2965
  if (action2 === "open") {
3028
2966
  const url = readInstanceUrl();
3029
- 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()}`;
3030
2968
  await open(target).catch(() => {
3031
2969
  });
3032
2970
  log3.message(chalk11.dim(`Opening ${target}`));
@@ -3056,15 +2994,15 @@ import { execFileSync as execFileSync4 } from "child_process";
3056
2994
  import ora2 from "ora";
3057
2995
 
3058
2996
  // src/lib/project-link.ts
3059
- import { readFileSync as readFileSync9, existsSync as existsSync14 } from "fs";
3060
- import { join as join14, dirname as dirname3, parse } from "path";
3061
- 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");
3062
3000
  function findProjectLinkPath(from = process.cwd()) {
3063
3001
  let dir = from;
3064
3002
  const root = parse(dir).root;
3065
3003
  for (; ; ) {
3066
- const candidate = join14(dir, LINK_REL);
3067
- if (existsSync14(candidate)) return candidate;
3004
+ const candidate = join12(dir, LINK_REL);
3005
+ if (existsSync13(candidate)) return candidate;
3068
3006
  if (dir === root) return null;
3069
3007
  dir = dirname3(dir);
3070
3008
  }
@@ -3073,7 +3011,7 @@ function readProjectLink(from) {
3073
3011
  const path2 = findProjectLinkPath(from);
3074
3012
  if (!path2) return null;
3075
3013
  try {
3076
- return JSON.parse(readFileSync9(path2, "utf8"));
3014
+ return JSON.parse(readFileSync7(path2, "utf8"));
3077
3015
  } catch {
3078
3016
  return null;
3079
3017
  }
@@ -3081,21 +3019,21 @@ function readProjectLink(from) {
3081
3019
 
3082
3020
  // src/lib/folder-deploy.ts
3083
3021
  import { execFileSync as execFileSync3 } from "child_process";
3084
- 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";
3085
3023
  import { tmpdir } from "os";
3086
- import { join as join15, basename } from "path";
3024
+ import { join as join13, basename } from "path";
3087
3025
  function detectPackageManager(dir) {
3088
- if (existsSync15(join15(dir, "bun.lockb")) || existsSync15(join15(dir, "bun.lock"))) return "bun";
3089
- if (existsSync15(join15(dir, "pnpm-lock.yaml"))) return "pnpm";
3090
- if (existsSync15(join15(dir, "yarn.lock"))) return "yarn";
3091
- 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";
3092
3030
  return void 0;
3093
3031
  }
3094
3032
  function detectStack(dir) {
3095
- if (existsSync15(join15(dir, "go.mod"))) return "go";
3096
- if (existsSync15(join15(dir, "Cargo.toml"))) return "rust";
3097
- if (existsSync15(join15(dir, "requirements.txt")) || existsSync15(join15(dir, "pyproject.toml"))) return "python";
3098
- 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";
3099
3037
  return void 0;
3100
3038
  }
3101
3039
  async function deployFolder(opts) {
@@ -3112,7 +3050,7 @@ async function deployFolder(opts) {
3112
3050
  throw new Error(session.error || "Failed to open upload session");
3113
3051
  }
3114
3052
  step("Packaging folder");
3115
- const tarball = join15(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3053
+ const tarball = join13(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3116
3054
  execFileSync3(
3117
3055
  "tar",
3118
3056
  [
@@ -3131,7 +3069,7 @@ async function deployFolder(opts) {
3131
3069
  );
3132
3070
  step("Uploading source");
3133
3071
  try {
3134
- const body = readFileSync10(tarball);
3072
+ const body = readFileSync8(tarball);
3135
3073
  const up = session.upload;
3136
3074
  const method = up.method || "POST";
3137
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 });
@@ -3585,12 +3523,19 @@ var sslStatus = new Command13("status").description("Check SSL certificate statu
3585
3523
  printJson(res);
3586
3524
  })
3587
3525
  );
3588
- 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(
3589
3527
  run2(async (domain, opts) => {
3590
3528
  const res = await apiRequest("/deployments/ssl/renew", {
3591
3529
  method: "POST",
3592
3530
  body: JSON.stringify({ domain, includeWww: opts.www === true })
3593
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
+ }
3594
3539
  report(res, `SSL renewal requested for ${domain}`);
3595
3540
  })
3596
3541
  );
@@ -4370,8 +4315,10 @@ function mapComposeService(name, def, baseDir) {
4370
4315
  const volumes = mapVolumes(d.volumes);
4371
4316
  if (volumes.length) svc.volumes = volumes;
4372
4317
  const command = d.command;
4373
- if (typeof command === "string") svc.command = command;
4374
- 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
+ }
4375
4322
  if (typeof d.restart === "string") svc.restart = d.restart;
4376
4323
  return svc;
4377
4324
  }
@@ -5137,7 +5084,7 @@ var serverCommand = server;
5137
5084
  // src/commands/system.ts
5138
5085
  import { Command as Command19 } from "commander";
5139
5086
  import ora5 from "ora";
5140
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
5087
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
5141
5088
  import { createInterface as createInterface6 } from "readline/promises";
5142
5089
  import { stdin as input5, stdout as output5 } from "process";
5143
5090
  async function guarded(fn) {
@@ -5437,7 +5384,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
5437
5384
  );
5438
5385
  spin4?.succeed("Export ready.");
5439
5386
  if (opts.out) {
5440
- writeFileSync10(opts.out, JSON.stringify(file));
5387
+ writeFileSync9(opts.out, JSON.stringify(file));
5441
5388
  const tables = Object.keys(file.dump?.tables ?? {}).length;
5442
5389
  report2(
5443
5390
  { out: opts.out, tables },
@@ -5463,7 +5410,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
5463
5410
  }
5464
5411
  let file;
5465
5412
  try {
5466
- file = JSON.parse(readFileSync11(opts.file, "utf8"));
5413
+ file = JSON.parse(readFileSync9(opts.file, "utf8"));
5467
5414
  } catch {
5468
5415
  err(`
5469
5416
  Could not read or parse ${opts.file}.
@@ -5814,7 +5761,7 @@ var mailCommand = new Command20("mail").description("Self-hosted mail server (iR
5814
5761
  // src/commands/backup.ts
5815
5762
  import { Command as Command21 } from "commander";
5816
5763
  import ora7 from "ora";
5817
- import { readFileSync as readFileSync12 } from "fs";
5764
+ import { readFileSync as readFileSync10 } from "fs";
5818
5765
  async function guard3(fn) {
5819
5766
  try {
5820
5767
  await fn();
@@ -6107,7 +6054,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
6107
6054
  let sftpPrivateKey = opts.sftpPrivateKey;
6108
6055
  if (opts.sftpPrivateKeyFile) {
6109
6056
  try {
6110
- sftpPrivateKey = readFileSync12(opts.sftpPrivateKeyFile, "utf8");
6057
+ sftpPrivateKey = readFileSync10(opts.sftpPrivateKeyFile, "utf8");
6111
6058
  } catch {
6112
6059
  throw new Error(`Cannot read key file: ${opts.sftpPrivateKeyFile}`);
6113
6060
  }
@@ -6219,13 +6166,28 @@ var createCmd3 = new Command22("create").description("Mint a new personal access
6219
6166
  "Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
6220
6167
  collectGrant,
6221
6168
  []
6169
+ ).option(
6170
+ "--full-access",
6171
+ "Mint an UNSCOPED token with your own full access (required when no --grant is given)",
6172
+ false
6222
6173
  ).action(async (name, opts) => {
6223
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
+ }
6224
6185
  const body = {
6225
6186
  name,
6226
6187
  readOnly: opts.readOnly || void 0,
6227
6188
  expiresInDays: Number.isFinite(opts.expires) ? opts.expires : void 0,
6228
- grants: grants.length > 0 ? grants : void 0
6189
+ grants: grants.length > 0 ? grants : void 0,
6190
+ fullAccess: grants.length === 0 ? true : void 0
6229
6191
  };
6230
6192
  const sp = spin3(`Creating token "${name}"\u2026`);
6231
6193
  try {
@@ -6299,16 +6261,7 @@ var apiCommand = new Command23("api").description("Make an authenticated request
6299
6261
  import { Command as Command24 } from "commander";
6300
6262
  import chalk19 from "chalk";
6301
6263
  import { intro as intro2, outro as outro2, password as passwordPrompt, isCancel as isCancel4, cancel as cancel3, log as log4 } from "@clack/prompts";
6302
- import { readFileSync as readFileSync13 } from "fs";
6303
- import { join as join16 } from "path";
6304
- function resolvedApiPort() {
6305
- try {
6306
- return JSON.parse(readFileSync13(join16(OS_DIR, "ports.json"), "utf8")).api;
6307
- } catch {
6308
- return void 0;
6309
- }
6310
- }
6311
- 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) => {
6312
6265
  intro2(chalk19.cyan("Reset Openship admin password"));
6313
6266
  let pw = opts.password;
6314
6267
  if (!pw) {
@@ -6334,7 +6287,7 @@ var resetAdminCommand = new Command24("reset-admin-password").description("Reset
6334
6287
  }
6335
6288
  pw = entered;
6336
6289
  }
6337
- const port = String(opts.port || resolvedApiPort() || 4e3);
6290
+ const port = String(opts.port || storedApiPort());
6338
6291
  let res;
6339
6292
  try {
6340
6293
  res = await fetch(`http://127.0.0.1:${port}/api/system/reset-admin-password`, {
@@ -6357,10 +6310,10 @@ var resetAdminCommand = new Command24("reset-admin-password").description("Reset
6357
6310
 
6358
6311
  // src/commands/install.ts
6359
6312
  import { Command as Command25 } from "commander";
6360
- 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";
6361
6314
  import { spawn as spawn2, spawnSync as spawnSync6 } from "child_process";
6362
6315
  import { homedir as homedir3 } from "os";
6363
- import { join as join17 } from "path";
6316
+ import { join as join14 } from "path";
6364
6317
  import ora9 from "ora";
6365
6318
  function assetForPlatform() {
6366
6319
  const { platform, arch } = process;
@@ -6374,10 +6327,10 @@ function assetForPlatform() {
6374
6327
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
6375
6328
  }
6376
6329
  function installDmg(dmg) {
6377
- const homeApps = join17(homedir3(), "Applications");
6330
+ const homeApps = join14(homedir3(), "Applications");
6378
6331
  let dest = homeApps;
6379
6332
  try {
6380
- mkdirSync11(homeApps, { recursive: true });
6333
+ mkdirSync10(homeApps, { recursive: true });
6381
6334
  } catch {
6382
6335
  dest = "/Applications";
6383
6336
  }
@@ -6387,14 +6340,14 @@ function installDmg(dmg) {
6387
6340
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
6388
6341
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
6389
6342
  if (!mount) throw new Error("Could not determine the mounted volume");
6390
- let target = join17(dest, "Openship.app");
6343
+ let target = join14(dest, "Openship.app");
6391
6344
  try {
6392
- const appInDmg = join17(mount, "Openship.app");
6393
- 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");
6394
6347
  spawnSync6("rm", ["-rf", target]);
6395
6348
  let copy = spawnSync6("ditto", [appInDmg, target], { encoding: "utf8" });
6396
6349
  if (copy.status !== 0 && dest === homeApps) {
6397
- target = join17("/Applications", "Openship.app");
6350
+ target = join14("/Applications", "Openship.app");
6398
6351
  spawnSync6("rm", ["-rf", target]);
6399
6352
  copy = spawnSync6("ditto", [appInDmg, target], { encoding: "utf8" });
6400
6353
  }
@@ -6409,9 +6362,9 @@ function installAppImage(appImage) {
6409
6362
  return appImage;
6410
6363
  }
6411
6364
  function installZip(zip) {
6412
- const localAppData = process.env.LOCALAPPDATA || join17(homedir3(), "AppData", "Local");
6413
- const target = join17(localAppData, "Programs", "Openship");
6414
- 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 });
6415
6368
  const expand = spawnSync6(
6416
6369
  "powershell",
6417
6370
  [
@@ -6438,8 +6391,8 @@ function launch(kind, target) {
6438
6391
  child.unref();
6439
6392
  return;
6440
6393
  }
6441
- const exe = join17(target, "Openship.exe");
6442
- const path2 = existsSync16(exe) ? exe : target;
6394
+ const exe = join14(target, "Openship.exe");
6395
+ const path2 = existsSync15(exe) ? exe : target;
6443
6396
  spawnSync6("cmd", ["/c", "start", "", path2]);
6444
6397
  }
6445
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) => {
@@ -6465,13 +6418,13 @@ var installCommand = new Command25("install").description("Download and install
6465
6418
  process.exit(1);
6466
6419
  }
6467
6420
  const dir = releaseDir(tag);
6468
- const assetPath = join17(dir, asset.name);
6421
+ const assetPath = join14(dir, asset.name);
6469
6422
  const sidecarPath = `${assetPath}.sha256`;
6470
6423
  const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
6471
6424
  const sidecarUrl = `${assetUrl2}.sha256`;
6472
6425
  let downloaded = false;
6473
6426
  let sha;
6474
- const cachedUsable = !opts.force && existsSync16(assetPath) && (existsSync16(sidecarPath) || opts.verify === false);
6427
+ const cachedUsable = !opts.force && existsSync15(assetPath) && (existsSync15(sidecarPath) || opts.verify === false);
6475
6428
  if (cachedUsable) {
6476
6429
  info(` Using cached ${asset.name} (${tag}).`);
6477
6430
  } else {
@@ -6493,8 +6446,8 @@ var installCommand = new Command25("install").description("Download and install
6493
6446
  const s2 = spin4("Verifying checksum\u2026");
6494
6447
  try {
6495
6448
  let sidecarBody;
6496
- if (existsSync16(sidecarPath) && !downloaded) {
6497
- sidecarBody = readFileSync14(sidecarPath, "utf8");
6449
+ if (existsSync15(sidecarPath) && !downloaded) {
6450
+ sidecarBody = readFileSync11(sidecarPath, "utf8");
6498
6451
  } else {
6499
6452
  sidecarBody = await fetchSidecar(sidecarUrl);
6500
6453
  }
@@ -6517,8 +6470,8 @@ var installCommand = new Command25("install").description("Download and install
6517
6470
  err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
6518
6471
  process.exit(1);
6519
6472
  }
6520
- mkdirSync11(dir, { recursive: true });
6521
- writeFileSync11(sidecarPath, sidecarBody);
6473
+ mkdirSync10(dir, { recursive: true });
6474
+ writeFileSync10(sidecarPath, sidecarBody);
6522
6475
  s2?.succeed("Checksum verified");
6523
6476
  } catch (e) {
6524
6477
  s2?.fail("Verification failed");
@@ -6605,7 +6558,7 @@ function detectPackageManager2(override) {
6605
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) => {
6606
6559
  const source = readSourceInstall();
6607
6560
  if (source) return runSourceUpdate(source, opts);
6608
- const current = "0.4.7";
6561
+ const current = "0.5.0";
6609
6562
  let latest;
6610
6563
  try {
6611
6564
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -6640,7 +6593,7 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
6640
6593
  return;
6641
6594
  }
6642
6595
  if (readInstallMethod() === "compose") {
6643
- const applied = composeUpdate(latest);
6596
+ const applied = await composeUpdate(latest);
6644
6597
  if (isJsonMode()) {
6645
6598
  printJson({ updated: true, from: current, to: latest, via: pm, method: "compose", applied });
6646
6599
  } else if (applied) {
@@ -6663,17 +6616,17 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
6663
6616
 
6664
6617
  // src/commands/cache.ts
6665
6618
  import { Command as Command27 } from "commander";
6666
- import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync15, rmSync as rmSync5, statSync as statSync2 } from "fs";
6667
- 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";
6668
6621
  function listAssets() {
6669
- if (!existsSync17(RELEASES_DIR)) return [];
6622
+ if (!existsSync16(RELEASES_DIR)) return [];
6670
6623
  const out = [];
6671
6624
  for (const tag of readdirSync2(RELEASES_DIR)) {
6672
6625
  const dir = releaseDir(tag);
6673
6626
  if (!statSync2(dir).isDirectory()) continue;
6674
6627
  for (const name of readdirSync2(dir)) {
6675
6628
  if (name.endsWith(".sha256")) continue;
6676
- const path2 = join18(dir, name);
6629
+ const path2 = join15(dir, name);
6677
6630
  const st = statSync2(path2);
6678
6631
  if (!st.isFile()) continue;
6679
6632
  out.push({
@@ -6681,7 +6634,7 @@ function listAssets() {
6681
6634
  name,
6682
6635
  path: path2,
6683
6636
  size: st.size,
6684
- hasSidecar: existsSync17(`${path2}.sha256`)
6637
+ hasSidecar: existsSync16(`${path2}.sha256`)
6685
6638
  });
6686
6639
  }
6687
6640
  }
@@ -6712,7 +6665,7 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
6712
6665
  results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
6713
6666
  continue;
6714
6667
  }
6715
- const expected = parseSha256(readFileSync15(`${a.path}.sha256`, "utf8"));
6668
+ const expected = parseSha256(readFileSync12(`${a.path}.sha256`, "utf8"));
6716
6669
  const actual = await hashFile(a.path);
6717
6670
  const okMatch = expected !== null && expected === actual;
6718
6671
  if (!okMatch) bad += 1;
@@ -6728,7 +6681,7 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
6728
6681
  });
6729
6682
  var cleanCmd = new Command27("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
6730
6683
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
6731
- if (!existsSync17(target)) {
6684
+ if (!existsSync16(target)) {
6732
6685
  if (isJsonMode()) printJson({ removed: false, path: target });
6733
6686
  else info(` Nothing to clean (${target}).`);
6734
6687
  return;
@@ -6748,8 +6701,8 @@ var cacheCommand = new Command27("cache").description("Manage the local download
6748
6701
  import chalk20 from "chalk";
6749
6702
  import open2 from "open";
6750
6703
  import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
6751
- import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync16, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "fs";
6752
- 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";
6753
6706
  import {
6754
6707
  intro as intro3,
6755
6708
  outro as outro3,
@@ -6895,13 +6848,13 @@ async function streamProvision(port, sessionId, s) {
6895
6848
  }
6896
6849
  return { ok: ok3, detail };
6897
6850
  }
6898
- var SETUP_LOCK = join19(OS_DIR, "setup-in-progress");
6851
+ var SETUP_LOCK = join16(OS_DIR, "setup-in-progress");
6899
6852
  function isSetupInProgress() {
6900
- return existsSync18(SETUP_LOCK);
6853
+ return existsSync17(SETUP_LOCK);
6901
6854
  }
6902
6855
  function markSetupStarted() {
6903
- mkdirSync12(OS_DIR, { recursive: true });
6904
- writeFileSync12(SETUP_LOCK, "1");
6856
+ mkdirSync11(OS_DIR, { recursive: true });
6857
+ writeFileSync11(SETUP_LOCK, "1");
6905
6858
  }
6906
6859
  function markSetupDone() {
6907
6860
  rmSync6(SETUP_LOCK, { force: true });
@@ -7162,17 +7115,22 @@ async function runWizard() {
7162
7115
  }
7163
7116
  let method = "bare";
7164
7117
  if (process.platform === "linux") {
7165
- if (hasDockerCompose()) {
7118
+ const gap = dockerGap();
7119
+ if (!gap) {
7166
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";
7167
7125
  } else {
7168
- log5.step("Docker isn't installed \u2014 installing it now (get.docker.com)\u2026");
7169
- 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";
7170
7128
  if (method === "bare") {
7171
7129
  log5.warn("Couldn't install Docker automatically \u2014 falling back to the bare process service.");
7172
7130
  }
7173
7131
  }
7174
7132
  }
7175
- const uiTag = `v${"0.4.7"}`;
7133
+ const uiTag = `v${"0.5.0"}`;
7176
7134
  if (method === "bare") {
7177
7135
  const dl = spinner4();
7178
7136
  dl.start("Pulling the Openship dist from GitHub");
@@ -7226,6 +7184,25 @@ async function runWizard() {
7226
7184
  }
7227
7185
  }
7228
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"));
7192
+ log5.step(
7193
+ sourceBuildDir() ? "Building the Openship images before touching :80/:443 (first run takes a few minutes)\u2026" : "Pulling images before touching :80/:443\u2026"
7194
+ );
7195
+ if (!composePrefetch({
7196
+ apiPort,
7197
+ dashboardPort,
7198
+ publicUrl,
7199
+ trustProxy: behindProxy,
7200
+ version: "0.5.0",
7201
+ noHostControl: !allowHostControl
7202
+ })) {
7203
+ cancel4("Couldn't fetch the Openship images \u2014 nothing on this box was changed.");
7204
+ process.exit(1);
7205
+ }
7229
7206
  const edgePlan = await planAndApplyHostEdge({});
7230
7207
  if (!edgePlan.proceed) {
7231
7208
  cancel4("Left the existing proxy on 80/443 running \u2014 re-run and choose migrate/takeover when ready.");
@@ -7234,13 +7211,15 @@ async function runWizard() {
7234
7211
  edgeAction = edgePlan.action;
7235
7212
  migratedSites = edgePlan.sites;
7236
7213
  migratedCertPems = edgePlan.certPems;
7237
- log5.step(
7238
- sourceBuildDir() ? "Building the Openship images from your source checkout (first run takes a few minutes)\u2026" : "Pulling images and starting the Docker Compose stack\u2026"
7239
- );
7240
- const up = composeUp({
7214
+ log5.step("Starting the Docker Compose stack\u2026");
7215
+ const up = await composeUp({
7216
+ // Prefetched above, before the preflight stopped anything.
7217
+ alreadyFetched: true,
7218
+ apiPort,
7219
+ dashboardPort,
7241
7220
  publicUrl,
7242
7221
  trustProxy: behindProxy,
7243
- version: "0.4.7",
7222
+ version: "0.5.0",
7244
7223
  noHostControl: !allowHostControl
7245
7224
  });
7246
7225
  if (!up.ok) {
@@ -7471,22 +7450,14 @@ async function runWizard() {
7471
7450
  byo: domainPlan.type === "byo"
7472
7451
  });
7473
7452
  }
7474
- function storedPorts2() {
7475
- const p = join19(OS_DIR, "ports.json");
7476
- try {
7477
- return existsSync18(p) ? JSON.parse(readFileSync16(p, "utf8")) : {};
7478
- } catch {
7479
- return {};
7480
- }
7481
- }
7482
7453
  async function runControl() {
7483
7454
  const svc = serviceStatus();
7484
7455
  const isCompose = readInstallMethod() === "compose";
7485
7456
  const running = isCompose ? composeRunning() : svc.running;
7486
7457
  const managerLabel = isCompose ? "docker compose" : svc.kind === "unsupported" ? "none" : svc.kind;
7487
- const ports = storedPorts2();
7488
- const apiPort2 = String(ports.api ?? 4e3);
7489
- const dashUrl = `http://localhost:${ports.dashboard ?? 3001}`;
7458
+ const ports = readStoredPorts();
7459
+ const apiPort = String(storedApiPort());
7460
+ const dashUrl = `http://localhost:${storedDashboardPort()}`;
7490
7461
  const publicUrl = readInstanceUrl();
7491
7462
  const primaryUrl = publicUrl && !/^https?:\/\/localhost/i.test(publicUrl) ? publicUrl : dashUrl;
7492
7463
  intro3(`${chalk20.bgCyan(chalk20.black(" Openship "))}${chalk20.dim(" control")}`);
@@ -7561,7 +7532,7 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7561
7532
  const pw = ensure2(
7562
7533
  await password({ message: "New admin password", validate: (v) => v && v.length >= 8 ? void 0 : "At least 8 characters" })
7563
7534
  );
7564
- 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 });
7565
7536
  outro3(
7566
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"}`)
7567
7538
  );
@@ -7577,7 +7548,7 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7577
7548
 
7578
7549
  // src/index.ts
7579
7550
  var program = new Command28();
7580
- program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.4.7").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) => {
7581
7552
  if (thisCommand.opts().json) setJsonMode(true);
7582
7553
  }).action(async () => {
7583
7554
  const installed = serviceStatus().installed || readInstallMethod() === "compose";