openship 0.2.0 → 0.2.2
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 +933 -307
- package/dist/server/index.js +7749 -3206
- package/dist/server/lua/rules_guard.lua +16 -2
- package/dist/server/migrations/0050_add_service_deployment_image_digest.sql +1 -0
- package/dist/server/migrations/0051_add_update_status.sql +19 -0
- package/dist/server/migrations/0052_add_notice_target.sql +2 -0
- package/dist/server/migrations/0053_colossal_sandman.sql +14 -0
- package/dist/server/migrations/0054_add_instance_smtp.sql +5 -0
- package/dist/server/migrations/0055_rainy_klaw.sql +22 -0
- package/dist/server/migrations/meta/0050_snapshot.json +8708 -0
- package/dist/server/migrations/meta/0051_snapshot.json +8862 -0
- package/dist/server/migrations/meta/0052_snapshot.json +8874 -0
- package/dist/server/migrations/meta/0053_snapshot.json +8962 -0
- package/dist/server/migrations/meta/0054_snapshot.json +8992 -0
- package/dist/server/migrations/meta/0055_snapshot.json +9170 -0
- package/dist/server/migrations/meta/_journal.json +42 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command26 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/lib/output.ts
|
|
7
7
|
import chalk from "chalk";
|
|
@@ -905,10 +905,17 @@ var PLANS = {
|
|
|
905
905
|
description: "Get started for free",
|
|
906
906
|
price: { monthly: 0, annual: 0 },
|
|
907
907
|
stripePriceId: { monthly: null, annual: null },
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
908
|
+
// Public dollar `price` on the paid tiers is `null` — the UI renders
|
|
909
|
+
// "coming soon" and the Subscribe CTA stays disabled. Cloud pricing is
|
|
910
|
+
// intentionally NOT published yet. `monthlyCredits` (the CPU-time/credit
|
|
911
|
+
// quota pushed to Oblien) is real so tiers still enforce — it's an internal
|
|
912
|
+
// number, never shown as a price. NOTE (tune before launch): these credit
|
|
913
|
+
// numbers are placeholders sized to Oblien's 10,000,000-credit ceiling; the
|
|
914
|
+
// true credit-per-$/per-cpu-minute rate is Oblien-configured. 1 openship
|
|
915
|
+
// credit = 1 Oblien credit = 1000 milli (the wrapper divides by 1000 at the
|
|
916
|
+
// Oblien boundary). Self-hosted is free and surfaces none of these numbers.
|
|
917
|
+
monthlyCredits: 5e5,
|
|
918
|
+
// 500 credits — free-tier allowance
|
|
912
919
|
oblienLimits: {
|
|
913
920
|
max_workspaces: 1,
|
|
914
921
|
max_vcpus: 2,
|
|
@@ -927,12 +934,13 @@ var PLANS = {
|
|
|
927
934
|
name: "Pro",
|
|
928
935
|
description: "For solo builders shipping production workloads",
|
|
929
936
|
price: { monthly: null, annual: null },
|
|
930
|
-
// coming soon
|
|
937
|
+
// coming soon — pricing not published
|
|
931
938
|
stripePriceId: {
|
|
932
939
|
monthly: process.env.STRIPE_PRICE_PRO_MONTHLY ?? "price_pro_monthly_placeholder",
|
|
933
940
|
annual: process.env.STRIPE_PRICE_PRO_ANNUAL ?? "price_pro_annual_placeholder"
|
|
934
941
|
},
|
|
935
|
-
monthlyCredits:
|
|
942
|
+
monthlyCredits: 1e7,
|
|
943
|
+
// 10,000 credits/mo (placeholder — tune before launch)
|
|
936
944
|
oblienLimits: {
|
|
937
945
|
max_workspaces: 10,
|
|
938
946
|
max_vcpus: 16,
|
|
@@ -951,12 +959,13 @@ var PLANS = {
|
|
|
951
959
|
name: "Team",
|
|
952
960
|
description: "For teams collaborating on shared infra",
|
|
953
961
|
price: { monthly: null, annual: null },
|
|
954
|
-
// coming soon
|
|
962
|
+
// coming soon — pricing not published
|
|
955
963
|
stripePriceId: {
|
|
956
964
|
monthly: process.env.STRIPE_PRICE_TEAM_MONTHLY ?? "price_team_monthly_placeholder",
|
|
957
965
|
annual: process.env.STRIPE_PRICE_TEAM_ANNUAL ?? "price_team_annual_placeholder"
|
|
958
966
|
},
|
|
959
|
-
monthlyCredits:
|
|
967
|
+
monthlyCredits: 6e7,
|
|
968
|
+
// 60,000 credits/mo (placeholder — tune before launch)
|
|
960
969
|
oblienLimits: {
|
|
961
970
|
max_workspaces: 50,
|
|
962
971
|
max_vcpus: 64,
|
|
@@ -2371,9 +2380,9 @@ import chalk5 from "chalk";
|
|
|
2371
2380
|
import ora from "ora";
|
|
2372
2381
|
import { spawn } from "child_process";
|
|
2373
2382
|
import { randomBytes } from "crypto";
|
|
2374
|
-
import { existsSync as
|
|
2375
|
-
import { homedir as
|
|
2376
|
-
import { dirname as dirname2, join as
|
|
2383
|
+
import { createWriteStream as createWriteStream2, existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
2384
|
+
import { homedir as homedir5 } from "os";
|
|
2385
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
2377
2386
|
import { fileURLToPath } from "url";
|
|
2378
2387
|
|
|
2379
2388
|
// src/lib/dashboard.ts
|
|
@@ -2480,16 +2489,29 @@ function assetName(tag) {
|
|
|
2480
2489
|
async function ensureDashboard(opts = {}) {
|
|
2481
2490
|
const override = process.env.OPENSHIP_DASHBOARD_DIR?.trim();
|
|
2482
2491
|
if (override) {
|
|
2483
|
-
const
|
|
2484
|
-
const
|
|
2485
|
-
if (!existsSync2(
|
|
2492
|
+
const cwd = join3(override, "apps", "dashboard");
|
|
2493
|
+
const entry = join3(cwd, "server.js");
|
|
2494
|
+
if (!existsSync2(entry)) {
|
|
2486
2495
|
throw new Error(
|
|
2487
|
-
`OPENSHIP_DASHBOARD_DIR=${override} but ${
|
|
2496
|
+
`OPENSHIP_DASHBOARD_DIR=${override} but ${entry} is missing \u2014 build the dashboard standalone first (see docs).`
|
|
2488
2497
|
);
|
|
2489
2498
|
}
|
|
2490
|
-
return { tag: "local", entry
|
|
2499
|
+
return { tag: "local", entry, cwd };
|
|
2500
|
+
}
|
|
2501
|
+
const requested = opts.tag ?? await resolveLatestTag();
|
|
2502
|
+
try {
|
|
2503
|
+
return await fetchBundle(requested, opts.onProgress);
|
|
2504
|
+
} catch (err2) {
|
|
2505
|
+
if (opts.tag && /\b404\b/.test(err2?.message ?? "")) {
|
|
2506
|
+
const latest = await resolveLatestTag();
|
|
2507
|
+
if (latest && latest !== requested) {
|
|
2508
|
+
return await fetchBundle(latest, opts.onProgress);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
throw err2;
|
|
2491
2512
|
}
|
|
2492
|
-
|
|
2513
|
+
}
|
|
2514
|
+
async function fetchBundle(tag, onProgress) {
|
|
2493
2515
|
const dir = join3(DASHBOARD_CACHE, tag);
|
|
2494
2516
|
const cwd = join3(dir, "apps", "dashboard");
|
|
2495
2517
|
const entry = join3(cwd, "server.js");
|
|
@@ -2501,7 +2523,7 @@ async function ensureDashboard(opts = {}) {
|
|
|
2501
2523
|
mkdirSync3(dir, { recursive: true });
|
|
2502
2524
|
const name = assetName(tag);
|
|
2503
2525
|
const tarball = join3(dir, name);
|
|
2504
|
-
const { sha256 } = await downloadToFile(assetUrl(tag, name), tarball,
|
|
2526
|
+
const { sha256 } = await downloadToFile(assetUrl(tag, name), tarball, onProgress);
|
|
2505
2527
|
const expected = await expectedSha256(tag, name);
|
|
2506
2528
|
if (!expected) {
|
|
2507
2529
|
throw new Error(
|
|
@@ -2527,7 +2549,7 @@ async function ensureDashboard(opts = {}) {
|
|
|
2527
2549
|
|
|
2528
2550
|
// src/lib/service.ts
|
|
2529
2551
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2530
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
|
|
2552
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2, readFileSync as readFileSync2 } from "fs";
|
|
2531
2553
|
import { homedir as homedir3 } from "os";
|
|
2532
2554
|
import { join as join4, resolve } from "path";
|
|
2533
2555
|
var HOME = homedir3();
|
|
@@ -2563,6 +2585,19 @@ function run(cmd, args) {
|
|
|
2563
2585
|
const r = spawnSync2(cmd, args, { encoding: "utf8" });
|
|
2564
2586
|
return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
2565
2587
|
}
|
|
2588
|
+
function sleepSync(ms) {
|
|
2589
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2590
|
+
}
|
|
2591
|
+
function launchdPid() {
|
|
2592
|
+
const r = run("launchctl", ["list", MAC_LABEL]);
|
|
2593
|
+
if (!r.ok) return null;
|
|
2594
|
+
const m = r.out.match(/"PID"\s*=\s*(\d+)/);
|
|
2595
|
+
return m ? Number(m[1]) : null;
|
|
2596
|
+
}
|
|
2597
|
+
function launchdLastExit() {
|
|
2598
|
+
const m = run("launchctl", ["list", MAC_LABEL]).out.match(/"LastExitStatus"\s*=\s*(-?\d+)/);
|
|
2599
|
+
return m ? Number(m[1]) : null;
|
|
2600
|
+
}
|
|
2566
2601
|
function isRoot() {
|
|
2567
2602
|
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
2568
2603
|
}
|
|
@@ -2683,7 +2718,16 @@ function restart() {
|
|
|
2683
2718
|
if (!existsSync3(MAC_PLIST)) return { restarted: false, detail: "no launchd agent installed" };
|
|
2684
2719
|
const uid = String(process.getuid?.() ?? "");
|
|
2685
2720
|
const r = run("launchctl", ["kickstart", "-k", `gui/${uid}/${MAC_LABEL}`]);
|
|
2686
|
-
return { restarted:
|
|
2721
|
+
if (!r.ok) return { restarted: false, detail: r.out || "launchctl kickstart failed" };
|
|
2722
|
+
for (let i = 0; i < 6; i++) {
|
|
2723
|
+
sleepSync(500);
|
|
2724
|
+
if (launchdPid() != null) return { restarted: true, detail: `restarted ${MAC_LABEL}` };
|
|
2725
|
+
}
|
|
2726
|
+
const exit = launchdLastExit();
|
|
2727
|
+
return {
|
|
2728
|
+
restarted: false,
|
|
2729
|
+
detail: `${MAC_LABEL} was killed but didn't stay up${exit != null ? ` (last exit ${exit})` : ""} \u2014 check logs in ${LOG_DIR}`
|
|
2730
|
+
};
|
|
2687
2731
|
}
|
|
2688
2732
|
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2689
2733
|
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
@@ -2699,27 +2743,163 @@ function restart() {
|
|
|
2699
2743
|
}
|
|
2700
2744
|
return { restarted: false, detail: "no supported service manager" };
|
|
2701
2745
|
}
|
|
2746
|
+
function serviceStatus() {
|
|
2747
|
+
const kind = detectKind();
|
|
2748
|
+
if (kind === "launchd") {
|
|
2749
|
+
return { kind, installed: existsSync3(MAC_PLIST), running: launchdPid() != null };
|
|
2750
|
+
}
|
|
2751
|
+
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2752
|
+
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
2753
|
+
const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
|
|
2754
|
+
return {
|
|
2755
|
+
kind,
|
|
2756
|
+
installed: existsSync3(unitPath),
|
|
2757
|
+
running: run("systemctl", [...sysArgs, "is-active", SYSTEMD_NAME]).out.trim() === "active"
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
if (kind === "schtasks") {
|
|
2761
|
+
const q = run("schtasks", ["/Query", "/TN", WIN_TASK]);
|
|
2762
|
+
return { kind, installed: q.ok, running: q.ok && /Running/i.test(q.out) };
|
|
2763
|
+
}
|
|
2764
|
+
return { kind, installed: false, running: false };
|
|
2765
|
+
}
|
|
2766
|
+
function sweepOrphanPorts() {
|
|
2767
|
+
if (process.platform === "win32") return;
|
|
2768
|
+
let ports = [];
|
|
2769
|
+
try {
|
|
2770
|
+
const raw = readFileSync2(join4(HOME, ".openship", "ports.json"), "utf8");
|
|
2771
|
+
const p = JSON.parse(raw);
|
|
2772
|
+
ports = [p.api, p.dashboard].filter((n) => typeof n === "number");
|
|
2773
|
+
} catch {
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
const self = process.pid;
|
|
2777
|
+
for (const port of ports) {
|
|
2778
|
+
const q = run("lsof", ["-ti", `tcp:${port}`]);
|
|
2779
|
+
if (!q.ok) continue;
|
|
2780
|
+
for (const token of q.out.split(/\s+/).filter(Boolean)) {
|
|
2781
|
+
const pid = Number(token);
|
|
2782
|
+
if (Number.isInteger(pid) && pid > 1 && pid !== self) {
|
|
2783
|
+
try {
|
|
2784
|
+
process.kill(pid, "SIGKILL");
|
|
2785
|
+
} catch {
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2702
2791
|
function stop() {
|
|
2703
2792
|
const kind = detectKind();
|
|
2793
|
+
let result;
|
|
2704
2794
|
if (kind === "launchd") {
|
|
2705
2795
|
run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
|
|
2706
2796
|
if (existsSync3(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
|
|
2707
|
-
|
|
2708
|
-
}
|
|
2709
|
-
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2797
|
+
result = { kind, detail: `launchd agent ${MAC_LABEL} stopped + removed` };
|
|
2798
|
+
} else if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2710
2799
|
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
2711
2800
|
run("systemctl", [...sysArgs, "disable", "--now", SYSTEMD_NAME]);
|
|
2712
2801
|
const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
|
|
2713
2802
|
if (existsSync3(unitPath)) rmSync2(unitPath, { force: true });
|
|
2714
2803
|
run("systemctl", [...sysArgs, "daemon-reload"]);
|
|
2715
|
-
|
|
2716
|
-
}
|
|
2717
|
-
if (kind === "schtasks") {
|
|
2804
|
+
result = { kind, detail: `systemd unit ${SYSTEMD_NAME} stopped + disabled` };
|
|
2805
|
+
} else if (kind === "schtasks") {
|
|
2718
2806
|
run("schtasks", ["/End", "/TN", WIN_TASK]);
|
|
2719
2807
|
run("schtasks", ["/Delete", "/TN", WIN_TASK, "/F"]);
|
|
2720
|
-
|
|
2808
|
+
result = { kind, detail: `Scheduled Task ${WIN_TASK} stopped + removed` };
|
|
2809
|
+
} else {
|
|
2810
|
+
result = { kind, detail: "no supported service manager \u2014 nothing to stop" };
|
|
2721
2811
|
}
|
|
2722
|
-
|
|
2812
|
+
sweepOrphanPorts();
|
|
2813
|
+
return result;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// src/lib/ports.ts
|
|
2817
|
+
import { createServer } from "net";
|
|
2818
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2819
|
+
import { homedir as homedir4 } from "os";
|
|
2820
|
+
import { join as join5 } from "path";
|
|
2821
|
+
var OS_DIR2 = join5(homedir4(), ".openship");
|
|
2822
|
+
var PORTS_FILE = join5(OS_DIR2, "ports.json");
|
|
2823
|
+
var INSTANCE_FILE = join5(OS_DIR2, "instance.json");
|
|
2824
|
+
function saveInstanceUrl(publicUrl) {
|
|
2825
|
+
try {
|
|
2826
|
+
if (!existsSync4(OS_DIR2)) mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
|
|
2827
|
+
writeFileSync4(INSTANCE_FILE, JSON.stringify({ publicUrl: publicUrl ?? null }));
|
|
2828
|
+
} catch {
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
function readInstanceUrl() {
|
|
2832
|
+
try {
|
|
2833
|
+
return JSON.parse(readFileSync3(INSTANCE_FILE, "utf8")).publicUrl ?? null;
|
|
2834
|
+
} catch {
|
|
2835
|
+
return null;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
var DEFAULT_API = 4e3;
|
|
2839
|
+
var DEFAULT_DASHBOARD = 3001;
|
|
2840
|
+
function isPortFree(port) {
|
|
2841
|
+
return new Promise((resolve2) => {
|
|
2842
|
+
const srv = createServer();
|
|
2843
|
+
srv.once("error", () => resolve2(false));
|
|
2844
|
+
srv.listen(port, "127.0.0.1", () => srv.close(() => resolve2(true)));
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
async function waitPortFree(port, opts = {}) {
|
|
2848
|
+
const timeoutMs = opts.timeoutMs ?? 6e3;
|
|
2849
|
+
const intervalMs = opts.intervalMs ?? 250;
|
|
2850
|
+
const deadline = Date.now() + timeoutMs;
|
|
2851
|
+
for (; ; ) {
|
|
2852
|
+
if (await isPortFree(port)) return true;
|
|
2853
|
+
if (Date.now() >= deadline) return false;
|
|
2854
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
function getFreePort() {
|
|
2858
|
+
return new Promise((resolve2, reject2) => {
|
|
2859
|
+
const srv = createServer();
|
|
2860
|
+
srv.once("error", reject2);
|
|
2861
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
2862
|
+
const addr = srv.address();
|
|
2863
|
+
const port = addr && typeof addr === "object" ? addr.port : 0;
|
|
2864
|
+
srv.close(() => port ? resolve2(port) : reject2(new Error("no free port")));
|
|
2865
|
+
});
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
function loadStoredPorts() {
|
|
2869
|
+
try {
|
|
2870
|
+
return JSON.parse(readFileSync3(PORTS_FILE, "utf-8"));
|
|
2871
|
+
} catch {
|
|
2872
|
+
return {};
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
function saveStoredPorts(api, dashboard) {
|
|
2876
|
+
try {
|
|
2877
|
+
if (!existsSync4(OS_DIR2)) mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
|
|
2878
|
+
writeFileSync4(PORTS_FILE, JSON.stringify({ api, dashboard }));
|
|
2879
|
+
} catch {
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
async function resolvePorts(prefs) {
|
|
2883
|
+
const stored = loadStoredPorts();
|
|
2884
|
+
const apiPref = prefs.api ?? stored.api ?? DEFAULT_API;
|
|
2885
|
+
const dashPref = prefs.dashboard ?? stored.dashboard ?? DEFAULT_DASHBOARD;
|
|
2886
|
+
const apiRemembered = prefs.api === void 0 && stored.api === apiPref;
|
|
2887
|
+
const dashRemembered = prefs.dashboard === void 0 && stored.dashboard === dashPref;
|
|
2888
|
+
let api;
|
|
2889
|
+
if (await isPortFree(apiPref)) api = apiPref;
|
|
2890
|
+
else if (apiRemembered && await waitPortFree(apiPref)) api = apiPref;
|
|
2891
|
+
else api = await getFreePort();
|
|
2892
|
+
let dashboard;
|
|
2893
|
+
if (dashPref !== api && await isPortFree(dashPref)) dashboard = dashPref;
|
|
2894
|
+
else if (dashPref !== api && dashRemembered && await waitPortFree(dashPref)) dashboard = dashPref;
|
|
2895
|
+
else dashboard = await getFreePort();
|
|
2896
|
+
if (dashboard === api) dashboard = await getFreePort();
|
|
2897
|
+
saveStoredPorts(api, dashboard);
|
|
2898
|
+
return {
|
|
2899
|
+
api,
|
|
2900
|
+
dashboard,
|
|
2901
|
+
switched: { api: api !== apiPref, dashboard: dashboard !== dashPref }
|
|
2902
|
+
};
|
|
2723
2903
|
}
|
|
2724
2904
|
|
|
2725
2905
|
// src/commands/up.ts
|
|
@@ -2744,22 +2924,22 @@ function normalizePublicUrl(raw) {
|
|
|
2744
2924
|
return url;
|
|
2745
2925
|
}
|
|
2746
2926
|
var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
2747
|
-
var SERVER_DIR =
|
|
2748
|
-
var
|
|
2927
|
+
var SERVER_DIR = join6(DIST_DIR, "server");
|
|
2928
|
+
var OS_DIR3 = join6(homedir5(), ".openship");
|
|
2749
2929
|
function ensureAuthSecret() {
|
|
2750
|
-
const path2 =
|
|
2751
|
-
if (
|
|
2752
|
-
|
|
2930
|
+
const path2 = join6(OS_DIR3, "auth-secret");
|
|
2931
|
+
if (existsSync5(path2)) return readFileSync4(path2, "utf8").trim();
|
|
2932
|
+
mkdirSync6(OS_DIR3, { recursive: true, mode: 448 });
|
|
2753
2933
|
const secret = randomBytes(32).toString("hex");
|
|
2754
|
-
|
|
2934
|
+
writeFileSync5(path2, secret, { mode: 384 });
|
|
2755
2935
|
return secret;
|
|
2756
2936
|
}
|
|
2757
2937
|
function ensureInternalToken() {
|
|
2758
|
-
const path2 =
|
|
2759
|
-
if (
|
|
2760
|
-
|
|
2938
|
+
const path2 = join6(OS_DIR3, "internal-token");
|
|
2939
|
+
if (existsSync5(path2)) return readFileSync4(path2, "utf8").trim();
|
|
2940
|
+
mkdirSync6(OS_DIR3, { recursive: true, mode: 448 });
|
|
2761
2941
|
const token = randomBytes(32).toString("hex");
|
|
2762
|
-
|
|
2942
|
+
writeFileSync5(path2, token, { mode: 384 });
|
|
2763
2943
|
return token;
|
|
2764
2944
|
}
|
|
2765
2945
|
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(
|
|
@@ -2773,16 +2953,45 @@ var upCommand = new Command4("up").description("Start Openship as a persistent s
|
|
|
2773
2953
|
"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
2954
|
).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").action(async (opts) => {
|
|
2775
2955
|
if (opts.foreground) return runForeground(opts);
|
|
2776
|
-
startService(opts);
|
|
2956
|
+
await startService(opts);
|
|
2777
2957
|
});
|
|
2778
|
-
function startService(opts, runOpts = {}) {
|
|
2958
|
+
async function startService(opts, runOpts = {}) {
|
|
2779
2959
|
const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
|
|
2780
|
-
|
|
2781
|
-
|
|
2960
|
+
if (opts.dryRun) {
|
|
2961
|
+
const p = preview({
|
|
2962
|
+
port: opts.port,
|
|
2963
|
+
dataDir: opts.dataDir,
|
|
2964
|
+
dashboardPort: opts.dashboardPort,
|
|
2965
|
+
ui: opts.ui,
|
|
2966
|
+
uiVersion: opts.uiVersion,
|
|
2967
|
+
publicUrl,
|
|
2968
|
+
trustProxy: opts.trustProxy || opts.managedEdge,
|
|
2969
|
+
managedEdge: opts.managedEdge,
|
|
2970
|
+
acmeEmail: opts.acmeEmail
|
|
2971
|
+
});
|
|
2972
|
+
console.log(
|
|
2973
|
+
chalk5.dim(`
|
|
2974
|
+
service manager: ${p.kind}
|
|
2975
|
+
path: ${p.path}
|
|
2976
|
+
|
|
2977
|
+
`) + p.content + "\n"
|
|
2978
|
+
);
|
|
2979
|
+
return {
|
|
2980
|
+
port: String(opts.port || "4000"),
|
|
2981
|
+
dashPort: String(opts.dashboardPort || "3001"),
|
|
2982
|
+
publicUrl
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
const resolved = await resolvePorts({
|
|
2986
|
+
api: opts.port ? Number(opts.port) : void 0,
|
|
2987
|
+
dashboard: opts.dashboardPort ? Number(opts.dashboardPort) : void 0
|
|
2988
|
+
});
|
|
2989
|
+
const port = String(resolved.api);
|
|
2990
|
+
const dashPort = String(resolved.dashboard);
|
|
2782
2991
|
const flags = {
|
|
2783
|
-
port
|
|
2992
|
+
port,
|
|
2784
2993
|
dataDir: opts.dataDir,
|
|
2785
|
-
dashboardPort:
|
|
2994
|
+
dashboardPort: dashPort,
|
|
2786
2995
|
ui: opts.ui,
|
|
2787
2996
|
uiVersion: opts.uiVersion,
|
|
2788
2997
|
publicUrl,
|
|
@@ -2791,20 +3000,15 @@ function startService(opts, runOpts = {}) {
|
|
|
2791
3000
|
managedEdge: opts.managedEdge,
|
|
2792
3001
|
acmeEmail: opts.acmeEmail
|
|
2793
3002
|
};
|
|
2794
|
-
if (opts.dryRun) {
|
|
2795
|
-
const p = preview(flags);
|
|
2796
|
-
console.log(
|
|
2797
|
-
chalk5.dim(`
|
|
2798
|
-
service manager: ${p.kind}
|
|
2799
|
-
path: ${p.path}
|
|
2800
|
-
|
|
2801
|
-
`) + p.content + "\n"
|
|
2802
|
-
);
|
|
2803
|
-
return { port, dashPort, publicUrl };
|
|
2804
|
-
}
|
|
2805
3003
|
try {
|
|
2806
3004
|
const res = installAndStart(flags);
|
|
2807
3005
|
if (!runOpts.quiet) {
|
|
3006
|
+
if (resolved.switched.api || resolved.switched.dashboard) {
|
|
3007
|
+
console.log(
|
|
3008
|
+
chalk5.yellow(`
|
|
3009
|
+
A preferred port was busy \u2014 using API ${port}, dashboard ${dashPort}.`)
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
2808
3012
|
const dashboardLine = publicUrl ? chalk5.dim(` Dashboard: ${publicUrl} (login required)
|
|
2809
3013
|
`) : chalk5.dim(` Dashboard: http://localhost:${dashPort} (login required)
|
|
2810
3014
|
`);
|
|
@@ -2826,19 +3030,27 @@ function startService(opts, runOpts = {}) {
|
|
|
2826
3030
|
}
|
|
2827
3031
|
}
|
|
2828
3032
|
async function runForeground(opts) {
|
|
2829
|
-
const serverEntry =
|
|
2830
|
-
if (!
|
|
3033
|
+
const serverEntry = join6(SERVER_DIR, "index.js");
|
|
3034
|
+
if (!existsSync5(serverEntry)) {
|
|
2831
3035
|
console.error(
|
|
2832
3036
|
chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
|
|
2833
3037
|
);
|
|
2834
3038
|
process.exit(1);
|
|
2835
3039
|
}
|
|
2836
|
-
const
|
|
2837
|
-
|
|
3040
|
+
const resolved = await resolvePorts({
|
|
3041
|
+
api: opts.port ? Number(opts.port) : void 0,
|
|
3042
|
+
dashboard: opts.dashboardPort ? Number(opts.dashboardPort) : void 0
|
|
3043
|
+
});
|
|
3044
|
+
const port = String(resolved.api);
|
|
3045
|
+
const dashPort = String(resolved.dashboard);
|
|
2838
3046
|
const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
|
|
2839
3047
|
const managedEdge = Boolean(opts.managedEdge && publicUrl);
|
|
2840
|
-
const dataDir = opts.dataDir ||
|
|
2841
|
-
|
|
3048
|
+
const dataDir = opts.dataDir || join6(OS_DIR3, "data");
|
|
3049
|
+
mkdirSync6(dataDir, { recursive: true });
|
|
3050
|
+
const logDir = join6(OS_DIR3, "logs");
|
|
3051
|
+
mkdirSync6(logDir, { recursive: true });
|
|
3052
|
+
const instanceLogPath = join6(logDir, "instance.log");
|
|
3053
|
+
const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
|
|
2842
3054
|
const env = {
|
|
2843
3055
|
...process.env,
|
|
2844
3056
|
PORT: port,
|
|
@@ -2848,13 +3060,15 @@ async function runForeground(opts) {
|
|
|
2848
3060
|
OPENSHIP_TARGET: "local",
|
|
2849
3061
|
OPENSHIP_JOB_RUNNER: "in-process",
|
|
2850
3062
|
PGLITE_DATA_DIR: dataDir,
|
|
2851
|
-
OPENSHIP_MIGRATIONS_DIR:
|
|
2852
|
-
OPENSHIP_PGLITE_ASSETS_DIR:
|
|
3063
|
+
OPENSHIP_MIGRATIONS_DIR: join6(SERVER_DIR, "migrations"),
|
|
3064
|
+
OPENSHIP_PGLITE_ASSETS_DIR: join6(SERVER_DIR, "pglite"),
|
|
2853
3065
|
BETTER_AUTH_SECRET: ensureAuthSecret()
|
|
2854
3066
|
};
|
|
2855
3067
|
env.OPENSHIP_REQUIRE_AUTH = "true";
|
|
2856
3068
|
env.INTERNAL_TOKEN = ensureInternalToken();
|
|
2857
3069
|
env.OPENSHIP_API_HOST = "127.0.0.1";
|
|
3070
|
+
env.OPENSHIP_DASHBOARD_PORT = dashPort;
|
|
3071
|
+
env.OPENSHIP_INSTANCE_LOG = instanceLogPath;
|
|
2858
3072
|
delete env.OPENSHIP_ALLOW_ZERO_AUTH;
|
|
2859
3073
|
if (publicUrl) {
|
|
2860
3074
|
env.OPENSHIP_PUBLIC_URL = publicUrl;
|
|
@@ -2868,7 +3082,13 @@ async function runForeground(opts) {
|
|
|
2868
3082
|
delete env.DATABASE_URL;
|
|
2869
3083
|
delete env.POSTGRES_URL;
|
|
2870
3084
|
const spinner3 = ora(`Starting Openship on http://localhost:${port} \u2026`).start();
|
|
2871
|
-
const child = spawn(process.execPath, [serverEntry], {
|
|
3085
|
+
const child = spawn(process.execPath, [serverEntry], {
|
|
3086
|
+
env,
|
|
3087
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3088
|
+
detached: process.platform !== "win32"
|
|
3089
|
+
});
|
|
3090
|
+
child.stdout.on("data", (d) => instanceLog.write(d));
|
|
3091
|
+
child.stderr.on("data", (d) => instanceLog.write(d));
|
|
2872
3092
|
let buffered = "";
|
|
2873
3093
|
const buffer = (d) => {
|
|
2874
3094
|
buffered += d.toString();
|
|
@@ -2903,26 +3123,33 @@ async function runForeground(opts) {
|
|
|
2903
3123
|
}
|
|
2904
3124
|
spinner3.succeed(`Openship API running at http://localhost:${port}`);
|
|
2905
3125
|
const children = [child];
|
|
2906
|
-
const
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
}
|
|
2912
|
-
setTimeout(() => {
|
|
2913
|
-
try {
|
|
2914
|
-
c.kill("SIGKILL");
|
|
2915
|
-
} catch {
|
|
2916
|
-
}
|
|
2917
|
-
}, 5e3).unref?.();
|
|
3126
|
+
const killTree = (c, sig) => {
|
|
3127
|
+
try {
|
|
3128
|
+
if (c.pid && process.platform !== "win32") process.kill(-c.pid, sig);
|
|
3129
|
+
else c.kill(sig);
|
|
3130
|
+
} catch {
|
|
2918
3131
|
}
|
|
2919
3132
|
};
|
|
3133
|
+
let stopping = false;
|
|
3134
|
+
const stopAll = (exitCode = 0) => {
|
|
3135
|
+
if (stopping) return;
|
|
3136
|
+
stopping = true;
|
|
3137
|
+
try {
|
|
3138
|
+
instanceLog.end();
|
|
3139
|
+
} catch {
|
|
3140
|
+
}
|
|
3141
|
+
for (const c of children) killTree(c, "SIGTERM");
|
|
3142
|
+
setTimeout(() => {
|
|
3143
|
+
for (const c of children) killTree(c, "SIGKILL");
|
|
3144
|
+
process.exit(exitCode);
|
|
3145
|
+
}, 1500);
|
|
3146
|
+
};
|
|
2920
3147
|
let dashboardUrl = null;
|
|
2921
3148
|
if (opts.ui !== false) {
|
|
2922
3149
|
const uiSpinner = ora("Preparing the dashboard\u2026").start();
|
|
2923
3150
|
try {
|
|
2924
3151
|
const bundle = await ensureDashboard({
|
|
2925
|
-
tag: opts.uiVersion || `v${"0.2.
|
|
3152
|
+
tag: opts.uiVersion || `v${"0.2.2"}`,
|
|
2926
3153
|
onProgress: (received, total) => {
|
|
2927
3154
|
if (total) {
|
|
2928
3155
|
uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
|
|
@@ -2932,6 +3159,7 @@ async function runForeground(opts) {
|
|
|
2932
3159
|
uiSpinner.text = "Starting the dashboard\u2026";
|
|
2933
3160
|
const dash = spawn(process.execPath, [bundle.entry], {
|
|
2934
3161
|
cwd: bundle.cwd,
|
|
3162
|
+
detached: process.platform !== "win32",
|
|
2935
3163
|
env: {
|
|
2936
3164
|
...process.env,
|
|
2937
3165
|
NODE_ENV: "production",
|
|
@@ -2946,13 +3174,24 @@ async function runForeground(opts) {
|
|
|
2946
3174
|
// the browser never needs to know where the API lives. Set in every
|
|
2947
3175
|
// mode; loopback because the dashboard runs on the same box.
|
|
2948
3176
|
INTERNAL_API_URL: `http://127.0.0.1:${port}`,
|
|
2949
|
-
//
|
|
2950
|
-
//
|
|
2951
|
-
|
|
3177
|
+
// ALWAYS tell the dashboard the real loopback API origin. The API port
|
|
3178
|
+
// is dynamic, so a browser opened on THIS box must learn it via
|
|
3179
|
+
// window.__OPENSHIP_API_ORIGIN__ (layout.tsx) — otherwise it falls back
|
|
3180
|
+
// to the static default :4000 and every call 404s. Use `localhost` (NOT
|
|
3181
|
+
// 127.0.0.1) to MATCH the host the dashboard is opened on — a host-only
|
|
3182
|
+
// SameSite session cookie set on 127.0.0.1 is never sent to localhost
|
|
3183
|
+
// (they're different sites to a browser), which is the login-reload loop.
|
|
3184
|
+
// Older dashboards use this origin verbatim; newer ones align it anyway.
|
|
3185
|
+
// `localhost` still reaches the 127.0.0.1-bound API. In proxy mode this
|
|
3186
|
+
// is just a fallback (sameOriginProxyOrigin wins for remote browsers).
|
|
3187
|
+
OPENSHIP_LOCAL_API_URL: `http://localhost:${port}`,
|
|
3188
|
+
...publicUrl ? { OPENSHIP_PUBLIC_URL: publicUrl } : {}
|
|
2952
3189
|
},
|
|
2953
3190
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2954
3191
|
});
|
|
2955
3192
|
children.push(dash);
|
|
3193
|
+
dash.stdout.on("data", (d) => instanceLog.write(d));
|
|
3194
|
+
dash.stderr.on("data", (d) => instanceLog.write(d));
|
|
2956
3195
|
let dashBuf = "";
|
|
2957
3196
|
const onDash = (d) => {
|
|
2958
3197
|
dashBuf += d.toString();
|
|
@@ -3009,12 +3248,9 @@ async function runForeground(opts) {
|
|
|
3009
3248
|
child.stderr.off("data", buffer);
|
|
3010
3249
|
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
3011
3250
|
child.stderr.on("data", (d) => process.stderr.write(d));
|
|
3012
|
-
process.on("SIGINT", stopAll);
|
|
3013
|
-
process.on("SIGTERM", stopAll);
|
|
3014
|
-
child.on("exit", (code) =>
|
|
3015
|
-
stopAll();
|
|
3016
|
-
process.exit(code ?? 0);
|
|
3017
|
-
});
|
|
3251
|
+
process.on("SIGINT", () => stopAll(0));
|
|
3252
|
+
process.on("SIGTERM", () => stopAll(0));
|
|
3253
|
+
child.on("exit", (code) => stopAll(code ?? 0));
|
|
3018
3254
|
}
|
|
3019
3255
|
|
|
3020
3256
|
// src/commands/stop.ts
|
|
@@ -3035,15 +3271,15 @@ var stopCommand = new Command5("stop").description("Stop the Openship service (s
|
|
|
3035
3271
|
|
|
3036
3272
|
// src/commands/init.ts
|
|
3037
3273
|
import { Command as Command6 } from "commander";
|
|
3038
|
-
import { existsSync as
|
|
3274
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
3039
3275
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
3040
3276
|
import { stdin as input2, stdout as output2 } from "process";
|
|
3041
|
-
import { join as
|
|
3277
|
+
import { join as join7 } from "path";
|
|
3042
3278
|
var initCommand = new Command6("init").description("Link the current directory to an Openship project (.openship/project.json)").option("--project <id>", "Project id to link (skips the picker)").option("--environment <name>", "Default deploy environment", "production").option("--dir <path>", "Directory to initialize", process.cwd()).option("--force", "Overwrite an existing project link").option("-y, --yes", "Non-interactive: fail instead of prompting").action(async (opts) => {
|
|
3043
3279
|
const root = opts.dir || process.cwd();
|
|
3044
|
-
const linkDir =
|
|
3045
|
-
const linkPath =
|
|
3046
|
-
if (
|
|
3280
|
+
const linkDir = join7(root, ".openship");
|
|
3281
|
+
const linkPath = join7(linkDir, "project.json");
|
|
3282
|
+
if (existsSync6(linkPath) && !opts.force) {
|
|
3047
3283
|
err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
|
|
3048
3284
|
process.exit(1);
|
|
3049
3285
|
}
|
|
@@ -3096,8 +3332,8 @@ var initCommand = new Command6("init").description("Link the current directory t
|
|
|
3096
3332
|
context: getActiveContext(),
|
|
3097
3333
|
defaults: { environment: opts.environment || "production" }
|
|
3098
3334
|
};
|
|
3099
|
-
|
|
3100
|
-
|
|
3335
|
+
mkdirSync7(linkDir, { recursive: true });
|
|
3336
|
+
writeFileSync6(linkPath, JSON.stringify(link, null, 2) + "\n");
|
|
3101
3337
|
if (isJsonMode()) {
|
|
3102
3338
|
printJson({ path: linkPath, link });
|
|
3103
3339
|
return;
|
|
@@ -3161,39 +3397,53 @@ var contextCommand = new Command7("context").alias("ctx").description("Manage co
|
|
|
3161
3397
|
// src/commands/status.ts
|
|
3162
3398
|
import { Command as Command8 } from "commander";
|
|
3163
3399
|
import chalk7 from "chalk";
|
|
3164
|
-
|
|
3400
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3401
|
+
import { homedir as homedir6 } from "os";
|
|
3402
|
+
import { join as join8 } from "path";
|
|
3403
|
+
function readPorts() {
|
|
3404
|
+
try {
|
|
3405
|
+
return JSON.parse(readFileSync5(join8(homedir6(), ".openship", "ports.json"), "utf8"));
|
|
3406
|
+
} catch {
|
|
3407
|
+
return {};
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
var statusCommand = new Command8("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
|
|
3165
3411
|
const context = getActiveContext();
|
|
3166
3412
|
const apiUrl = getApiUrl2();
|
|
3167
|
-
|
|
3168
|
-
|
|
3413
|
+
const svc = serviceStatus();
|
|
3414
|
+
const ports = readPorts();
|
|
3415
|
+
let health = null;
|
|
3416
|
+
let envInfo = null;
|
|
3417
|
+
let reachable = true;
|
|
3418
|
+
let unreachableMsg = "";
|
|
3169
3419
|
try {
|
|
3170
3420
|
health = await apiRequest("/health", { signal: AbortSignal.timeout(8e3) });
|
|
3171
3421
|
envInfo = await apiRequest("/health/env", { signal: AbortSignal.timeout(8e3) });
|
|
3172
3422
|
} catch (e) {
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
} else {
|
|
3176
|
-
const msg = e instanceof ApiError ? e.message : e.message;
|
|
3177
|
-
err(`
|
|
3178
|
-
Cannot reach the API at ${apiUrl}: ${msg}
|
|
3179
|
-
`);
|
|
3180
|
-
}
|
|
3181
|
-
process.exit(1);
|
|
3423
|
+
reachable = false;
|
|
3424
|
+
unreachableMsg = e instanceof ApiError ? e.message : e.message;
|
|
3182
3425
|
}
|
|
3183
3426
|
if (isJsonMode()) {
|
|
3184
|
-
printJson({ context, apiUrl,
|
|
3185
|
-
|
|
3427
|
+
printJson({ context, apiUrl, service: svc, ports, reachable, health, env: envInfo });
|
|
3428
|
+
process.exit(reachable ? 0 : 1);
|
|
3186
3429
|
}
|
|
3187
3430
|
const row = (label, value) => ` ${chalk7.dim(label.padEnd(14))}${value ?? chalk7.dim("-")}
|
|
3188
3431
|
`;
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
)
|
|
3432
|
+
const serviceState = svc.running ? chalk7.green("running") : svc.installed ? chalk7.yellow("installed \xB7 stopped") : chalk7.dim("not installed");
|
|
3433
|
+
let out = chalk7.bold("\n Openship status\n\n") + row("Service", serviceState) + row("Manager", svc.kind === "unsupported" ? chalk7.dim("none") : svc.kind) + (ports.api ? row("API port", ports.api) : "") + (ports.dashboard ? row("Dashboard port", ports.dashboard) : "") + row("Context", context) + row("API", apiUrl);
|
|
3434
|
+
if (reachable && health && envInfo) {
|
|
3435
|
+
out += row("Health", chalk7.green(health.status ?? "ok")) + row("Mode", envInfo.selfHosted ? "self-hosted" : "cloud") + row("Deploy", envInfo.deployMode) + row("Auth", envInfo.authMode) + row("Team", envInfo.teamMode) + (envInfo.hostDomain ? row("Host domain", envInfo.hostDomain) : "") + (envInfo.machineName ? row("Machine", envInfo.machineName) : "");
|
|
3436
|
+
} else {
|
|
3437
|
+
out += row("Health", chalk7.red("not reachable")) + chalk7.dim(` ${unreachableMsg}
|
|
3438
|
+
`) + chalk7.dim(svc.running ? " (service is up \u2014 it may still be starting)\n" : " Start it with `openship up`.\n");
|
|
3439
|
+
}
|
|
3440
|
+
process.stdout.write(out + "\n");
|
|
3441
|
+
if (!reachable) process.exit(1);
|
|
3192
3442
|
});
|
|
3193
3443
|
|
|
3194
3444
|
// src/commands/doctor.ts
|
|
3195
3445
|
import { Command as Command9 } from "commander";
|
|
3196
|
-
import { existsSync as
|
|
3446
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3197
3447
|
import { execFileSync } from "child_process";
|
|
3198
3448
|
import chalk8 from "chalk";
|
|
3199
3449
|
function bunVersion() {
|
|
@@ -3207,7 +3457,7 @@ function bunVersion() {
|
|
|
3207
3457
|
}
|
|
3208
3458
|
var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
|
|
3209
3459
|
const checks = [];
|
|
3210
|
-
const hasConfig =
|
|
3460
|
+
const hasConfig = existsSync7(CONFIG_PATH);
|
|
3211
3461
|
checks.push({
|
|
3212
3462
|
name: "config",
|
|
3213
3463
|
status: hasConfig ? "pass" : "warn",
|
|
@@ -3260,15 +3510,15 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
3260
3510
|
import ora2 from "ora";
|
|
3261
3511
|
|
|
3262
3512
|
// src/lib/project-link.ts
|
|
3263
|
-
import { readFileSync as
|
|
3264
|
-
import { join as
|
|
3265
|
-
var LINK_REL =
|
|
3513
|
+
import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
|
|
3514
|
+
import { join as join9, dirname as dirname3, parse } from "path";
|
|
3515
|
+
var LINK_REL = join9(".openship", "project.json");
|
|
3266
3516
|
function findProjectLinkPath(from = process.cwd()) {
|
|
3267
3517
|
let dir = from;
|
|
3268
3518
|
const root = parse(dir).root;
|
|
3269
3519
|
for (; ; ) {
|
|
3270
|
-
const candidate =
|
|
3271
|
-
if (
|
|
3520
|
+
const candidate = join9(dir, LINK_REL);
|
|
3521
|
+
if (existsSync8(candidate)) return candidate;
|
|
3272
3522
|
if (dir === root) return null;
|
|
3273
3523
|
dir = dirname3(dir);
|
|
3274
3524
|
}
|
|
@@ -3277,7 +3527,7 @@ function readProjectLink(from) {
|
|
|
3277
3527
|
const path2 = findProjectLinkPath(from);
|
|
3278
3528
|
if (!path2) return null;
|
|
3279
3529
|
try {
|
|
3280
|
-
return JSON.parse(
|
|
3530
|
+
return JSON.parse(readFileSync6(path2, "utf8"));
|
|
3281
3531
|
} catch {
|
|
3282
3532
|
return null;
|
|
3283
3533
|
}
|
|
@@ -3285,21 +3535,21 @@ function readProjectLink(from) {
|
|
|
3285
3535
|
|
|
3286
3536
|
// src/lib/folder-deploy.ts
|
|
3287
3537
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3288
|
-
import { readFileSync as
|
|
3538
|
+
import { readFileSync as readFileSync7, existsSync as existsSync9, rmSync as rmSync3 } from "fs";
|
|
3289
3539
|
import { tmpdir } from "os";
|
|
3290
|
-
import { join as
|
|
3540
|
+
import { join as join10, basename } from "path";
|
|
3291
3541
|
function detectPackageManager(dir) {
|
|
3292
|
-
if (
|
|
3293
|
-
if (
|
|
3294
|
-
if (
|
|
3295
|
-
if (
|
|
3542
|
+
if (existsSync9(join10(dir, "bun.lockb")) || existsSync9(join10(dir, "bun.lock"))) return "bun";
|
|
3543
|
+
if (existsSync9(join10(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
3544
|
+
if (existsSync9(join10(dir, "yarn.lock"))) return "yarn";
|
|
3545
|
+
if (existsSync9(join10(dir, "package.json"))) return "npm";
|
|
3296
3546
|
return void 0;
|
|
3297
3547
|
}
|
|
3298
3548
|
function detectStack(dir) {
|
|
3299
|
-
if (
|
|
3300
|
-
if (
|
|
3301
|
-
if (
|
|
3302
|
-
if (
|
|
3549
|
+
if (existsSync9(join10(dir, "go.mod"))) return "go";
|
|
3550
|
+
if (existsSync9(join10(dir, "Cargo.toml"))) return "rust";
|
|
3551
|
+
if (existsSync9(join10(dir, "requirements.txt")) || existsSync9(join10(dir, "pyproject.toml"))) return "python";
|
|
3552
|
+
if (existsSync9(join10(dir, "package.json"))) return "node";
|
|
3303
3553
|
return void 0;
|
|
3304
3554
|
}
|
|
3305
3555
|
async function deployFolder(opts) {
|
|
@@ -3316,7 +3566,7 @@ async function deployFolder(opts) {
|
|
|
3316
3566
|
throw new Error(session.error || "Failed to open upload session");
|
|
3317
3567
|
}
|
|
3318
3568
|
step("Packaging folder");
|
|
3319
|
-
const tarball =
|
|
3569
|
+
const tarball = join10(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
|
|
3320
3570
|
execFileSync2(
|
|
3321
3571
|
"tar",
|
|
3322
3572
|
[
|
|
@@ -3335,7 +3585,7 @@ async function deployFolder(opts) {
|
|
|
3335
3585
|
);
|
|
3336
3586
|
step("Uploading source");
|
|
3337
3587
|
try {
|
|
3338
|
-
const body =
|
|
3588
|
+
const body = readFileSync7(tarball);
|
|
3339
3589
|
const up = session.upload;
|
|
3340
3590
|
const method = up.method || "POST";
|
|
3341
3591
|
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 });
|
|
@@ -5091,6 +5341,50 @@ server.command("check <serverId>").description("Run component health checks agai
|
|
|
5091
5341
|
else err(` Missing required components: ${res.missing.join(", ") || "none"}`);
|
|
5092
5342
|
})
|
|
5093
5343
|
);
|
|
5344
|
+
server.command("update <serverId>").description("Check for and apply native-module migrations (OpenResty, \u2026)").option("-c, --component <name...>", "Limit to specific modules").option("--check", "Only report drift; don't apply").action(
|
|
5345
|
+
guard(async (serverId, o) => {
|
|
5346
|
+
const base = `/system/servers/${encodeURIComponent(serverId)}/modules`;
|
|
5347
|
+
await apiRequest(`${base}/scan`, { method: "POST", body: "{}" }).catch(() => {
|
|
5348
|
+
});
|
|
5349
|
+
let mods = await apiRequest(base);
|
|
5350
|
+
if (o.component?.length) mods = mods.filter((m) => o.component.includes(m.moduleName));
|
|
5351
|
+
if (o.check) {
|
|
5352
|
+
if (isJsonMode()) return printJson(mods);
|
|
5353
|
+
printTable(
|
|
5354
|
+
mods.map((m) => ({
|
|
5355
|
+
module: m.moduleName,
|
|
5356
|
+
installed: m.installedVersion ?? "-",
|
|
5357
|
+
current: m.migrationVersion ?? "-",
|
|
5358
|
+
available: m.availableVersion ?? "-",
|
|
5359
|
+
behind: m.behind ? "yes" : "no",
|
|
5360
|
+
consent: String(m.detail?.pendingConsent?.length ?? 0)
|
|
5361
|
+
})),
|
|
5362
|
+
["module", "installed", "current", "available", "behind", "consent"]
|
|
5363
|
+
);
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
const behind = mods.filter((m) => m.behind);
|
|
5367
|
+
if (!behind.length) return ok(" All modules up to date.");
|
|
5368
|
+
for (const m of behind) {
|
|
5369
|
+
const consent = m.detail?.pendingConsent ?? [];
|
|
5370
|
+
if (consent.length && !isJsonMode()) {
|
|
5371
|
+
info(` ${m.moduleName}: includes consent migrations \u2014 ${consent.map((c) => c.warning ?? c.id).join("; ")}`);
|
|
5372
|
+
}
|
|
5373
|
+
const spinner3 = isJsonMode() ? null : ora4(`Updating ${m.moduleName}\u2026`).start();
|
|
5374
|
+
const res = await apiRequest(`${base}/${encodeURIComponent(m.moduleName)}/apply`, {
|
|
5375
|
+
method: "POST",
|
|
5376
|
+
body: "{}"
|
|
5377
|
+
});
|
|
5378
|
+
spinner3?.stop();
|
|
5379
|
+
if (isJsonMode()) {
|
|
5380
|
+
printJson(res);
|
|
5381
|
+
continue;
|
|
5382
|
+
}
|
|
5383
|
+
if (res.ok) ok(` ${m.moduleName}: ${res.fromVersion} \u2192 ${res.toVersion} (${res.appliedSteps.length} step(s))`);
|
|
5384
|
+
else err(` ${m.moduleName}: ${res.error ?? "update failed"}`);
|
|
5385
|
+
}
|
|
5386
|
+
})
|
|
5387
|
+
);
|
|
5094
5388
|
server.command("install <serverId>").description("Install components on a server").requiredOption("-c, --component <name...>", `Components to install (${INSTALLABLE.join("|")})`).option("--follow", "Stream install logs live (SSE)").action(
|
|
5095
5389
|
guard(async (serverId, o) => {
|
|
5096
5390
|
const components = o.component;
|
|
@@ -5233,7 +5527,7 @@ var serverCommand = server;
|
|
|
5233
5527
|
// src/commands/system.ts
|
|
5234
5528
|
import { Command as Command17 } from "commander";
|
|
5235
5529
|
import ora5 from "ora";
|
|
5236
|
-
import { readFileSync as
|
|
5530
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
5237
5531
|
import { createInterface as createInterface6 } from "readline/promises";
|
|
5238
5532
|
import { stdin as input5, stdout as output5 } from "process";
|
|
5239
5533
|
async function guarded(fn) {
|
|
@@ -5533,7 +5827,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
|
|
|
5533
5827
|
);
|
|
5534
5828
|
spin4?.succeed("Export ready.");
|
|
5535
5829
|
if (opts.out) {
|
|
5536
|
-
|
|
5830
|
+
writeFileSync7(opts.out, JSON.stringify(file));
|
|
5537
5831
|
const tables = Object.keys(file.dump?.tables ?? {}).length;
|
|
5538
5832
|
report2(
|
|
5539
5833
|
{ out: opts.out, tables },
|
|
@@ -5559,7 +5853,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
|
|
|
5559
5853
|
}
|
|
5560
5854
|
let file;
|
|
5561
5855
|
try {
|
|
5562
|
-
file = JSON.parse(
|
|
5856
|
+
file = JSON.parse(readFileSync8(opts.file, "utf8"));
|
|
5563
5857
|
} catch {
|
|
5564
5858
|
err(`
|
|
5565
5859
|
Could not read or parse ${opts.file}.
|
|
@@ -5902,7 +6196,7 @@ var mailCommand = new Command18("mail").description("Self-hosted mail server (iR
|
|
|
5902
6196
|
// src/commands/backup.ts
|
|
5903
6197
|
import { Command as Command19 } from "commander";
|
|
5904
6198
|
import ora7 from "ora";
|
|
5905
|
-
import { readFileSync as
|
|
6199
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
5906
6200
|
async function guard3(fn) {
|
|
5907
6201
|
try {
|
|
5908
6202
|
await fn();
|
|
@@ -6195,7 +6489,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
|
|
|
6195
6489
|
let sftpPrivateKey = opts.sftpPrivateKey;
|
|
6196
6490
|
if (opts.sftpPrivateKeyFile) {
|
|
6197
6491
|
try {
|
|
6198
|
-
sftpPrivateKey =
|
|
6492
|
+
sftpPrivateKey = readFileSync9(opts.sftpPrivateKeyFile, "utf8");
|
|
6199
6493
|
} catch {
|
|
6200
6494
|
throw new Error(`Cannot read key file: ${opts.sftpPrivateKeyFile}`);
|
|
6201
6495
|
}
|
|
@@ -6383,12 +6677,73 @@ var apiCommand = new Command21("api").description("Make an authenticated request
|
|
|
6383
6677
|
}
|
|
6384
6678
|
});
|
|
6385
6679
|
|
|
6386
|
-
// src/commands/
|
|
6680
|
+
// src/commands/reset-admin.ts
|
|
6387
6681
|
import { Command as Command22 } from "commander";
|
|
6388
|
-
import
|
|
6682
|
+
import chalk16 from "chalk";
|
|
6683
|
+
import { intro, outro, password as passwordPrompt, isCancel, cancel as cancel2, log } from "@clack/prompts";
|
|
6684
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
6685
|
+
import { homedir as homedir7 } from "os";
|
|
6686
|
+
import { join as join11 } from "path";
|
|
6687
|
+
function resolvedApiPort() {
|
|
6688
|
+
try {
|
|
6689
|
+
return JSON.parse(readFileSync10(join11(homedir7(), ".openship", "ports.json"), "utf8")).api;
|
|
6690
|
+
} catch {
|
|
6691
|
+
return void 0;
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
var resetAdminCommand = new Command22("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) => {
|
|
6695
|
+
intro(chalk16.cyan("Reset Openship admin password"));
|
|
6696
|
+
let pw = opts.password;
|
|
6697
|
+
if (!pw) {
|
|
6698
|
+
if (!process.stdin.isTTY) {
|
|
6699
|
+
log.error("--password is required in non-interactive mode.");
|
|
6700
|
+
process.exit(1);
|
|
6701
|
+
}
|
|
6702
|
+
const entered = await passwordPrompt({
|
|
6703
|
+
message: "New admin password",
|
|
6704
|
+
validate: (v) => v && v.length >= 8 && v.length <= 128 ? void 0 : "8\u2013128 characters"
|
|
6705
|
+
});
|
|
6706
|
+
if (isCancel(entered)) {
|
|
6707
|
+
cancel2("Cancelled.");
|
|
6708
|
+
process.exit(0);
|
|
6709
|
+
}
|
|
6710
|
+
const confirm3 = await passwordPrompt({
|
|
6711
|
+
message: "Confirm password",
|
|
6712
|
+
validate: (v) => v === entered ? void 0 : "Passwords don't match"
|
|
6713
|
+
});
|
|
6714
|
+
if (isCancel(confirm3)) {
|
|
6715
|
+
cancel2("Cancelled.");
|
|
6716
|
+
process.exit(0);
|
|
6717
|
+
}
|
|
6718
|
+
pw = entered;
|
|
6719
|
+
}
|
|
6720
|
+
const port = String(opts.port || resolvedApiPort() || 4e3);
|
|
6721
|
+
let res;
|
|
6722
|
+
try {
|
|
6723
|
+
res = await fetch(`http://127.0.0.1:${port}/api/system/reset-admin-password`, {
|
|
6724
|
+
method: "POST",
|
|
6725
|
+
headers: { "Content-Type": "application/json", "X-Internal-Token": ensureInternalToken() },
|
|
6726
|
+
body: JSON.stringify({ password: pw, email: opts.email, name: opts.name })
|
|
6727
|
+
});
|
|
6728
|
+
} catch {
|
|
6729
|
+
log.error(`Couldn't reach the Openship API on port ${port}. Is it running? (openship status)`);
|
|
6730
|
+
log.info("If it's listening on another port, pass --port <n>.");
|
|
6731
|
+
process.exit(1);
|
|
6732
|
+
}
|
|
6733
|
+
const data = await res.json().catch(() => ({}));
|
|
6734
|
+
if (!res.ok || !data.ok) {
|
|
6735
|
+
log.error(`Reset failed: ${data.error || res.statusText}`);
|
|
6736
|
+
process.exit(1);
|
|
6737
|
+
}
|
|
6738
|
+
outro(chalk16.green(`Password reset. Log in as ${data.email} with your new password.`));
|
|
6739
|
+
});
|
|
6740
|
+
|
|
6741
|
+
// src/commands/install.ts
|
|
6742
|
+
import { Command as Command23 } from "commander";
|
|
6743
|
+
import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
6389
6744
|
import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
|
|
6390
|
-
import { homedir as
|
|
6391
|
-
import { join as
|
|
6745
|
+
import { homedir as homedir8 } from "os";
|
|
6746
|
+
import { join as join12 } from "path";
|
|
6392
6747
|
import ora9 from "ora";
|
|
6393
6748
|
function assetForPlatform() {
|
|
6394
6749
|
const { platform, arch } = process;
|
|
@@ -6400,10 +6755,10 @@ function assetForPlatform() {
|
|
|
6400
6755
|
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
|
6401
6756
|
}
|
|
6402
6757
|
function installDmg(dmg) {
|
|
6403
|
-
const homeApps =
|
|
6758
|
+
const homeApps = join12(homedir8(), "Applications");
|
|
6404
6759
|
let dest = homeApps;
|
|
6405
6760
|
try {
|
|
6406
|
-
|
|
6761
|
+
mkdirSync8(homeApps, { recursive: true });
|
|
6407
6762
|
} catch {
|
|
6408
6763
|
dest = "/Applications";
|
|
6409
6764
|
}
|
|
@@ -6413,14 +6768,14 @@ function installDmg(dmg) {
|
|
|
6413
6768
|
if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
|
|
6414
6769
|
const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
|
|
6415
6770
|
if (!mount) throw new Error("Could not determine the mounted volume");
|
|
6416
|
-
let target =
|
|
6771
|
+
let target = join12(dest, "Openship.app");
|
|
6417
6772
|
try {
|
|
6418
|
-
const appInDmg =
|
|
6419
|
-
if (!
|
|
6773
|
+
const appInDmg = join12(mount, "Openship.app");
|
|
6774
|
+
if (!existsSync10(appInDmg)) throw new Error("Openship.app not found in the disk image");
|
|
6420
6775
|
spawnSync4("rm", ["-rf", target]);
|
|
6421
6776
|
let copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
|
|
6422
6777
|
if (copy.status !== 0 && dest === homeApps) {
|
|
6423
|
-
target =
|
|
6778
|
+
target = join12("/Applications", "Openship.app");
|
|
6424
6779
|
spawnSync4("rm", ["-rf", target]);
|
|
6425
6780
|
copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
|
|
6426
6781
|
}
|
|
@@ -6435,9 +6790,9 @@ function installAppImage(appImage) {
|
|
|
6435
6790
|
return appImage;
|
|
6436
6791
|
}
|
|
6437
6792
|
function installZip(zip) {
|
|
6438
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
6439
|
-
const target =
|
|
6440
|
-
|
|
6793
|
+
const localAppData = process.env.LOCALAPPDATA || join12(homedir8(), "AppData", "Local");
|
|
6794
|
+
const target = join12(localAppData, "Programs", "Openship");
|
|
6795
|
+
mkdirSync8(target, { recursive: true });
|
|
6441
6796
|
const expand = spawnSync4(
|
|
6442
6797
|
"powershell",
|
|
6443
6798
|
[
|
|
@@ -6464,11 +6819,11 @@ function launch(kind, target) {
|
|
|
6464
6819
|
child.unref();
|
|
6465
6820
|
return;
|
|
6466
6821
|
}
|
|
6467
|
-
const exe =
|
|
6468
|
-
const path2 =
|
|
6822
|
+
const exe = join12(target, "Openship.exe");
|
|
6823
|
+
const path2 = existsSync10(exe) ? exe : target;
|
|
6469
6824
|
spawnSync4("cmd", ["/c", "start", "", path2]);
|
|
6470
6825
|
}
|
|
6471
|
-
var installCommand = new
|
|
6826
|
+
var installCommand = new Command23("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) => {
|
|
6472
6827
|
let asset;
|
|
6473
6828
|
try {
|
|
6474
6829
|
asset = assetForPlatform();
|
|
@@ -6491,13 +6846,13 @@ var installCommand = new Command22("install").description("Download and install
|
|
|
6491
6846
|
process.exit(1);
|
|
6492
6847
|
}
|
|
6493
6848
|
const dir = releaseDir(tag);
|
|
6494
|
-
const assetPath =
|
|
6849
|
+
const assetPath = join12(dir, asset.name);
|
|
6495
6850
|
const sidecarPath = `${assetPath}.sha256`;
|
|
6496
6851
|
const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
|
|
6497
6852
|
const sidecarUrl = `${assetUrl2}.sha256`;
|
|
6498
6853
|
let downloaded = false;
|
|
6499
6854
|
let sha;
|
|
6500
|
-
const cachedUsable = !opts.force &&
|
|
6855
|
+
const cachedUsable = !opts.force && existsSync10(assetPath) && (existsSync10(sidecarPath) || opts.verify === false);
|
|
6501
6856
|
if (cachedUsable) {
|
|
6502
6857
|
info(` Using cached ${asset.name} (${tag}).`);
|
|
6503
6858
|
} else {
|
|
@@ -6519,8 +6874,8 @@ var installCommand = new Command22("install").description("Download and install
|
|
|
6519
6874
|
const s2 = spin4("Verifying checksum\u2026");
|
|
6520
6875
|
try {
|
|
6521
6876
|
let sidecarBody;
|
|
6522
|
-
if (
|
|
6523
|
-
sidecarBody =
|
|
6877
|
+
if (existsSync10(sidecarPath) && !downloaded) {
|
|
6878
|
+
sidecarBody = readFileSync11(sidecarPath, "utf8");
|
|
6524
6879
|
} else {
|
|
6525
6880
|
sidecarBody = await fetchSidecar(sidecarUrl);
|
|
6526
6881
|
}
|
|
@@ -6543,8 +6898,8 @@ var installCommand = new Command22("install").description("Download and install
|
|
|
6543
6898
|
err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
|
|
6544
6899
|
process.exit(1);
|
|
6545
6900
|
}
|
|
6546
|
-
|
|
6547
|
-
|
|
6901
|
+
mkdirSync8(dir, { recursive: true });
|
|
6902
|
+
writeFileSync8(sidecarPath, sidecarBody);
|
|
6548
6903
|
s2?.succeed("Checksum verified");
|
|
6549
6904
|
} catch (e) {
|
|
6550
6905
|
s2?.fail("Verification failed");
|
|
@@ -6583,15 +6938,15 @@ var installCommand = new Command22("install").description("Download and install
|
|
|
6583
6938
|
});
|
|
6584
6939
|
|
|
6585
6940
|
// src/commands/update.ts
|
|
6586
|
-
import { Command as
|
|
6941
|
+
import { Command as Command24 } from "commander";
|
|
6587
6942
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
6588
6943
|
function detectPackageManager2(override) {
|
|
6589
6944
|
if (override === "bun" || override === "npm") return override;
|
|
6590
6945
|
const hasBun = spawnSync5("bun", ["--version"], { stdio: "ignore" }).status === 0;
|
|
6591
6946
|
return hasBun ? "bun" : "npm";
|
|
6592
6947
|
}
|
|
6593
|
-
var updateCommand = new
|
|
6594
|
-
const current = "0.2.
|
|
6948
|
+
var updateCommand = new Command24("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) => {
|
|
6949
|
+
const current = "0.2.2";
|
|
6595
6950
|
let latest;
|
|
6596
6951
|
try {
|
|
6597
6952
|
latest = (await resolveLatestTag()).replace(/^v/, "");
|
|
@@ -6636,18 +6991,18 @@ var updateCommand = new Command23("update").description("Update the Openship CLI
|
|
|
6636
6991
|
});
|
|
6637
6992
|
|
|
6638
6993
|
// src/commands/cache.ts
|
|
6639
|
-
import { Command as
|
|
6640
|
-
import { existsSync as
|
|
6641
|
-
import { join as
|
|
6994
|
+
import { Command as Command25 } from "commander";
|
|
6995
|
+
import { existsSync as existsSync11, readdirSync, readFileSync as readFileSync12, rmSync as rmSync4, statSync } from "fs";
|
|
6996
|
+
import { join as join13 } from "path";
|
|
6642
6997
|
function listAssets() {
|
|
6643
|
-
if (!
|
|
6998
|
+
if (!existsSync11(RELEASES_DIR)) return [];
|
|
6644
6999
|
const out = [];
|
|
6645
7000
|
for (const tag of readdirSync(RELEASES_DIR)) {
|
|
6646
7001
|
const dir = releaseDir(tag);
|
|
6647
7002
|
if (!statSync(dir).isDirectory()) continue;
|
|
6648
7003
|
for (const name of readdirSync(dir)) {
|
|
6649
7004
|
if (name.endsWith(".sha256")) continue;
|
|
6650
|
-
const path2 =
|
|
7005
|
+
const path2 = join13(dir, name);
|
|
6651
7006
|
const st = statSync(path2);
|
|
6652
7007
|
if (!st.isFile()) continue;
|
|
6653
7008
|
out.push({
|
|
@@ -6655,17 +7010,17 @@ function listAssets() {
|
|
|
6655
7010
|
name,
|
|
6656
7011
|
path: path2,
|
|
6657
7012
|
size: st.size,
|
|
6658
|
-
hasSidecar:
|
|
7013
|
+
hasSidecar: existsSync11(`${path2}.sha256`)
|
|
6659
7014
|
});
|
|
6660
7015
|
}
|
|
6661
7016
|
}
|
|
6662
7017
|
return out;
|
|
6663
7018
|
}
|
|
6664
|
-
var pathCmd = new
|
|
7019
|
+
var pathCmd = new Command25("path").description("Print the cache directory path").action(() => {
|
|
6665
7020
|
if (isJsonMode()) printJson({ path: CACHE_DIR });
|
|
6666
7021
|
else process.stdout.write(CACHE_DIR + "\n");
|
|
6667
7022
|
});
|
|
6668
|
-
var listCmd6 = new
|
|
7023
|
+
var listCmd6 = new Command25("list").alias("ls").description("List cached release assets").action(() => {
|
|
6669
7024
|
const assets = listAssets();
|
|
6670
7025
|
printTable(
|
|
6671
7026
|
assets.map((a) => ({
|
|
@@ -6677,7 +7032,7 @@ var listCmd6 = new Command24("list").alias("ls").description("List cached releas
|
|
|
6677
7032
|
["tag", "asset", "size", "sidecar"]
|
|
6678
7033
|
);
|
|
6679
7034
|
});
|
|
6680
|
-
var verifyCmd2 = new
|
|
7035
|
+
var verifyCmd2 = new Command25("verify").description("Re-hash cached assets and compare to their .sha256 sidecar").argument("[tag]", "Only verify assets under this release tag").action(async (tag) => {
|
|
6681
7036
|
const assets = listAssets().filter((a) => !tag || a.tag === tag);
|
|
6682
7037
|
const results = [];
|
|
6683
7038
|
let bad = 0;
|
|
@@ -6686,7 +7041,7 @@ var verifyCmd2 = new Command24("verify").description("Re-hash cached assets and
|
|
|
6686
7041
|
results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
|
|
6687
7042
|
continue;
|
|
6688
7043
|
}
|
|
6689
|
-
const expected = parseSha256(
|
|
7044
|
+
const expected = parseSha256(readFileSync12(`${a.path}.sha256`, "utf8"));
|
|
6690
7045
|
const actual = await hashFile(a.path);
|
|
6691
7046
|
const okMatch = expected !== null && expected === actual;
|
|
6692
7047
|
if (!okMatch) bad += 1;
|
|
@@ -6700,9 +7055,9 @@ var verifyCmd2 = new Command24("verify").description("Re-hash cached assets and
|
|
|
6700
7055
|
}
|
|
6701
7056
|
if (bad > 0) process.exit(1);
|
|
6702
7057
|
});
|
|
6703
|
-
var cleanCmd = new
|
|
7058
|
+
var cleanCmd = new Command25("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
|
|
6704
7059
|
const target = tag ? releaseDir(tag) : RELEASES_DIR;
|
|
6705
|
-
if (!
|
|
7060
|
+
if (!existsSync11(target)) {
|
|
6706
7061
|
if (isJsonMode()) printJson({ removed: false, path: target });
|
|
6707
7062
|
else info(` Nothing to clean (${target}).`);
|
|
6708
7063
|
return;
|
|
@@ -6713,32 +7068,34 @@ var cleanCmd = new Command24("clean").description("Delete cached release assets"
|
|
|
6713
7068
|
Removed ${target}
|
|
6714
7069
|
`);
|
|
6715
7070
|
});
|
|
6716
|
-
var cacheCommand = new
|
|
7071
|
+
var cacheCommand = new Command25("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
|
|
6717
7072
|
err("Specify a subcommand: path | list | verify | clean");
|
|
6718
7073
|
process.exit(1);
|
|
6719
7074
|
}).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
|
|
6720
7075
|
|
|
6721
7076
|
// src/commands/wizard.ts
|
|
6722
|
-
import
|
|
7077
|
+
import chalk17 from "chalk";
|
|
6723
7078
|
import open from "open";
|
|
6724
|
-
import { createServer } from "http";
|
|
7079
|
+
import { createServer as createServer2 } from "http";
|
|
6725
7080
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
7081
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
|
|
7082
|
+
import { homedir as homedir9 } from "os";
|
|
7083
|
+
import { join as join14 } from "path";
|
|
6726
7084
|
import {
|
|
6727
|
-
intro,
|
|
6728
|
-
outro,
|
|
7085
|
+
intro as intro2,
|
|
7086
|
+
outro as outro2,
|
|
6729
7087
|
text,
|
|
6730
7088
|
password,
|
|
6731
7089
|
select,
|
|
6732
|
-
confirm as confirm3,
|
|
6733
7090
|
spinner as spinner2,
|
|
6734
7091
|
note,
|
|
6735
|
-
log,
|
|
6736
|
-
cancel as
|
|
6737
|
-
isCancel
|
|
7092
|
+
log as log2,
|
|
7093
|
+
cancel as cancel3,
|
|
7094
|
+
isCancel as isCancel2
|
|
6738
7095
|
} from "@clack/prompts";
|
|
6739
7096
|
function ensure(value) {
|
|
6740
|
-
if (
|
|
6741
|
-
|
|
7097
|
+
if (isCancel2(value)) {
|
|
7098
|
+
cancel3("Setup cancelled.");
|
|
6742
7099
|
process.exit(0);
|
|
6743
7100
|
}
|
|
6744
7101
|
return value;
|
|
@@ -6776,6 +7133,19 @@ async function bootstrapAdmin(apiPort, admin) {
|
|
|
6776
7133
|
if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
|
|
6777
7134
|
return { ok: false, message: data?.error || "failed" };
|
|
6778
7135
|
}
|
|
7136
|
+
function lastServiceError() {
|
|
7137
|
+
for (const name of ["up.err.log", "up.log"]) {
|
|
7138
|
+
const p = join14(homedir9(), ".openship", "logs", name);
|
|
7139
|
+
if (!existsSync12(p)) continue;
|
|
7140
|
+
try {
|
|
7141
|
+
const lines = readFileSync13(p, "utf8").trim().split("\n");
|
|
7142
|
+
const hit = [...lines].reverse().find((l) => /error|locked|EADDRINUSE|throw|cannot/i.test(l));
|
|
7143
|
+
if (hit) return hit.trim().slice(0, 200);
|
|
7144
|
+
} catch {
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
return null;
|
|
7148
|
+
}
|
|
6779
7149
|
async function waitHealthy(apiPort, seconds = 90) {
|
|
6780
7150
|
for (let i = 0; i < seconds; i++) {
|
|
6781
7151
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
@@ -6787,6 +7157,20 @@ async function waitHealthy(apiPort, seconds = 90) {
|
|
|
6787
7157
|
}
|
|
6788
7158
|
return false;
|
|
6789
7159
|
}
|
|
7160
|
+
async function waitDashboard(dashPort, seconds = 45) {
|
|
7161
|
+
for (let i = 0; i < seconds; i++) {
|
|
7162
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
7163
|
+
try {
|
|
7164
|
+
const res = await fetch(`http://127.0.0.1:${dashPort}/`, {
|
|
7165
|
+
redirect: "manual",
|
|
7166
|
+
signal: AbortSignal.timeout(2e3)
|
|
7167
|
+
});
|
|
7168
|
+
if (res.status > 0) return true;
|
|
7169
|
+
} catch {
|
|
7170
|
+
}
|
|
7171
|
+
}
|
|
7172
|
+
return false;
|
|
7173
|
+
}
|
|
6790
7174
|
async function detectPublicIp() {
|
|
6791
7175
|
for (const url of ["https://api.ipify.org", "https://ifconfig.me/ip"]) {
|
|
6792
7176
|
try {
|
|
@@ -6803,20 +7187,26 @@ var b64url = (buf) => buf.toString("base64").replace(/\+/g, "-").replace(/\//g,
|
|
|
6803
7187
|
async function connectOpenshipCloud(port) {
|
|
6804
7188
|
const already = await internalGet(port, "/api/system/cloud-status");
|
|
6805
7189
|
if (already?.connected) {
|
|
6806
|
-
|
|
6807
|
-
return
|
|
7190
|
+
log2.success(`Already connected to Openship Cloud${already.user?.email ? ` as ${already.user.email}` : ""}.`);
|
|
7191
|
+
return { email: already.user?.email ?? null };
|
|
6808
7192
|
}
|
|
6809
7193
|
const capsEnv = await internalGet(port, "/api/health/env");
|
|
6810
7194
|
const cloudApiUrl = capsEnv?.cloudApiUrl;
|
|
6811
7195
|
if (!cloudApiUrl) {
|
|
6812
|
-
|
|
6813
|
-
return
|
|
7196
|
+
log2.error("Couldn't discover the Openship Cloud URL \u2014 free domain unavailable. Use a custom domain instead.");
|
|
7197
|
+
return null;
|
|
6814
7198
|
}
|
|
6815
7199
|
const verifier = b64url(randomBytes2(32));
|
|
6816
7200
|
const challenge = b64url(createHash2("sha256").update(verifier).digest());
|
|
6817
7201
|
const state = b64url(randomBytes2(16));
|
|
6818
7202
|
const codePromise = new Promise((resolve2) => {
|
|
6819
|
-
const
|
|
7203
|
+
const page = (ok3) => {
|
|
7204
|
+
const icon = ok3 ? '<path d="M20 6 9 17l-5-5"/>' : '<path d="M18 6 6 18M6 6l12 12"/>';
|
|
7205
|
+
const title = ok3 ? "Connected to Openship Cloud" : "Connection didn\u2019t complete";
|
|
7206
|
+
const msg = ok3 ? "Your instance is now linked to your Openship Cloud account." : "Something went wrong. Return to your terminal and run the connect step again.";
|
|
7207
|
+
return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Openship</title><style>:root{color-scheme:dark}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:#09090b;color:#e7e7ea;font:15px/1.55 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}.card{width:min(92vw,420px);padding:40px 36px;text-align:center}.badge{margin:0 auto 20px;display:grid;place-items:center}svg{width:40px;height:40px}h1{margin:0 0 8px;font-size:19px;font-weight:600;letter-spacing:-.2px}p{margin:0;color:#9a9aa2;font-size:14px}.hint{margin-top:22px;font-size:12.5px;color:#6a6a72}</style></head><body><div class="card"><div class="badge"><svg viewBox="0 0 24 24" fill="none" stroke="#e7e7ea" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">${icon}</svg></div><h1>${title}</h1><p>${msg}</p><div class="hint">You can close this tab and return to your terminal.</div></div><script>setTimeout(function(){try{window.close()}catch(e){}},1200)</script></body></html>`;
|
|
7208
|
+
};
|
|
7209
|
+
const server2 = createServer2((req, res2) => {
|
|
6820
7210
|
const u = new URL(req.url || "/", "http://127.0.0.1");
|
|
6821
7211
|
if (!u.pathname.startsWith("/callback")) {
|
|
6822
7212
|
res2.writeHead(404).end();
|
|
@@ -6824,11 +7214,10 @@ async function connectOpenshipCloud(port) {
|
|
|
6824
7214
|
}
|
|
6825
7215
|
const code2 = u.searchParams.get("code");
|
|
6826
7216
|
const gotState = u.searchParams.get("state");
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
);
|
|
7217
|
+
const ok3 = !!(code2 && gotState === state);
|
|
7218
|
+
res2.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(page(ok3));
|
|
6830
7219
|
server2.close();
|
|
6831
|
-
resolve2(
|
|
7220
|
+
resolve2(ok3 ? code2 : null);
|
|
6832
7221
|
});
|
|
6833
7222
|
server2.on("error", () => resolve2(null));
|
|
6834
7223
|
server2.listen(0, "127.0.0.1", () => {
|
|
@@ -6852,16 +7241,16 @@ async function connectOpenshipCloud(port) {
|
|
|
6852
7241
|
const code = await codePromise;
|
|
6853
7242
|
if (!code) {
|
|
6854
7243
|
s.stop("Openship Cloud wasn't authorized.", 1);
|
|
6855
|
-
return
|
|
7244
|
+
return null;
|
|
6856
7245
|
}
|
|
6857
7246
|
s.message("Linking this instance to Openship Cloud");
|
|
6858
7247
|
const res = await internalPost(port, "/api/system/cloud-connect", { code, codeVerifier: verifier });
|
|
6859
7248
|
if (!res.ok) {
|
|
6860
7249
|
s.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
|
|
6861
|
-
return
|
|
7250
|
+
return null;
|
|
6862
7251
|
}
|
|
6863
|
-
s.stop(
|
|
6864
|
-
return
|
|
7252
|
+
s.stop(`Connected to Openship Cloud${res.data?.email ? ` as ${res.data.email}` : ""}.`);
|
|
7253
|
+
return { email: res.data?.email ?? null };
|
|
6865
7254
|
}
|
|
6866
7255
|
async function promptLocalAdmin() {
|
|
6867
7256
|
const name = ensure(await text({ message: "Your name", validate: (v) => v?.trim() ? void 0 : "Required" })).trim();
|
|
@@ -6922,161 +7311,315 @@ async function streamProvision(port, sessionId, s) {
|
|
|
6922
7311
|
return ok3;
|
|
6923
7312
|
}
|
|
6924
7313
|
async function runWizard() {
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
7314
|
+
intro2(`${chalk17.bgCyan(chalk17.black(" Openship "))}${chalk17.dim(" setup")}`);
|
|
7315
|
+
log2.message(
|
|
7316
|
+
chalk17.dim(
|
|
6928
7317
|
"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
7318
|
)
|
|
6930
7319
|
);
|
|
7320
|
+
log2.message(chalk17.dim("First, your instance login (email + password) \u2014 this is how you sign in. Domain and Openship Cloud come next and never replace it."));
|
|
7321
|
+
const admin = await promptLocalAdmin();
|
|
7322
|
+
let cloudEmail = null;
|
|
6931
7323
|
let publicUrl;
|
|
6932
7324
|
let behindProxy = false;
|
|
6933
7325
|
let managedEdge = false;
|
|
6934
7326
|
let domainPlan = { type: "none" };
|
|
6935
|
-
const
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
7327
|
+
const canManage = process.platform === "linux";
|
|
7328
|
+
const BACK = "__back__";
|
|
7329
|
+
let slug = "";
|
|
7330
|
+
let customDomainInput = "";
|
|
7331
|
+
let byoDomainInput = "";
|
|
7332
|
+
let publicHost = null;
|
|
7333
|
+
async function resolvePublicHost() {
|
|
7334
|
+
if (publicHost) return publicHost;
|
|
7335
|
+
const sp = spinner2();
|
|
7336
|
+
sp.start("Detecting this server's public IP");
|
|
7337
|
+
const detected = await detectPublicIp();
|
|
7338
|
+
if (detected) {
|
|
7339
|
+
sp.stop(`Public IP: ${chalk17.bold(detected)}`);
|
|
7340
|
+
publicHost = detected;
|
|
7341
|
+
return detected;
|
|
7342
|
+
}
|
|
7343
|
+
sp.stop("Couldn't detect the public IP automatically.", 1);
|
|
7344
|
+
publicHost = ensure(
|
|
7345
|
+
await text({
|
|
7346
|
+
message: "This server's public IP or hostname",
|
|
7347
|
+
placeholder: "203.0.113.10",
|
|
7348
|
+
validate: (v) => v?.trim() ? void 0 : "Required \u2014 the edge proxy routes traffic to this address"
|
|
6956
7349
|
})
|
|
6957
|
-
);
|
|
6958
|
-
|
|
6959
|
-
|
|
7350
|
+
).trim();
|
|
7351
|
+
return publicHost;
|
|
7352
|
+
}
|
|
7353
|
+
let stage = "reach";
|
|
7354
|
+
log2.message(chalk17.dim("These are just starting choices \u2014 domain, Cloud, team, and the rest are all editable later in Settings."));
|
|
7355
|
+
planning: while (true) {
|
|
7356
|
+
if (stage === "reach") {
|
|
7357
|
+
const reach = ensure(
|
|
7358
|
+
await select({
|
|
7359
|
+
message: "How should this instance be reachable?",
|
|
7360
|
+
// Default to public — most people setting up on a server/VPS want a
|
|
7361
|
+
// domain + HTTPS; localhost-only is the deliberate opt-out.
|
|
7362
|
+
initialValue: "public",
|
|
7363
|
+
options: [
|
|
7364
|
+
{ value: "public", label: "Public (server / VPS)", hint: "a domain + HTTPS, reachable from anywhere" },
|
|
7365
|
+
{ value: "private", label: "This machine only", hint: "localhost \u2014 no domain, log in on this box" }
|
|
7366
|
+
]
|
|
7367
|
+
})
|
|
7368
|
+
);
|
|
7369
|
+
if (reach === "private") {
|
|
7370
|
+
domainPlan = { type: "none" };
|
|
7371
|
+
publicUrl = void 0;
|
|
7372
|
+
behindProxy = false;
|
|
7373
|
+
managedEdge = false;
|
|
7374
|
+
break planning;
|
|
7375
|
+
}
|
|
7376
|
+
stage = "type";
|
|
7377
|
+
continue;
|
|
7378
|
+
}
|
|
7379
|
+
if (stage === "type") {
|
|
7380
|
+
const domainType = ensure(
|
|
7381
|
+
await select({
|
|
7382
|
+
message: "How do you want a domain + HTTPS?",
|
|
7383
|
+
initialValue: "free",
|
|
7384
|
+
options: [
|
|
7385
|
+
{ value: "free", label: "Free domain", hint: "name.opsh.io via Openship Cloud \u2014 HTTPS handled for you" },
|
|
7386
|
+
...canManage ? [{ value: "custom", label: "Custom domain", hint: "your domain + free Let's Encrypt on this box" }] : [],
|
|
7387
|
+
{ value: "byo", label: "Bring your own", hint: "your domain, behind your own reverse proxy" },
|
|
7388
|
+
{ value: BACK, label: "\u2190 Back" }
|
|
7389
|
+
]
|
|
7390
|
+
})
|
|
7391
|
+
);
|
|
7392
|
+
if (domainType === BACK) {
|
|
7393
|
+
stage = "reach";
|
|
7394
|
+
continue;
|
|
7395
|
+
}
|
|
7396
|
+
stage = domainType;
|
|
7397
|
+
continue;
|
|
7398
|
+
}
|
|
7399
|
+
if (stage === "free") {
|
|
7400
|
+
slug = ensure(
|
|
6960
7401
|
await text({
|
|
6961
7402
|
message: "Choose your subdomain",
|
|
6962
7403
|
placeholder: "my-openship",
|
|
7404
|
+
initialValue: slug || void 0,
|
|
6963
7405
|
validate: (v) => v && SLUG_RE.test(v.trim().toLowerCase()) ? void 0 : "Lowercase letters, digits, hyphens"
|
|
6964
7406
|
})
|
|
6965
7407
|
).trim().toLowerCase();
|
|
6966
|
-
const
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
7408
|
+
const host = await resolvePublicHost();
|
|
7409
|
+
note(
|
|
7410
|
+
`${chalk17.cyan(`https://${slug}.opsh.io`)}
|
|
7411
|
+
|
|
7412
|
+
${chalk17.dim("served via")} Openship Cloud edge ${chalk17.dim("\u2192")} ${chalk17.cyan(host)}
|
|
7413
|
+
|
|
7414
|
+
` + chalk17.dim("Openship Cloud terminates HTTPS and forwards to this server."),
|
|
7415
|
+
"Confirm free domain"
|
|
7416
|
+
);
|
|
7417
|
+
const go2 = ensure(
|
|
7418
|
+
await select({
|
|
7419
|
+
message: "Create this free domain?",
|
|
7420
|
+
options: [
|
|
7421
|
+
{ value: "go", label: "Create it" },
|
|
7422
|
+
{ value: BACK, label: "\u2190 Back", hint: "change subdomain or IP" }
|
|
7423
|
+
]
|
|
7424
|
+
})
|
|
7425
|
+
);
|
|
7426
|
+
if (go2 === BACK) {
|
|
7427
|
+
stage = "type";
|
|
7428
|
+
continue;
|
|
7429
|
+
}
|
|
6970
7430
|
publicUrl = `https://${slug}.opsh.io`;
|
|
6971
7431
|
behindProxy = true;
|
|
6972
|
-
domainPlan = { type: "free", slug, publicHost };
|
|
6973
|
-
|
|
6974
|
-
|
|
7432
|
+
domainPlan = { type: "free", slug, publicHost: host };
|
|
7433
|
+
break planning;
|
|
7434
|
+
}
|
|
7435
|
+
if (stage === "custom") {
|
|
7436
|
+
const raw2 = ensure(
|
|
6975
7437
|
await text({
|
|
6976
7438
|
message: "Your domain",
|
|
6977
7439
|
placeholder: "ops.example.com",
|
|
7440
|
+
initialValue: customDomainInput || void 0,
|
|
6978
7441
|
validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
|
|
6979
7442
|
})
|
|
6980
7443
|
);
|
|
6981
|
-
|
|
6982
|
-
const
|
|
6983
|
-
|
|
6984
|
-
behindProxy = true;
|
|
7444
|
+
customDomainInput = raw2;
|
|
7445
|
+
const url2 = normalizeUrl(raw2).replace(/^http:/i, "https:");
|
|
7446
|
+
const hostname2 = new URL(url2).hostname;
|
|
6985
7447
|
if (typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
6986
|
-
|
|
7448
|
+
log2.warn("Managed HTTPS installs OpenResty + certbot \u2014 that needs root. Re-run with sudo if it can't install.");
|
|
6987
7449
|
}
|
|
6988
|
-
const
|
|
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.");
|
|
7450
|
+
const host = await resolvePublicHost();
|
|
6992
7451
|
note(
|
|
6993
|
-
`Add a DNS ${
|
|
7452
|
+
`Add a DNS ${chalk17.bold("A record")}:
|
|
6994
7453
|
|
|
6995
|
-
${
|
|
7454
|
+
${chalk17.cyan(hostname2)} \u2192 ${chalk17.cyan(host)}
|
|
6996
7455
|
|
|
6997
|
-
` +
|
|
7456
|
+
` + chalk17.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
|
|
6998
7457
|
"DNS"
|
|
6999
7458
|
);
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
|
|
7459
|
+
const go2 = ensure(
|
|
7460
|
+
await select({
|
|
7461
|
+
message: "A record added?",
|
|
7462
|
+
options: [
|
|
7463
|
+
{ value: "go", label: "Continue", hint: "HTTPS provisions once DNS resolves \u2014 it retries" },
|
|
7464
|
+
{ value: BACK, label: "\u2190 Back", hint: "change the domain" }
|
|
7465
|
+
]
|
|
7008
7466
|
})
|
|
7009
7467
|
);
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
log.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
|
|
7468
|
+
if (go2 === BACK) {
|
|
7469
|
+
stage = "type";
|
|
7470
|
+
continue;
|
|
7014
7471
|
}
|
|
7015
|
-
|
|
7472
|
+
publicUrl = url2;
|
|
7473
|
+
managedEdge = true;
|
|
7474
|
+
behindProxy = true;
|
|
7475
|
+
domainPlan = { type: "custom", hostname: hostname2 };
|
|
7476
|
+
break planning;
|
|
7477
|
+
}
|
|
7478
|
+
const raw = ensure(
|
|
7479
|
+
await text({
|
|
7480
|
+
message: "Your domain (served behind your proxy)",
|
|
7481
|
+
placeholder: "ops.example.com",
|
|
7482
|
+
initialValue: byoDomainInput || void 0,
|
|
7483
|
+
validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
|
|
7484
|
+
})
|
|
7485
|
+
);
|
|
7486
|
+
byoDomainInput = raw;
|
|
7487
|
+
const url = normalizeUrl(raw);
|
|
7488
|
+
const hostname = new URL(url).hostname;
|
|
7489
|
+
if (url.startsWith("http://")) {
|
|
7490
|
+
log2.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
|
|
7491
|
+
}
|
|
7492
|
+
note(
|
|
7493
|
+
`${chalk17.cyan(url)}
|
|
7494
|
+
|
|
7495
|
+
` + chalk17.dim("Point your reverse proxy at the dashboard port shown at the end."),
|
|
7496
|
+
"Confirm"
|
|
7497
|
+
);
|
|
7498
|
+
const go = ensure(
|
|
7499
|
+
await select({
|
|
7500
|
+
message: "Continue?",
|
|
7501
|
+
options: [
|
|
7502
|
+
{ value: "go", label: "Continue" },
|
|
7503
|
+
{ value: BACK, label: "\u2190 Back", hint: "change the domain" }
|
|
7504
|
+
]
|
|
7505
|
+
})
|
|
7506
|
+
);
|
|
7507
|
+
if (go === BACK) {
|
|
7508
|
+
stage = "type";
|
|
7509
|
+
continue;
|
|
7016
7510
|
}
|
|
7511
|
+
publicUrl = url;
|
|
7512
|
+
behindProxy = true;
|
|
7513
|
+
domainPlan = { type: "byo", hostname };
|
|
7514
|
+
break planning;
|
|
7515
|
+
}
|
|
7516
|
+
const uiTag = `v${"0.2.2"}`;
|
|
7517
|
+
const dl = spinner2();
|
|
7518
|
+
dl.start("Pulling the Openship dist from GitHub");
|
|
7519
|
+
try {
|
|
7520
|
+
await ensureDashboard({
|
|
7521
|
+
tag: uiTag,
|
|
7522
|
+
onProgress: (received, total) => {
|
|
7523
|
+
if (total) dl.message(`Pulling the Openship dist from GitHub \u2014 ${Math.round(received / total * 100)}%`);
|
|
7524
|
+
}
|
|
7525
|
+
});
|
|
7526
|
+
dl.stop("Openship dist ready.");
|
|
7527
|
+
} catch (e) {
|
|
7528
|
+
dl.stop(`Couldn't pull the Openship dist: ${e.message}`, 1);
|
|
7529
|
+
log2.info("Check your network / that this release published its dashboard asset, then re-run `openship`.");
|
|
7530
|
+
process.exit(1);
|
|
7017
7531
|
}
|
|
7018
|
-
const isCloudDomain = domainPlan.type === "free";
|
|
7019
|
-
const admin = isCloudDomain ? null : await promptLocalAdmin();
|
|
7020
7532
|
const s = spinner2();
|
|
7021
7533
|
s.start("Installing Openship as a service");
|
|
7022
7534
|
let started;
|
|
7023
7535
|
try {
|
|
7024
|
-
started = startService(
|
|
7025
|
-
{ publicUrl, trustProxy: behindProxy, managedEdge, acmeEmail: managedEdge ? admin
|
|
7536
|
+
started = await startService(
|
|
7537
|
+
{ publicUrl, trustProxy: behindProxy, managedEdge, acmeEmail: managedEdge ? admin.email : void 0, uiVersion: uiTag },
|
|
7026
7538
|
{ quiet: true }
|
|
7027
7539
|
);
|
|
7028
7540
|
} catch (e) {
|
|
7029
7541
|
s.stop("Couldn't install the service.", 1);
|
|
7030
|
-
|
|
7031
|
-
|
|
7542
|
+
log2.error(e.message);
|
|
7543
|
+
log2.info("Run `openship up --foreground` to run it attached and see the error.");
|
|
7032
7544
|
process.exit(1);
|
|
7033
7545
|
}
|
|
7034
|
-
s.message("Waiting for Openship
|
|
7546
|
+
s.message("Waiting for the Openship API");
|
|
7035
7547
|
if (!await waitHealthy(started.port)) {
|
|
7036
7548
|
s.stop("Openship didn't become healthy in time.", 1);
|
|
7037
|
-
|
|
7549
|
+
const reason = lastServiceError();
|
|
7550
|
+
if (reason) log2.error(reason);
|
|
7551
|
+
if (reason && /lock/i.test(reason)) {
|
|
7552
|
+
log2.info("The database is locked by another instance \u2014 run `openship stop`, then re-run `openship`.");
|
|
7553
|
+
} else {
|
|
7554
|
+
log2.info("Check logs: `openship logs` (or `openship up --foreground`).");
|
|
7555
|
+
}
|
|
7556
|
+
process.exit(1);
|
|
7557
|
+
}
|
|
7558
|
+
s.message("Creating your admin account");
|
|
7559
|
+
const adminRes = await bootstrapAdmin(started.port, admin);
|
|
7560
|
+
if (!adminRes.ok) {
|
|
7561
|
+
s.stop(`Couldn't create the admin account: ${adminRes.message}`, 1);
|
|
7038
7562
|
process.exit(1);
|
|
7039
7563
|
}
|
|
7040
|
-
if (
|
|
7041
|
-
s.message("
|
|
7042
|
-
const
|
|
7043
|
-
|
|
7044
|
-
|
|
7564
|
+
if (adminRes.message === "already-exists") {
|
|
7565
|
+
s.message("Applying your admin login");
|
|
7566
|
+
const rr = await internalPost(started.port, "/api/system/reset-admin-password", {
|
|
7567
|
+
email: admin.email,
|
|
7568
|
+
name: admin.name,
|
|
7569
|
+
password: admin.password
|
|
7570
|
+
});
|
|
7571
|
+
if (!rr.ok) {
|
|
7572
|
+
s.stop(`Couldn't set your admin login: ${rr.data?.error || "failed"}`, 1);
|
|
7045
7573
|
process.exit(1);
|
|
7046
7574
|
}
|
|
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
7575
|
}
|
|
7576
|
+
s.message(`Admin ready for ${admin.email}`);
|
|
7577
|
+
s.message("Starting the Openship dashboard");
|
|
7578
|
+
await waitDashboard(started.dashPort);
|
|
7579
|
+
s.stop("Deployed.");
|
|
7053
7580
|
let liveUrl = publicUrl ?? `http://localhost:${started.dashPort}`;
|
|
7054
7581
|
const port = started.port;
|
|
7055
7582
|
if (domainPlan.type === "free") {
|
|
7056
|
-
const
|
|
7057
|
-
if (!
|
|
7058
|
-
|
|
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
|
-
}
|
|
7583
|
+
const cloud = await connectOpenshipCloud(port);
|
|
7584
|
+
if (!cloud) {
|
|
7585
|
+
log2.warn("Openship Cloud wasn't connected \u2014 skipping the free domain. Your local admin login still works; add the domain later in Settings \u2192 Cloud.");
|
|
7065
7586
|
await internalPost(port, "/api/system/self-register", { domainType: "byo" });
|
|
7066
7587
|
} else {
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7588
|
+
cloudEmail = cloud.email;
|
|
7589
|
+
let regSlug = domainPlan.slug;
|
|
7590
|
+
while (true) {
|
|
7591
|
+
const s2 = spinner2();
|
|
7592
|
+
s2.start(`Registering ${chalk17.bold(`${regSlug}.opsh.io`)} with Openship Cloud`);
|
|
7593
|
+
const res = await internalPost(port, "/api/system/self-register", {
|
|
7594
|
+
domainType: "free",
|
|
7595
|
+
slug: regSlug,
|
|
7596
|
+
publicHost: domainPlan.publicHost,
|
|
7597
|
+
dashPort: Number(started.dashPort)
|
|
7598
|
+
});
|
|
7599
|
+
if (res.ok && res.data?.url) {
|
|
7600
|
+
liveUrl = res.data.url;
|
|
7601
|
+
s2.stop(`Free domain live: ${res.data.url}`);
|
|
7602
|
+
break;
|
|
7603
|
+
}
|
|
7604
|
+
s2.stop(`Couldn't register ${regSlug}.opsh.io: ${res.data?.error || "failed"}`, 1);
|
|
7605
|
+
const next = ensure(
|
|
7606
|
+
await select({
|
|
7607
|
+
message: "Try a different subdomain?",
|
|
7608
|
+
options: [
|
|
7609
|
+
{ value: "retry", label: "Pick another subdomain" },
|
|
7610
|
+
{ value: "skip", label: "Skip for now", hint: "log in on this server; add a domain later in Settings \u2192 Cloud" }
|
|
7611
|
+
]
|
|
7612
|
+
})
|
|
7613
|
+
);
|
|
7614
|
+
if (next === "skip") break;
|
|
7615
|
+
regSlug = ensure(
|
|
7616
|
+
await text({
|
|
7617
|
+
message: "Choose your subdomain",
|
|
7618
|
+
placeholder: "my-openship",
|
|
7619
|
+
initialValue: regSlug,
|
|
7620
|
+
validate: (v) => v && SLUG_RE.test(v.trim().toLowerCase()) ? void 0 : "Lowercase letters, digits, hyphens"
|
|
7621
|
+
})
|
|
7622
|
+
).trim().toLowerCase();
|
|
7080
7623
|
}
|
|
7081
7624
|
}
|
|
7082
7625
|
} else if (domainPlan.type === "custom") {
|
|
@@ -7115,7 +7658,7 @@ async function runWizard() {
|
|
|
7115
7658
|
else edgeTakeover = true;
|
|
7116
7659
|
}
|
|
7117
7660
|
if (!proceedCustom) {
|
|
7118
|
-
|
|
7661
|
+
log2.warn(
|
|
7119
7662
|
"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
7663
|
);
|
|
7121
7664
|
await internalPost(port, "/api/system/self-register", {
|
|
@@ -7140,7 +7683,7 @@ async function runWizard() {
|
|
|
7140
7683
|
if (done) s2.stop(`HTTPS ready: ${liveUrl}`);
|
|
7141
7684
|
else s2.stop("HTTPS isn't ready yet \u2014 it retries on reboot; the site serves over HTTP meanwhile.", 1);
|
|
7142
7685
|
} else {
|
|
7143
|
-
|
|
7686
|
+
log2.warn(`Couldn't start domain provisioning: ${res.data?.error || "failed"}`);
|
|
7144
7687
|
}
|
|
7145
7688
|
}
|
|
7146
7689
|
} else if (domainPlan.type === "byo") {
|
|
@@ -7152,23 +7695,105 @@ async function runWizard() {
|
|
|
7152
7695
|
} else {
|
|
7153
7696
|
await internalPost(port, "/api/system/self-register", { domainType: "byo" });
|
|
7154
7697
|
}
|
|
7698
|
+
saveInstanceUrl(liveUrl);
|
|
7699
|
+
const pad = (label) => chalk17.dim(label.padEnd(11));
|
|
7700
|
+
log2.success(chalk17.bold("Openship is live"));
|
|
7701
|
+
log2.message(
|
|
7702
|
+
`${pad("URL")}${chalk17.bold(liveUrl)}
|
|
7703
|
+
${pad("Dashboard")}http://localhost:${started.dashPort}
|
|
7704
|
+
${pad("API")}http://localhost:${started.port}
|
|
7705
|
+
${pad("Login")}${admin.email} ${chalk17.dim("(email + password you set)")}
|
|
7706
|
+
` + (cloudEmail ? `${pad("Cloud")}${chalk17.dim("connected as ")}${cloudEmail}${chalk17.dim(" \u2014 free domain + mail only")}
|
|
7707
|
+
` : "") + `${pad("Status")}${chalk17.green("running")} ${chalk17.dim("\xB7 service (restarts on boot)")}`
|
|
7708
|
+
);
|
|
7709
|
+
log2.message(
|
|
7710
|
+
chalk17.dim("Sign in with the email + password you just set. Openship appears under your Apps.\n") + chalk17.dim("Change the domain, Openship Cloud, team, and everything else anytime in Settings.\n") + chalk17.dim(`Locked out? Run ${chalk17.reset("openship reset-admin-password")}${chalk17.dim(" on this machine \u2014 resets your login without signing in.")}`)
|
|
7711
|
+
);
|
|
7712
|
+
outro2(
|
|
7713
|
+
domainPlan.type === "byo" ? chalk17.dim("Point your reverse proxy at the dashboard port above.") : chalk17.green("Happy shipping.")
|
|
7714
|
+
);
|
|
7715
|
+
}
|
|
7716
|
+
function storedPorts() {
|
|
7717
|
+
const p = join14(homedir9(), ".openship", "ports.json");
|
|
7718
|
+
try {
|
|
7719
|
+
return existsSync12(p) ? JSON.parse(readFileSync13(p, "utf8")) : {};
|
|
7720
|
+
} catch {
|
|
7721
|
+
return {};
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
async function runControl() {
|
|
7725
|
+
const svc = serviceStatus();
|
|
7726
|
+
const ports = storedPorts();
|
|
7727
|
+
const apiPort = String(ports.api ?? 4e3);
|
|
7728
|
+
const dashUrl = `http://localhost:${ports.dashboard ?? 3001}`;
|
|
7729
|
+
const publicUrl = readInstanceUrl();
|
|
7730
|
+
const primaryUrl = publicUrl && !/^https?:\/\/localhost/i.test(publicUrl) ? publicUrl : dashUrl;
|
|
7731
|
+
intro2(`${chalk17.bgCyan(chalk17.black(" Openship "))}${chalk17.dim(" control")}`);
|
|
7155
7732
|
note(
|
|
7156
|
-
`${
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7733
|
+
`${chalk17.dim("URL".padEnd(11))}${chalk17.bold(primaryUrl)}
|
|
7734
|
+
${chalk17.dim("Service".padEnd(11))}${svc.running ? chalk17.green("running") : chalk17.yellow("stopped")}
|
|
7735
|
+
${chalk17.dim("Dashboard".padEnd(11))}${dashUrl}
|
|
7736
|
+
` + (ports.api ? `${chalk17.dim("API".padEnd(11))}http://localhost:${ports.api}
|
|
7737
|
+
` : "") + `${chalk17.dim("Manager".padEnd(11))}${svc.kind === "unsupported" ? "none" : svc.kind}`,
|
|
7738
|
+
"Openship is already set up"
|
|
7160
7739
|
);
|
|
7161
|
-
|
|
7162
|
-
|
|
7740
|
+
const action2 = ensure(
|
|
7741
|
+
await select({
|
|
7742
|
+
message: "What would you like to do?",
|
|
7743
|
+
options: [
|
|
7744
|
+
{ value: "open", label: "Open the dashboard" },
|
|
7745
|
+
svc.running ? { value: "restart", label: "Restart the service" } : { value: "start", label: "Start the service" },
|
|
7746
|
+
{ value: "stop", label: "Stop the service", hint: "won't restart on boot" },
|
|
7747
|
+
{ value: "reset", label: "Reset admin password", hint: "sets a local email + password login" },
|
|
7748
|
+
{ value: "reconfigure", label: "Re-run setup", hint: "reconfigure domain / cloud / admin" },
|
|
7749
|
+
{ value: "quit", label: "Quit" }
|
|
7750
|
+
]
|
|
7751
|
+
})
|
|
7163
7752
|
);
|
|
7753
|
+
switch (action2) {
|
|
7754
|
+
case "open":
|
|
7755
|
+
await open(primaryUrl).catch(() => {
|
|
7756
|
+
});
|
|
7757
|
+
outro2(chalk17.dim(`Opening ${primaryUrl}`));
|
|
7758
|
+
return;
|
|
7759
|
+
case "start":
|
|
7760
|
+
await startService({});
|
|
7761
|
+
return;
|
|
7762
|
+
case "restart": {
|
|
7763
|
+
const r = restart();
|
|
7764
|
+
outro2(r.restarted ? chalk17.green("Restarted.") : chalk17.yellow(r.detail));
|
|
7765
|
+
return;
|
|
7766
|
+
}
|
|
7767
|
+
case "stop": {
|
|
7768
|
+
const r = stop();
|
|
7769
|
+
outro2(chalk17.green(`Stopped. ${chalk17.dim(r.detail)}`));
|
|
7770
|
+
return;
|
|
7771
|
+
}
|
|
7772
|
+
case "reset": {
|
|
7773
|
+
const pw = ensure(
|
|
7774
|
+
await password({ message: "New admin password", validate: (v) => v && v.length >= 8 ? void 0 : "At least 8 characters" })
|
|
7775
|
+
);
|
|
7776
|
+
const rr = await internalPost(apiPort, "/api/system/reset-admin-password", { password: pw });
|
|
7777
|
+
outro2(
|
|
7778
|
+
rr.ok ? chalk17.green(`Password reset. Sign in at ${dashUrl} with your email + new password.`) : chalk17.red(`Couldn't reset: ${rr.data?.error || "failed"}`)
|
|
7779
|
+
);
|
|
7780
|
+
return;
|
|
7781
|
+
}
|
|
7782
|
+
case "reconfigure":
|
|
7783
|
+
await runWizard();
|
|
7784
|
+
return;
|
|
7785
|
+
default:
|
|
7786
|
+
outro2(chalk17.dim("Nothing changed."));
|
|
7787
|
+
}
|
|
7164
7788
|
}
|
|
7165
7789
|
|
|
7166
7790
|
// src/index.ts
|
|
7167
|
-
var program = new
|
|
7168
|
-
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.
|
|
7791
|
+
var program = new Command26();
|
|
7792
|
+
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.2").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
|
|
7169
7793
|
if (thisCommand.opts().json) setJsonMode(true);
|
|
7170
7794
|
}).action(async () => {
|
|
7171
|
-
await
|
|
7795
|
+
if (serviceStatus().installed) await runControl();
|
|
7796
|
+
else await runWizard();
|
|
7172
7797
|
});
|
|
7173
7798
|
program.addCommand(upCommand);
|
|
7174
7799
|
program.addCommand(stopCommand);
|
|
@@ -7193,5 +7818,6 @@ program.addCommand(mailCommand);
|
|
|
7193
7818
|
program.addCommand(backupCommand);
|
|
7194
7819
|
program.addCommand(tokenCommand);
|
|
7195
7820
|
program.addCommand(apiCommand);
|
|
7821
|
+
program.addCommand(resetAdminCommand);
|
|
7196
7822
|
installCommand.addCommand(cacheCommand);
|
|
7197
7823
|
program.parse();
|