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