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