openship 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/bare-IWS2QHJ3.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-BS2AXYZX.js → chunk-2NZSUXOB.js} +647 -214
- 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-NNPEG6HM.js → chunk-3NZICQHF.js} +2 -2
- 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-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-RB35YN3J.js → chunk-EW6KB2J7.js} +391 -63
- package/dist/chunk-I3DPMTSF.js +29 -0
- package/dist/{chunk-NXFIRK2Z.js → chunk-IOT5ZX23.js} +573 -459
- 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-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-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
- package/dist/{chunk-6APIPIJY.js → chunk-QWHBRY2K.js} +5 -5
- package/dist/{chunk-JLKKYUZA.js → chunk-RRJJGVKE.js} +7 -7
- 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-K2RPVGPN.js → chunk-UC7YIEHT.js} +36 -17
- package/dist/{chunk-I6PSTQIR.js → chunk-VDDMADOF.js} +3 -3
- 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-YIPMDGG6.js +703 -0
- 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-7QYOGF7F.js → cloud-KRTZ2IL5.js} +3 -3
- package/dist/cloud-M2YLWEXO.js +31 -0
- 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-SAYTRJLO.js +48 -0
- package/dist/ensure-container-edge-CFSCPGZK.js +32 -0
- package/dist/{event-streams-EHRAKL75.js → event-streams-TR4GAC53.js} +4 -4
- package/dist/{executor-LOYMPU67.js → executor-PT6UEJ5M.js} +8 -8
- package/dist/index.js +1228 -463
- 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-YOEEFBTE.js +31 -0
- package/dist/node-entry.js +4 -0
- package/dist/{noop-FMMFG2EZ.js → noop-NJ72F46T.js} +3 -3
- package/dist/openresty-lua-WGBO2M3Z.js +81 -0
- package/dist/server/index.js +23276 -8015
- package/dist/server/lua/geo_country.lua +34 -8
- package/dist/server/lua/maxminddb.lua +403 -0
- package/dist/server/lua/mgmt_api.lua +431 -68
- package/dist/server/lua/pipe_stream.lua +52 -39
- package/dist/server/lua/site_logger.lua +236 -30
- 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-M2ZDGH3O.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/server/lua/pipe_log.lua +0 -71
- 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-2NZSUXOB.js";
|
|
50
|
+
import "./chunk-XZYFZ6A5.js";
|
|
51
|
+
import "./chunk-PDW4L3NV.js";
|
|
52
|
+
import "./chunk-QWHBRY2K.js";
|
|
53
|
+
import "./chunk-DOD2BXHQ.js";
|
|
54
|
+
import "./chunk-42WQABXM.js";
|
|
55
|
+
import "./chunk-DRK3MEDS.js";
|
|
56
|
+
import "./chunk-3NQQXMDR.js";
|
|
57
|
+
import "./chunk-TWJCLD4W.js";
|
|
58
|
+
import "./chunk-RRJJGVKE.js";
|
|
59
|
+
import "./chunk-WNWQBB7F.js";
|
|
60
|
+
import "./chunk-2GWYYHOR.js";
|
|
61
|
+
import "./chunk-RXQBD4ZY.js";
|
|
62
|
+
import "./chunk-3NZICQHF.js";
|
|
55
63
|
import {
|
|
56
64
|
beginEdgeTakeover,
|
|
57
65
|
collectProxyCerts,
|
|
58
66
|
completeEdgeTakeover,
|
|
67
|
+
copyStaticRootIntoEdge,
|
|
59
68
|
detectInstalledProxy,
|
|
60
69
|
edgeProxy,
|
|
61
70
|
edgeProxyFor,
|
|
@@ -63,42 +72,45 @@ import {
|
|
|
63
72
|
importSites,
|
|
64
73
|
recoverInterruptedTakeover,
|
|
65
74
|
rollbackEdgeTakeover,
|
|
66
|
-
scanImportableSites
|
|
67
|
-
|
|
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-IOT5ZX23.js";
|
|
78
|
+
import "./chunk-ONKEYSMY.js";
|
|
79
|
+
import "./chunk-ZWLDLTJP.js";
|
|
80
|
+
import "./chunk-3FYIXVX5.js";
|
|
81
|
+
import "./chunk-EMXI4UCU.js";
|
|
82
|
+
import "./chunk-IXM5245F.js";
|
|
83
|
+
import "./chunk-JIMUYEKN.js";
|
|
84
|
+
import "./chunk-IW2UKBVF.js";
|
|
85
|
+
import "./chunk-VDDMADOF.js";
|
|
86
|
+
import "./chunk-UC7YIEHT.js";
|
|
87
|
+
import "./chunk-6ODUBKUB.js";
|
|
88
|
+
import "./chunk-ZCFVHA25.js";
|
|
79
89
|
import {
|
|
80
90
|
LocalExecutor
|
|
81
|
-
} from "./chunk-
|
|
82
|
-
import "./chunk-
|
|
83
|
-
import "./chunk-
|
|
91
|
+
} from "./chunk-6FH4AXVY.js";
|
|
92
|
+
import "./chunk-EW6KB2J7.js";
|
|
93
|
+
import "./chunk-I3DPMTSF.js";
|
|
84
94
|
import {
|
|
85
95
|
edgeCrashReason,
|
|
86
96
|
edgeIsBroken,
|
|
87
97
|
ourEdgeContainerRunning
|
|
88
|
-
} from "./chunk-
|
|
89
|
-
import "./chunk-
|
|
90
|
-
import "./chunk-
|
|
98
|
+
} from "./chunk-VZ2ZHPS2.js";
|
|
99
|
+
import "./chunk-3LDWWY7U.js";
|
|
100
|
+
import "./chunk-YIPMDGG6.js";
|
|
101
|
+
import "./chunk-ZFJXPCQP.js";
|
|
91
102
|
import {
|
|
92
103
|
CLOUD_DASHBOARD_URL,
|
|
93
104
|
LOCAL_API_URL,
|
|
94
105
|
LOCAL_DASHBOARD_URL,
|
|
106
|
+
buildMailImageRef,
|
|
95
107
|
cliInstallCommand,
|
|
96
108
|
commandToArgv,
|
|
97
109
|
isValidEmail,
|
|
98
110
|
parseOpenshipConfigJson,
|
|
99
111
|
resolveCliUpdatePlan
|
|
100
|
-
} from "./chunk-
|
|
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.1" : "dev"}`;
|
|
259
272
|
function getApiUrl2() {
|
|
260
273
|
return `${getApiUrl()}/api`;
|
|
261
274
|
}
|
|
@@ -272,6 +285,7 @@ var ApiError = class extends Error {
|
|
|
272
285
|
function buildHeaders(extra) {
|
|
273
286
|
const headers = new Headers(extra);
|
|
274
287
|
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
288
|
+
if (!headers.has("User-Agent")) headers.set("User-Agent", USER_AGENT);
|
|
275
289
|
const token = getToken();
|
|
276
290
|
if (token && !headers.has("Authorization")) headers.set("Authorization", `Bearer ${token}`);
|
|
277
291
|
return headers;
|
|
@@ -496,12 +510,12 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
|
|
|
496
510
|
|
|
497
511
|
// src/commands/up.ts
|
|
498
512
|
import { Command as Command4 } from "commander";
|
|
499
|
-
import
|
|
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.1"}`,
|
|
1961
2282
|
onProgress: (received, total) => {
|
|
1962
2283
|
if (total) {
|
|
1963
2284
|
uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
|
|
@@ -2032,7 +2353,7 @@ async function runForeground(opts, source) {
|
|
|
2032
2353
|
} catch (e) {
|
|
2033
2354
|
uiSpinner.warn(`Dashboard unavailable: ${e.message}`);
|
|
2034
2355
|
console.log(
|
|
2035
|
-
|
|
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");
|
|
@@ -6510,8 +6908,257 @@ var installCommand = new Command25("install").description("Download and install
|
|
|
6510
6908
|
});
|
|
6511
6909
|
|
|
6512
6910
|
// src/commands/update.ts
|
|
6513
|
-
import { Command as Command26 } from "commander";
|
|
6514
|
-
import { spawnSync as
|
|
6911
|
+
import { Command as Command26, Option } from "commander";
|
|
6912
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
6913
|
+
|
|
6914
|
+
// src/lib/cli-payload.ts
|
|
6915
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
6916
|
+
import {
|
|
6917
|
+
existsSync as existsSync18,
|
|
6918
|
+
lstatSync,
|
|
6919
|
+
mkdirSync as mkdirSync12,
|
|
6920
|
+
readdirSync as readdirSync2,
|
|
6921
|
+
rmSync as rmSync5,
|
|
6922
|
+
symlinkSync,
|
|
6923
|
+
writeFileSync as writeFileSync12
|
|
6924
|
+
} from "fs";
|
|
6925
|
+
import { join as join17 } from "path";
|
|
6926
|
+
var CLI_DIR = join17(OS_DIR, "cli");
|
|
6927
|
+
var CLI_CURRENT = join17(CLI_DIR, "current");
|
|
6928
|
+
var KEEP = 2;
|
|
6929
|
+
function assetName2(tag) {
|
|
6930
|
+
return `openship-cli-${tag}.tar.gz`;
|
|
6931
|
+
}
|
|
6932
|
+
function repoint(link, targetTag) {
|
|
6933
|
+
try {
|
|
6934
|
+
if (lstatSync(link)) rmSync5(link, { recursive: true, force: true });
|
|
6935
|
+
} catch {
|
|
6936
|
+
}
|
|
6937
|
+
symlinkSync(targetTag, link, process.platform === "win32" ? "junction" : void 0);
|
|
6938
|
+
}
|
|
6939
|
+
function prune(activeTag) {
|
|
6940
|
+
let entries;
|
|
6941
|
+
try {
|
|
6942
|
+
entries = readdirSync2(CLI_DIR);
|
|
6943
|
+
} catch {
|
|
6944
|
+
return;
|
|
6945
|
+
}
|
|
6946
|
+
const tags = entries.filter((e) => e !== "current" && existsSync18(join17(CLI_DIR, e, ".extracted")));
|
|
6947
|
+
const ranked = tags.map((t) => ({ t, m: safeMtime(join17(CLI_DIR, t)) })).sort((a, b) => b.m - a.m).map((x) => x.t);
|
|
6948
|
+
const keep2 = /* @__PURE__ */ new Set([activeTag, ...ranked.slice(0, KEEP)]);
|
|
6949
|
+
for (const t of tags) {
|
|
6950
|
+
if (!keep2.has(t)) rmSync5(join17(CLI_DIR, t), { recursive: true, force: true });
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
function safeMtime(p) {
|
|
6954
|
+
try {
|
|
6955
|
+
return lstatSync(p).mtimeMs;
|
|
6956
|
+
} catch {
|
|
6957
|
+
return 0;
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
async function ensureCliPayload(tag, onProgress) {
|
|
6961
|
+
const resolved = tag ?? await resolveLatestTag();
|
|
6962
|
+
const dir = join17(CLI_DIR, resolved);
|
|
6963
|
+
const entry = join17(dir, "dist", "index.js");
|
|
6964
|
+
const marker = join17(dir, ".extracted");
|
|
6965
|
+
if (existsSync18(marker) && existsSync18(entry)) {
|
|
6966
|
+
repoint(CLI_CURRENT, resolved);
|
|
6967
|
+
prune(resolved);
|
|
6968
|
+
return { tag: resolved, dir, entry };
|
|
6969
|
+
}
|
|
6970
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
6971
|
+
mkdirSync12(dir, { recursive: true });
|
|
6972
|
+
const name = assetName2(resolved);
|
|
6973
|
+
const override = process.env.OPENSHIP_CLI_ASSET_URL?.trim();
|
|
6974
|
+
const url = override || assetUrl(resolved, name);
|
|
6975
|
+
const tarball = join17(dir, name);
|
|
6976
|
+
const { sha256 } = await downloadToFile(url, tarball, onProgress);
|
|
6977
|
+
const expected = override ? parseSha256(await fetchSidecar(`${url}.sha256`) ?? "") : await expectedSha256(resolved, name);
|
|
6978
|
+
if (!expected) {
|
|
6979
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
6980
|
+
throw new Error(`No .sha256 sidecar for ${name}; refusing to install an unverified CLI payload.`);
|
|
6981
|
+
}
|
|
6982
|
+
if (expected !== sha256) {
|
|
6983
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
6984
|
+
throw new Error(`CLI payload checksum mismatch (expected ${expected}, got ${sha256}).`);
|
|
6985
|
+
}
|
|
6986
|
+
const untar = spawnSync8("tar", ["-xzf", tarball, "-C", dir], { stdio: "inherit" });
|
|
6987
|
+
if (untar.status !== 0) {
|
|
6988
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
6989
|
+
throw new Error("Failed to extract the CLI payload (is `tar` available?).");
|
|
6990
|
+
}
|
|
6991
|
+
rmSync5(tarball, { force: true });
|
|
6992
|
+
if (!existsSync18(entry)) {
|
|
6993
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
6994
|
+
throw new Error(`CLI payload extracted but ${entry} is missing (unexpected layout).`);
|
|
6995
|
+
}
|
|
6996
|
+
writeFileSync12(marker, `${resolved}
|
|
6997
|
+
`);
|
|
6998
|
+
repoint(CLI_CURRENT, resolved);
|
|
6999
|
+
prune(resolved);
|
|
7000
|
+
return { tag: resolved, dir, entry };
|
|
7001
|
+
}
|
|
7002
|
+
|
|
7003
|
+
// src/lib/node-runtime.ts
|
|
7004
|
+
import { execFileSync as execFileSync5, spawnSync as spawnSync9 } from "child_process";
|
|
7005
|
+
import { chmodSync as chmodSync3, existsSync as existsSync19, lstatSync as lstatSync2, mkdirSync as mkdirSync13, rmSync as rmSync6, symlinkSync as symlinkSync2, writeFileSync as writeFileSync13 } from "fs";
|
|
7006
|
+
import { join as join18 } from "path";
|
|
7007
|
+
var NODE_MAJOR = 22;
|
|
7008
|
+
var NODE_DIST = "https://nodejs.org/dist";
|
|
7009
|
+
var RUNTIME_DIR = join18(OS_DIR, "runtime");
|
|
7010
|
+
var RUNTIME_CURRENT = join18(RUNTIME_DIR, "current");
|
|
7011
|
+
var BIN_DIR = join18(OS_DIR, "bin");
|
|
7012
|
+
var LAUNCHER_PATH = join18(BIN_DIR, "openship");
|
|
7013
|
+
var LAUNCHER_SH = `#!/bin/sh
|
|
7014
|
+
# Openship CLI launcher \u2014 runs the tarball-installed CLI under Node.
|
|
7015
|
+
# Managed by scripts/install.sh and \`openship update\`; manual edits are overwritten.
|
|
7016
|
+
SELF="$0"
|
|
7017
|
+
while [ -L "$SELF" ]; do
|
|
7018
|
+
_t="$(readlink "$SELF")"
|
|
7019
|
+
case "$_t" in /*) SELF="$_t" ;; *) SELF="$(dirname "$SELF")/$_t" ;; esac
|
|
7020
|
+
done
|
|
7021
|
+
BIN_DIR="$(cd "$(dirname "$SELF")" && pwd)"
|
|
7022
|
+
HOME_DIR="$(dirname "$BIN_DIR")"
|
|
7023
|
+
CLI="$HOME_DIR/cli/current/dist/index.js"
|
|
7024
|
+
VENDORED_NODE="$HOME_DIR/runtime/current/bin/node"
|
|
7025
|
+
# Prefer a system node >= ${NODE_MAJOR}; fall back to the vendored runtime.
|
|
7026
|
+
if command -v node >/dev/null 2>&1 && node -e 'process.exit(+process.versions.node.split(".")[0]>=${NODE_MAJOR}?0:1)' 2>/dev/null; then
|
|
7027
|
+
exec node "$CLI" "$@"
|
|
7028
|
+
elif [ -x "$VENDORED_NODE" ]; then
|
|
7029
|
+
exec "$VENDORED_NODE" "$CLI" "$@"
|
|
7030
|
+
else
|
|
7031
|
+
echo "openship: no Node >= ${NODE_MAJOR} found (system or vendored). Reinstall: curl -fsSL https://get.openship.io | sh" >&2
|
|
7032
|
+
exit 127
|
|
7033
|
+
fi
|
|
7034
|
+
`;
|
|
7035
|
+
function nodeDistOs() {
|
|
7036
|
+
if (process.platform === "darwin") return "darwin";
|
|
7037
|
+
if (process.platform === "linux") return "linux";
|
|
7038
|
+
throw new Error(`Unsupported platform for vendored Node: ${process.platform}`);
|
|
7039
|
+
}
|
|
7040
|
+
function nodeDistArch() {
|
|
7041
|
+
if (process.arch === "x64") return "x64";
|
|
7042
|
+
if (process.arch === "arm64") return "arm64";
|
|
7043
|
+
throw new Error(`Unsupported arch for vendored Node: ${process.arch}`);
|
|
7044
|
+
}
|
|
7045
|
+
function majorOf(bin) {
|
|
7046
|
+
try {
|
|
7047
|
+
const out = execFileSync5(bin, ["-e", "process.stdout.write(process.versions.node)"], {
|
|
7048
|
+
encoding: "utf8",
|
|
7049
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
7050
|
+
});
|
|
7051
|
+
const m = Number(out.trim().split(".")[0]);
|
|
7052
|
+
return Number.isInteger(m) ? m : null;
|
|
7053
|
+
} catch {
|
|
7054
|
+
return null;
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
7057
|
+
function writeLauncher() {
|
|
7058
|
+
mkdirSync13(BIN_DIR, { recursive: true });
|
|
7059
|
+
writeFileSync13(LAUNCHER_PATH, LAUNCHER_SH);
|
|
7060
|
+
chmodSync3(LAUNCHER_PATH, 493);
|
|
7061
|
+
return LAUNCHER_PATH;
|
|
7062
|
+
}
|
|
7063
|
+
function repoint2(link, target) {
|
|
7064
|
+
try {
|
|
7065
|
+
if (lstatSync2(link)) rmSync6(link, { recursive: true, force: true });
|
|
7066
|
+
} catch {
|
|
7067
|
+
}
|
|
7068
|
+
symlinkSync2(target, link);
|
|
7069
|
+
}
|
|
7070
|
+
async function resolveLatestNode() {
|
|
7071
|
+
const res = await fetch(`${NODE_DIST}/index.json`, {
|
|
7072
|
+
headers: { "User-Agent": "openship-cli" },
|
|
7073
|
+
signal: AbortSignal.timeout(15e3)
|
|
7074
|
+
});
|
|
7075
|
+
if (!res.ok) throw new Error(`Node dist index lookup failed: HTTP ${res.status}`);
|
|
7076
|
+
const rows = await res.json();
|
|
7077
|
+
const prefix = `v${NODE_MAJOR}.`;
|
|
7078
|
+
const row2 = rows.find((r) => r.version.startsWith(prefix) && r.lts) ?? rows.find((r) => r.version.startsWith(prefix));
|
|
7079
|
+
if (!row2) throw new Error(`No Node ${NODE_MAJOR}.x release found on nodejs.org`);
|
|
7080
|
+
return row2.version;
|
|
7081
|
+
}
|
|
7082
|
+
async function fetchVendoredNode(onLog) {
|
|
7083
|
+
const ver = await resolveLatestNode();
|
|
7084
|
+
const os = nodeDistOs();
|
|
7085
|
+
const arch = nodeDistArch();
|
|
7086
|
+
const name = `node-${ver}-${os}-${arch}.tar.gz`;
|
|
7087
|
+
mkdirSync13(RUNTIME_DIR, { recursive: true });
|
|
7088
|
+
const tarball = join18(RUNTIME_DIR, name);
|
|
7089
|
+
onLog?.(`Fetching Node ${ver} (${os}-${arch}) from nodejs.org\u2026`);
|
|
7090
|
+
const { sha256 } = await downloadToFile(`${NODE_DIST}/${ver}/${name}`, tarball);
|
|
7091
|
+
const shaRes = await fetch(`${NODE_DIST}/${ver}/SHASUMS256.txt`, {
|
|
7092
|
+
headers: { "User-Agent": "openship-cli" },
|
|
7093
|
+
signal: AbortSignal.timeout(15e3)
|
|
7094
|
+
});
|
|
7095
|
+
if (!shaRes.ok) {
|
|
7096
|
+
rmSync6(tarball, { force: true });
|
|
7097
|
+
throw new Error(`Node checksum list fetch failed: HTTP ${shaRes.status}`);
|
|
7098
|
+
}
|
|
7099
|
+
const expected = (await shaRes.text()).split("\n").map((l) => l.trim()).find((l) => l.endsWith(` ${name}`) || l.endsWith(` ${name}`))?.split(/\s+/)[0]?.toLowerCase();
|
|
7100
|
+
if (!expected) {
|
|
7101
|
+
rmSync6(tarball, { force: true });
|
|
7102
|
+
throw new Error(`No checksum for ${name} in SHASUMS256.txt`);
|
|
7103
|
+
}
|
|
7104
|
+
if (expected !== sha256) {
|
|
7105
|
+
rmSync6(tarball, { force: true });
|
|
7106
|
+
throw new Error(`Node download checksum mismatch (expected ${expected}, got ${sha256}).`);
|
|
7107
|
+
}
|
|
7108
|
+
const unpacked = join18(RUNTIME_DIR, `node-${ver}-${os}-${arch}`);
|
|
7109
|
+
rmSync6(unpacked, { recursive: true, force: true });
|
|
7110
|
+
const untar = spawnSync9("tar", ["-xzf", tarball, "-C", RUNTIME_DIR], { stdio: "inherit" });
|
|
7111
|
+
if (untar.status !== 0) throw new Error("Failed to extract the Node runtime (is `tar` available?).");
|
|
7112
|
+
rmSync6(tarball, { force: true });
|
|
7113
|
+
const nodeBin = join18(unpacked, "bin", "node");
|
|
7114
|
+
if (!existsSync19(nodeBin)) throw new Error(`Node unpacked but ${nodeBin} is missing (unexpected layout).`);
|
|
7115
|
+
repoint2(RUNTIME_CURRENT, unpacked);
|
|
7116
|
+
return nodeBin;
|
|
7117
|
+
}
|
|
7118
|
+
async function ensureNodeRuntime(onLog) {
|
|
7119
|
+
if ((majorOf("node") ?? 0) >= NODE_MAJOR) return { node: "node", source: "system" };
|
|
7120
|
+
const existing = join18(RUNTIME_CURRENT, "bin", "node");
|
|
7121
|
+
if (existsSync19(existing) && (majorOf(existing) ?? 0) >= NODE_MAJOR) {
|
|
7122
|
+
return { node: existing, source: "vendored" };
|
|
7123
|
+
}
|
|
7124
|
+
return { node: await fetchVendoredNode(onLog), source: "vendored" };
|
|
7125
|
+
}
|
|
7126
|
+
|
|
7127
|
+
// src/lib/update-reexec.ts
|
|
7128
|
+
var REEXEC_ENV = "OPENSHIP_UPDATE_REEXEC";
|
|
7129
|
+
function isReexecChild(env = process.env) {
|
|
7130
|
+
return env[REEXEC_ENV] === "1";
|
|
7131
|
+
}
|
|
7132
|
+
function applyOnlyArgv(selfArgs, json) {
|
|
7133
|
+
return [...selfArgs, ...json ? ["--json"] : [], "update", "--apply-only"];
|
|
7134
|
+
}
|
|
7135
|
+
|
|
7136
|
+
// src/commands/update.ts
|
|
7137
|
+
async function runReconcile() {
|
|
7138
|
+
if (readInstallMethod() === "compose") {
|
|
7139
|
+
const applied = await composeUpdate();
|
|
7140
|
+
if (isJsonMode()) printJson({ reconciled: applied, method: "compose" });
|
|
7141
|
+
else if (applied) ok("Compose stack regenerated, images pulled, services recreated.");
|
|
7142
|
+
else err("Bringing the compose stack up failed. Re-run `openship update`, or `openship up` to retry.");
|
|
7143
|
+
if (!applied) process.exitCode = 1;
|
|
7144
|
+
return applied;
|
|
7145
|
+
}
|
|
7146
|
+
const { restarted } = restart();
|
|
7147
|
+
if (isJsonMode()) printJson({ reconciled: restarted, method: "bare" });
|
|
7148
|
+
else if (restarted) ok("Restarted the service onto the new version.");
|
|
7149
|
+
else ok("Restart the server to run the new version: openship up");
|
|
7150
|
+
return restarted;
|
|
7151
|
+
}
|
|
7152
|
+
function reexecApply() {
|
|
7153
|
+
if (isReexecChild()) return { spawned: false, status: null };
|
|
7154
|
+
const { runtime, args } = selfInvocation();
|
|
7155
|
+
const argv = applyOnlyArgv(args, isJsonMode());
|
|
7156
|
+
const res = spawnSync10(runtime, argv, {
|
|
7157
|
+
stdio: "inherit",
|
|
7158
|
+
env: { ...process.env, [REEXEC_ENV]: "1" }
|
|
7159
|
+
});
|
|
7160
|
+
return { spawned: res.error == null, status: res.status };
|
|
7161
|
+
}
|
|
6515
7162
|
async function runSourceUpdate(source, opts) {
|
|
6516
7163
|
const current = shortSha(source.dir);
|
|
6517
7164
|
const remote = remoteSha(source.repo, source.ref);
|
|
@@ -6541,24 +7188,105 @@ async function runSourceUpdate(source, opts) {
|
|
|
6541
7188
|
process.exitCode = 1;
|
|
6542
7189
|
return;
|
|
6543
7190
|
}
|
|
7191
|
+
info(`Rebuilt from source (${source.ref} @ ${sha}).`);
|
|
7192
|
+
const r = reexecApply();
|
|
7193
|
+
if (r.spawned) {
|
|
7194
|
+
process.exitCode = r.status ?? 0;
|
|
7195
|
+
return;
|
|
7196
|
+
}
|
|
6544
7197
|
const { restarted } = restart();
|
|
6545
7198
|
if (isJsonMode()) {
|
|
6546
7199
|
printJson({ updated: true, source: true, ref: source.ref, from: current, to: sha, restarted });
|
|
6547
7200
|
} else if (restarted) {
|
|
6548
|
-
ok(`
|
|
7201
|
+
ok(`Restarted the service \u2014 you're on ${sha}.`);
|
|
6549
7202
|
} else {
|
|
6550
|
-
ok(`
|
|
7203
|
+
ok(`Restart to run it: openship up`);
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
7206
|
+
var REINSTALL_HINT = "curl -fsSL https://get.openship.io | sh";
|
|
7207
|
+
async function runTarballUpdate(install, opts) {
|
|
7208
|
+
const current = "0.6.1";
|
|
7209
|
+
let latest;
|
|
7210
|
+
try {
|
|
7211
|
+
latest = (await resolveLatestTag()).replace(/^v/, "");
|
|
7212
|
+
} catch {
|
|
7213
|
+
err(`Could not reach GitHub to check for updates. Try again, or reinstall: ${REINSTALL_HINT}`);
|
|
7214
|
+
process.exitCode = 1;
|
|
7215
|
+
return;
|
|
7216
|
+
}
|
|
7217
|
+
const plan = resolveCliUpdatePlan(current, latest);
|
|
7218
|
+
if (opts.check) {
|
|
7219
|
+
if (isJsonMode()) {
|
|
7220
|
+
printJson({ current, latest, updateAvailable: plan.action === "install", method: "tarball" });
|
|
7221
|
+
} else if (plan.action === "install") {
|
|
7222
|
+
info(`Update available: v${current} \u2192 v${latest}. Run \`openship update\`.`);
|
|
7223
|
+
} else {
|
|
7224
|
+
ok(`Up to date (v${current}).`);
|
|
7225
|
+
}
|
|
7226
|
+
return;
|
|
7227
|
+
}
|
|
7228
|
+
if (plan.action === "up-to-date") {
|
|
7229
|
+
ok(`Already on the latest version (v${current}).`);
|
|
7230
|
+
return;
|
|
7231
|
+
}
|
|
7232
|
+
const tag = `v${latest}`;
|
|
7233
|
+
info(`Updating v${current} \u2192 v${latest} (verified payload)\u2026`);
|
|
7234
|
+
let runtime = install.runtime;
|
|
7235
|
+
try {
|
|
7236
|
+
runtime = (await ensureNodeRuntime((m) => info(m))).source;
|
|
7237
|
+
} catch (e) {
|
|
7238
|
+
info(`Keeping the current Node runtime (couldn't refresh it: ${e.message}).`);
|
|
7239
|
+
}
|
|
7240
|
+
try {
|
|
7241
|
+
await ensureCliPayload(tag);
|
|
7242
|
+
if (process.platform !== "win32") writeLauncher();
|
|
7243
|
+
} catch (e) {
|
|
7244
|
+
err(`Update failed: ${e.message} Reinstall: ${REINSTALL_HINT}`);
|
|
7245
|
+
process.exitCode = 1;
|
|
7246
|
+
return;
|
|
7247
|
+
}
|
|
7248
|
+
writeCliInstall({ ...install, tag, runtime });
|
|
7249
|
+
const applyViaChild = reexecApply();
|
|
7250
|
+
if (applyViaChild.spawned) {
|
|
7251
|
+
process.exitCode = applyViaChild.status ?? 0;
|
|
7252
|
+
return;
|
|
7253
|
+
}
|
|
7254
|
+
if (readInstallMethod() === "compose") {
|
|
7255
|
+
const applied = await composeUpdate(latest);
|
|
7256
|
+
if (isJsonMode()) {
|
|
7257
|
+
printJson({ updated: true, from: current, to: latest, method: "compose", applied });
|
|
7258
|
+
} else if (applied) {
|
|
7259
|
+
ok(`Updated to v${latest} \u2014 compose stack regenerated, images pulled, services recreated.`);
|
|
7260
|
+
} else {
|
|
7261
|
+
err(`Updated the CLI to v${latest}, but bringing the compose stack up failed. Re-run \`openship update\`, or \`openship up\` to retry.`);
|
|
7262
|
+
process.exitCode = 1;
|
|
7263
|
+
}
|
|
7264
|
+
return;
|
|
7265
|
+
}
|
|
7266
|
+
const { restarted } = restart();
|
|
7267
|
+
if (isJsonMode()) {
|
|
7268
|
+
printJson({ updated: true, from: current, to: latest, method: "tarball", restarted });
|
|
7269
|
+
} else if (restarted) {
|
|
7270
|
+
ok(`Updated to v${latest} and restarted the service \u2014 you're on the new version.`);
|
|
7271
|
+
} else {
|
|
7272
|
+
ok(`Updated to v${latest}. Restart the server to run the new version: openship up`);
|
|
6551
7273
|
}
|
|
6552
7274
|
}
|
|
6553
7275
|
function detectPackageManager2(override) {
|
|
6554
7276
|
if (override === "bun" || override === "npm") return override;
|
|
6555
|
-
const hasBun =
|
|
7277
|
+
const hasBun = spawnSync10("bun", ["--version"], { stdio: "ignore" }).status === 0;
|
|
6556
7278
|
return hasBun ? "bun" : "npm";
|
|
6557
7279
|
}
|
|
6558
|
-
var updateCommand = new Command26("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").option("--rebuild", "From-source installs: rebuild even if already at the remote tip").action(async (opts) => {
|
|
7280
|
+
var updateCommand = new Command26("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").option("--rebuild", "From-source installs: rebuild even if already at the remote tip").addOption(new Option("--apply-only").hideHelp()).action(async (opts) => {
|
|
7281
|
+
if (opts.applyOnly) {
|
|
7282
|
+
await runReconcile();
|
|
7283
|
+
return;
|
|
7284
|
+
}
|
|
6559
7285
|
const source = readSourceInstall();
|
|
6560
7286
|
if (source) return runSourceUpdate(source, opts);
|
|
6561
|
-
const
|
|
7287
|
+
const cliInstall = readCliInstall();
|
|
7288
|
+
if (cliInstall?.method === "tarball") return runTarballUpdate(cliInstall, opts);
|
|
7289
|
+
const current = "0.6.1";
|
|
6562
7290
|
let latest;
|
|
6563
7291
|
try {
|
|
6564
7292
|
latest = (await resolveLatestTag()).replace(/^v/, "");
|
|
@@ -6586,12 +7314,17 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
|
|
|
6586
7314
|
const ref = `openship@${latest}`;
|
|
6587
7315
|
const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
|
|
6588
7316
|
info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
|
|
6589
|
-
const res =
|
|
7317
|
+
const res = spawnSync10(pm, argv, { stdio: "inherit", shell: process.platform === "win32" });
|
|
6590
7318
|
if (res.status !== 0) {
|
|
6591
7319
|
err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
|
|
6592
7320
|
process.exitCode = 1;
|
|
6593
7321
|
return;
|
|
6594
7322
|
}
|
|
7323
|
+
const applyViaChild = reexecApply();
|
|
7324
|
+
if (applyViaChild.spawned) {
|
|
7325
|
+
process.exitCode = applyViaChild.status ?? 0;
|
|
7326
|
+
return;
|
|
7327
|
+
}
|
|
6595
7328
|
if (readInstallMethod() === "compose") {
|
|
6596
7329
|
const applied = await composeUpdate(latest);
|
|
6597
7330
|
if (isJsonMode()) {
|
|
@@ -6616,17 +7349,17 @@ var updateCommand = new Command26("update").description("Update the Openship CLI
|
|
|
6616
7349
|
|
|
6617
7350
|
// src/commands/cache.ts
|
|
6618
7351
|
import { Command as Command27 } from "commander";
|
|
6619
|
-
import { existsSync as
|
|
6620
|
-
import { join as
|
|
7352
|
+
import { existsSync as existsSync20, readdirSync as readdirSync3, readFileSync as readFileSync14, rmSync as rmSync7, statSync as statSync2 } from "fs";
|
|
7353
|
+
import { join as join19 } from "path";
|
|
6621
7354
|
function listAssets() {
|
|
6622
|
-
if (!
|
|
7355
|
+
if (!existsSync20(RELEASES_DIR)) return [];
|
|
6623
7356
|
const out = [];
|
|
6624
|
-
for (const tag of
|
|
7357
|
+
for (const tag of readdirSync3(RELEASES_DIR)) {
|
|
6625
7358
|
const dir = releaseDir(tag);
|
|
6626
7359
|
if (!statSync2(dir).isDirectory()) continue;
|
|
6627
|
-
for (const name of
|
|
7360
|
+
for (const name of readdirSync3(dir)) {
|
|
6628
7361
|
if (name.endsWith(".sha256")) continue;
|
|
6629
|
-
const path2 =
|
|
7362
|
+
const path2 = join19(dir, name);
|
|
6630
7363
|
const st = statSync2(path2);
|
|
6631
7364
|
if (!st.isFile()) continue;
|
|
6632
7365
|
out.push({
|
|
@@ -6634,7 +7367,7 @@ function listAssets() {
|
|
|
6634
7367
|
name,
|
|
6635
7368
|
path: path2,
|
|
6636
7369
|
size: st.size,
|
|
6637
|
-
hasSidecar:
|
|
7370
|
+
hasSidecar: existsSync20(`${path2}.sha256`)
|
|
6638
7371
|
});
|
|
6639
7372
|
}
|
|
6640
7373
|
}
|
|
@@ -6665,7 +7398,7 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
|
|
|
6665
7398
|
results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
|
|
6666
7399
|
continue;
|
|
6667
7400
|
}
|
|
6668
|
-
const expected = parseSha256(
|
|
7401
|
+
const expected = parseSha256(readFileSync14(`${a.path}.sha256`, "utf8"));
|
|
6669
7402
|
const actual = await hashFile(a.path);
|
|
6670
7403
|
const okMatch = expected !== null && expected === actual;
|
|
6671
7404
|
if (!okMatch) bad += 1;
|
|
@@ -6681,12 +7414,12 @@ var verifyCmd2 = new Command27("verify").description("Re-hash cached assets and
|
|
|
6681
7414
|
});
|
|
6682
7415
|
var cleanCmd = new Command27("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
|
|
6683
7416
|
const target = tag ? releaseDir(tag) : RELEASES_DIR;
|
|
6684
|
-
if (!
|
|
7417
|
+
if (!existsSync20(target)) {
|
|
6685
7418
|
if (isJsonMode()) printJson({ removed: false, path: target });
|
|
6686
7419
|
else info(` Nothing to clean (${target}).`);
|
|
6687
7420
|
return;
|
|
6688
7421
|
}
|
|
6689
|
-
|
|
7422
|
+
rmSync7(target, { recursive: true, force: true });
|
|
6690
7423
|
if (isJsonMode()) printJson({ removed: true, path: target });
|
|
6691
7424
|
else ok(`
|
|
6692
7425
|
Removed ${target}
|
|
@@ -6698,11 +7431,11 @@ var cacheCommand = new Command27("cache").description("Manage the local download
|
|
|
6698
7431
|
}).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
|
|
6699
7432
|
|
|
6700
7433
|
// src/commands/wizard.ts
|
|
6701
|
-
import
|
|
7434
|
+
import chalk21 from "chalk";
|
|
6702
7435
|
import open2 from "open";
|
|
6703
7436
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
6704
|
-
import { existsSync as
|
|
6705
|
-
import { join as
|
|
7437
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync14, rmSync as rmSync8, writeFileSync as writeFileSync14 } from "fs";
|
|
7438
|
+
import { join as join20 } from "path";
|
|
6706
7439
|
import {
|
|
6707
7440
|
intro as intro3,
|
|
6708
7441
|
outro as outro3,
|
|
@@ -6743,7 +7476,7 @@ async function connectOpenshipCloud(port, token) {
|
|
|
6743
7476
|
const handoff = `${apiBase}/api/cloud/connect-handoff?state=${encodeURIComponent(state)}&code_challenge=${challenge}&mode=device`;
|
|
6744
7477
|
const overSsh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT);
|
|
6745
7478
|
log5.step("Open this URL in your browser to authorize, then click Authorize:");
|
|
6746
|
-
console.log("\n" +
|
|
7479
|
+
console.log("\n" + chalk21.cyan.underline(handoff) + "\n");
|
|
6747
7480
|
if (!overSsh) void open2(handoff).catch(() => {
|
|
6748
7481
|
});
|
|
6749
7482
|
const s = spinner4();
|
|
@@ -6848,16 +7581,16 @@ async function streamProvision(port, sessionId, s) {
|
|
|
6848
7581
|
}
|
|
6849
7582
|
return { ok: ok3, detail };
|
|
6850
7583
|
}
|
|
6851
|
-
var SETUP_LOCK =
|
|
7584
|
+
var SETUP_LOCK = join20(OS_DIR, "setup-in-progress");
|
|
6852
7585
|
function isSetupInProgress() {
|
|
6853
|
-
return
|
|
7586
|
+
return existsSync21(SETUP_LOCK);
|
|
6854
7587
|
}
|
|
6855
7588
|
function markSetupStarted() {
|
|
6856
|
-
|
|
6857
|
-
|
|
7589
|
+
mkdirSync14(OS_DIR, { recursive: true });
|
|
7590
|
+
writeFileSync14(SETUP_LOCK, "1");
|
|
6858
7591
|
}
|
|
6859
7592
|
function markSetupDone() {
|
|
6860
|
-
|
|
7593
|
+
rmSync8(SETUP_LOCK, { force: true });
|
|
6861
7594
|
}
|
|
6862
7595
|
function wizardInputs(admin, plan) {
|
|
6863
7596
|
switch (plan.type) {
|
|
@@ -6874,20 +7607,20 @@ function wizardInputs(admin, plan) {
|
|
|
6874
7607
|
function finishSetup(opts) {
|
|
6875
7608
|
saveInstanceUrl(opts.liveUrl);
|
|
6876
7609
|
markSetupDone();
|
|
6877
|
-
const pad = (label) =>
|
|
6878
|
-
log5.success(
|
|
7610
|
+
const pad = (label) => chalk21.dim(label.padEnd(11));
|
|
7611
|
+
log5.success(chalk21.bold("Openship is live"));
|
|
6879
7612
|
log5.message(
|
|
6880
|
-
`${pad("URL")}${
|
|
7613
|
+
`${pad("URL")}${chalk21.bold(opts.liveUrl)}
|
|
6881
7614
|
${pad("Dashboard")}http://localhost:${opts.dashPort}
|
|
6882
7615
|
${pad("API")}http://localhost:${opts.apiPort}
|
|
6883
|
-
${pad("Login")}${opts.adminEmail} ${
|
|
6884
|
-
` + (opts.cloudEmail ? `${pad("Cloud")}${
|
|
6885
|
-
` : "") + `${pad("Status")}${
|
|
7616
|
+
${pad("Login")}${opts.adminEmail} ${chalk21.dim("(email + password you set)")}
|
|
7617
|
+
` + (opts.cloudEmail ? `${pad("Cloud")}${chalk21.dim("connected as ")}${opts.cloudEmail}${chalk21.dim(" \u2014 free domain + mail only")}
|
|
7618
|
+
` : "") + `${pad("Status")}${chalk21.green("running")} ${chalk21.dim(opts.method === "compose" ? "\xB7 Docker Compose stack (restarts on boot)" : "\xB7 service (restarts on boot)")}`
|
|
6886
7619
|
);
|
|
6887
7620
|
log5.message(
|
|
6888
|
-
|
|
7621
|
+
chalk21.dim("Sign in with the email + password you just set. Openship appears under your Apps.\n") + chalk21.dim("Change the domain, Openship Cloud, team, and everything else anytime in Settings.\n") + chalk21.dim(`Locked out? Run ${chalk21.reset("openship reset-admin-password")}${chalk21.dim(" on this machine \u2014 resets your login without signing in.")}`)
|
|
6889
7622
|
);
|
|
6890
|
-
outro3(opts.byo ?
|
|
7623
|
+
outro3(opts.byo ? chalk21.dim("Point your reverse proxy at the dashboard port above.") : chalk21.green("Happy shipping."));
|
|
6891
7624
|
}
|
|
6892
7625
|
async function promptLocalEdgeTakeover(port) {
|
|
6893
7626
|
const pf = await internalPost(port, "/api/system/self-edge/preflight", {});
|
|
@@ -6902,22 +7635,36 @@ async function promptLocalEdgeTakeover(port) {
|
|
|
6902
7635
|
renderEdgeConflict({ owner, sites, warnings });
|
|
6903
7636
|
const action2 = await confirmEdgeAction({ owner, known, importable: sites.length });
|
|
6904
7637
|
if (action2 === "cancel") return { proceed: false, edgeMigrate: false, edgeTakeover: false };
|
|
6905
|
-
|
|
7638
|
+
let staticRootOverrides;
|
|
7639
|
+
if (action2 === "migrate") {
|
|
7640
|
+
const unreachable = pf.ok && Array.isArray(pf.data?.unreachableStaticRoots) ? pf.data.unreachableStaticRoots : [];
|
|
7641
|
+
staticRootOverrides = await remediateUnreachableStaticRoots({
|
|
7642
|
+
unreachable,
|
|
7643
|
+
executor: new LocalExecutor(),
|
|
7644
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
7645
|
+
});
|
|
7646
|
+
}
|
|
7647
|
+
return {
|
|
7648
|
+
proceed: true,
|
|
7649
|
+
edgeMigrate: action2 === "migrate",
|
|
7650
|
+
edgeTakeover: action2 === "takeover",
|
|
7651
|
+
...staticRootOverrides ? { staticRootOverrides } : {}
|
|
7652
|
+
};
|
|
6906
7653
|
}
|
|
6907
7654
|
async function runWizard() {
|
|
6908
7655
|
const resuming = isSetupInProgress();
|
|
6909
|
-
intro3(`${
|
|
7656
|
+
intro3(`${chalk21.bgCyan(chalk21.black(" Openship "))}${chalk21.dim(" setup")}`);
|
|
6910
7657
|
if (resuming) {
|
|
6911
7658
|
log5.warn(
|
|
6912
7659
|
"Your last setup didn't finish \u2014 picking it back up. Re-enter your details to complete it (or run `openship up` to just keep the server running)."
|
|
6913
7660
|
);
|
|
6914
7661
|
}
|
|
6915
7662
|
log5.message(
|
|
6916
|
-
|
|
7663
|
+
chalk21.dim(
|
|
6917
7664
|
"Deploy Openship on this machine \u2014 a few questions, then it installs itself\nas a service, registers as an app, and prints the URL to log in."
|
|
6918
7665
|
)
|
|
6919
7666
|
);
|
|
6920
|
-
log5.message(
|
|
7667
|
+
log5.message(chalk21.dim("First, your instance login (email + password) \u2014 this is how you sign in. Domain and Openship Cloud come next and never replace it."));
|
|
6921
7668
|
const admin = await promptLocalAdmin();
|
|
6922
7669
|
let cloudEmail = null;
|
|
6923
7670
|
let publicUrl;
|
|
@@ -6936,7 +7683,7 @@ async function runWizard() {
|
|
|
6936
7683
|
sp.start("Detecting this server's public IP");
|
|
6937
7684
|
const detected = await detectPublicIp();
|
|
6938
7685
|
if (detected) {
|
|
6939
|
-
sp.stop(`Public IP: ${
|
|
7686
|
+
sp.stop(`Public IP: ${chalk21.bold(detected)}`);
|
|
6940
7687
|
publicHost = detected;
|
|
6941
7688
|
return detected;
|
|
6942
7689
|
}
|
|
@@ -6951,7 +7698,7 @@ async function runWizard() {
|
|
|
6951
7698
|
return publicHost;
|
|
6952
7699
|
}
|
|
6953
7700
|
let stage = "reach";
|
|
6954
|
-
log5.message(
|
|
7701
|
+
log5.message(chalk21.dim("These are just starting choices \u2014 domain, Cloud, team, and the rest are all editable later in Settings."));
|
|
6955
7702
|
planning: while (true) {
|
|
6956
7703
|
if (stage === "reach") {
|
|
6957
7704
|
const reach = ensure2(
|
|
@@ -7007,11 +7754,11 @@ async function runWizard() {
|
|
|
7007
7754
|
).trim().toLowerCase();
|
|
7008
7755
|
const host = await resolvePublicHost();
|
|
7009
7756
|
note3(
|
|
7010
|
-
`${
|
|
7757
|
+
`${chalk21.cyan(`https://${slug}.opsh.io`)}
|
|
7011
7758
|
|
|
7012
|
-
${
|
|
7759
|
+
${chalk21.dim("served via")} Openship Cloud edge ${chalk21.dim("\u2192")} ${chalk21.cyan(host)}
|
|
7013
7760
|
|
|
7014
|
-
` +
|
|
7761
|
+
` + chalk21.dim("Openship Cloud terminates HTTPS and forwards to this server."),
|
|
7015
7762
|
"Confirm free domain"
|
|
7016
7763
|
);
|
|
7017
7764
|
const go2 = ensure2(
|
|
@@ -7049,11 +7796,11 @@ async function runWizard() {
|
|
|
7049
7796
|
}
|
|
7050
7797
|
const host = await resolvePublicHost();
|
|
7051
7798
|
note3(
|
|
7052
|
-
`Add a DNS ${
|
|
7799
|
+
`Add a DNS ${chalk21.bold("A record")}:
|
|
7053
7800
|
|
|
7054
|
-
${
|
|
7801
|
+
${chalk21.cyan(hostname2)} \u2192 ${chalk21.cyan(host)}
|
|
7055
7802
|
|
|
7056
|
-
` +
|
|
7803
|
+
` + chalk21.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
|
|
7057
7804
|
"DNS"
|
|
7058
7805
|
);
|
|
7059
7806
|
const go2 = ensure2(
|
|
@@ -7090,9 +7837,9 @@ async function runWizard() {
|
|
|
7090
7837
|
log5.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
|
|
7091
7838
|
}
|
|
7092
7839
|
note3(
|
|
7093
|
-
`${
|
|
7840
|
+
`${chalk21.cyan(url)}
|
|
7094
7841
|
|
|
7095
|
-
` +
|
|
7842
|
+
` + chalk21.dim("Point your reverse proxy at the dashboard port shown at the end."),
|
|
7096
7843
|
"Confirm"
|
|
7097
7844
|
);
|
|
7098
7845
|
const go = ensure2(
|
|
@@ -7130,7 +7877,7 @@ async function runWizard() {
|
|
|
7130
7877
|
}
|
|
7131
7878
|
}
|
|
7132
7879
|
}
|
|
7133
|
-
const uiTag = `v${"0.
|
|
7880
|
+
const uiTag = `v${"0.6.1"}`;
|
|
7134
7881
|
if (method === "bare") {
|
|
7135
7882
|
const dl = spinner4();
|
|
7136
7883
|
dl.start("Pulling the Openship dist from GitHub");
|
|
@@ -7155,6 +7902,7 @@ async function runWizard() {
|
|
|
7155
7902
|
let edgeAction;
|
|
7156
7903
|
let migratedSites;
|
|
7157
7904
|
let migratedCertPems;
|
|
7905
|
+
let migratedStaticRootOverrides;
|
|
7158
7906
|
let allowHostControl = true;
|
|
7159
7907
|
if (method === "compose") {
|
|
7160
7908
|
allowHostControl = ensure2(
|
|
@@ -7177,7 +7925,7 @@ async function runWizard() {
|
|
|
7177
7925
|
) === "allow";
|
|
7178
7926
|
if (!allowHostControl) {
|
|
7179
7927
|
log5.message(
|
|
7180
|
-
|
|
7928
|
+
chalk21.dim(
|
|
7181
7929
|
" No host key will be created or mounted, and this box won't be offered as a deploy target.\n The Docker socket stays mounted (deployments need it), so this is defense in depth, not isolation."
|
|
7182
7930
|
)
|
|
7183
7931
|
);
|
|
@@ -7197,7 +7945,7 @@ async function runWizard() {
|
|
|
7197
7945
|
dashboardPort,
|
|
7198
7946
|
publicUrl,
|
|
7199
7947
|
trustProxy: behindProxy,
|
|
7200
|
-
version: "0.
|
|
7948
|
+
version: "0.6.1",
|
|
7201
7949
|
noHostControl: !allowHostControl
|
|
7202
7950
|
})) {
|
|
7203
7951
|
cancel4("Couldn't fetch the Openship images \u2014 nothing on this box was changed.");
|
|
@@ -7205,12 +7953,15 @@ async function runWizard() {
|
|
|
7205
7953
|
}
|
|
7206
7954
|
const edgePlan = await planAndApplyHostEdge({});
|
|
7207
7955
|
if (!edgePlan.proceed) {
|
|
7208
|
-
cancel4(
|
|
7209
|
-
|
|
7956
|
+
cancel4(
|
|
7957
|
+
edgePlan.blockedBy ? `Can't take over the edge: ${edgePlan.blockedBy}. Nothing was started.` : "Left the existing proxy on 80/443 running \u2014 re-run and choose migrate/takeover when ready."
|
|
7958
|
+
);
|
|
7959
|
+
process.exit(edgePlan.blockedBy ? 1 : 0);
|
|
7210
7960
|
}
|
|
7211
7961
|
edgeAction = edgePlan.action;
|
|
7212
7962
|
migratedSites = edgePlan.sites;
|
|
7213
7963
|
migratedCertPems = edgePlan.certPems;
|
|
7964
|
+
migratedStaticRootOverrides = edgePlan.staticRootOverrides;
|
|
7214
7965
|
log5.step("Starting the Docker Compose stack\u2026");
|
|
7215
7966
|
const up = await composeUp({
|
|
7216
7967
|
// Prefetched above, before the preflight stopped anything.
|
|
@@ -7219,7 +7970,7 @@ async function runWizard() {
|
|
|
7219
7970
|
dashboardPort,
|
|
7220
7971
|
publicUrl,
|
|
7221
7972
|
trustProxy: behindProxy,
|
|
7222
|
-
version: "0.
|
|
7973
|
+
version: "0.6.1",
|
|
7223
7974
|
noHostControl: !allowHostControl
|
|
7224
7975
|
});
|
|
7225
7976
|
if (!up.ok) {
|
|
@@ -7268,7 +8019,12 @@ async function runWizard() {
|
|
|
7268
8019
|
await waitDashboard(started.dashPort);
|
|
7269
8020
|
s.stop("Deployed.");
|
|
7270
8021
|
if (edgeAction === "migrate" && migratedSites?.length) {
|
|
7271
|
-
const imported = await importMigratedSites(
|
|
8022
|
+
const imported = await importMigratedSites(
|
|
8023
|
+
started.port,
|
|
8024
|
+
migratedSites,
|
|
8025
|
+
migratedCertPems,
|
|
8026
|
+
migratedStaticRootOverrides
|
|
8027
|
+
);
|
|
7272
8028
|
if (!imported.ok) {
|
|
7273
8029
|
log5.warn(
|
|
7274
8030
|
`Your ${migratedSites.length} existing site${migratedSites.length === 1 ? "" : "s"} aren't served yet \u2014 re-run \`openship up\` to retry the import.`
|
|
@@ -7291,7 +8047,7 @@ async function runWizard() {
|
|
|
7291
8047
|
inputs: wizardInputs(admin, domainPlan),
|
|
7292
8048
|
token: provisionToken,
|
|
7293
8049
|
method: "compose",
|
|
7294
|
-
onLog: (msg) => log5.message(
|
|
8050
|
+
onLog: (msg) => log5.message(chalk21.dim(msg)),
|
|
7295
8051
|
// .opsh.io is a SHARED zone, and Cloud only connects after the stack is up —
|
|
7296
8052
|
// so a taken subdomain can't be detected at prompt time. Recover here, where
|
|
7297
8053
|
// we still have a TTY, instead of ending the run with no domain.
|
|
@@ -7377,10 +8133,11 @@ async function runWizard() {
|
|
|
7377
8133
|
// the operator declined the takeover (their proxy keeps 80/443).
|
|
7378
8134
|
localEdge: edge.proceed,
|
|
7379
8135
|
edgeTakeover: edge.edgeTakeover,
|
|
7380
|
-
edgeMigrate: edge.edgeMigrate
|
|
8136
|
+
edgeMigrate: edge.edgeMigrate,
|
|
8137
|
+
...edge.staticRootOverrides ? { staticRootOverrides: edge.staticRootOverrides } : {}
|
|
7381
8138
|
}
|
|
7382
8139
|
},
|
|
7383
|
-
onLog: (msg) => log5.message(
|
|
8140
|
+
onLog: (msg) => log5.message(chalk21.dim(msg)),
|
|
7384
8141
|
onSlugTaken: async (taken) => {
|
|
7385
8142
|
log5.warn(`"${taken}.opsh.io" is already taken.`);
|
|
7386
8143
|
const next = await text({
|
|
@@ -7415,7 +8172,8 @@ async function runWizard() {
|
|
|
7415
8172
|
dashPort: Number(started.dashPort),
|
|
7416
8173
|
acmeEmail: admin?.email,
|
|
7417
8174
|
edgeTakeover,
|
|
7418
|
-
edgeMigrate
|
|
8175
|
+
edgeMigrate,
|
|
8176
|
+
...edge.staticRootOverrides ? { staticRootOverrides: edge.staticRootOverrides } : {}
|
|
7419
8177
|
});
|
|
7420
8178
|
if (res.ok && res.data?.sessionId) {
|
|
7421
8179
|
const s2 = spinner4();
|
|
@@ -7460,18 +8218,18 @@ async function runControl() {
|
|
|
7460
8218
|
const dashUrl = `http://localhost:${storedDashboardPort()}`;
|
|
7461
8219
|
const publicUrl = readInstanceUrl();
|
|
7462
8220
|
const primaryUrl = publicUrl && !/^https?:\/\/localhost/i.test(publicUrl) ? publicUrl : dashUrl;
|
|
7463
|
-
intro3(`${
|
|
8221
|
+
intro3(`${chalk21.bgCyan(chalk21.black(" Openship "))}${chalk21.dim(" control")}`);
|
|
7464
8222
|
note3(
|
|
7465
|
-
`${
|
|
7466
|
-
${
|
|
7467
|
-
${
|
|
7468
|
-
` + (ports.api ? `${
|
|
7469
|
-
` : "") + `${
|
|
8223
|
+
`${chalk21.dim("URL".padEnd(11))}${chalk21.bold(primaryUrl)}
|
|
8224
|
+
${chalk21.dim("Service".padEnd(11))}${running ? chalk21.green("running") : chalk21.yellow("stopped")}
|
|
8225
|
+
${chalk21.dim("Dashboard".padEnd(11))}${dashUrl}
|
|
8226
|
+
` + (ports.api ? `${chalk21.dim("API".padEnd(11))}http://localhost:${ports.api}
|
|
8227
|
+
` : "") + `${chalk21.dim("Manager".padEnd(11))}${managerLabel}`,
|
|
7470
8228
|
"Openship is already set up"
|
|
7471
8229
|
);
|
|
7472
8230
|
const corrupted = looksCorrupted();
|
|
7473
8231
|
if (corrupted) {
|
|
7474
|
-
note3(
|
|
8232
|
+
note3(chalk21.red("The service is installed but keeps failing to start \u2014 the database looks corrupted."), "Needs repair");
|
|
7475
8233
|
}
|
|
7476
8234
|
const action2 = ensure2(
|
|
7477
8235
|
await select4({
|
|
@@ -7491,18 +8249,18 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
|
|
|
7491
8249
|
switch (action2) {
|
|
7492
8250
|
case "repair": {
|
|
7493
8251
|
const res = await runRepair();
|
|
7494
|
-
outro3(res.healed ?
|
|
8252
|
+
outro3(res.healed ? chalk21.green(res.detail) : chalk21.yellow(res.detail));
|
|
7495
8253
|
return;
|
|
7496
8254
|
}
|
|
7497
8255
|
case "open":
|
|
7498
8256
|
await open2(primaryUrl).catch(() => {
|
|
7499
8257
|
});
|
|
7500
|
-
outro3(
|
|
8258
|
+
outro3(chalk21.dim(`Opening ${primaryUrl}`));
|
|
7501
8259
|
return;
|
|
7502
8260
|
case "start": {
|
|
7503
8261
|
if (isCompose) {
|
|
7504
8262
|
const ok3 = composeStart();
|
|
7505
|
-
outro3(ok3 ?
|
|
8263
|
+
outro3(ok3 ? chalk21.green("Started.") : chalk21.yellow("Couldn't start the stack \u2014 run `openship up` to see the error."));
|
|
7506
8264
|
return;
|
|
7507
8265
|
}
|
|
7508
8266
|
await startService({});
|
|
@@ -7511,21 +8269,21 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
|
|
|
7511
8269
|
case "restart": {
|
|
7512
8270
|
if (isCompose) {
|
|
7513
8271
|
const ok3 = composeRestart();
|
|
7514
|
-
outro3(ok3 ?
|
|
8272
|
+
outro3(ok3 ? chalk21.green("Restarted.") : chalk21.yellow("Couldn't restart the stack."));
|
|
7515
8273
|
return;
|
|
7516
8274
|
}
|
|
7517
8275
|
const r = restart();
|
|
7518
|
-
outro3(r.restarted ?
|
|
8276
|
+
outro3(r.restarted ? chalk21.green("Restarted.") : chalk21.yellow(r.detail));
|
|
7519
8277
|
return;
|
|
7520
8278
|
}
|
|
7521
8279
|
case "stop": {
|
|
7522
8280
|
if (isCompose) {
|
|
7523
8281
|
const ok3 = composeDown();
|
|
7524
|
-
outro3(ok3 ?
|
|
8282
|
+
outro3(ok3 ? chalk21.green("Stopped. Won't restart on boot.") : chalk21.yellow("Couldn't stop the stack."));
|
|
7525
8283
|
return;
|
|
7526
8284
|
}
|
|
7527
8285
|
const r = stop();
|
|
7528
|
-
outro3(
|
|
8286
|
+
outro3(chalk21.green(`Stopped. ${chalk21.dim(r.detail)}`));
|
|
7529
8287
|
return;
|
|
7530
8288
|
}
|
|
7531
8289
|
case "reset": {
|
|
@@ -7534,7 +8292,7 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
|
|
|
7534
8292
|
);
|
|
7535
8293
|
const rr = await internalPost(apiPort, "/api/system/reset-admin-password", { password: pw });
|
|
7536
8294
|
outro3(
|
|
7537
|
-
rr.ok ?
|
|
8295
|
+
rr.ok ? chalk21.green(`Password reset. Sign in at ${dashUrl} with your email + new password.`) : chalk21.red(`Couldn't reset: ${rr.data?.error || "failed"}`)
|
|
7538
8296
|
);
|
|
7539
8297
|
return;
|
|
7540
8298
|
}
|
|
@@ -7542,13 +8300,19 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
|
|
|
7542
8300
|
await runWizard();
|
|
7543
8301
|
return;
|
|
7544
8302
|
default:
|
|
7545
|
-
outro3(
|
|
8303
|
+
outro3(chalk21.dim("Nothing changed."));
|
|
7546
8304
|
}
|
|
7547
8305
|
}
|
|
7548
8306
|
|
|
8307
|
+
// src/commands/completion.ts
|
|
8308
|
+
import tab from "@bomb.sh/tab/commander";
|
|
8309
|
+
function attachCompletion(program2) {
|
|
8310
|
+
tab(program2, { completionCommandName: "completion" });
|
|
8311
|
+
}
|
|
8312
|
+
|
|
7549
8313
|
// src/index.ts
|
|
7550
8314
|
var program = new Command28();
|
|
7551
|
-
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.
|
|
8315
|
+
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.6.1").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
|
|
7552
8316
|
if (thisCommand.opts().json) setJsonMode(true);
|
|
7553
8317
|
}).action(async () => {
|
|
7554
8318
|
const installed = serviceStatus().installed || readInstallMethod() === "compose";
|
|
@@ -7582,4 +8346,5 @@ program.addCommand(tokenCommand);
|
|
|
7582
8346
|
program.addCommand(apiCommand);
|
|
7583
8347
|
program.addCommand(resetAdminCommand);
|
|
7584
8348
|
installCommand.addCommand(cacheCommand);
|
|
8349
|
+
attachCompletion(program);
|
|
7585
8350
|
program.parse();
|