openship 0.1.11 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +680 -104
- package/dist/server/index.js +17063 -8214
- package/dist/server/lua/geo_country.lua +101 -0
- package/dist/server/lua/mgmt_api.lua +291 -0
- package/dist/server/lua/pipe_log.lua +71 -0
- package/dist/server/lua/pipe_stream.lua +90 -0
- package/dist/server/lua/rules_guard.lua +118 -0
- package/dist/server/lua/rules_lib.lua +176 -0
- package/dist/server/lua/site_logger.lua +182 -0
- package/dist/server/lua/webhook_handler.lua +126 -0
- package/dist/server/migrations/0035_thankful_morgan_stark.sql +1 -0
- package/dist/server/migrations/0036_gorgeous_scalphunter.sql +17 -0
- package/dist/server/migrations/0037_clever_makkari.sql +28 -0
- package/dist/server/migrations/0038_young_magneto.sql +1 -0
- package/dist/server/migrations/0039_secret_lady_ursula.sql +15 -0
- package/dist/server/migrations/0040_sharp_red_wolf.sql +16 -0
- package/dist/server/migrations/0041_round_spyke.sql +4 -0
- package/dist/server/migrations/0042_dapper_havok.sql +1 -0
- package/dist/server/migrations/0043_fast_sharon_ventura.sql +15 -0
- package/dist/server/migrations/0044_amused_shape.sql +8 -0
- package/dist/server/migrations/0045_amusing_korath.sql +1 -0
- package/dist/server/migrations/0046_white_harrier.sql +35 -0
- package/dist/server/migrations/0047_sloppy_strong_guy.sql +2 -0
- package/dist/server/migrations/0048_funny_emma_frost.sql +2 -0
- package/dist/server/migrations/0049_nappy_piledriver.sql +3 -0
- package/dist/server/migrations/meta/0035_snapshot.json +7610 -0
- package/dist/server/migrations/meta/0036_snapshot.json +7752 -0
- package/dist/server/migrations/meta/0037_snapshot.json +7978 -0
- package/dist/server/migrations/meta/0038_snapshot.json +7985 -0
- package/dist/server/migrations/meta/0039_snapshot.json +8090 -0
- package/dist/server/migrations/meta/0040_snapshot.json +8198 -0
- package/dist/server/migrations/meta/0041_snapshot.json +8211 -0
- package/dist/server/migrations/meta/0042_snapshot.json +8217 -0
- package/dist/server/migrations/meta/0043_snapshot.json +8316 -0
- package/dist/server/migrations/meta/0044_snapshot.json +8360 -0
- package/dist/server/migrations/meta/0045_snapshot.json +8367 -0
- package/dist/server/migrations/meta/0046_snapshot.json +8657 -0
- package/dist/server/migrations/meta/0047_snapshot.json +8669 -0
- package/dist/server/migrations/meta/0048_snapshot.json +8683 -0
- package/dist/server/migrations/meta/0049_snapshot.json +8702 -0
- package/dist/server/migrations/meta/_journal.json +105 -0
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -902,11 +902,13 @@ var PLANS = {
|
|
|
902
902
|
free: {
|
|
903
903
|
id: "free",
|
|
904
904
|
name: "Free",
|
|
905
|
-
description: "Get started
|
|
905
|
+
description: "Get started for free",
|
|
906
906
|
price: { monthly: 0, annual: 0 },
|
|
907
907
|
stripePriceId: { monthly: null, annual: null },
|
|
908
|
-
|
|
909
|
-
//
|
|
908
|
+
// Paid tiers below are `null` (price + credits) until Openship Cloud pricing
|
|
909
|
+
// is finalized — the UI renders "coming soon". Self-hosted is free and never
|
|
910
|
+
// surfaces any of these numbers.
|
|
911
|
+
monthlyCredits: null,
|
|
910
912
|
oblienLimits: {
|
|
911
913
|
max_workspaces: 1,
|
|
912
914
|
max_vcpus: 2,
|
|
@@ -914,7 +916,6 @@ var PLANS = {
|
|
|
914
916
|
max_disk_gb: 10
|
|
915
917
|
},
|
|
916
918
|
features: [
|
|
917
|
-
"500 credits/mo",
|
|
918
919
|
"1 workspace",
|
|
919
920
|
"Community support"
|
|
920
921
|
],
|
|
@@ -925,13 +926,13 @@ var PLANS = {
|
|
|
925
926
|
id: "pro",
|
|
926
927
|
name: "Pro",
|
|
927
928
|
description: "For solo builders shipping production workloads",
|
|
928
|
-
price: { monthly:
|
|
929
|
-
//
|
|
929
|
+
price: { monthly: null, annual: null },
|
|
930
|
+
// coming soon
|
|
930
931
|
stripePriceId: {
|
|
931
932
|
monthly: process.env.STRIPE_PRICE_PRO_MONTHLY ?? "price_pro_monthly_placeholder",
|
|
932
933
|
annual: process.env.STRIPE_PRICE_PRO_ANNUAL ?? "price_pro_annual_placeholder"
|
|
933
934
|
},
|
|
934
|
-
monthlyCredits:
|
|
935
|
+
monthlyCredits: null,
|
|
935
936
|
oblienLimits: {
|
|
936
937
|
max_workspaces: 10,
|
|
937
938
|
max_vcpus: 16,
|
|
@@ -939,7 +940,6 @@ var PLANS = {
|
|
|
939
940
|
max_disk_gb: 100
|
|
940
941
|
},
|
|
941
942
|
features: [
|
|
942
|
-
"5,000 credits/mo",
|
|
943
943
|
"Up to 10 workspaces",
|
|
944
944
|
"Email support"
|
|
945
945
|
],
|
|
@@ -950,12 +950,13 @@ var PLANS = {
|
|
|
950
950
|
id: "team",
|
|
951
951
|
name: "Team",
|
|
952
952
|
description: "For teams collaborating on shared infra",
|
|
953
|
-
price: { monthly:
|
|
953
|
+
price: { monthly: null, annual: null },
|
|
954
|
+
// coming soon
|
|
954
955
|
stripePriceId: {
|
|
955
956
|
monthly: process.env.STRIPE_PRICE_TEAM_MONTHLY ?? "price_team_monthly_placeholder",
|
|
956
957
|
annual: process.env.STRIPE_PRICE_TEAM_ANNUAL ?? "price_team_annual_placeholder"
|
|
957
958
|
},
|
|
958
|
-
monthlyCredits:
|
|
959
|
+
monthlyCredits: null,
|
|
959
960
|
oblienLimits: {
|
|
960
961
|
max_workspaces: 50,
|
|
961
962
|
max_vcpus: 64,
|
|
@@ -963,7 +964,6 @@ var PLANS = {
|
|
|
963
964
|
max_disk_gb: 500
|
|
964
965
|
},
|
|
965
966
|
features: [
|
|
966
|
-
"25,000 credits/mo",
|
|
967
967
|
"Up to 50 workspaces",
|
|
968
968
|
"Team collaboration",
|
|
969
969
|
"Priority email support"
|
|
@@ -1128,8 +1128,12 @@ var localhost = (port) => `http://localhost:${port}`;
|
|
|
1128
1128
|
var LOCAL_WEB_URL = localhost(DEFAULT_PORT.web);
|
|
1129
1129
|
var LOCAL_DASHBOARD_URL = localhost(DEFAULT_PORT.dashboard);
|
|
1130
1130
|
var LOCAL_API_URL = localhost(DEFAULT_PORT.api);
|
|
1131
|
-
var
|
|
1132
|
-
|
|
1131
|
+
var envUrl = (key) => {
|
|
1132
|
+
const v = typeof process !== "undefined" ? process.env?.[key] : void 0;
|
|
1133
|
+
return v && v.trim() ? v.trim() : void 0;
|
|
1134
|
+
};
|
|
1135
|
+
var CLOUD_DASHBOARD_URL = envUrl("OPENSHIP_CLOUD_DASHBOARD_URL") ?? "https://app.openship.io";
|
|
1136
|
+
var CLOUD_API_URL = envUrl("OPENSHIP_CLOUD_API_URL") ?? "https://api.openship.io";
|
|
1133
1137
|
var DASHBOARD_RUNTIME_TARGETS = {
|
|
1134
1138
|
local: {
|
|
1135
1139
|
dashboard: LOCAL_DASHBOARD_URL,
|
|
@@ -2230,8 +2234,8 @@ var loginCommand = new Command("login").description("Authenticate with a Persona
|
|
|
2230
2234
|
chalk2.bold("\n Openship login\n") + chalk2.dim(" Create a Personal Access Token in Settings \u2192 Personal Access Tokens,\n") + chalk2.dim(" then paste it here.\n")
|
|
2231
2235
|
);
|
|
2232
2236
|
try {
|
|
2233
|
-
const { default:
|
|
2234
|
-
await
|
|
2237
|
+
const { default: open2 } = await import("open");
|
|
2238
|
+
await open2(settingsUrl);
|
|
2235
2239
|
} catch {
|
|
2236
2240
|
}
|
|
2237
2241
|
console.log(
|
|
@@ -2350,8 +2354,8 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
|
|
|
2350
2354
|
}
|
|
2351
2355
|
}
|
|
2352
2356
|
try {
|
|
2353
|
-
const { default:
|
|
2354
|
-
await
|
|
2357
|
+
const { default: open2 } = await import("open");
|
|
2358
|
+
await open2(target);
|
|
2355
2359
|
console.log(chalk4.dim(`
|
|
2356
2360
|
Opening ${target}
|
|
2357
2361
|
`));
|
|
@@ -2474,6 +2478,17 @@ function assetName(tag) {
|
|
|
2474
2478
|
return `openship-dashboard-${tag}.tar.gz`;
|
|
2475
2479
|
}
|
|
2476
2480
|
async function ensureDashboard(opts = {}) {
|
|
2481
|
+
const override = process.env.OPENSHIP_DASHBOARD_DIR?.trim();
|
|
2482
|
+
if (override) {
|
|
2483
|
+
const cwd2 = join3(override, "apps", "dashboard");
|
|
2484
|
+
const entry2 = join3(cwd2, "server.js");
|
|
2485
|
+
if (!existsSync2(entry2)) {
|
|
2486
|
+
throw new Error(
|
|
2487
|
+
`OPENSHIP_DASHBOARD_DIR=${override} but ${entry2} is missing \u2014 build the dashboard standalone first (see docs).`
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
return { tag: "local", entry: entry2, cwd: cwd2 };
|
|
2491
|
+
}
|
|
2477
2492
|
const tag = opts.tag ?? await resolveLatestTag();
|
|
2478
2493
|
const dir = join3(DASHBOARD_CACHE, tag);
|
|
2479
2494
|
const cwd = join3(dir, "apps", "dashboard");
|
|
@@ -2534,6 +2549,10 @@ function upArgs(flags) {
|
|
|
2534
2549
|
if (flags.dashboardPort) a.push("--dashboard-port", flags.dashboardPort);
|
|
2535
2550
|
if (flags.ui === false) a.push("--no-ui");
|
|
2536
2551
|
if (flags.uiVersion) a.push("--ui-version", flags.uiVersion);
|
|
2552
|
+
if (flags.publicUrl) a.push("--public-url", flags.publicUrl);
|
|
2553
|
+
if (flags.trustProxy) a.push("--trust-proxy");
|
|
2554
|
+
if (flags.managedEdge) a.push("--managed-edge");
|
|
2555
|
+
if (flags.acmeEmail) a.push("--acme-email", flags.acmeEmail);
|
|
2537
2556
|
return a;
|
|
2538
2557
|
}
|
|
2539
2558
|
function runArgv(flags) {
|
|
@@ -2562,10 +2581,17 @@ function hasSystemd() {
|
|
|
2562
2581
|
function xmlEscape(s) {
|
|
2563
2582
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2564
2583
|
}
|
|
2584
|
+
function serviceEnv() {
|
|
2585
|
+
const extra = {};
|
|
2586
|
+
const dashDir = process.env.OPENSHIP_DASHBOARD_DIR?.trim();
|
|
2587
|
+
if (dashDir) extra.OPENSHIP_DASHBOARD_DIR = dashDir;
|
|
2588
|
+
return extra;
|
|
2589
|
+
}
|
|
2565
2590
|
function plist(flags) {
|
|
2566
2591
|
const argv = runArgv(flags);
|
|
2567
2592
|
const items = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
2568
2593
|
const path2 = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", join4(HOME, ".bun/bin")].join(":");
|
|
2594
|
+
const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `<key>${xmlEscape(k)}</key><string>${xmlEscape(v)}</string>`).join("");
|
|
2569
2595
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2570
2596
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2571
2597
|
<plist version="1.0">
|
|
@@ -2580,7 +2606,7 @@ ${items}
|
|
|
2580
2606
|
<key>StandardOutPath</key><string>${join4(LOG_DIR, "up.log")}</string>
|
|
2581
2607
|
<key>StandardErrorPath</key><string>${join4(LOG_DIR, "up.err.log")}</string>
|
|
2582
2608
|
<key>EnvironmentVariables</key>
|
|
2583
|
-
<dict><key>PATH</key><string>${path2}</string
|
|
2609
|
+
<dict><key>PATH</key><string>${path2}</string>${extraEnv}</dict>
|
|
2584
2610
|
</dict>
|
|
2585
2611
|
</plist>
|
|
2586
2612
|
`;
|
|
@@ -2588,6 +2614,8 @@ ${items}
|
|
|
2588
2614
|
function systemdUnit(flags) {
|
|
2589
2615
|
const argv = runArgv(flags);
|
|
2590
2616
|
const execStart = argv.map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
|
|
2617
|
+
const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `Environment=${k}=${v}
|
|
2618
|
+
`).join("");
|
|
2591
2619
|
return `[Unit]
|
|
2592
2620
|
Description=Openship control plane
|
|
2593
2621
|
After=network-online.target
|
|
@@ -2599,7 +2627,7 @@ ExecStart=${execStart}
|
|
|
2599
2627
|
Restart=always
|
|
2600
2628
|
RestartSec=2
|
|
2601
2629
|
Environment=NODE_ENV=production
|
|
2602
|
-
|
|
2630
|
+
${extraEnv}
|
|
2603
2631
|
[Install]
|
|
2604
2632
|
WantedBy=default.target
|
|
2605
2633
|
`;
|
|
@@ -2649,6 +2677,28 @@ function installAndStart(flags) {
|
|
|
2649
2677
|
"No supported service manager found (need systemd on Linux). Run `openship up --foreground` instead, or use docker compose for always-on."
|
|
2650
2678
|
);
|
|
2651
2679
|
}
|
|
2680
|
+
function restart() {
|
|
2681
|
+
const kind = detectKind();
|
|
2682
|
+
if (kind === "launchd") {
|
|
2683
|
+
if (!existsSync3(MAC_PLIST)) return { restarted: false, detail: "no launchd agent installed" };
|
|
2684
|
+
const uid = String(process.getuid?.() ?? "");
|
|
2685
|
+
const r = run("launchctl", ["kickstart", "-k", `gui/${uid}/${MAC_LABEL}`]);
|
|
2686
|
+
return { restarted: r.ok, detail: r.ok ? `restarted ${MAC_LABEL}` : r.out };
|
|
2687
|
+
}
|
|
2688
|
+
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2689
|
+
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
2690
|
+
const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
|
|
2691
|
+
if (!existsSync3(unitPath)) return { restarted: false, detail: "no systemd unit installed" };
|
|
2692
|
+
const r = run("systemctl", [...sysArgs, "restart", SYSTEMD_NAME]);
|
|
2693
|
+
return { restarted: r.ok, detail: r.ok ? `restarted ${SYSTEMD_NAME}` : r.out };
|
|
2694
|
+
}
|
|
2695
|
+
if (kind === "schtasks") {
|
|
2696
|
+
run("schtasks", ["/End", "/TN", WIN_TASK]);
|
|
2697
|
+
const r = run("schtasks", ["/Run", "/TN", WIN_TASK]);
|
|
2698
|
+
return { restarted: r.ok, detail: r.ok ? `restarted ${WIN_TASK}` : r.out };
|
|
2699
|
+
}
|
|
2700
|
+
return { restarted: false, detail: "no supported service manager" };
|
|
2701
|
+
}
|
|
2652
2702
|
function stop() {
|
|
2653
2703
|
const kind = detectKind();
|
|
2654
2704
|
if (kind === "launchd") {
|
|
@@ -2673,6 +2723,26 @@ function stop() {
|
|
|
2673
2723
|
}
|
|
2674
2724
|
|
|
2675
2725
|
// src/commands/up.ts
|
|
2726
|
+
function normalizeUrl(raw) {
|
|
2727
|
+
const value = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
2728
|
+
try {
|
|
2729
|
+
const u = new URL(value);
|
|
2730
|
+
return `${u.protocol}//${u.host}`;
|
|
2731
|
+
} catch {
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
function normalizePublicUrl(raw) {
|
|
2736
|
+
const url = normalizeUrl(raw);
|
|
2737
|
+
if (!url) {
|
|
2738
|
+
console.error(
|
|
2739
|
+
chalk5.red(`
|
|
2740
|
+
Invalid --public-url: ${raw}`) + chalk5.dim("\n Expected something like https://ops.example.com\n")
|
|
2741
|
+
);
|
|
2742
|
+
process.exit(1);
|
|
2743
|
+
}
|
|
2744
|
+
return url;
|
|
2745
|
+
}
|
|
2676
2746
|
var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
2677
2747
|
var SERVER_DIR = join5(DIST_DIR, "server");
|
|
2678
2748
|
var OS_DIR2 = join5(homedir4(), ".openship");
|
|
@@ -2684,17 +2754,42 @@ function ensureAuthSecret() {
|
|
|
2684
2754
|
writeFileSync4(path2, secret, { mode: 384 });
|
|
2685
2755
|
return secret;
|
|
2686
2756
|
}
|
|
2687
|
-
|
|
2757
|
+
function ensureInternalToken() {
|
|
2758
|
+
const path2 = join5(OS_DIR2, "internal-token");
|
|
2759
|
+
if (existsSync4(path2)) return readFileSync2(path2, "utf8").trim();
|
|
2760
|
+
mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
|
|
2761
|
+
const token = randomBytes(32).toString("hex");
|
|
2762
|
+
writeFileSync4(path2, token, { mode: 384 });
|
|
2763
|
+
return token;
|
|
2764
|
+
}
|
|
2765
|
+
var upCommand = new Command4("up").description("Start Openship as a persistent service (boot + auto-restart); --foreground to run attached").option("--port <port>", "API port to listen on", "4000").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port", "3001").option("--no-ui", "Run the API only \u2014 don't download/serve the dashboard").option("--ui-version <tag>", "Dashboard release tag to run (default: this CLI's version)").option("-f, --foreground", "Run attached in this terminal instead of as a background service").option("--dry-run", "Print the service definition that would be installed, then exit").option(
|
|
2766
|
+
"--public-url <url>",
|
|
2767
|
+
"Serve remotely at this public URL (VPS): binds the dashboard to all interfaces, proxies the API same-origin, and requires login"
|
|
2768
|
+
).option(
|
|
2769
|
+
"--trust-proxy",
|
|
2770
|
+
"Trust the X-Real-IP set by a reverse proxy in front (the proxy MUST overwrite X-Real-IP with the real client IP, e.g. `proxy_set_header X-Real-IP $remote_addr`, and the app port MUST be firewalled so only the proxy can reach it; enables per-client rate limiting)"
|
|
2771
|
+
).option(
|
|
2772
|
+
"--managed-edge",
|
|
2773
|
+
"Managed edge: install OpenResty + a free Let's Encrypt cert on this box and route --public-url's domain to the dashboard (no reverse proxy needed)"
|
|
2774
|
+
).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").action(async (opts) => {
|
|
2688
2775
|
if (opts.foreground) return runForeground(opts);
|
|
2689
2776
|
startService(opts);
|
|
2690
2777
|
});
|
|
2691
|
-
function startService(opts) {
|
|
2778
|
+
function startService(opts, runOpts = {}) {
|
|
2779
|
+
const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
|
|
2780
|
+
const port = String(opts.port || "4000");
|
|
2781
|
+
const dashPort = String(opts.dashboardPort || "3001");
|
|
2692
2782
|
const flags = {
|
|
2693
2783
|
port: opts.port,
|
|
2694
2784
|
dataDir: opts.dataDir,
|
|
2695
2785
|
dashboardPort: opts.dashboardPort,
|
|
2696
2786
|
ui: opts.ui,
|
|
2697
|
-
uiVersion: opts.uiVersion
|
|
2787
|
+
uiVersion: opts.uiVersion,
|
|
2788
|
+
publicUrl,
|
|
2789
|
+
trustProxy: opts.trustProxy || opts.managedEdge,
|
|
2790
|
+
// managed edge = OpenResty sets XFF
|
|
2791
|
+
managedEdge: opts.managedEdge,
|
|
2792
|
+
acmeEmail: opts.acmeEmail
|
|
2698
2793
|
};
|
|
2699
2794
|
if (opts.dryRun) {
|
|
2700
2795
|
const p = preview(flags);
|
|
@@ -2705,19 +2800,23 @@ function startService(opts) {
|
|
|
2705
2800
|
|
|
2706
2801
|
`) + p.content + "\n"
|
|
2707
2802
|
);
|
|
2708
|
-
return;
|
|
2803
|
+
return { port, dashPort, publicUrl };
|
|
2709
2804
|
}
|
|
2710
2805
|
try {
|
|
2711
2806
|
const res = installAndStart(flags);
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2807
|
+
if (!runOpts.quiet) {
|
|
2808
|
+
const dashboardLine = publicUrl ? chalk5.dim(` Dashboard: ${publicUrl} (login required)
|
|
2809
|
+
`) : chalk5.dim(` Dashboard: http://localhost:${dashPort} (login required)
|
|
2810
|
+
`);
|
|
2811
|
+
console.log(
|
|
2812
|
+
chalk5.green("\n \u2714 Openship is running as a service.\n") + (opts.ui !== false ? dashboardLine : "") + (publicUrl ? chalk5.dim(" API is proxied through the dashboard (not exposed). Point your reverse proxy / DNS at the dashboard port.\n") : chalk5.dim(` API: http://localhost:${port}/api
|
|
2813
|
+
`)) + chalk5.dim(` ${res.detail}
|
|
2718
2814
|
`) + chalk5.dim(" Starts on boot and auto-restarts. Stop with `openship stop`.\n")
|
|
2719
|
-
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
return { port, dashPort, publicUrl };
|
|
2720
2818
|
} catch (e) {
|
|
2819
|
+
if (runOpts.quiet) throw e;
|
|
2721
2820
|
console.error(
|
|
2722
2821
|
chalk5.red(`
|
|
2723
2822
|
Couldn't install the service: ${e.message}
|
|
@@ -2735,26 +2834,40 @@ async function runForeground(opts) {
|
|
|
2735
2834
|
process.exit(1);
|
|
2736
2835
|
}
|
|
2737
2836
|
const port = String(opts.port || "4000");
|
|
2837
|
+
const dashPort = String(opts.dashboardPort || "3001");
|
|
2838
|
+
const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
|
|
2839
|
+
const managedEdge = Boolean(opts.managedEdge && publicUrl);
|
|
2738
2840
|
const dataDir = opts.dataDir || join5(OS_DIR2, "data");
|
|
2739
2841
|
mkdirSync5(dataDir, { recursive: true });
|
|
2740
2842
|
const env = {
|
|
2741
2843
|
...process.env,
|
|
2742
2844
|
PORT: port,
|
|
2743
2845
|
NODE_ENV: "production",
|
|
2744
|
-
// desktop mode → in-process job runner (no Redis)
|
|
2745
|
-
// so a local single-user box needs no PAT over 127.0.0.1.
|
|
2846
|
+
// desktop mode → in-process job runner (no Redis).
|
|
2746
2847
|
DEPLOY_MODE: "desktop",
|
|
2747
2848
|
OPENSHIP_TARGET: "local",
|
|
2748
2849
|
OPENSHIP_JOB_RUNNER: "in-process",
|
|
2749
|
-
OPENSHIP_ALLOW_ZERO_AUTH: "true",
|
|
2750
2850
|
PGLITE_DATA_DIR: dataDir,
|
|
2751
2851
|
OPENSHIP_MIGRATIONS_DIR: join5(SERVER_DIR, "migrations"),
|
|
2752
2852
|
OPENSHIP_PGLITE_ASSETS_DIR: join5(SERVER_DIR, "pglite"),
|
|
2753
2853
|
BETTER_AUTH_SECRET: ensureAuthSecret()
|
|
2754
2854
|
};
|
|
2855
|
+
env.OPENSHIP_REQUIRE_AUTH = "true";
|
|
2856
|
+
env.INTERNAL_TOKEN = ensureInternalToken();
|
|
2857
|
+
env.OPENSHIP_API_HOST = "127.0.0.1";
|
|
2858
|
+
delete env.OPENSHIP_ALLOW_ZERO_AUTH;
|
|
2859
|
+
if (publicUrl) {
|
|
2860
|
+
env.OPENSHIP_PUBLIC_URL = publicUrl;
|
|
2861
|
+
}
|
|
2862
|
+
if (opts.trustProxy || managedEdge) env.TRUST_PROXY = "true";
|
|
2863
|
+
if (managedEdge) {
|
|
2864
|
+
env.OPENSHIP_MANAGED_EDGE = "true";
|
|
2865
|
+
env.OPENSHIP_DASHBOARD_PORT = dashPort;
|
|
2866
|
+
if (opts.acmeEmail) env.OPENSHIP_ACME_EMAIL = opts.acmeEmail;
|
|
2867
|
+
}
|
|
2755
2868
|
delete env.DATABASE_URL;
|
|
2756
2869
|
delete env.POSTGRES_URL;
|
|
2757
|
-
const
|
|
2870
|
+
const spinner3 = ora(`Starting Openship on http://localhost:${port} \u2026`).start();
|
|
2758
2871
|
const child = spawn(process.execPath, [serverEntry], { env, stdio: ["ignore", "pipe", "pipe"] });
|
|
2759
2872
|
let buffered = "";
|
|
2760
2873
|
const buffer = (d) => {
|
|
@@ -2764,7 +2877,7 @@ async function runForeground(opts) {
|
|
|
2764
2877
|
child.stderr.on("data", buffer);
|
|
2765
2878
|
child.on("exit", (code) => {
|
|
2766
2879
|
if (code && code !== 0) {
|
|
2767
|
-
|
|
2880
|
+
spinner3.fail(`Openship server exited (code ${code})`);
|
|
2768
2881
|
process.stderr.write(buffered.slice(-2e3));
|
|
2769
2882
|
process.exit(code);
|
|
2770
2883
|
}
|
|
@@ -2783,12 +2896,12 @@ async function runForeground(opts) {
|
|
|
2783
2896
|
}
|
|
2784
2897
|
}
|
|
2785
2898
|
if (!healthy) {
|
|
2786
|
-
|
|
2899
|
+
spinner3.fail("Openship did not become healthy in time");
|
|
2787
2900
|
process.stderr.write(buffered.slice(-2e3));
|
|
2788
2901
|
child.kill("SIGTERM");
|
|
2789
2902
|
process.exit(1);
|
|
2790
2903
|
}
|
|
2791
|
-
|
|
2904
|
+
spinner3.succeed(`Openship API running at http://localhost:${port}`);
|
|
2792
2905
|
const children = [child];
|
|
2793
2906
|
const stopAll = () => {
|
|
2794
2907
|
for (const c of children) {
|
|
@@ -2806,11 +2919,10 @@ async function runForeground(opts) {
|
|
|
2806
2919
|
};
|
|
2807
2920
|
let dashboardUrl = null;
|
|
2808
2921
|
if (opts.ui !== false) {
|
|
2809
|
-
const dashPort = String(opts.dashboardPort || "3001");
|
|
2810
2922
|
const uiSpinner = ora("Preparing the dashboard\u2026").start();
|
|
2811
2923
|
try {
|
|
2812
2924
|
const bundle = await ensureDashboard({
|
|
2813
|
-
tag: opts.uiVersion || `v${"0.1
|
|
2925
|
+
tag: opts.uiVersion || `v${"0.2.1"}`,
|
|
2814
2926
|
onProgress: (received, total) => {
|
|
2815
2927
|
if (total) {
|
|
2816
2928
|
uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
|
|
@@ -2825,12 +2937,18 @@ async function runForeground(opts) {
|
|
|
2825
2937
|
NODE_ENV: "production",
|
|
2826
2938
|
OPENSHIP_TARGET: "local",
|
|
2827
2939
|
PORT: dashPort,
|
|
2828
|
-
|
|
2829
|
-
//
|
|
2830
|
-
//
|
|
2831
|
-
|
|
2832
|
-
//
|
|
2833
|
-
|
|
2940
|
+
// Reachable remotely when public; loopback-only otherwise. Under
|
|
2941
|
+
// managed edge the local OpenResty fronts the dashboard, so it stays
|
|
2942
|
+
// on loopback even though there's a public URL.
|
|
2943
|
+
HOSTNAME: publicUrl && !managedEdge ? "0.0.0.0" : "127.0.0.1",
|
|
2944
|
+
// The dashboard's same-origin proxy (NEXT_PUBLIC_API_PROXY, baked
|
|
2945
|
+
// into the release build) forwards /api/proxy/* to this address, so
|
|
2946
|
+
// the browser never needs to know where the API lives. Set in every
|
|
2947
|
+
// mode; loopback because the dashboard runs on the same box.
|
|
2948
|
+
INTERNAL_API_URL: `http://127.0.0.1:${port}`,
|
|
2949
|
+
// Public URL feeds the SSR proxy-origin resolver; local mode keeps
|
|
2950
|
+
// the window.__OPENSHIP_API_ORIGIN__ fallback for direct API calls.
|
|
2951
|
+
...publicUrl ? { OPENSHIP_PUBLIC_URL: publicUrl } : { OPENSHIP_LOCAL_API_URL: `http://127.0.0.1:${port}` }
|
|
2834
2952
|
},
|
|
2835
2953
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2836
2954
|
});
|
|
@@ -2854,7 +2972,7 @@ async function runForeground(opts) {
|
|
|
2854
2972
|
}
|
|
2855
2973
|
}
|
|
2856
2974
|
if (dashUp) {
|
|
2857
|
-
dashboardUrl = `http://localhost:${dashPort}`;
|
|
2975
|
+
dashboardUrl = publicUrl ?? `http://localhost:${dashPort}`;
|
|
2858
2976
|
uiSpinner.succeed(`Dashboard running at ${dashboardUrl}`);
|
|
2859
2977
|
dash.stdout.off("data", onDash);
|
|
2860
2978
|
dash.stderr.off("data", onDash);
|
|
@@ -2873,12 +2991,20 @@ async function runForeground(opts) {
|
|
|
2873
2991
|
);
|
|
2874
2992
|
}
|
|
2875
2993
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2994
|
+
if (publicUrl) {
|
|
2995
|
+
console.log(
|
|
2996
|
+
(dashboardUrl ? chalk5.dim(` Dashboard: ${dashboardUrl} (login required)
|
|
2997
|
+
`) : "") + chalk5.dim(" API is proxied through the dashboard (bound to loopback, not exposed).\n") + chalk5.dim(` Data: ${dataDir}
|
|
2998
|
+
`) + (managedEdge ? chalk5.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") : chalk5.dim(" Point your reverse proxy / DNS at the dashboard port. Stop with Ctrl-C.\n"))
|
|
2999
|
+
);
|
|
3000
|
+
} else {
|
|
3001
|
+
console.log(
|
|
3002
|
+
chalk5.dim(` API: http://localhost:${port}/api
|
|
3003
|
+
`) + (dashboardUrl ? chalk5.dim(` Dashboard: ${dashboardUrl} (login required)
|
|
2879
3004
|
`) : "") + chalk5.dim(` Data: ${dataDir}
|
|
2880
|
-
`) + chalk5.dim("
|
|
2881
|
-
|
|
3005
|
+
`) + chalk5.dim(" Log in with your admin account (run `openship` to create one). Stop with Ctrl-C.\n")
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
2882
3008
|
child.stdout.off("data", buffer);
|
|
2883
3009
|
child.stderr.off("data", buffer);
|
|
2884
3010
|
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
@@ -3402,7 +3528,7 @@ var deployCommand = new Command10("deploy").description("Trigger a deployment fo
|
|
|
3402
3528
|
let deploymentId;
|
|
3403
3529
|
let payload;
|
|
3404
3530
|
if (!inGitRepo && !gitOnlyFlags) {
|
|
3405
|
-
const
|
|
3531
|
+
const spinner3 = isJsonMode() ? null : ora2("Deploying folder").start();
|
|
3406
3532
|
try {
|
|
3407
3533
|
const result2 = await deployFolder({
|
|
3408
3534
|
cwd: process.cwd(),
|
|
@@ -3410,14 +3536,14 @@ var deployCommand = new Command10("deploy").description("Trigger a deployment fo
|
|
|
3410
3536
|
projectId: opts.project || link?.projectId,
|
|
3411
3537
|
environment: env,
|
|
3412
3538
|
onStep: (m) => {
|
|
3413
|
-
if (
|
|
3539
|
+
if (spinner3) spinner3.text = m;
|
|
3414
3540
|
}
|
|
3415
3541
|
});
|
|
3416
3542
|
deploymentId = result2.deploymentId;
|
|
3417
3543
|
payload = { success: true, deployment_id: result2.deploymentId, project_id: result2.projectId };
|
|
3418
|
-
|
|
3544
|
+
spinner3?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
|
|
3419
3545
|
} catch (e) {
|
|
3420
|
-
|
|
3546
|
+
spinner3?.fail("Folder deploy failed");
|
|
3421
3547
|
err(e instanceof ApiError ? e.message : String(e));
|
|
3422
3548
|
process.exit(1);
|
|
3423
3549
|
}
|
|
@@ -3439,7 +3565,7 @@ var deployCommand = new Command10("deploy").description("Trigger a deployment fo
|
|
|
3439
3565
|
smartRoute: opts.smartRoute || void 0,
|
|
3440
3566
|
refresh: opts.refresh || void 0
|
|
3441
3567
|
};
|
|
3442
|
-
const
|
|
3568
|
+
const spinner3 = isJsonMode() ? null : ora2("Triggering deployment").start();
|
|
3443
3569
|
let res;
|
|
3444
3570
|
try {
|
|
3445
3571
|
res = await apiRequest("/deployments", {
|
|
@@ -3447,13 +3573,13 @@ var deployCommand = new Command10("deploy").description("Trigger a deployment fo
|
|
|
3447
3573
|
body: JSON.stringify(body)
|
|
3448
3574
|
});
|
|
3449
3575
|
} catch (e) {
|
|
3450
|
-
|
|
3576
|
+
spinner3?.fail("Deployment failed to start");
|
|
3451
3577
|
err(e instanceof ApiError ? e.message : String(e));
|
|
3452
3578
|
process.exit(1);
|
|
3453
3579
|
}
|
|
3454
3580
|
deploymentId = res.data?.deployment_id;
|
|
3455
3581
|
payload = res.data ?? res;
|
|
3456
|
-
|
|
3582
|
+
spinner3?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
|
|
3457
3583
|
}
|
|
3458
3584
|
if (isJsonMode() && !opts.watch) {
|
|
3459
3585
|
printJson(payload ?? {});
|
|
@@ -3585,7 +3711,7 @@ var cancel = new Command11("cancel").description("Cancel an in-progress deployme
|
|
|
3585
3711
|
report(res, `Cancelled ${id}`);
|
|
3586
3712
|
})
|
|
3587
3713
|
);
|
|
3588
|
-
var
|
|
3714
|
+
var restart2 = new Command11("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
|
|
3589
3715
|
run2(async (id) => {
|
|
3590
3716
|
const res = await apiRequest(`/deployments/${id}/restart`, { method: "POST" });
|
|
3591
3717
|
report(res, `Restarted ${id}`);
|
|
@@ -3632,7 +3758,7 @@ var sslRenew = new Command11("renew").description("Renew (issue) an SSL certific
|
|
|
3632
3758
|
})
|
|
3633
3759
|
);
|
|
3634
3760
|
var ssl = new Command11("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
|
|
3635
|
-
var deploymentCommand = new Command11("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(
|
|
3761
|
+
var deploymentCommand = new Command11("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(restart2).addCommand(reject).addCommand(keep).addCommand(rm).addCommand(ssl);
|
|
3636
3762
|
|
|
3637
3763
|
// src/commands/logs.ts
|
|
3638
3764
|
import { Command as Command12 } from "commander";
|
|
@@ -4666,8 +4792,8 @@ serviceCommand.addCommand(execCmd);
|
|
|
4666
4792
|
import { Command as Command15 } from "commander";
|
|
4667
4793
|
import chalk11 from "chalk";
|
|
4668
4794
|
import ora3 from "ora";
|
|
4669
|
-
function spin(
|
|
4670
|
-
return isJsonMode() ? null : ora3(
|
|
4795
|
+
function spin(text2) {
|
|
4796
|
+
return isJsonMode() ? null : ora3(text2).start();
|
|
4671
4797
|
}
|
|
4672
4798
|
function fail2(e) {
|
|
4673
4799
|
if (e instanceof ApiError) {
|
|
@@ -4925,31 +5051,31 @@ server.command("rm <id>").alias("remove").description("Delete a server").action(
|
|
|
4925
5051
|
);
|
|
4926
5052
|
server.command("test-connection").alias("test").description("Test an SSH connection without saving it").requiredOption("--host <host>", "SSH host / IP").option("--port <port>", "SSH port", "22").option("--user <user>", "SSH user", "root").option("--auth-method <method>", "Auth method (password|key|agent)").option("--password <password>", "SSH password").option("--key-path <path>", "Path to private key").option("--key-passphrase <passphrase>", "Private key passphrase").option("--jump-host <host>", "SSH jump / bastion host").option("--ssh-args <args>", "Extra raw ssh args").action(
|
|
4927
5053
|
guard(async (o) => {
|
|
4928
|
-
const
|
|
5054
|
+
const spinner3 = isJsonMode() ? null : ora4(`Connecting to ${o.host}\u2026`).start();
|
|
4929
5055
|
try {
|
|
4930
5056
|
const res = await apiRequest("/system/test-connection", {
|
|
4931
5057
|
method: "POST",
|
|
4932
5058
|
body: JSON.stringify(connBody(o))
|
|
4933
5059
|
});
|
|
4934
|
-
|
|
5060
|
+
spinner3?.stop();
|
|
4935
5061
|
if (isJsonMode()) return printJson(res);
|
|
4936
5062
|
if (res.ok) return ok(` ${res.message}`);
|
|
4937
5063
|
err(` ${res.message}`);
|
|
4938
5064
|
process.exit(1);
|
|
4939
5065
|
} catch (e) {
|
|
4940
|
-
|
|
5066
|
+
spinner3?.stop();
|
|
4941
5067
|
throw e;
|
|
4942
5068
|
}
|
|
4943
5069
|
})
|
|
4944
5070
|
);
|
|
4945
5071
|
server.command("check <serverId>").description("Run component health checks against a saved server").option("-c, --component <name...>", "Limit to specific components").action(
|
|
4946
5072
|
guard(async (serverId, o) => {
|
|
4947
|
-
const
|
|
5073
|
+
const spinner3 = isJsonMode() ? null : ora4("Checking components\u2026").start();
|
|
4948
5074
|
const res = await apiRequest("/system/check", {
|
|
4949
5075
|
method: "POST",
|
|
4950
5076
|
body: JSON.stringify({ serverId, components: o.component })
|
|
4951
5077
|
});
|
|
4952
|
-
|
|
5078
|
+
spinner3?.stop();
|
|
4953
5079
|
if (isJsonMode()) return printJson(res);
|
|
4954
5080
|
printTable(
|
|
4955
5081
|
res.components.map((c) => ({
|
|
@@ -5005,15 +5131,15 @@ server.command("install <serverId>").description("Install components on a server
|
|
|
5005
5131
|
}
|
|
5006
5132
|
const results = [];
|
|
5007
5133
|
for (const component of components) {
|
|
5008
|
-
const
|
|
5134
|
+
const spinner3 = isJsonMode() ? null : ora4(`Installing ${component}\u2026`).start();
|
|
5009
5135
|
const res = await apiRequest(
|
|
5010
5136
|
"/system/install",
|
|
5011
5137
|
{ method: "POST", body: JSON.stringify({ serverId, component }) }
|
|
5012
5138
|
);
|
|
5013
5139
|
results.push(res);
|
|
5014
|
-
if (isJsonMode())
|
|
5015
|
-
else if (res.success)
|
|
5016
|
-
else
|
|
5140
|
+
if (isJsonMode()) spinner3?.stop();
|
|
5141
|
+
else if (res.success) spinner3?.succeed(`${component} installed${res.version ? ` (${res.version})` : ""}`);
|
|
5142
|
+
else spinner3?.fail(`${component} failed: ${res.error ?? "unknown error"}`);
|
|
5017
5143
|
}
|
|
5018
5144
|
if (isJsonMode()) printJson(results);
|
|
5019
5145
|
})
|
|
@@ -5128,8 +5254,8 @@ function report2(obj, human) {
|
|
|
5128
5254
|
if (isJsonMode()) printJson(obj);
|
|
5129
5255
|
else human();
|
|
5130
5256
|
}
|
|
5131
|
-
function spinner(
|
|
5132
|
-
return isJsonMode() ? null : ora5(
|
|
5257
|
+
function spinner(text2) {
|
|
5258
|
+
return isJsonMode() ? null : ora5(text2).start();
|
|
5133
5259
|
}
|
|
5134
5260
|
async function confirm2(message, yes) {
|
|
5135
5261
|
if (yes) return true;
|
|
@@ -5230,13 +5356,13 @@ var upgradeToAuthCommand = new Command17("upgrade-to-auth").description("Promote
|
|
|
5230
5356
|
await guarded(async () => {
|
|
5231
5357
|
const name = opts.name;
|
|
5232
5358
|
const email = opts.email;
|
|
5233
|
-
let
|
|
5234
|
-
if (!
|
|
5359
|
+
let password2 = opts.password;
|
|
5360
|
+
if (!password2) {
|
|
5235
5361
|
if (!process.stdin.isTTY || isJsonMode()) {
|
|
5236
5362
|
err("\n --password is required in non-interactive mode.\n");
|
|
5237
5363
|
process.exit(1);
|
|
5238
5364
|
}
|
|
5239
|
-
|
|
5365
|
+
password2 = await promptHidden(" New password: ");
|
|
5240
5366
|
}
|
|
5241
5367
|
const res = await apiRequest(
|
|
5242
5368
|
"/system/upgrade-to-auth",
|
|
@@ -5245,7 +5371,7 @@ var upgradeToAuthCommand = new Command17("upgrade-to-auth").description("Promote
|
|
|
5245
5371
|
body: JSON.stringify({
|
|
5246
5372
|
name,
|
|
5247
5373
|
email,
|
|
5248
|
-
password,
|
|
5374
|
+
password: password2,
|
|
5249
5375
|
useOwnMailServer: opts.useOwnMailServer === true
|
|
5250
5376
|
})
|
|
5251
5377
|
}
|
|
@@ -5484,8 +5610,8 @@ function guard2(fn) {
|
|
|
5484
5610
|
}
|
|
5485
5611
|
};
|
|
5486
5612
|
}
|
|
5487
|
-
function spin2(
|
|
5488
|
-
return isJsonMode() ? null : ora6(
|
|
5613
|
+
function spin2(text2) {
|
|
5614
|
+
return isJsonMode() ? null : ora6(text2).start();
|
|
5489
5615
|
}
|
|
5490
5616
|
function safeParse4(data) {
|
|
5491
5617
|
try {
|
|
@@ -5747,24 +5873,24 @@ var postmasterCmd = new Command18("postmaster").description("Manage the postmast
|
|
|
5747
5873
|
postmasterCmd.addCommand(
|
|
5748
5874
|
new Command18("set-password").description("Rotate the postmaster password").argument("<serverId>", "Mail server ID").option("--password <password>", "New password (min 12 chars); prompted if omitted").action(
|
|
5749
5875
|
guard2(async (serverId, opts) => {
|
|
5750
|
-
let
|
|
5751
|
-
if (!
|
|
5876
|
+
let password2 = opts.password;
|
|
5877
|
+
if (!password2) {
|
|
5752
5878
|
if (isJsonMode()) {
|
|
5753
5879
|
err(" --password is required in JSON mode.");
|
|
5754
5880
|
process.exit(1);
|
|
5755
5881
|
}
|
|
5756
5882
|
const rl = createInterface7({ input: input6, output: output6 });
|
|
5757
|
-
|
|
5883
|
+
password2 = (await rl.question(" New postmaster password (min 12 chars): ")).trim();
|
|
5758
5884
|
rl.close();
|
|
5759
5885
|
}
|
|
5760
|
-
if (!
|
|
5886
|
+
if (!password2 || password2.length < 12) {
|
|
5761
5887
|
err(" Password must be at least 12 characters.");
|
|
5762
5888
|
process.exit(1);
|
|
5763
5889
|
}
|
|
5764
5890
|
const sp = spin2("Updating postmaster password\u2026");
|
|
5765
5891
|
const res = await apiRequest("/mail/credentials/postmaster", {
|
|
5766
5892
|
method: "POST",
|
|
5767
|
-
body: JSON.stringify({ serverId, password })
|
|
5893
|
+
body: JSON.stringify({ serverId, password: password2 })
|
|
5768
5894
|
});
|
|
5769
5895
|
sp?.succeed("Postmaster password updated.");
|
|
5770
5896
|
if (isJsonMode()) printJson(res);
|
|
@@ -5820,7 +5946,7 @@ function fmtBytes2(n) {
|
|
|
5820
5946
|
return `${v.toFixed(i === 0 ? 0 : 1)}${units[i]}`;
|
|
5821
5947
|
}
|
|
5822
5948
|
async function followStream(path2, label) {
|
|
5823
|
-
const
|
|
5949
|
+
const spinner3 = isJsonMode() ? null : ora7(`${label}: connecting\u2026`).start();
|
|
5824
5950
|
let status = "unknown";
|
|
5825
5951
|
try {
|
|
5826
5952
|
for await (const ev of sseRequest(path2)) {
|
|
@@ -5835,28 +5961,28 @@ async function followStream(path2, label) {
|
|
|
5835
5961
|
if (type === "snapshot") {
|
|
5836
5962
|
const rec = payload.run ?? payload.restore;
|
|
5837
5963
|
status = rec?.status ?? status;
|
|
5838
|
-
if (
|
|
5964
|
+
if (spinner3) spinner3.text = `${label}: ${status}`;
|
|
5839
5965
|
} else if (type === "transition") {
|
|
5840
5966
|
status = payload.status ?? status;
|
|
5841
5967
|
const bytes = payload.bytesTransferred ?? payload.bytesRestored;
|
|
5842
|
-
if (
|
|
5968
|
+
if (spinner3) spinner3.text = `${label}: ${status}${bytes ? ` (${fmtBytes2(bytes)})` : ""}`;
|
|
5843
5969
|
} else if (type === "progress") {
|
|
5844
5970
|
const artifact = payload.currentArtifact ?? "working";
|
|
5845
5971
|
const bytes = payload.bytesTransferred;
|
|
5846
|
-
if (
|
|
5972
|
+
if (spinner3) spinner3.text = `${label}: ${artifact} ${fmtBytes2(bytes)}`.trimEnd();
|
|
5847
5973
|
} else if (type === "complete") {
|
|
5848
5974
|
status = payload.status ?? status;
|
|
5849
5975
|
const errMsg = payload.errorMessage;
|
|
5850
|
-
if (status === "succeeded")
|
|
5851
|
-
else
|
|
5976
|
+
if (status === "succeeded") spinner3?.succeed(`${label} succeeded`);
|
|
5977
|
+
else spinner3?.fail(`${label} ${status}${errMsg ? `: ${errMsg}` : ""}`);
|
|
5852
5978
|
break;
|
|
5853
5979
|
}
|
|
5854
5980
|
}
|
|
5855
5981
|
} catch (e) {
|
|
5856
|
-
|
|
5982
|
+
spinner3?.stop();
|
|
5857
5983
|
throw e;
|
|
5858
5984
|
}
|
|
5859
|
-
|
|
5985
|
+
spinner3?.stop();
|
|
5860
5986
|
return status;
|
|
5861
5987
|
}
|
|
5862
5988
|
var policyCmd = new Command19("policy").description("Backup policies (schedules) for a project");
|
|
@@ -6104,7 +6230,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
|
|
|
6104
6230
|
);
|
|
6105
6231
|
destinationCmd.command("preflight").description("Verify a destination (write + read + delete a probe object)").argument("<destinationId>", "Destination ID").action(
|
|
6106
6232
|
(destinationId) => guard3(async () => {
|
|
6107
|
-
const
|
|
6233
|
+
const spinner3 = isJsonMode() ? null : ora7("Running preflight\u2026").start();
|
|
6108
6234
|
const { data } = await apiRequest(
|
|
6109
6235
|
`/backup-destinations/${encodeURIComponent(destinationId)}/preflight`,
|
|
6110
6236
|
{ method: "POST", body: JSON.stringify({}) }
|
|
@@ -6113,9 +6239,9 @@ destinationCmd.command("preflight").description("Verify a destination (write + r
|
|
|
6113
6239
|
printJson(data);
|
|
6114
6240
|
return;
|
|
6115
6241
|
}
|
|
6116
|
-
if (data.ok)
|
|
6242
|
+
if (data.ok) spinner3?.succeed("Destination reachable");
|
|
6117
6243
|
else {
|
|
6118
|
-
|
|
6244
|
+
spinner3?.fail(`Preflight failed: ${data.reason ?? "unknown"}`);
|
|
6119
6245
|
process.exit(1);
|
|
6120
6246
|
}
|
|
6121
6247
|
})
|
|
@@ -6129,8 +6255,8 @@ import chalk15 from "chalk";
|
|
|
6129
6255
|
// src/lib/cmd-helpers.ts
|
|
6130
6256
|
import chalk14 from "chalk";
|
|
6131
6257
|
import ora8 from "ora";
|
|
6132
|
-
function spin3(
|
|
6133
|
-
return isJsonMode() ? null : ora8(
|
|
6258
|
+
function spin3(text2) {
|
|
6259
|
+
return isJsonMode() ? null : ora8(text2).start();
|
|
6134
6260
|
}
|
|
6135
6261
|
function fail3(e) {
|
|
6136
6262
|
if (e instanceof ApiError) {
|
|
@@ -6239,10 +6365,10 @@ var apiCommand = new Command21("api").description("Make an authenticated request
|
|
|
6239
6365
|
if (opts.data) init.body = opts.data;
|
|
6240
6366
|
try {
|
|
6241
6367
|
const res = await apiRaw(url, init);
|
|
6242
|
-
const
|
|
6243
|
-
let body =
|
|
6368
|
+
const text2 = await res.text();
|
|
6369
|
+
let body = text2;
|
|
6244
6370
|
try {
|
|
6245
|
-
body = JSON.parse(
|
|
6371
|
+
body = JSON.parse(text2);
|
|
6246
6372
|
} catch {
|
|
6247
6373
|
}
|
|
6248
6374
|
if (typeof body === "string") {
|
|
@@ -6350,7 +6476,7 @@ var installCommand = new Command22("install").description("Download and install
|
|
|
6350
6476
|
err(e.message);
|
|
6351
6477
|
process.exit(1);
|
|
6352
6478
|
}
|
|
6353
|
-
const spin4 = (
|
|
6479
|
+
const spin4 = (text2) => isJsonMode() ? null : ora9(text2).start();
|
|
6354
6480
|
let tag;
|
|
6355
6481
|
try {
|
|
6356
6482
|
if (opts.version) {
|
|
@@ -6465,7 +6591,7 @@ function detectPackageManager2(override) {
|
|
|
6465
6591
|
return hasBun ? "bun" : "npm";
|
|
6466
6592
|
}
|
|
6467
6593
|
var updateCommand = new Command23("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").action(async (opts) => {
|
|
6468
|
-
const current = "0.1
|
|
6594
|
+
const current = "0.2.1";
|
|
6469
6595
|
let latest;
|
|
6470
6596
|
try {
|
|
6471
6597
|
latest = (await resolveLatestTag()).replace(/^v/, "");
|
|
@@ -6499,8 +6625,11 @@ var updateCommand = new Command23("update").description("Update the Openship CLI
|
|
|
6499
6625
|
process.exitCode = 1;
|
|
6500
6626
|
return;
|
|
6501
6627
|
}
|
|
6628
|
+
const { restarted } = restart();
|
|
6502
6629
|
if (isJsonMode()) {
|
|
6503
|
-
printJson({ updated: true, from: current, to: latest, via: pm });
|
|
6630
|
+
printJson({ updated: true, from: current, to: latest, via: pm, restarted });
|
|
6631
|
+
} else if (restarted) {
|
|
6632
|
+
ok(`Updated to v${latest} and restarted the service \u2014 you're on the new version.`);
|
|
6504
6633
|
} else {
|
|
6505
6634
|
ok(`Updated to v${latest}. Restart the server to run the new version: openship up`);
|
|
6506
6635
|
}
|
|
@@ -6589,10 +6718,457 @@ var cacheCommand = new Command24("cache").description("Manage the local download
|
|
|
6589
6718
|
process.exit(1);
|
|
6590
6719
|
}).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
|
|
6591
6720
|
|
|
6721
|
+
// src/commands/wizard.ts
|
|
6722
|
+
import chalk16 from "chalk";
|
|
6723
|
+
import open from "open";
|
|
6724
|
+
import { createServer } from "http";
|
|
6725
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
6726
|
+
import {
|
|
6727
|
+
intro,
|
|
6728
|
+
outro,
|
|
6729
|
+
text,
|
|
6730
|
+
password,
|
|
6731
|
+
select,
|
|
6732
|
+
confirm as confirm3,
|
|
6733
|
+
spinner as spinner2,
|
|
6734
|
+
note,
|
|
6735
|
+
log,
|
|
6736
|
+
cancel as cancel2,
|
|
6737
|
+
isCancel
|
|
6738
|
+
} from "@clack/prompts";
|
|
6739
|
+
function ensure(value) {
|
|
6740
|
+
if (isCancel(value)) {
|
|
6741
|
+
cancel2("Setup cancelled.");
|
|
6742
|
+
process.exit(0);
|
|
6743
|
+
}
|
|
6744
|
+
return value;
|
|
6745
|
+
}
|
|
6746
|
+
var SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
6747
|
+
async function internalGet(port, path2) {
|
|
6748
|
+
try {
|
|
6749
|
+
const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
|
|
6750
|
+
headers: { "X-Internal-Token": ensureInternalToken() },
|
|
6751
|
+
signal: AbortSignal.timeout(1e4)
|
|
6752
|
+
});
|
|
6753
|
+
if (!res.ok) return null;
|
|
6754
|
+
return await res.json();
|
|
6755
|
+
} catch {
|
|
6756
|
+
return null;
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
async function internalPost(port, path2, body) {
|
|
6760
|
+
try {
|
|
6761
|
+
const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
|
|
6762
|
+
method: "POST",
|
|
6763
|
+
headers: { "Content-Type": "application/json", "X-Internal-Token": ensureInternalToken() },
|
|
6764
|
+
body: JSON.stringify(body),
|
|
6765
|
+
signal: AbortSignal.timeout(3e4)
|
|
6766
|
+
});
|
|
6767
|
+
const data = await res.json().catch(() => ({}));
|
|
6768
|
+
return { ok: res.ok, data };
|
|
6769
|
+
} catch (err2) {
|
|
6770
|
+
return { ok: false, data: { error: err2.message } };
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
async function bootstrapAdmin(apiPort, admin) {
|
|
6774
|
+
const { ok: ok3, data } = await internalPost(apiPort, "/api/system/bootstrap-admin", admin);
|
|
6775
|
+
if (ok3) return { ok: true };
|
|
6776
|
+
if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
|
|
6777
|
+
return { ok: false, message: data?.error || "failed" };
|
|
6778
|
+
}
|
|
6779
|
+
async function waitHealthy(apiPort, seconds = 90) {
|
|
6780
|
+
for (let i = 0; i < seconds; i++) {
|
|
6781
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
6782
|
+
try {
|
|
6783
|
+
await fetch(`http://127.0.0.1:${apiPort}/api/health`, { signal: AbortSignal.timeout(2e3) });
|
|
6784
|
+
return true;
|
|
6785
|
+
} catch {
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
return false;
|
|
6789
|
+
}
|
|
6790
|
+
async function detectPublicIp() {
|
|
6791
|
+
for (const url of ["https://api.ipify.org", "https://ifconfig.me/ip"]) {
|
|
6792
|
+
try {
|
|
6793
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3e3) });
|
|
6794
|
+
if (!res.ok) continue;
|
|
6795
|
+
const ip = (await res.text()).trim();
|
|
6796
|
+
if (/^[0-9.]+$/.test(ip) || ip.includes(":")) return ip;
|
|
6797
|
+
} catch {
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
return null;
|
|
6801
|
+
}
|
|
6802
|
+
var b64url = (buf) => buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6803
|
+
async function connectOpenshipCloud(port) {
|
|
6804
|
+
const already = await internalGet(port, "/api/system/cloud-status");
|
|
6805
|
+
if (already?.connected) {
|
|
6806
|
+
log.success(`Already connected to Openship Cloud${already.user?.email ? ` as ${already.user.email}` : ""}.`);
|
|
6807
|
+
return true;
|
|
6808
|
+
}
|
|
6809
|
+
const capsEnv = await internalGet(port, "/api/health/env");
|
|
6810
|
+
const cloudApiUrl = capsEnv?.cloudApiUrl;
|
|
6811
|
+
if (!cloudApiUrl) {
|
|
6812
|
+
log.error("Couldn't discover the Openship Cloud URL \u2014 free domain unavailable. Use a custom domain instead.");
|
|
6813
|
+
return false;
|
|
6814
|
+
}
|
|
6815
|
+
const verifier = b64url(randomBytes2(32));
|
|
6816
|
+
const challenge = b64url(createHash2("sha256").update(verifier).digest());
|
|
6817
|
+
const state = b64url(randomBytes2(16));
|
|
6818
|
+
const codePromise = new Promise((resolve2) => {
|
|
6819
|
+
const server2 = createServer((req, res2) => {
|
|
6820
|
+
const u = new URL(req.url || "/", "http://127.0.0.1");
|
|
6821
|
+
if (!u.pathname.startsWith("/callback")) {
|
|
6822
|
+
res2.writeHead(404).end();
|
|
6823
|
+
return;
|
|
6824
|
+
}
|
|
6825
|
+
const code2 = u.searchParams.get("code");
|
|
6826
|
+
const gotState = u.searchParams.get("state");
|
|
6827
|
+
res2.writeHead(200, { "Content-Type": "text/html" }).end(
|
|
6828
|
+
"<html><body style='font:16px system-ui;padding:3rem;text-align:center'><h2>Openship Cloud connected</h2><p>You can close this window and return to your terminal.</p></body></html>"
|
|
6829
|
+
);
|
|
6830
|
+
server2.close();
|
|
6831
|
+
resolve2(code2 && gotState === state ? code2 : null);
|
|
6832
|
+
});
|
|
6833
|
+
server2.on("error", () => resolve2(null));
|
|
6834
|
+
server2.listen(0, "127.0.0.1", () => {
|
|
6835
|
+
const cbPort = server2.address().port;
|
|
6836
|
+
const redirect = `http://127.0.0.1:${cbPort}/callback`;
|
|
6837
|
+
const handoff = `${cloudApiUrl.replace(/\/$/, "")}/api/cloud/connect-handoff?redirect=${encodeURIComponent(redirect)}&state=${state}&code_challenge=${challenge}`;
|
|
6838
|
+
note(handoff, "Open this URL to authorize (opening your browser\u2026)");
|
|
6839
|
+
void open(handoff).catch(() => {
|
|
6840
|
+
});
|
|
6841
|
+
});
|
|
6842
|
+
setTimeout(() => {
|
|
6843
|
+
try {
|
|
6844
|
+
server2.close();
|
|
6845
|
+
} catch {
|
|
6846
|
+
}
|
|
6847
|
+
resolve2(null);
|
|
6848
|
+
}, 3e5);
|
|
6849
|
+
});
|
|
6850
|
+
const s = spinner2();
|
|
6851
|
+
s.start("Waiting for Openship Cloud authorization in your browser");
|
|
6852
|
+
const code = await codePromise;
|
|
6853
|
+
if (!code) {
|
|
6854
|
+
s.stop("Openship Cloud wasn't authorized.", 1);
|
|
6855
|
+
return false;
|
|
6856
|
+
}
|
|
6857
|
+
s.message("Linking this instance to Openship Cloud");
|
|
6858
|
+
const res = await internalPost(port, "/api/system/cloud-connect", { code, codeVerifier: verifier });
|
|
6859
|
+
if (!res.ok) {
|
|
6860
|
+
s.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
|
|
6861
|
+
return false;
|
|
6862
|
+
}
|
|
6863
|
+
s.stop("Connected to Openship Cloud.");
|
|
6864
|
+
return true;
|
|
6865
|
+
}
|
|
6866
|
+
async function promptLocalAdmin() {
|
|
6867
|
+
const name = ensure(await text({ message: "Your name", validate: (v) => v?.trim() ? void 0 : "Required" })).trim();
|
|
6868
|
+
const email = ensure(
|
|
6869
|
+
await text({
|
|
6870
|
+
message: "Email",
|
|
6871
|
+
placeholder: "you@example.com",
|
|
6872
|
+
validate: (v) => v?.includes("@") ? void 0 : "Enter a valid email"
|
|
6873
|
+
})
|
|
6874
|
+
).trim().toLowerCase();
|
|
6875
|
+
const pw = ensure(
|
|
6876
|
+
await password({ message: "Password", validate: (v) => v && v.length >= 8 ? void 0 : "At least 8 characters" })
|
|
6877
|
+
);
|
|
6878
|
+
ensure(await password({ message: "Confirm password", validate: (v) => v === pw ? void 0 : "Passwords don't match" }));
|
|
6879
|
+
return { name, email, password: pw };
|
|
6880
|
+
}
|
|
6881
|
+
async function streamProvision(port, sessionId, s) {
|
|
6882
|
+
let ok3 = false;
|
|
6883
|
+
try {
|
|
6884
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/system/self-register/stream?id=${sessionId}`, {
|
|
6885
|
+
headers: { "X-Internal-Token": ensureInternalToken() },
|
|
6886
|
+
signal: AbortSignal.timeout(3e5)
|
|
6887
|
+
});
|
|
6888
|
+
if (!res.ok || !res.body) return false;
|
|
6889
|
+
const reader = res.body.getReader();
|
|
6890
|
+
const decoder = new TextDecoder();
|
|
6891
|
+
let buffer = "";
|
|
6892
|
+
for (; ; ) {
|
|
6893
|
+
const { value, done } = await reader.read();
|
|
6894
|
+
if (done) break;
|
|
6895
|
+
buffer += decoder.decode(value, { stream: true });
|
|
6896
|
+
let sep;
|
|
6897
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
6898
|
+
const frame = buffer.slice(0, sep);
|
|
6899
|
+
buffer = buffer.slice(sep + 2);
|
|
6900
|
+
const event = /event:\s*(.*)/.exec(frame)?.[1]?.trim();
|
|
6901
|
+
const dataRaw = /data:\s*([\s\S]*)/.exec(frame)?.[1]?.trim();
|
|
6902
|
+
if (!event) continue;
|
|
6903
|
+
if (event === "log" && dataRaw) {
|
|
6904
|
+
try {
|
|
6905
|
+
const d = JSON.parse(dataRaw);
|
|
6906
|
+
if (d.message) s.message(String(d.message).replace(/\s+/g, " ").slice(0, 68));
|
|
6907
|
+
} catch {
|
|
6908
|
+
}
|
|
6909
|
+
} else if (event === "complete" && dataRaw) {
|
|
6910
|
+
try {
|
|
6911
|
+
ok3 = JSON.parse(dataRaw).status === "completed";
|
|
6912
|
+
} catch {
|
|
6913
|
+
}
|
|
6914
|
+
} else if (event === "end") {
|
|
6915
|
+
return ok3;
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
} catch {
|
|
6920
|
+
return ok3;
|
|
6921
|
+
}
|
|
6922
|
+
return ok3;
|
|
6923
|
+
}
|
|
6924
|
+
async function runWizard() {
|
|
6925
|
+
intro(`${chalk16.bgCyan(chalk16.black(" Openship "))}${chalk16.dim(" setup")}`);
|
|
6926
|
+
log.message(
|
|
6927
|
+
chalk16.dim(
|
|
6928
|
+
"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."
|
|
6929
|
+
)
|
|
6930
|
+
);
|
|
6931
|
+
let publicUrl;
|
|
6932
|
+
let behindProxy = false;
|
|
6933
|
+
let managedEdge = false;
|
|
6934
|
+
let domainPlan = { type: "none" };
|
|
6935
|
+
const reach = ensure(
|
|
6936
|
+
await select({
|
|
6937
|
+
message: "How should this instance be reachable?",
|
|
6938
|
+
initialValue: "private",
|
|
6939
|
+
options: [
|
|
6940
|
+
{ value: "private", label: "Private", hint: "this machine only (localhost)" },
|
|
6941
|
+
{ value: "public", label: "Public", hint: "a server / VPS, reachable from other machines" }
|
|
6942
|
+
]
|
|
6943
|
+
})
|
|
6944
|
+
);
|
|
6945
|
+
if (reach === "public") {
|
|
6946
|
+
const canManage = process.platform === "linux";
|
|
6947
|
+
const domainType = ensure(
|
|
6948
|
+
await select({
|
|
6949
|
+
message: "How do you want a domain + HTTPS?",
|
|
6950
|
+
initialValue: "free",
|
|
6951
|
+
options: [
|
|
6952
|
+
{ value: "free", label: "Free domain", hint: "name.opsh.io via Openship Cloud \u2014 HTTPS handled for you" },
|
|
6953
|
+
...canManage ? [{ value: "custom", label: "Custom domain", hint: "your domain + free Let's Encrypt on this box" }] : [],
|
|
6954
|
+
{ value: "byo", label: "Bring your own", hint: "your domain, behind your own reverse proxy" }
|
|
6955
|
+
]
|
|
6956
|
+
})
|
|
6957
|
+
);
|
|
6958
|
+
if (domainType === "free") {
|
|
6959
|
+
const slug = ensure(
|
|
6960
|
+
await text({
|
|
6961
|
+
message: "Choose your subdomain",
|
|
6962
|
+
placeholder: "my-openship",
|
|
6963
|
+
validate: (v) => v && SLUG_RE.test(v.trim().toLowerCase()) ? void 0 : "Lowercase letters, digits, hyphens"
|
|
6964
|
+
})
|
|
6965
|
+
).trim().toLowerCase();
|
|
6966
|
+
const s2 = spinner2();
|
|
6967
|
+
s2.start("Detecting this server's public IP");
|
|
6968
|
+
const publicHost = await detectPublicIp();
|
|
6969
|
+
s2.stop(publicHost ? `Public IP: ${chalk16.bold(publicHost)}` : "Couldn't detect the public IP automatically.");
|
|
6970
|
+
publicUrl = `https://${slug}.opsh.io`;
|
|
6971
|
+
behindProxy = true;
|
|
6972
|
+
domainPlan = { type: "free", slug, publicHost };
|
|
6973
|
+
} else if (domainType === "custom") {
|
|
6974
|
+
const raw = ensure(
|
|
6975
|
+
await text({
|
|
6976
|
+
message: "Your domain",
|
|
6977
|
+
placeholder: "ops.example.com",
|
|
6978
|
+
validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
|
|
6979
|
+
})
|
|
6980
|
+
);
|
|
6981
|
+
publicUrl = normalizeUrl(raw).replace(/^http:/i, "https:");
|
|
6982
|
+
const hostname = new URL(publicUrl).hostname;
|
|
6983
|
+
managedEdge = true;
|
|
6984
|
+
behindProxy = true;
|
|
6985
|
+
if (typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
6986
|
+
log.warn("Managed HTTPS installs OpenResty + certbot \u2014 that needs root. Re-run with sudo if it can't install.");
|
|
6987
|
+
}
|
|
6988
|
+
const s2 = spinner2();
|
|
6989
|
+
s2.start("Detecting this server's public IP");
|
|
6990
|
+
const ip = await detectPublicIp();
|
|
6991
|
+
s2.stop(ip ? `Public IP: ${chalk16.bold(ip)}` : "Couldn't detect the public IP automatically.");
|
|
6992
|
+
note(
|
|
6993
|
+
`Add a DNS ${chalk16.bold("A record")}:
|
|
6994
|
+
|
|
6995
|
+
${chalk16.cyan(hostname)} \u2192 ${chalk16.cyan(ip ?? "<this server's public IP>")}
|
|
6996
|
+
|
|
6997
|
+
` + chalk16.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
|
|
6998
|
+
"DNS"
|
|
6999
|
+
);
|
|
7000
|
+
ensure(await confirm3({ message: "A record set? (continue either way \u2014 it retries)", initialValue: true }));
|
|
7001
|
+
domainPlan = { type: "custom", hostname };
|
|
7002
|
+
} else {
|
|
7003
|
+
const raw = ensure(
|
|
7004
|
+
await text({
|
|
7005
|
+
message: "Your domain (served behind your proxy)",
|
|
7006
|
+
placeholder: "ops.example.com",
|
|
7007
|
+
validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
|
|
7008
|
+
})
|
|
7009
|
+
);
|
|
7010
|
+
publicUrl = normalizeUrl(raw);
|
|
7011
|
+
behindProxy = true;
|
|
7012
|
+
if (publicUrl.startsWith("http://")) {
|
|
7013
|
+
log.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
|
|
7014
|
+
}
|
|
7015
|
+
domainPlan = { type: "byo", hostname: new URL(publicUrl).hostname };
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
const isCloudDomain = domainPlan.type === "free";
|
|
7019
|
+
const admin = isCloudDomain ? null : await promptLocalAdmin();
|
|
7020
|
+
const s = spinner2();
|
|
7021
|
+
s.start("Installing Openship as a service");
|
|
7022
|
+
let started;
|
|
7023
|
+
try {
|
|
7024
|
+
started = startService(
|
|
7025
|
+
{ publicUrl, trustProxy: behindProxy, managedEdge, acmeEmail: managedEdge ? admin?.email : void 0 },
|
|
7026
|
+
{ quiet: true }
|
|
7027
|
+
);
|
|
7028
|
+
} catch (e) {
|
|
7029
|
+
s.stop("Couldn't install the service.", 1);
|
|
7030
|
+
log.error(e.message);
|
|
7031
|
+
log.info("Run `openship up --foreground` to run it attached and see the error.");
|
|
7032
|
+
process.exit(1);
|
|
7033
|
+
}
|
|
7034
|
+
s.message("Waiting for Openship to come up");
|
|
7035
|
+
if (!await waitHealthy(started.port)) {
|
|
7036
|
+
s.stop("Openship didn't become healthy in time.", 1);
|
|
7037
|
+
log.info("Check logs: `openship logs` (or `openship up --foreground`).");
|
|
7038
|
+
process.exit(1);
|
|
7039
|
+
}
|
|
7040
|
+
if (admin) {
|
|
7041
|
+
s.message("Creating your admin account");
|
|
7042
|
+
const adminRes = await bootstrapAdmin(started.port, admin);
|
|
7043
|
+
if (!adminRes.ok) {
|
|
7044
|
+
s.stop(`Couldn't create the admin account: ${adminRes.message}`, 1);
|
|
7045
|
+
process.exit(1);
|
|
7046
|
+
}
|
|
7047
|
+
s.stop(
|
|
7048
|
+
adminRes.message === "already-exists" ? "An admin already exists \u2014 use your existing login." : `Admin account created for ${admin.email}.`
|
|
7049
|
+
);
|
|
7050
|
+
} else {
|
|
7051
|
+
s.stop("Openship is up.");
|
|
7052
|
+
}
|
|
7053
|
+
let liveUrl = publicUrl ?? `http://localhost:${started.dashPort}`;
|
|
7054
|
+
const port = started.port;
|
|
7055
|
+
if (domainPlan.type === "free") {
|
|
7056
|
+
const linked = await connectOpenshipCloud(port);
|
|
7057
|
+
if (!linked) {
|
|
7058
|
+
log.warn("Openship Cloud wasn't connected \u2014 set up a local admin instead. You can add the free domain later in Settings \u2192 Cloud.");
|
|
7059
|
+
const fb = await promptLocalAdmin();
|
|
7060
|
+
const abr = await bootstrapAdmin(port, fb);
|
|
7061
|
+
if (!abr.ok) {
|
|
7062
|
+
log.error(`Couldn't create the admin account: ${abr.message}`);
|
|
7063
|
+
process.exit(1);
|
|
7064
|
+
}
|
|
7065
|
+
await internalPost(port, "/api/system/self-register", { domainType: "byo" });
|
|
7066
|
+
} else {
|
|
7067
|
+
const s2 = spinner2();
|
|
7068
|
+
s2.start("Registering your free domain with Openship Cloud");
|
|
7069
|
+
const res = await internalPost(port, "/api/system/self-register", {
|
|
7070
|
+
domainType: "free",
|
|
7071
|
+
slug: domainPlan.slug,
|
|
7072
|
+
publicHost: domainPlan.publicHost,
|
|
7073
|
+
dashPort: Number(started.dashPort)
|
|
7074
|
+
});
|
|
7075
|
+
if (res.ok && res.data?.url) {
|
|
7076
|
+
liveUrl = res.data.url;
|
|
7077
|
+
s2.stop(`Free domain live: ${res.data.url}`);
|
|
7078
|
+
} else {
|
|
7079
|
+
s2.stop(`Couldn't register the free domain: ${res.data?.error || "failed"}`, 1);
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
} else if (domainPlan.type === "custom") {
|
|
7083
|
+
let edgeTakeover = false;
|
|
7084
|
+
let edgeMigrate = false;
|
|
7085
|
+
let proceedCustom = true;
|
|
7086
|
+
const pf = await internalPost(port, "/api/system/self-edge/preflight", {});
|
|
7087
|
+
const status = pf.ok ? pf.data?.status : void 0;
|
|
7088
|
+
const importable = pf.ok && Array.isArray(pf.data?.sites) ? pf.data.sites.length : 0;
|
|
7089
|
+
if (status && !status.canProceedClean && status.occupants?.length) {
|
|
7090
|
+
const owner = status.occupants.map((o) => o.command ?? `port ${o.port}`).join(", ");
|
|
7091
|
+
const known = status.classification === "known";
|
|
7092
|
+
const choice = ensure(
|
|
7093
|
+
await select({
|
|
7094
|
+
message: known ? `An existing reverse proxy (${owner}) is serving ports 80/443.` : `Ports 80/443 are in use by ${owner}, which we couldn't identify.`,
|
|
7095
|
+
options: [
|
|
7096
|
+
...importable > 0 ? [{
|
|
7097
|
+
value: "migrate",
|
|
7098
|
+
label: `Migrate ${importable} site${importable === 1 ? "" : "s"} & take over`,
|
|
7099
|
+
hint: "import the existing sites into Openship, then take 80/443"
|
|
7100
|
+
}] : [],
|
|
7101
|
+
{
|
|
7102
|
+
value: "override",
|
|
7103
|
+
label: "Stop it & take over 80/443",
|
|
7104
|
+
hint: known ? "the existing sites stop being served" : "may interrupt a running service"
|
|
7105
|
+
},
|
|
7106
|
+
{ value: "cancel", label: "Cancel \u2014 leave it running" }
|
|
7107
|
+
],
|
|
7108
|
+
// Per product decision: unknown owner pre-selects takeover; a known
|
|
7109
|
+
// proxy defaults to cancel so the user chooses deliberately.
|
|
7110
|
+
initialValue: known ? "cancel" : "override"
|
|
7111
|
+
})
|
|
7112
|
+
);
|
|
7113
|
+
if (choice === "cancel") proceedCustom = false;
|
|
7114
|
+
else if (choice === "migrate") edgeMigrate = true;
|
|
7115
|
+
else edgeTakeover = true;
|
|
7116
|
+
}
|
|
7117
|
+
if (!proceedCustom) {
|
|
7118
|
+
log.warn(
|
|
7119
|
+
"Left the existing proxy on 80/443 running. Registering Openship without managed HTTPS \u2014 front it with your proxy, or re-run setup to take over."
|
|
7120
|
+
);
|
|
7121
|
+
await internalPost(port, "/api/system/self-register", {
|
|
7122
|
+
domainType: "byo",
|
|
7123
|
+
hostname: domainPlan.hostname
|
|
7124
|
+
});
|
|
7125
|
+
liveUrl = `https://${domainPlan.hostname}`;
|
|
7126
|
+
} else {
|
|
7127
|
+
const res = await internalPost(port, "/api/system/self-register", {
|
|
7128
|
+
domainType: "custom",
|
|
7129
|
+
hostname: domainPlan.hostname,
|
|
7130
|
+
dashPort: Number(started.dashPort),
|
|
7131
|
+
acmeEmail: admin?.email,
|
|
7132
|
+
edgeTakeover,
|
|
7133
|
+
edgeMigrate
|
|
7134
|
+
});
|
|
7135
|
+
if (res.ok && res.data?.sessionId) {
|
|
7136
|
+
const s2 = spinner2();
|
|
7137
|
+
s2.start("Issuing HTTPS certificate (OpenResty + Let's Encrypt)");
|
|
7138
|
+
const done = await streamProvision(port, res.data.sessionId, s2);
|
|
7139
|
+
liveUrl = res.data.url ?? liveUrl;
|
|
7140
|
+
if (done) s2.stop(`HTTPS ready: ${liveUrl}`);
|
|
7141
|
+
else s2.stop("HTTPS isn't ready yet \u2014 it retries on reboot; the site serves over HTTP meanwhile.", 1);
|
|
7142
|
+
} else {
|
|
7143
|
+
log.warn(`Couldn't start domain provisioning: ${res.data?.error || "failed"}`);
|
|
7144
|
+
}
|
|
7145
|
+
}
|
|
7146
|
+
} else if (domainPlan.type === "byo") {
|
|
7147
|
+
const res = await internalPost(port, "/api/system/self-register", {
|
|
7148
|
+
domainType: "byo",
|
|
7149
|
+
hostname: domainPlan.hostname
|
|
7150
|
+
});
|
|
7151
|
+
if (res.ok && res.data?.url) liveUrl = res.data.url;
|
|
7152
|
+
} else {
|
|
7153
|
+
await internalPost(port, "/api/system/self-register", { domainType: "byo" });
|
|
7154
|
+
}
|
|
7155
|
+
note(
|
|
7156
|
+
`${chalk16.bold(liveUrl)}
|
|
7157
|
+
|
|
7158
|
+
` + chalk16.dim(`${admin ? `Log in as ${admin.email}` : "Log in with Openship Cloud"}. Openship now appears under your Apps, and runs as a service (restarts on boot).`),
|
|
7159
|
+
"Openship is live"
|
|
7160
|
+
);
|
|
7161
|
+
outro(
|
|
7162
|
+
domainPlan.type === "byo" ? chalk16.dim("Point your reverse proxy at the dashboard port above.") : chalk16.green("Happy shipping.")
|
|
7163
|
+
);
|
|
7164
|
+
}
|
|
7165
|
+
|
|
6592
7166
|
// src/index.ts
|
|
6593
7167
|
var program = new Command25();
|
|
6594
|
-
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.1
|
|
7168
|
+
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.1").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
|
|
6595
7169
|
if (thisCommand.opts().json) setJsonMode(true);
|
|
7170
|
+
}).action(async () => {
|
|
7171
|
+
await runWizard();
|
|
6596
7172
|
});
|
|
6597
7173
|
program.addCommand(upCommand);
|
|
6598
7174
|
program.addCommand(stopCommand);
|