openship 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1132 -335
- package/dist/server/index.js +17089 -8373
- package/dist/server/lua/geo_country.lua +101 -0
- package/dist/server/lua/mgmt_api.lua +291 -0
- package/dist/server/lua/pipe_log.lua +71 -0
- package/dist/server/lua/pipe_stream.lua +90 -0
- package/dist/server/lua/rules_guard.lua +118 -0
- package/dist/server/lua/rules_lib.lua +176 -0
- package/dist/server/lua/site_logger.lua +182 -0
- package/dist/server/lua/webhook_handler.lua +126 -0
- package/dist/server/migrations/0035_thankful_morgan_stark.sql +1 -0
- package/dist/server/migrations/0036_gorgeous_scalphunter.sql +17 -0
- package/dist/server/migrations/0037_clever_makkari.sql +28 -0
- package/dist/server/migrations/0038_young_magneto.sql +1 -0
- package/dist/server/migrations/0039_secret_lady_ursula.sql +15 -0
- package/dist/server/migrations/0040_sharp_red_wolf.sql +16 -0
- package/dist/server/migrations/0041_round_spyke.sql +4 -0
- package/dist/server/migrations/0042_dapper_havok.sql +1 -0
- package/dist/server/migrations/0043_fast_sharon_ventura.sql +15 -0
- package/dist/server/migrations/0044_amused_shape.sql +8 -0
- package/dist/server/migrations/0045_amusing_korath.sql +1 -0
- package/dist/server/migrations/0046_white_harrier.sql +35 -0
- package/dist/server/migrations/0047_sloppy_strong_guy.sql +2 -0
- package/dist/server/migrations/0048_funny_emma_frost.sql +2 -0
- package/dist/server/migrations/0049_nappy_piledriver.sql +3 -0
- package/dist/server/migrations/meta/0035_snapshot.json +7610 -0
- package/dist/server/migrations/meta/0036_snapshot.json +7752 -0
- package/dist/server/migrations/meta/0037_snapshot.json +7978 -0
- package/dist/server/migrations/meta/0038_snapshot.json +7985 -0
- package/dist/server/migrations/meta/0039_snapshot.json +8090 -0
- package/dist/server/migrations/meta/0040_snapshot.json +8198 -0
- package/dist/server/migrations/meta/0041_snapshot.json +8211 -0
- package/dist/server/migrations/meta/0042_snapshot.json +8217 -0
- package/dist/server/migrations/meta/0043_snapshot.json +8316 -0
- package/dist/server/migrations/meta/0044_snapshot.json +8360 -0
- package/dist/server/migrations/meta/0045_snapshot.json +8367 -0
- package/dist/server/migrations/meta/0046_snapshot.json +8657 -0
- package/dist/server/migrations/meta/0047_snapshot.json +8669 -0
- package/dist/server/migrations/meta/0048_snapshot.json +8683 -0
- package/dist/server/migrations/meta/0049_snapshot.json +8702 -0
- package/dist/server/migrations/meta/_journal.json +105 -0
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -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 Command25 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/lib/output.ts
|
|
7
7
|
import chalk from "chalk";
|
|
@@ -902,11 +902,13 @@ var PLANS = {
|
|
|
902
902
|
free: {
|
|
903
903
|
id: "free",
|
|
904
904
|
name: "Free",
|
|
905
|
-
description: "Get started
|
|
905
|
+
description: "Get started for free",
|
|
906
906
|
price: { monthly: 0, annual: 0 },
|
|
907
907
|
stripePriceId: { monthly: null, annual: null },
|
|
908
|
-
|
|
909
|
-
//
|
|
908
|
+
// Paid tiers below are `null` (price + credits) until Openship Cloud pricing
|
|
909
|
+
// is finalized — the UI renders "coming soon". Self-hosted is free and never
|
|
910
|
+
// surfaces any of these numbers.
|
|
911
|
+
monthlyCredits: null,
|
|
910
912
|
oblienLimits: {
|
|
911
913
|
max_workspaces: 1,
|
|
912
914
|
max_vcpus: 2,
|
|
@@ -914,7 +916,6 @@ var PLANS = {
|
|
|
914
916
|
max_disk_gb: 10
|
|
915
917
|
},
|
|
916
918
|
features: [
|
|
917
|
-
"500 credits/mo",
|
|
918
919
|
"1 workspace",
|
|
919
920
|
"Community support"
|
|
920
921
|
],
|
|
@@ -925,13 +926,13 @@ var PLANS = {
|
|
|
925
926
|
id: "pro",
|
|
926
927
|
name: "Pro",
|
|
927
928
|
description: "For solo builders shipping production workloads",
|
|
928
|
-
price: { monthly:
|
|
929
|
-
//
|
|
929
|
+
price: { monthly: null, annual: null },
|
|
930
|
+
// coming soon
|
|
930
931
|
stripePriceId: {
|
|
931
932
|
monthly: process.env.STRIPE_PRICE_PRO_MONTHLY ?? "price_pro_monthly_placeholder",
|
|
932
933
|
annual: process.env.STRIPE_PRICE_PRO_ANNUAL ?? "price_pro_annual_placeholder"
|
|
933
934
|
},
|
|
934
|
-
monthlyCredits:
|
|
935
|
+
monthlyCredits: null,
|
|
935
936
|
oblienLimits: {
|
|
936
937
|
max_workspaces: 10,
|
|
937
938
|
max_vcpus: 16,
|
|
@@ -939,7 +940,6 @@ var PLANS = {
|
|
|
939
940
|
max_disk_gb: 100
|
|
940
941
|
},
|
|
941
942
|
features: [
|
|
942
|
-
"5,000 credits/mo",
|
|
943
943
|
"Up to 10 workspaces",
|
|
944
944
|
"Email support"
|
|
945
945
|
],
|
|
@@ -950,12 +950,13 @@ var PLANS = {
|
|
|
950
950
|
id: "team",
|
|
951
951
|
name: "Team",
|
|
952
952
|
description: "For teams collaborating on shared infra",
|
|
953
|
-
price: { monthly:
|
|
953
|
+
price: { monthly: null, annual: null },
|
|
954
|
+
// coming soon
|
|
954
955
|
stripePriceId: {
|
|
955
956
|
monthly: process.env.STRIPE_PRICE_TEAM_MONTHLY ?? "price_team_monthly_placeholder",
|
|
956
957
|
annual: process.env.STRIPE_PRICE_TEAM_ANNUAL ?? "price_team_annual_placeholder"
|
|
957
958
|
},
|
|
958
|
-
monthlyCredits:
|
|
959
|
+
monthlyCredits: null,
|
|
959
960
|
oblienLimits: {
|
|
960
961
|
max_workspaces: 50,
|
|
961
962
|
max_vcpus: 64,
|
|
@@ -963,7 +964,6 @@ var PLANS = {
|
|
|
963
964
|
max_disk_gb: 500
|
|
964
965
|
},
|
|
965
966
|
features: [
|
|
966
|
-
"25,000 credits/mo",
|
|
967
967
|
"Up to 50 workspaces",
|
|
968
968
|
"Team collaboration",
|
|
969
969
|
"Priority email support"
|
|
@@ -1128,8 +1128,12 @@ var localhost = (port) => `http://localhost:${port}`;
|
|
|
1128
1128
|
var LOCAL_WEB_URL = localhost(DEFAULT_PORT.web);
|
|
1129
1129
|
var LOCAL_DASHBOARD_URL = localhost(DEFAULT_PORT.dashboard);
|
|
1130
1130
|
var LOCAL_API_URL = localhost(DEFAULT_PORT.api);
|
|
1131
|
-
var
|
|
1132
|
-
|
|
1131
|
+
var envUrl = (key) => {
|
|
1132
|
+
const v = typeof process !== "undefined" ? process.env?.[key] : void 0;
|
|
1133
|
+
return v && v.trim() ? v.trim() : void 0;
|
|
1134
|
+
};
|
|
1135
|
+
var CLOUD_DASHBOARD_URL = envUrl("OPENSHIP_CLOUD_DASHBOARD_URL") ?? "https://app.openship.io";
|
|
1136
|
+
var CLOUD_API_URL = envUrl("OPENSHIP_CLOUD_API_URL") ?? "https://api.openship.io";
|
|
1133
1137
|
var DASHBOARD_RUNTIME_TARGETS = {
|
|
1134
1138
|
local: {
|
|
1135
1139
|
dashboard: LOCAL_DASHBOARD_URL,
|
|
@@ -2230,8 +2234,8 @@ var loginCommand = new Command("login").description("Authenticate with a Persona
|
|
|
2230
2234
|
chalk2.bold("\n Openship login\n") + chalk2.dim(" Create a Personal Access Token in Settings \u2192 Personal Access Tokens,\n") + chalk2.dim(" then paste it here.\n")
|
|
2231
2235
|
);
|
|
2232
2236
|
try {
|
|
2233
|
-
const { default:
|
|
2234
|
-
await
|
|
2237
|
+
const { default: open2 } = await import("open");
|
|
2238
|
+
await open2(settingsUrl);
|
|
2235
2239
|
} catch {
|
|
2236
2240
|
}
|
|
2237
2241
|
console.log(
|
|
@@ -2350,8 +2354,8 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
|
|
|
2350
2354
|
}
|
|
2351
2355
|
}
|
|
2352
2356
|
try {
|
|
2353
|
-
const { default:
|
|
2354
|
-
await
|
|
2357
|
+
const { default: open2 } = await import("open");
|
|
2358
|
+
await open2(target);
|
|
2355
2359
|
console.log(chalk4.dim(`
|
|
2356
2360
|
Opening ${target}
|
|
2357
2361
|
`));
|
|
@@ -2367,9 +2371,9 @@ import chalk5 from "chalk";
|
|
|
2367
2371
|
import ora from "ora";
|
|
2368
2372
|
import { spawn } from "child_process";
|
|
2369
2373
|
import { randomBytes } from "crypto";
|
|
2370
|
-
import { existsSync as
|
|
2371
|
-
import { homedir as
|
|
2372
|
-
import { dirname as dirname2, join as
|
|
2374
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2375
|
+
import { homedir as homedir4 } from "os";
|
|
2376
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
2373
2377
|
import { fileURLToPath } from "url";
|
|
2374
2378
|
|
|
2375
2379
|
// src/lib/dashboard.ts
|
|
@@ -2419,8 +2423,8 @@ async function downloadToFile(url, dest, onProgress) {
|
|
|
2419
2423
|
} finally {
|
|
2420
2424
|
file.end();
|
|
2421
2425
|
}
|
|
2422
|
-
await new Promise((
|
|
2423
|
-
file.on("finish", () =>
|
|
2426
|
+
await new Promise((resolve2, reject2) => {
|
|
2427
|
+
file.on("finish", () => resolve2());
|
|
2424
2428
|
file.on("error", reject2);
|
|
2425
2429
|
});
|
|
2426
2430
|
return { sha256: hash.digest("hex"), size: received };
|
|
@@ -2474,6 +2478,17 @@ function assetName(tag) {
|
|
|
2474
2478
|
return `openship-dashboard-${tag}.tar.gz`;
|
|
2475
2479
|
}
|
|
2476
2480
|
async function ensureDashboard(opts = {}) {
|
|
2481
|
+
const override = process.env.OPENSHIP_DASHBOARD_DIR?.trim();
|
|
2482
|
+
if (override) {
|
|
2483
|
+
const cwd2 = join3(override, "apps", "dashboard");
|
|
2484
|
+
const entry2 = join3(cwd2, "server.js");
|
|
2485
|
+
if (!existsSync2(entry2)) {
|
|
2486
|
+
throw new Error(
|
|
2487
|
+
`OPENSHIP_DASHBOARD_DIR=${override} but ${entry2} is missing \u2014 build the dashboard standalone first (see docs).`
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
return { tag: "local", entry: entry2, cwd: cwd2 };
|
|
2491
|
+
}
|
|
2477
2492
|
const tag = opts.tag ?? await resolveLatestTag();
|
|
2478
2493
|
const dir = join3(DASHBOARD_CACHE, tag);
|
|
2479
2494
|
const cwd = join3(dir, "apps", "dashboard");
|
|
@@ -2510,47 +2525,349 @@ async function ensureDashboard(opts = {}) {
|
|
|
2510
2525
|
return { tag, entry, cwd };
|
|
2511
2526
|
}
|
|
2512
2527
|
|
|
2528
|
+
// src/lib/service.ts
|
|
2529
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2530
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
|
|
2531
|
+
import { homedir as homedir3 } from "os";
|
|
2532
|
+
import { join as join4, resolve } from "path";
|
|
2533
|
+
var HOME = homedir3();
|
|
2534
|
+
var OS_DIR = join4(HOME, ".openship");
|
|
2535
|
+
var LOG_DIR = join4(OS_DIR, "logs");
|
|
2536
|
+
var MAC_LABEL = "io.openship.up";
|
|
2537
|
+
var MAC_PLIST = join4(HOME, "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
|
|
2538
|
+
var SYSTEMD_NAME = "openship";
|
|
2539
|
+
var WIN_TASK = "Openship";
|
|
2540
|
+
function selfInvocation() {
|
|
2541
|
+
const runtime = process.execPath;
|
|
2542
|
+
const entry = resolve(process.argv[1] ?? "");
|
|
2543
|
+
return { runtime, args: [entry] };
|
|
2544
|
+
}
|
|
2545
|
+
function upArgs(flags) {
|
|
2546
|
+
const a = ["up", "--foreground"];
|
|
2547
|
+
if (flags.port) a.push("--port", flags.port);
|
|
2548
|
+
if (flags.dataDir) a.push("--data-dir", flags.dataDir);
|
|
2549
|
+
if (flags.dashboardPort) a.push("--dashboard-port", flags.dashboardPort);
|
|
2550
|
+
if (flags.ui === false) a.push("--no-ui");
|
|
2551
|
+
if (flags.uiVersion) a.push("--ui-version", flags.uiVersion);
|
|
2552
|
+
if (flags.publicUrl) a.push("--public-url", flags.publicUrl);
|
|
2553
|
+
if (flags.trustProxy) a.push("--trust-proxy");
|
|
2554
|
+
if (flags.managedEdge) a.push("--managed-edge");
|
|
2555
|
+
if (flags.acmeEmail) a.push("--acme-email", flags.acmeEmail);
|
|
2556
|
+
return a;
|
|
2557
|
+
}
|
|
2558
|
+
function runArgv(flags) {
|
|
2559
|
+
const { runtime, args } = selfInvocation();
|
|
2560
|
+
return [runtime, ...args, ...upArgs(flags)];
|
|
2561
|
+
}
|
|
2562
|
+
function run(cmd, args) {
|
|
2563
|
+
const r = spawnSync2(cmd, args, { encoding: "utf8" });
|
|
2564
|
+
return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
2565
|
+
}
|
|
2566
|
+
function isRoot() {
|
|
2567
|
+
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
2568
|
+
}
|
|
2569
|
+
function detectKind() {
|
|
2570
|
+
if (process.platform === "darwin") return "launchd";
|
|
2571
|
+
if (process.platform === "linux") {
|
|
2572
|
+
if (!hasSystemd()) return "unsupported";
|
|
2573
|
+
return isRoot() ? "systemd-system" : "systemd-user";
|
|
2574
|
+
}
|
|
2575
|
+
if (process.platform === "win32") return "schtasks";
|
|
2576
|
+
return "unsupported";
|
|
2577
|
+
}
|
|
2578
|
+
function hasSystemd() {
|
|
2579
|
+
return spawnSync2("sh", ["-c", "command -v systemctl"]).status === 0;
|
|
2580
|
+
}
|
|
2581
|
+
function xmlEscape(s) {
|
|
2582
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2583
|
+
}
|
|
2584
|
+
function serviceEnv() {
|
|
2585
|
+
const extra = {};
|
|
2586
|
+
const dashDir = process.env.OPENSHIP_DASHBOARD_DIR?.trim();
|
|
2587
|
+
if (dashDir) extra.OPENSHIP_DASHBOARD_DIR = dashDir;
|
|
2588
|
+
return extra;
|
|
2589
|
+
}
|
|
2590
|
+
function plist(flags) {
|
|
2591
|
+
const argv = runArgv(flags);
|
|
2592
|
+
const items = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
2593
|
+
const path2 = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", join4(HOME, ".bun/bin")].join(":");
|
|
2594
|
+
const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `<key>${xmlEscape(k)}</key><string>${xmlEscape(v)}</string>`).join("");
|
|
2595
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2596
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2597
|
+
<plist version="1.0">
|
|
2598
|
+
<dict>
|
|
2599
|
+
<key>Label</key><string>${MAC_LABEL}</string>
|
|
2600
|
+
<key>ProgramArguments</key>
|
|
2601
|
+
<array>
|
|
2602
|
+
${items}
|
|
2603
|
+
</array>
|
|
2604
|
+
<key>RunAtLoad</key><true/>
|
|
2605
|
+
<key>KeepAlive</key><true/>
|
|
2606
|
+
<key>StandardOutPath</key><string>${join4(LOG_DIR, "up.log")}</string>
|
|
2607
|
+
<key>StandardErrorPath</key><string>${join4(LOG_DIR, "up.err.log")}</string>
|
|
2608
|
+
<key>EnvironmentVariables</key>
|
|
2609
|
+
<dict><key>PATH</key><string>${path2}</string>${extraEnv}</dict>
|
|
2610
|
+
</dict>
|
|
2611
|
+
</plist>
|
|
2612
|
+
`;
|
|
2613
|
+
}
|
|
2614
|
+
function systemdUnit(flags) {
|
|
2615
|
+
const argv = runArgv(flags);
|
|
2616
|
+
const execStart = argv.map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
|
|
2617
|
+
const extraEnv = Object.entries(serviceEnv()).map(([k, v]) => `Environment=${k}=${v}
|
|
2618
|
+
`).join("");
|
|
2619
|
+
return `[Unit]
|
|
2620
|
+
Description=Openship control plane
|
|
2621
|
+
After=network-online.target
|
|
2622
|
+
Wants=network-online.target
|
|
2623
|
+
|
|
2624
|
+
[Service]
|
|
2625
|
+
Type=simple
|
|
2626
|
+
ExecStart=${execStart}
|
|
2627
|
+
Restart=always
|
|
2628
|
+
RestartSec=2
|
|
2629
|
+
Environment=NODE_ENV=production
|
|
2630
|
+
${extraEnv}
|
|
2631
|
+
[Install]
|
|
2632
|
+
WantedBy=default.target
|
|
2633
|
+
`;
|
|
2634
|
+
}
|
|
2635
|
+
function preview(flags) {
|
|
2636
|
+
const kind = detectKind();
|
|
2637
|
+
if (kind === "launchd") return { kind, path: MAC_PLIST, content: plist(flags) };
|
|
2638
|
+
if (kind === "systemd-user") return { kind, path: join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`), content: systemdUnit(flags) };
|
|
2639
|
+
if (kind === "systemd-system") return { kind, path: `/etc/systemd/system/${SYSTEMD_NAME}.service`, content: systemdUnit(flags) };
|
|
2640
|
+
if (kind === "schtasks") return { kind, path: WIN_TASK, content: runArgv(flags).join(" ") };
|
|
2641
|
+
return { kind, path: "", content: "" };
|
|
2642
|
+
}
|
|
2643
|
+
function installAndStart(flags) {
|
|
2644
|
+
mkdirSync4(LOG_DIR, { recursive: true });
|
|
2645
|
+
const kind = detectKind();
|
|
2646
|
+
if (kind === "launchd") {
|
|
2647
|
+
mkdirSync4(join4(HOME, "Library", "LaunchAgents"), { recursive: true });
|
|
2648
|
+
run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
|
|
2649
|
+
writeFileSync3(MAC_PLIST, plist(flags));
|
|
2650
|
+
const uid = String(process.getuid?.() ?? "");
|
|
2651
|
+
let r = run("launchctl", ["bootstrap", `gui/${uid}`, MAC_PLIST]);
|
|
2652
|
+
if (!r.ok) r = run("launchctl", ["load", "-w", MAC_PLIST]);
|
|
2653
|
+
if (!r.ok) throw new Error(`launchctl failed to load the agent: ${r.out}`);
|
|
2654
|
+
return { kind, detail: `launchd agent ${MAC_LABEL} (logs: ${LOG_DIR})` };
|
|
2655
|
+
}
|
|
2656
|
+
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2657
|
+
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
2658
|
+
const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
|
|
2659
|
+
mkdirSync4(unitPath.slice(0, unitPath.lastIndexOf("/")), { recursive: true });
|
|
2660
|
+
writeFileSync3(unitPath, systemdUnit(flags));
|
|
2661
|
+
run("systemctl", [...sysArgs, "daemon-reload"]);
|
|
2662
|
+
const r = run("systemctl", [...sysArgs, "enable", "--now", SYSTEMD_NAME]);
|
|
2663
|
+
if (!r.ok) throw new Error(`systemctl enable failed: ${r.out}`);
|
|
2664
|
+
if (kind === "systemd-user") {
|
|
2665
|
+
run("loginctl", ["enable-linger", process.env.USER ?? ""]);
|
|
2666
|
+
}
|
|
2667
|
+
return { kind, detail: `systemd unit ${SYSTEMD_NAME} (${kind === "systemd-user" ? "--user" : "system"})` };
|
|
2668
|
+
}
|
|
2669
|
+
if (kind === "schtasks") {
|
|
2670
|
+
const tr = runArgv(flags).map((a) => `\\"${a}\\"`).join(" ");
|
|
2671
|
+
const r = run("schtasks", ["/Create", "/TN", WIN_TASK, "/SC", "ONLOGON", "/RL", "HIGHEST", "/TR", tr, "/F"]);
|
|
2672
|
+
if (!r.ok) throw new Error(`schtasks create failed: ${r.out}`);
|
|
2673
|
+
run("schtasks", ["/Run", "/TN", WIN_TASK]);
|
|
2674
|
+
return { kind, detail: `Scheduled Task ${WIN_TASK} (best-effort; runs at logon)` };
|
|
2675
|
+
}
|
|
2676
|
+
throw new Error(
|
|
2677
|
+
"No supported service manager found (need systemd on Linux). Run `openship up --foreground` instead, or use docker compose for always-on."
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
function restart() {
|
|
2681
|
+
const kind = detectKind();
|
|
2682
|
+
if (kind === "launchd") {
|
|
2683
|
+
if (!existsSync3(MAC_PLIST)) return { restarted: false, detail: "no launchd agent installed" };
|
|
2684
|
+
const uid = String(process.getuid?.() ?? "");
|
|
2685
|
+
const r = run("launchctl", ["kickstart", "-k", `gui/${uid}/${MAC_LABEL}`]);
|
|
2686
|
+
return { restarted: r.ok, detail: r.ok ? `restarted ${MAC_LABEL}` : r.out };
|
|
2687
|
+
}
|
|
2688
|
+
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2689
|
+
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
2690
|
+
const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
|
|
2691
|
+
if (!existsSync3(unitPath)) return { restarted: false, detail: "no systemd unit installed" };
|
|
2692
|
+
const r = run("systemctl", [...sysArgs, "restart", SYSTEMD_NAME]);
|
|
2693
|
+
return { restarted: r.ok, detail: r.ok ? `restarted ${SYSTEMD_NAME}` : r.out };
|
|
2694
|
+
}
|
|
2695
|
+
if (kind === "schtasks") {
|
|
2696
|
+
run("schtasks", ["/End", "/TN", WIN_TASK]);
|
|
2697
|
+
const r = run("schtasks", ["/Run", "/TN", WIN_TASK]);
|
|
2698
|
+
return { restarted: r.ok, detail: r.ok ? `restarted ${WIN_TASK}` : r.out };
|
|
2699
|
+
}
|
|
2700
|
+
return { restarted: false, detail: "no supported service manager" };
|
|
2701
|
+
}
|
|
2702
|
+
function stop() {
|
|
2703
|
+
const kind = detectKind();
|
|
2704
|
+
if (kind === "launchd") {
|
|
2705
|
+
run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
|
|
2706
|
+
if (existsSync3(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
|
|
2707
|
+
return { kind, detail: `launchd agent ${MAC_LABEL} stopped + removed` };
|
|
2708
|
+
}
|
|
2709
|
+
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2710
|
+
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
2711
|
+
run("systemctl", [...sysArgs, "disable", "--now", SYSTEMD_NAME]);
|
|
2712
|
+
const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
|
|
2713
|
+
if (existsSync3(unitPath)) rmSync2(unitPath, { force: true });
|
|
2714
|
+
run("systemctl", [...sysArgs, "daemon-reload"]);
|
|
2715
|
+
return { kind, detail: `systemd unit ${SYSTEMD_NAME} stopped + disabled` };
|
|
2716
|
+
}
|
|
2717
|
+
if (kind === "schtasks") {
|
|
2718
|
+
run("schtasks", ["/End", "/TN", WIN_TASK]);
|
|
2719
|
+
run("schtasks", ["/Delete", "/TN", WIN_TASK, "/F"]);
|
|
2720
|
+
return { kind, detail: `Scheduled Task ${WIN_TASK} stopped + removed` };
|
|
2721
|
+
}
|
|
2722
|
+
return { kind, detail: "no supported service manager \u2014 nothing to stop" };
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2513
2725
|
// src/commands/up.ts
|
|
2726
|
+
function normalizeUrl(raw) {
|
|
2727
|
+
const value = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
2728
|
+
try {
|
|
2729
|
+
const u = new URL(value);
|
|
2730
|
+
return `${u.protocol}//${u.host}`;
|
|
2731
|
+
} catch {
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
function normalizePublicUrl(raw) {
|
|
2736
|
+
const url = normalizeUrl(raw);
|
|
2737
|
+
if (!url) {
|
|
2738
|
+
console.error(
|
|
2739
|
+
chalk5.red(`
|
|
2740
|
+
Invalid --public-url: ${raw}`) + chalk5.dim("\n Expected something like https://ops.example.com\n")
|
|
2741
|
+
);
|
|
2742
|
+
process.exit(1);
|
|
2743
|
+
}
|
|
2744
|
+
return url;
|
|
2745
|
+
}
|
|
2514
2746
|
var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
2515
|
-
var SERVER_DIR =
|
|
2516
|
-
var
|
|
2747
|
+
var SERVER_DIR = join5(DIST_DIR, "server");
|
|
2748
|
+
var OS_DIR2 = join5(homedir4(), ".openship");
|
|
2517
2749
|
function ensureAuthSecret() {
|
|
2518
|
-
const path2 =
|
|
2519
|
-
if (
|
|
2520
|
-
|
|
2750
|
+
const path2 = join5(OS_DIR2, "auth-secret");
|
|
2751
|
+
if (existsSync4(path2)) return readFileSync2(path2, "utf8").trim();
|
|
2752
|
+
mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
|
|
2521
2753
|
const secret = randomBytes(32).toString("hex");
|
|
2522
|
-
|
|
2754
|
+
writeFileSync4(path2, secret, { mode: 384 });
|
|
2523
2755
|
return secret;
|
|
2524
2756
|
}
|
|
2525
|
-
|
|
2526
|
-
const
|
|
2527
|
-
if (
|
|
2757
|
+
function ensureInternalToken() {
|
|
2758
|
+
const path2 = join5(OS_DIR2, "internal-token");
|
|
2759
|
+
if (existsSync4(path2)) return readFileSync2(path2, "utf8").trim();
|
|
2760
|
+
mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
|
|
2761
|
+
const token = randomBytes(32).toString("hex");
|
|
2762
|
+
writeFileSync4(path2, token, { mode: 384 });
|
|
2763
|
+
return token;
|
|
2764
|
+
}
|
|
2765
|
+
var upCommand = new Command4("up").description("Start Openship as a persistent service (boot + auto-restart); --foreground to run attached").option("--port <port>", "API port to listen on", "4000").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port", "3001").option("--no-ui", "Run the API only \u2014 don't download/serve the dashboard").option("--ui-version <tag>", "Dashboard release tag to run (default: this CLI's version)").option("-f, --foreground", "Run attached in this terminal instead of as a background service").option("--dry-run", "Print the service definition that would be installed, then exit").option(
|
|
2766
|
+
"--public-url <url>",
|
|
2767
|
+
"Serve remotely at this public URL (VPS): binds the dashboard to all interfaces, proxies the API same-origin, and requires login"
|
|
2768
|
+
).option(
|
|
2769
|
+
"--trust-proxy",
|
|
2770
|
+
"Trust the X-Real-IP set by a reverse proxy in front (the proxy MUST overwrite X-Real-IP with the real client IP, e.g. `proxy_set_header X-Real-IP $remote_addr`, and the app port MUST be firewalled so only the proxy can reach it; enables per-client rate limiting)"
|
|
2771
|
+
).option(
|
|
2772
|
+
"--managed-edge",
|
|
2773
|
+
"Managed edge: install OpenResty + a free Let's Encrypt cert on this box and route --public-url's domain to the dashboard (no reverse proxy needed)"
|
|
2774
|
+
).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").action(async (opts) => {
|
|
2775
|
+
if (opts.foreground) return runForeground(opts);
|
|
2776
|
+
startService(opts);
|
|
2777
|
+
});
|
|
2778
|
+
function startService(opts, runOpts = {}) {
|
|
2779
|
+
const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
|
|
2780
|
+
const port = String(opts.port || "4000");
|
|
2781
|
+
const dashPort = String(opts.dashboardPort || "3001");
|
|
2782
|
+
const flags = {
|
|
2783
|
+
port: opts.port,
|
|
2784
|
+
dataDir: opts.dataDir,
|
|
2785
|
+
dashboardPort: opts.dashboardPort,
|
|
2786
|
+
ui: opts.ui,
|
|
2787
|
+
uiVersion: opts.uiVersion,
|
|
2788
|
+
publicUrl,
|
|
2789
|
+
trustProxy: opts.trustProxy || opts.managedEdge,
|
|
2790
|
+
// managed edge = OpenResty sets XFF
|
|
2791
|
+
managedEdge: opts.managedEdge,
|
|
2792
|
+
acmeEmail: opts.acmeEmail
|
|
2793
|
+
};
|
|
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
|
+
try {
|
|
2806
|
+
const res = installAndStart(flags);
|
|
2807
|
+
if (!runOpts.quiet) {
|
|
2808
|
+
const dashboardLine = publicUrl ? chalk5.dim(` Dashboard: ${publicUrl} (login required)
|
|
2809
|
+
`) : chalk5.dim(` Dashboard: http://localhost:${dashPort} (login required)
|
|
2810
|
+
`);
|
|
2811
|
+
console.log(
|
|
2812
|
+
chalk5.green("\n \u2714 Openship is running as a service.\n") + (opts.ui !== false ? dashboardLine : "") + (publicUrl ? chalk5.dim(" API is proxied through the dashboard (not exposed). Point your reverse proxy / DNS at the dashboard port.\n") : chalk5.dim(` API: http://localhost:${port}/api
|
|
2813
|
+
`)) + chalk5.dim(` ${res.detail}
|
|
2814
|
+
`) + chalk5.dim(" Starts on boot and auto-restarts. Stop with `openship stop`.\n")
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
return { port, dashPort, publicUrl };
|
|
2818
|
+
} catch (e) {
|
|
2819
|
+
if (runOpts.quiet) throw e;
|
|
2820
|
+
console.error(
|
|
2821
|
+
chalk5.red(`
|
|
2822
|
+
Couldn't install the service: ${e.message}
|
|
2823
|
+
`) + chalk5.dim(" Run `openship up --foreground` to run it attached instead.\n")
|
|
2824
|
+
);
|
|
2825
|
+
process.exit(1);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
async function runForeground(opts) {
|
|
2829
|
+
const serverEntry = join5(SERVER_DIR, "index.js");
|
|
2830
|
+
if (!existsSync4(serverEntry)) {
|
|
2528
2831
|
console.error(
|
|
2529
2832
|
chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
|
|
2530
2833
|
);
|
|
2531
2834
|
process.exit(1);
|
|
2532
2835
|
}
|
|
2533
2836
|
const port = String(opts.port || "4000");
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2837
|
+
const dashPort = String(opts.dashboardPort || "3001");
|
|
2838
|
+
const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
|
|
2839
|
+
const managedEdge = Boolean(opts.managedEdge && publicUrl);
|
|
2840
|
+
const dataDir = opts.dataDir || join5(OS_DIR2, "data");
|
|
2841
|
+
mkdirSync5(dataDir, { recursive: true });
|
|
2536
2842
|
const env = {
|
|
2537
2843
|
...process.env,
|
|
2538
2844
|
PORT: port,
|
|
2539
2845
|
NODE_ENV: "production",
|
|
2540
|
-
// desktop mode → in-process job runner (no Redis)
|
|
2541
|
-
// so a local single-user box needs no PAT over 127.0.0.1.
|
|
2846
|
+
// desktop mode → in-process job runner (no Redis).
|
|
2542
2847
|
DEPLOY_MODE: "desktop",
|
|
2543
2848
|
OPENSHIP_TARGET: "local",
|
|
2544
2849
|
OPENSHIP_JOB_RUNNER: "in-process",
|
|
2545
|
-
OPENSHIP_ALLOW_ZERO_AUTH: "true",
|
|
2546
2850
|
PGLITE_DATA_DIR: dataDir,
|
|
2547
|
-
OPENSHIP_MIGRATIONS_DIR:
|
|
2548
|
-
OPENSHIP_PGLITE_ASSETS_DIR:
|
|
2851
|
+
OPENSHIP_MIGRATIONS_DIR: join5(SERVER_DIR, "migrations"),
|
|
2852
|
+
OPENSHIP_PGLITE_ASSETS_DIR: join5(SERVER_DIR, "pglite"),
|
|
2549
2853
|
BETTER_AUTH_SECRET: ensureAuthSecret()
|
|
2550
2854
|
};
|
|
2855
|
+
env.OPENSHIP_REQUIRE_AUTH = "true";
|
|
2856
|
+
env.INTERNAL_TOKEN = ensureInternalToken();
|
|
2857
|
+
env.OPENSHIP_API_HOST = "127.0.0.1";
|
|
2858
|
+
delete env.OPENSHIP_ALLOW_ZERO_AUTH;
|
|
2859
|
+
if (publicUrl) {
|
|
2860
|
+
env.OPENSHIP_PUBLIC_URL = publicUrl;
|
|
2861
|
+
}
|
|
2862
|
+
if (opts.trustProxy || managedEdge) env.TRUST_PROXY = "true";
|
|
2863
|
+
if (managedEdge) {
|
|
2864
|
+
env.OPENSHIP_MANAGED_EDGE = "true";
|
|
2865
|
+
env.OPENSHIP_DASHBOARD_PORT = dashPort;
|
|
2866
|
+
if (opts.acmeEmail) env.OPENSHIP_ACME_EMAIL = opts.acmeEmail;
|
|
2867
|
+
}
|
|
2551
2868
|
delete env.DATABASE_URL;
|
|
2552
2869
|
delete env.POSTGRES_URL;
|
|
2553
|
-
const
|
|
2870
|
+
const spinner3 = ora(`Starting Openship on http://localhost:${port} \u2026`).start();
|
|
2554
2871
|
const child = spawn(process.execPath, [serverEntry], { env, stdio: ["ignore", "pipe", "pipe"] });
|
|
2555
2872
|
let buffered = "";
|
|
2556
2873
|
const buffer = (d) => {
|
|
@@ -2560,7 +2877,7 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2560
2877
|
child.stderr.on("data", buffer);
|
|
2561
2878
|
child.on("exit", (code) => {
|
|
2562
2879
|
if (code && code !== 0) {
|
|
2563
|
-
|
|
2880
|
+
spinner3.fail(`Openship server exited (code ${code})`);
|
|
2564
2881
|
process.stderr.write(buffered.slice(-2e3));
|
|
2565
2882
|
process.exit(code);
|
|
2566
2883
|
}
|
|
@@ -2579,12 +2896,12 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2579
2896
|
}
|
|
2580
2897
|
}
|
|
2581
2898
|
if (!healthy) {
|
|
2582
|
-
|
|
2899
|
+
spinner3.fail("Openship did not become healthy in time");
|
|
2583
2900
|
process.stderr.write(buffered.slice(-2e3));
|
|
2584
2901
|
child.kill("SIGTERM");
|
|
2585
2902
|
process.exit(1);
|
|
2586
2903
|
}
|
|
2587
|
-
|
|
2904
|
+
spinner3.succeed(`Openship API running at http://localhost:${port}`);
|
|
2588
2905
|
const children = [child];
|
|
2589
2906
|
const stopAll = () => {
|
|
2590
2907
|
for (const c of children) {
|
|
@@ -2602,11 +2919,10 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2602
2919
|
};
|
|
2603
2920
|
let dashboardUrl = null;
|
|
2604
2921
|
if (opts.ui !== false) {
|
|
2605
|
-
const dashPort = String(opts.dashboardPort || "3001");
|
|
2606
2922
|
const uiSpinner = ora("Preparing the dashboard\u2026").start();
|
|
2607
2923
|
try {
|
|
2608
2924
|
const bundle = await ensureDashboard({
|
|
2609
|
-
tag: opts.uiVersion || `v${"0.
|
|
2925
|
+
tag: opts.uiVersion || `v${"0.2.0"}`,
|
|
2610
2926
|
onProgress: (received, total) => {
|
|
2611
2927
|
if (total) {
|
|
2612
2928
|
uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
|
|
@@ -2621,12 +2937,18 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2621
2937
|
NODE_ENV: "production",
|
|
2622
2938
|
OPENSHIP_TARGET: "local",
|
|
2623
2939
|
PORT: dashPort,
|
|
2624
|
-
|
|
2625
|
-
//
|
|
2626
|
-
//
|
|
2627
|
-
|
|
2628
|
-
//
|
|
2629
|
-
|
|
2940
|
+
// Reachable remotely when public; loopback-only otherwise. Under
|
|
2941
|
+
// managed edge the local OpenResty fronts the dashboard, so it stays
|
|
2942
|
+
// on loopback even though there's a public URL.
|
|
2943
|
+
HOSTNAME: publicUrl && !managedEdge ? "0.0.0.0" : "127.0.0.1",
|
|
2944
|
+
// The dashboard's same-origin proxy (NEXT_PUBLIC_API_PROXY, baked
|
|
2945
|
+
// into the release build) forwards /api/proxy/* to this address, so
|
|
2946
|
+
// the browser never needs to know where the API lives. Set in every
|
|
2947
|
+
// mode; loopback because the dashboard runs on the same box.
|
|
2948
|
+
INTERNAL_API_URL: `http://127.0.0.1:${port}`,
|
|
2949
|
+
// Public URL feeds the SSR proxy-origin resolver; local mode keeps
|
|
2950
|
+
// the window.__OPENSHIP_API_ORIGIN__ fallback for direct API calls.
|
|
2951
|
+
...publicUrl ? { OPENSHIP_PUBLIC_URL: publicUrl } : { OPENSHIP_LOCAL_API_URL: `http://127.0.0.1:${port}` }
|
|
2630
2952
|
},
|
|
2631
2953
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2632
2954
|
});
|
|
@@ -2650,7 +2972,7 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2650
2972
|
}
|
|
2651
2973
|
}
|
|
2652
2974
|
if (dashUp) {
|
|
2653
|
-
dashboardUrl = `http://localhost:${dashPort}`;
|
|
2975
|
+
dashboardUrl = publicUrl ?? `http://localhost:${dashPort}`;
|
|
2654
2976
|
uiSpinner.succeed(`Dashboard running at ${dashboardUrl}`);
|
|
2655
2977
|
dash.stdout.off("data", onDash);
|
|
2656
2978
|
dash.stderr.off("data", onDash);
|
|
@@ -2669,12 +2991,20 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2669
2991
|
);
|
|
2670
2992
|
}
|
|
2671
2993
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2994
|
+
if (publicUrl) {
|
|
2995
|
+
console.log(
|
|
2996
|
+
(dashboardUrl ? chalk5.dim(` Dashboard: ${dashboardUrl} (login required)
|
|
2997
|
+
`) : "") + chalk5.dim(" API is proxied through the dashboard (bound to loopback, not exposed).\n") + chalk5.dim(` Data: ${dataDir}
|
|
2998
|
+
`) + (managedEdge ? chalk5.dim(" Managed edge (OpenResty + Let's Encrypt) fronts this box \u2014 point your domain's A record at this server's IP. Stop with Ctrl-C.\n") : chalk5.dim(" Point your reverse proxy / DNS at the dashboard port. Stop with Ctrl-C.\n"))
|
|
2999
|
+
);
|
|
3000
|
+
} else {
|
|
3001
|
+
console.log(
|
|
3002
|
+
chalk5.dim(` API: http://localhost:${port}/api
|
|
3003
|
+
`) + (dashboardUrl ? chalk5.dim(` Dashboard: ${dashboardUrl} (login required)
|
|
2675
3004
|
`) : "") + chalk5.dim(` Data: ${dataDir}
|
|
2676
|
-
`) + chalk5.dim("
|
|
2677
|
-
|
|
3005
|
+
`) + chalk5.dim(" Log in with your admin account (run `openship` to create one). Stop with Ctrl-C.\n")
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
2678
3008
|
child.stdout.off("data", buffer);
|
|
2679
3009
|
child.stderr.off("data", buffer);
|
|
2680
3010
|
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
@@ -2685,19 +3015,35 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2685
3015
|
stopAll();
|
|
2686
3016
|
process.exit(code ?? 0);
|
|
2687
3017
|
});
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// src/commands/stop.ts
|
|
3021
|
+
import { Command as Command5 } from "commander";
|
|
3022
|
+
import chalk6 from "chalk";
|
|
3023
|
+
var stopCommand = new Command5("stop").description("Stop the Openship service (started by `openship up`) \u2014 it won't restart or return on reboot").action(() => {
|
|
3024
|
+
try {
|
|
3025
|
+
const res = stop();
|
|
3026
|
+
console.log(chalk6.green("\n \u2714 Openship stopped.\n") + chalk6.dim(` ${res.detail}
|
|
3027
|
+
`));
|
|
3028
|
+
} catch (e) {
|
|
3029
|
+
console.error(chalk6.red(`
|
|
3030
|
+
Couldn't stop the service: ${e.message}
|
|
3031
|
+
`));
|
|
3032
|
+
process.exit(1);
|
|
3033
|
+
}
|
|
2688
3034
|
});
|
|
2689
3035
|
|
|
2690
3036
|
// src/commands/init.ts
|
|
2691
|
-
import { Command as
|
|
2692
|
-
import { existsSync as
|
|
3037
|
+
import { Command as Command6 } from "commander";
|
|
3038
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2693
3039
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2694
3040
|
import { stdin as input2, stdout as output2 } from "process";
|
|
2695
|
-
import { join as
|
|
2696
|
-
var initCommand = new
|
|
3041
|
+
import { join as join6 } from "path";
|
|
3042
|
+
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) => {
|
|
2697
3043
|
const root = opts.dir || process.cwd();
|
|
2698
|
-
const linkDir =
|
|
2699
|
-
const linkPath =
|
|
2700
|
-
if (
|
|
3044
|
+
const linkDir = join6(root, ".openship");
|
|
3045
|
+
const linkPath = join6(linkDir, "project.json");
|
|
3046
|
+
if (existsSync5(linkPath) && !opts.force) {
|
|
2701
3047
|
err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
|
|
2702
3048
|
process.exit(1);
|
|
2703
3049
|
}
|
|
@@ -2750,8 +3096,8 @@ var initCommand = new Command5("init").description("Link the current directory t
|
|
|
2750
3096
|
context: getActiveContext(),
|
|
2751
3097
|
defaults: { environment: opts.environment || "production" }
|
|
2752
3098
|
};
|
|
2753
|
-
|
|
2754
|
-
|
|
3099
|
+
mkdirSync6(linkDir, { recursive: true });
|
|
3100
|
+
writeFileSync5(linkPath, JSON.stringify(link, null, 2) + "\n");
|
|
2755
3101
|
if (isJsonMode()) {
|
|
2756
3102
|
printJson({ path: linkPath, link });
|
|
2757
3103
|
return;
|
|
@@ -2762,7 +3108,7 @@ var initCommand = new Command5("init").description("Link the current directory t
|
|
|
2762
3108
|
});
|
|
2763
3109
|
|
|
2764
3110
|
// src/commands/context.ts
|
|
2765
|
-
import { Command as
|
|
3111
|
+
import { Command as Command7 } from "commander";
|
|
2766
3112
|
function renderContexts() {
|
|
2767
3113
|
const rows = listContexts().map((c) => ({
|
|
2768
3114
|
current: c.current ? "*" : "",
|
|
@@ -2773,8 +3119,8 @@ function renderContexts() {
|
|
|
2773
3119
|
}));
|
|
2774
3120
|
printTable(rows, ["current", "name", "apiUrl", "dashboardUrl", "auth"]);
|
|
2775
3121
|
}
|
|
2776
|
-
var listCmd = new
|
|
2777
|
-
var useCmd = new
|
|
3122
|
+
var listCmd = new Command7("list").alias("ls").description("List configured contexts").action(renderContexts);
|
|
3123
|
+
var useCmd = new Command7("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
|
|
2778
3124
|
try {
|
|
2779
3125
|
setActiveContext(name);
|
|
2780
3126
|
ok(`
|
|
@@ -2785,7 +3131,7 @@ var useCmd = new Command6("use").description("Switch the active context").argume
|
|
|
2785
3131
|
process.exit(1);
|
|
2786
3132
|
}
|
|
2787
3133
|
});
|
|
2788
|
-
var addCmd = new
|
|
3134
|
+
var addCmd = new Command7("add").description("Create or update a context's endpoints/token").argument("<name>", "Context name").option("--api-url <url>", "API base URL").option("--dashboard-url <url>", "Dashboard base URL").option("--token <token>", "Personal Access Token to store").option("--use", "Switch to this context after adding").action((name, opts) => {
|
|
2789
3135
|
addContext(name, {
|
|
2790
3136
|
apiUrl: opts.apiUrl,
|
|
2791
3137
|
dashboardUrl: opts.dashboardUrl,
|
|
@@ -2796,7 +3142,7 @@ var addCmd = new Command6("add").description("Create or update a context's endpo
|
|
|
2796
3142
|
Saved context "${name}"${opts.use ? " (now active)" : ""}.
|
|
2797
3143
|
`);
|
|
2798
3144
|
});
|
|
2799
|
-
var rmCmd = new
|
|
3145
|
+
var rmCmd = new Command7("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
|
|
2800
3146
|
try {
|
|
2801
3147
|
removeContext(name);
|
|
2802
3148
|
ok(`
|
|
@@ -2807,15 +3153,15 @@ var rmCmd = new Command6("rm").alias("remove").description("Remove a context (ca
|
|
|
2807
3153
|
process.exit(1);
|
|
2808
3154
|
}
|
|
2809
3155
|
});
|
|
2810
|
-
var contextCommand = new
|
|
3156
|
+
var contextCommand = new Command7("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
|
|
2811
3157
|
ok(` Active context: ${getActiveContext()}`);
|
|
2812
3158
|
renderContexts();
|
|
2813
3159
|
}).addCommand(listCmd).addCommand(useCmd).addCommand(addCmd).addCommand(rmCmd);
|
|
2814
3160
|
|
|
2815
3161
|
// src/commands/status.ts
|
|
2816
|
-
import { Command as
|
|
2817
|
-
import
|
|
2818
|
-
var statusCommand = new
|
|
3162
|
+
import { Command as Command8 } from "commander";
|
|
3163
|
+
import chalk7 from "chalk";
|
|
3164
|
+
var statusCommand = new Command8("status").description("Show the active context's API health and deployment info").action(async () => {
|
|
2819
3165
|
const context = getActiveContext();
|
|
2820
3166
|
const apiUrl = getApiUrl2();
|
|
2821
3167
|
let health;
|
|
@@ -2838,18 +3184,18 @@ var statusCommand = new Command7("status").description("Show the active context'
|
|
|
2838
3184
|
printJson({ context, apiUrl, reachable: true, health, env: envInfo });
|
|
2839
3185
|
return;
|
|
2840
3186
|
}
|
|
2841
|
-
const row = (label, value) => ` ${
|
|
3187
|
+
const row = (label, value) => ` ${chalk7.dim(label.padEnd(14))}${value ?? chalk7.dim("-")}
|
|
2842
3188
|
`;
|
|
2843
3189
|
process.stdout.write(
|
|
2844
|
-
|
|
3190
|
+
chalk7.bold("\n Openship status\n\n") + row("Context", context) + row("API", apiUrl) + 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) : "") + "\n"
|
|
2845
3191
|
);
|
|
2846
3192
|
});
|
|
2847
3193
|
|
|
2848
3194
|
// src/commands/doctor.ts
|
|
2849
|
-
import { Command as
|
|
2850
|
-
import { existsSync as
|
|
3195
|
+
import { Command as Command9 } from "commander";
|
|
3196
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2851
3197
|
import { execFileSync } from "child_process";
|
|
2852
|
-
import
|
|
3198
|
+
import chalk8 from "chalk";
|
|
2853
3199
|
function bunVersion() {
|
|
2854
3200
|
const embedded = process.versions.bun;
|
|
2855
3201
|
if (embedded) return embedded;
|
|
@@ -2859,9 +3205,9 @@ function bunVersion() {
|
|
|
2859
3205
|
return null;
|
|
2860
3206
|
}
|
|
2861
3207
|
}
|
|
2862
|
-
var doctorCommand = new
|
|
3208
|
+
var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
|
|
2863
3209
|
const checks = [];
|
|
2864
|
-
const hasConfig =
|
|
3210
|
+
const hasConfig = existsSync6(CONFIG_PATH);
|
|
2865
3211
|
checks.push({
|
|
2866
3212
|
name: "config",
|
|
2867
3213
|
status: hasConfig ? "pass" : "warn",
|
|
@@ -2897,10 +3243,10 @@ var doctorCommand = new Command8("doctor").description("Diagnose the CLI setup (
|
|
|
2897
3243
|
if (isJsonMode()) {
|
|
2898
3244
|
printJson({ context, apiUrl, reachable, checks });
|
|
2899
3245
|
} else {
|
|
2900
|
-
const glyph = { pass:
|
|
2901
|
-
process.stdout.write(
|
|
3246
|
+
const glyph = { pass: chalk8.green("\u2713"), warn: chalk8.yellow("!"), fail: chalk8.red("\u2717") };
|
|
3247
|
+
process.stdout.write(chalk8.bold("\n Openship doctor\n\n"));
|
|
2902
3248
|
for (const c of checks) {
|
|
2903
|
-
process.stdout.write(` ${glyph[c.status]} ${
|
|
3249
|
+
process.stdout.write(` ${glyph[c.status]} ${chalk8.bold(c.name.padEnd(8))} ${c.detail}
|
|
2904
3250
|
`);
|
|
2905
3251
|
}
|
|
2906
3252
|
process.stdout.write("\n");
|
|
@@ -2909,20 +3255,20 @@ var doctorCommand = new Command8("doctor").description("Diagnose the CLI setup (
|
|
|
2909
3255
|
});
|
|
2910
3256
|
|
|
2911
3257
|
// src/commands/deploy.ts
|
|
2912
|
-
import { Command as
|
|
3258
|
+
import { Command as Command10 } from "commander";
|
|
2913
3259
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2914
3260
|
import ora2 from "ora";
|
|
2915
3261
|
|
|
2916
3262
|
// src/lib/project-link.ts
|
|
2917
|
-
import { readFileSync as readFileSync3, existsSync as
|
|
2918
|
-
import { join as
|
|
2919
|
-
var LINK_REL =
|
|
3263
|
+
import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
|
|
3264
|
+
import { join as join7, dirname as dirname3, parse } from "path";
|
|
3265
|
+
var LINK_REL = join7(".openship", "project.json");
|
|
2920
3266
|
function findProjectLinkPath(from = process.cwd()) {
|
|
2921
3267
|
let dir = from;
|
|
2922
3268
|
const root = parse(dir).root;
|
|
2923
3269
|
for (; ; ) {
|
|
2924
|
-
const candidate =
|
|
2925
|
-
if (
|
|
3270
|
+
const candidate = join7(dir, LINK_REL);
|
|
3271
|
+
if (existsSync7(candidate)) return candidate;
|
|
2926
3272
|
if (dir === root) return null;
|
|
2927
3273
|
dir = dirname3(dir);
|
|
2928
3274
|
}
|
|
@@ -2939,21 +3285,21 @@ function readProjectLink(from) {
|
|
|
2939
3285
|
|
|
2940
3286
|
// src/lib/folder-deploy.ts
|
|
2941
3287
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2942
|
-
import { readFileSync as readFileSync4, existsSync as
|
|
3288
|
+
import { readFileSync as readFileSync4, existsSync as existsSync8, rmSync as rmSync3 } from "fs";
|
|
2943
3289
|
import { tmpdir } from "os";
|
|
2944
|
-
import { join as
|
|
3290
|
+
import { join as join8, basename } from "path";
|
|
2945
3291
|
function detectPackageManager(dir) {
|
|
2946
|
-
if (
|
|
2947
|
-
if (
|
|
2948
|
-
if (
|
|
2949
|
-
if (
|
|
3292
|
+
if (existsSync8(join8(dir, "bun.lockb")) || existsSync8(join8(dir, "bun.lock"))) return "bun";
|
|
3293
|
+
if (existsSync8(join8(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
3294
|
+
if (existsSync8(join8(dir, "yarn.lock"))) return "yarn";
|
|
3295
|
+
if (existsSync8(join8(dir, "package.json"))) return "npm";
|
|
2950
3296
|
return void 0;
|
|
2951
3297
|
}
|
|
2952
3298
|
function detectStack(dir) {
|
|
2953
|
-
if (
|
|
2954
|
-
if (
|
|
2955
|
-
if (
|
|
2956
|
-
if (
|
|
3299
|
+
if (existsSync8(join8(dir, "go.mod"))) return "go";
|
|
3300
|
+
if (existsSync8(join8(dir, "Cargo.toml"))) return "rust";
|
|
3301
|
+
if (existsSync8(join8(dir, "requirements.txt")) || existsSync8(join8(dir, "pyproject.toml"))) return "python";
|
|
3302
|
+
if (existsSync8(join8(dir, "package.json"))) return "node";
|
|
2957
3303
|
return void 0;
|
|
2958
3304
|
}
|
|
2959
3305
|
async function deployFolder(opts) {
|
|
@@ -2970,7 +3316,7 @@ async function deployFolder(opts) {
|
|
|
2970
3316
|
throw new Error(session.error || "Failed to open upload session");
|
|
2971
3317
|
}
|
|
2972
3318
|
step("Packaging folder");
|
|
2973
|
-
const tarball =
|
|
3319
|
+
const tarball = join8(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
|
|
2974
3320
|
execFileSync2(
|
|
2975
3321
|
"tar",
|
|
2976
3322
|
[
|
|
@@ -2996,7 +3342,7 @@ async function deployFolder(opts) {
|
|
|
2996
3342
|
if (!res.ok) throw new Error(`upload failed (HTTP ${res.status})`);
|
|
2997
3343
|
} finally {
|
|
2998
3344
|
try {
|
|
2999
|
-
|
|
3345
|
+
rmSync3(tarball, { force: true });
|
|
3000
3346
|
} catch {
|
|
3001
3347
|
}
|
|
3002
3348
|
}
|
|
@@ -3170,7 +3516,7 @@ function git(args) {
|
|
|
3170
3516
|
return void 0;
|
|
3171
3517
|
}
|
|
3172
3518
|
}
|
|
3173
|
-
var deployCommand = new
|
|
3519
|
+
var deployCommand = new Command10("deploy").description("Trigger a deployment for the current project").option("--project <id>", "Project ID (defaults to the linked project in .openship/project.json)").option("--branch <name>", "Git branch to deploy (defaults to the current branch)").option("--commit <sha>", "Specific commit SHA (defaults to the latest commit on the branch)").option("--env <environment>", "Target environment: production | preview", "production").option("--force-all", "Rebuild every enabled service (skip smart per-service routing)").option("--service-ids <ids>", "Comma-separated service IDs to deploy (smart routing)").option("--smart-route", "Rebuild only services changed since the active deploy").option("--refresh", "Re-apply current env to the active deploy (no git pull, no rebuild)").option("--name <name>", "Project name for a folder (non-git) deploy (defaults to the directory name)").option("--watch", "Stream the deployment logs until it finishes").action(async (opts) => {
|
|
3174
3520
|
const link = readProjectLink();
|
|
3175
3521
|
const env = opts.env;
|
|
3176
3522
|
if (env !== "production" && env !== "preview") {
|
|
@@ -3182,7 +3528,7 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
|
|
|
3182
3528
|
let deploymentId;
|
|
3183
3529
|
let payload;
|
|
3184
3530
|
if (!inGitRepo && !gitOnlyFlags) {
|
|
3185
|
-
const
|
|
3531
|
+
const spinner3 = isJsonMode() ? null : ora2("Deploying folder").start();
|
|
3186
3532
|
try {
|
|
3187
3533
|
const result2 = await deployFolder({
|
|
3188
3534
|
cwd: process.cwd(),
|
|
@@ -3190,14 +3536,14 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
|
|
|
3190
3536
|
projectId: opts.project || link?.projectId,
|
|
3191
3537
|
environment: env,
|
|
3192
3538
|
onStep: (m) => {
|
|
3193
|
-
if (
|
|
3539
|
+
if (spinner3) spinner3.text = m;
|
|
3194
3540
|
}
|
|
3195
3541
|
});
|
|
3196
3542
|
deploymentId = result2.deploymentId;
|
|
3197
3543
|
payload = { success: true, deployment_id: result2.deploymentId, project_id: result2.projectId };
|
|
3198
|
-
|
|
3544
|
+
spinner3?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
|
|
3199
3545
|
} catch (e) {
|
|
3200
|
-
|
|
3546
|
+
spinner3?.fail("Folder deploy failed");
|
|
3201
3547
|
err(e instanceof ApiError ? e.message : String(e));
|
|
3202
3548
|
process.exit(1);
|
|
3203
3549
|
}
|
|
@@ -3219,7 +3565,7 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
|
|
|
3219
3565
|
smartRoute: opts.smartRoute || void 0,
|
|
3220
3566
|
refresh: opts.refresh || void 0
|
|
3221
3567
|
};
|
|
3222
|
-
const
|
|
3568
|
+
const spinner3 = isJsonMode() ? null : ora2("Triggering deployment").start();
|
|
3223
3569
|
let res;
|
|
3224
3570
|
try {
|
|
3225
3571
|
res = await apiRequest("/deployments", {
|
|
@@ -3227,13 +3573,13 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
|
|
|
3227
3573
|
body: JSON.stringify(body)
|
|
3228
3574
|
});
|
|
3229
3575
|
} catch (e) {
|
|
3230
|
-
|
|
3576
|
+
spinner3?.fail("Deployment failed to start");
|
|
3231
3577
|
err(e instanceof ApiError ? e.message : String(e));
|
|
3232
3578
|
process.exit(1);
|
|
3233
3579
|
}
|
|
3234
3580
|
deploymentId = res.data?.deployment_id;
|
|
3235
3581
|
payload = res.data ?? res;
|
|
3236
|
-
|
|
3582
|
+
spinner3?.succeed(deploymentId ? `Deployment queued: ${deploymentId}` : "Deployment queued");
|
|
3237
3583
|
}
|
|
3238
3584
|
if (isJsonMode() && !opts.watch) {
|
|
3239
3585
|
printJson(payload ?? {});
|
|
@@ -3252,9 +3598,9 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
|
|
|
3252
3598
|
});
|
|
3253
3599
|
|
|
3254
3600
|
// src/commands/deployment.ts
|
|
3255
|
-
import { Command as
|
|
3601
|
+
import { Command as Command11 } from "commander";
|
|
3256
3602
|
import { createInterface as createInterface3 } from "readline";
|
|
3257
|
-
function
|
|
3603
|
+
function run2(fn) {
|
|
3258
3604
|
return async (...args) => {
|
|
3259
3605
|
try {
|
|
3260
3606
|
await fn(...args);
|
|
@@ -3274,12 +3620,12 @@ function shortSha(v) {
|
|
|
3274
3620
|
async function confirm(question) {
|
|
3275
3621
|
if (!process.stdin.isTTY) return true;
|
|
3276
3622
|
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
3277
|
-
const answer = await new Promise((
|
|
3623
|
+
const answer = await new Promise((resolve2) => rl.question(`${question} [y/N] `, resolve2));
|
|
3278
3624
|
rl.close();
|
|
3279
3625
|
return /^y(es)?$/i.test(answer.trim());
|
|
3280
3626
|
}
|
|
3281
|
-
var list = new
|
|
3282
|
-
|
|
3627
|
+
var list = new Command11("list").description("List deployments (org-wide, or scoped to a project)").option("--project <id>", "Scope to a project (defaults to the linked project)").option("--env <environment>", "Filter by environment: production | preview").option("--limit <n>", "Max rows to fetch", "50").action(
|
|
3628
|
+
run2(async (opts) => {
|
|
3283
3629
|
const projectId = opts.project || readProjectLink()?.projectId;
|
|
3284
3630
|
const params = new URLSearchParams();
|
|
3285
3631
|
if (projectId) params.set("projectId", projectId);
|
|
@@ -3301,8 +3647,8 @@ var list = new Command10("list").description("List deployments (org-wide, or sco
|
|
|
3301
3647
|
printTable(rows, ["id", "status", "env", "branch", "commit", "active", "created"]);
|
|
3302
3648
|
})
|
|
3303
3649
|
);
|
|
3304
|
-
var get = new
|
|
3305
|
-
|
|
3650
|
+
var get = new Command11("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
|
|
3651
|
+
run2(async (id) => {
|
|
3306
3652
|
const res = await apiRequest(`/deployments/${id}`);
|
|
3307
3653
|
const d = res.data ?? {};
|
|
3308
3654
|
if (isJsonMode()) return printJson(d);
|
|
@@ -3322,20 +3668,20 @@ var get = new Command10("get").description("Show a single deployment").argument(
|
|
|
3322
3668
|
);
|
|
3323
3669
|
})
|
|
3324
3670
|
);
|
|
3325
|
-
var info2 = new
|
|
3326
|
-
|
|
3671
|
+
var info2 = new Command11("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
|
|
3672
|
+
run2(async (id) => {
|
|
3327
3673
|
const res = await apiRequest(`/deployments/${id}/info`);
|
|
3328
3674
|
printJson(res.data ?? res);
|
|
3329
3675
|
})
|
|
3330
3676
|
);
|
|
3331
|
-
var usage = new
|
|
3332
|
-
|
|
3677
|
+
var usage = new Command11("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
|
|
3678
|
+
run2(async (id) => {
|
|
3333
3679
|
const res = await apiRequest(`/deployments/${id}/usage`);
|
|
3334
3680
|
printJson(res.data ?? res);
|
|
3335
3681
|
})
|
|
3336
3682
|
);
|
|
3337
|
-
var redeploy = new
|
|
3338
|
-
|
|
3683
|
+
var redeploy = new Command11("redeploy").description("Redeploy from an existing deployment").argument("<id>", "Deployment ID").option("--use-existing-commit", "Rebuild the same commit instead of the latest on the branch").action(
|
|
3684
|
+
run2(async (id, opts) => {
|
|
3339
3685
|
const res = await apiRequest(`/deployments/${id}/redeploy`, {
|
|
3340
3686
|
method: "POST",
|
|
3341
3687
|
body: JSON.stringify({ useExistingCommit: opts.useExistingCommit === true })
|
|
@@ -3343,14 +3689,14 @@ var redeploy = new Command10("redeploy").description("Redeploy from an existing
|
|
|
3343
3689
|
report(res, `Redeploy triggered for ${id}`);
|
|
3344
3690
|
})
|
|
3345
3691
|
);
|
|
3346
|
-
var rollback = new
|
|
3347
|
-
|
|
3692
|
+
var rollback = new Command11("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
|
|
3693
|
+
run2(async (id) => {
|
|
3348
3694
|
const res = await apiRequest(`/deployments/${id}/rollback`, { method: "POST" });
|
|
3349
3695
|
report(res, `Rolled back to ${id}`);
|
|
3350
3696
|
})
|
|
3351
3697
|
);
|
|
3352
|
-
var pin = new
|
|
3353
|
-
|
|
3698
|
+
var pin = new Command11("pin").description("Pin (or unpin) a deployment's rollback artifact").argument("<id>", "Deployment ID").option("--off", "Unpin instead of pin").action(
|
|
3699
|
+
run2(async (id, opts) => {
|
|
3354
3700
|
const pinned = !opts.off;
|
|
3355
3701
|
const res = await apiRequest(`/deployments/${id}/pin`, {
|
|
3356
3702
|
method: "POST",
|
|
@@ -3359,32 +3705,32 @@ var pin = new Command10("pin").description("Pin (or unpin) a deployment's rollba
|
|
|
3359
3705
|
report(res, `${pinned ? "Pinned" : "Unpinned"} ${id}`);
|
|
3360
3706
|
})
|
|
3361
3707
|
);
|
|
3362
|
-
var cancel = new
|
|
3363
|
-
|
|
3708
|
+
var cancel = new Command11("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
|
|
3709
|
+
run2(async (id) => {
|
|
3364
3710
|
const res = await apiRequest(`/deployments/${id}/cancel`, { method: "POST" });
|
|
3365
3711
|
report(res, `Cancelled ${id}`);
|
|
3366
3712
|
})
|
|
3367
3713
|
);
|
|
3368
|
-
var
|
|
3369
|
-
|
|
3714
|
+
var restart2 = new Command11("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
|
|
3715
|
+
run2(async (id) => {
|
|
3370
3716
|
const res = await apiRequest(`/deployments/${id}/restart`, { method: "POST" });
|
|
3371
3717
|
report(res, `Restarted ${id}`);
|
|
3372
3718
|
})
|
|
3373
3719
|
);
|
|
3374
|
-
var reject = new
|
|
3375
|
-
|
|
3720
|
+
var reject = new Command11("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
|
|
3721
|
+
run2(async (id) => {
|
|
3376
3722
|
const res = await apiRequest(`/deployments/${id}/reject`, { method: "POST" });
|
|
3377
3723
|
report(res, `Rejected ${id}`);
|
|
3378
3724
|
})
|
|
3379
3725
|
);
|
|
3380
|
-
var keep = new
|
|
3381
|
-
|
|
3726
|
+
var keep = new Command11("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
|
|
3727
|
+
run2(async (id) => {
|
|
3382
3728
|
const res = await apiRequest(`/deployments/${id}/keep`, { method: "POST" });
|
|
3383
3729
|
report(res, `Kept ${id}`);
|
|
3384
3730
|
})
|
|
3385
3731
|
);
|
|
3386
|
-
var rm = new
|
|
3387
|
-
|
|
3732
|
+
var rm = new Command11("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
|
|
3733
|
+
run2(async (id, opts) => {
|
|
3388
3734
|
if (!opts.yes && !isJsonMode() && !await confirm(`Delete deployment ${id}?`)) {
|
|
3389
3735
|
err("Aborted.");
|
|
3390
3736
|
process.exit(1);
|
|
@@ -3393,8 +3739,8 @@ var rm = new Command10("rm").description("Delete a deployment").argument("<id>",
|
|
|
3393
3739
|
report(res, `Deleted ${id}`);
|
|
3394
3740
|
})
|
|
3395
3741
|
);
|
|
3396
|
-
var sslStatus = new
|
|
3397
|
-
|
|
3742
|
+
var sslStatus = new Command11("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
|
|
3743
|
+
run2(async (domain) => {
|
|
3398
3744
|
const res = await apiRequest("/deployments/ssl/status", {
|
|
3399
3745
|
method: "POST",
|
|
3400
3746
|
body: JSON.stringify({ domain })
|
|
@@ -3402,8 +3748,8 @@ var sslStatus = new Command10("status").description("Check SSL certificate statu
|
|
|
3402
3748
|
printJson(res);
|
|
3403
3749
|
})
|
|
3404
3750
|
);
|
|
3405
|
-
var sslRenew = new
|
|
3406
|
-
|
|
3751
|
+
var sslRenew = new Command11("renew").description("Renew (issue) an SSL certificate for a domain").argument("<domain>", "Domain to renew").option("--www", "Also include the www subdomain").action(
|
|
3752
|
+
run2(async (domain, opts) => {
|
|
3407
3753
|
const res = await apiRequest("/deployments/ssl/renew", {
|
|
3408
3754
|
method: "POST",
|
|
3409
3755
|
body: JSON.stringify({ domain, includeWww: opts.www === true })
|
|
@@ -3411,12 +3757,12 @@ var sslRenew = new Command10("renew").description("Renew (issue) an SSL certific
|
|
|
3411
3757
|
report(res, `SSL renewal requested for ${domain}`);
|
|
3412
3758
|
})
|
|
3413
3759
|
);
|
|
3414
|
-
var ssl = new
|
|
3415
|
-
var deploymentCommand = new
|
|
3760
|
+
var ssl = new Command11("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
|
|
3761
|
+
var deploymentCommand = new Command11("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(restart2).addCommand(reject).addCommand(keep).addCommand(rm).addCommand(ssl);
|
|
3416
3762
|
|
|
3417
3763
|
// src/commands/logs.ts
|
|
3418
|
-
import { Command as
|
|
3419
|
-
var logsCommand = new
|
|
3764
|
+
import { Command as Command12 } from "commander";
|
|
3765
|
+
var logsCommand = new Command12("logs").description("View or stream a deployment's logs").argument("<deploymentId>", "Deployment ID").option("-f, --follow", "Stream live logs via SSE until the deployment finishes").option("--tail <n>", "Show only the last N log lines (snapshot mode)").action(async (deploymentId, opts) => {
|
|
3420
3766
|
if (opts.follow) {
|
|
3421
3767
|
try {
|
|
3422
3768
|
const result = await streamDeploymentLogs(deploymentId);
|
|
@@ -3448,8 +3794,8 @@ var logsCommand = new Command11("logs").description("View or stream a deployment
|
|
|
3448
3794
|
});
|
|
3449
3795
|
|
|
3450
3796
|
// src/commands/project.ts
|
|
3451
|
-
import { Command as
|
|
3452
|
-
import
|
|
3797
|
+
import { Command as Command13 } from "commander";
|
|
3798
|
+
import chalk9 from "chalk";
|
|
3453
3799
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
3454
3800
|
import { stdin as input3, stdout as output3 } from "process";
|
|
3455
3801
|
function action(fn) {
|
|
@@ -3480,12 +3826,12 @@ function printProject(project) {
|
|
|
3480
3826
|
];
|
|
3481
3827
|
for (const [k, v] of fields) {
|
|
3482
3828
|
if (v === null || v === void 0 || v === "") continue;
|
|
3483
|
-
process.stdout.write(` ${
|
|
3829
|
+
process.stdout.write(` ${chalk9.dim(k.padEnd(12))} ${String(v)}
|
|
3484
3830
|
`);
|
|
3485
3831
|
}
|
|
3486
3832
|
}
|
|
3487
3833
|
var ENVIRONMENTS = ["production", "preview", "development"];
|
|
3488
|
-
var listCmd2 = new
|
|
3834
|
+
var listCmd2 = new Command13("list").alias("ls").description("List projects in the active organization").action(
|
|
3489
3835
|
action(async () => {
|
|
3490
3836
|
const rows = [];
|
|
3491
3837
|
for await (const p of paginate("/projects")) {
|
|
@@ -3500,7 +3846,7 @@ var listCmd2 = new Command12("list").alias("ls").description("List projects in t
|
|
|
3500
3846
|
printTable(rows, ["id", "name", "slug", "repo", "source"]);
|
|
3501
3847
|
})
|
|
3502
3848
|
);
|
|
3503
|
-
var getCmd = new
|
|
3849
|
+
var getCmd = new Command13("get").description("Show a single project").argument("<id>", "Project ID").action(
|
|
3504
3850
|
action(async (id) => {
|
|
3505
3851
|
const { data } = await apiRequest(
|
|
3506
3852
|
`/projects/${encodeURIComponent(id)}`
|
|
@@ -3508,7 +3854,7 @@ var getCmd = new Command12("get").description("Show a single project").argument(
|
|
|
3508
3854
|
printProject(data);
|
|
3509
3855
|
})
|
|
3510
3856
|
);
|
|
3511
|
-
var createCmd = new
|
|
3857
|
+
var createCmd = new Command13("create").description("Create a project").requiredOption("--name <name>", "Project name").option("--slug <slug>", "Free-subdomain slug (slug.opsh.io)").option("--git-owner <owner>", "GitHub owner/org").option("--git-repo <repo>", "GitHub repository name").option("--git-branch <branch>", "Git branch to deploy").option("--framework <framework>", "Stack/framework id").option("--local-path <path>", "Local source path").option("--port <port>", "Container port", (v) => Number(v)).option(
|
|
3512
3858
|
"--type <type>",
|
|
3513
3859
|
"Project type: app | docker | services | monorepo"
|
|
3514
3860
|
).action(
|
|
@@ -3532,12 +3878,12 @@ var createCmd = new Command12("create").description("Create a project").required
|
|
|
3532
3878
|
printProject(data);
|
|
3533
3879
|
})
|
|
3534
3880
|
);
|
|
3535
|
-
var deleteCmd = new
|
|
3881
|
+
var deleteCmd = new Command13("delete").alias("rm").description("Delete a project (tears down all resources)").argument("<id>", "Project ID").option("--force", "Cancel active work and delete anyway").option("--force-orphan", "Orphan resources that won't destroy, then drop the row").option("--wipe-volumes", "Also destroy persistent volumes").option("-y, --yes", "Skip the confirmation prompt").action(
|
|
3536
3882
|
action(async (id, opts) => {
|
|
3537
3883
|
if (!opts.yes) {
|
|
3538
3884
|
const rl = createInterface4({ input: input3, output: output3 });
|
|
3539
3885
|
const answer = await rl.question(
|
|
3540
|
-
|
|
3886
|
+
chalk9.yellow(` Delete project ${id}? This cannot be undone. `) + "(y/N) "
|
|
3541
3887
|
);
|
|
3542
3888
|
rl.close();
|
|
3543
3889
|
if (answer.trim().toLowerCase() !== "y") {
|
|
@@ -3563,7 +3909,7 @@ var deleteCmd = new Command12("delete").alias("rm").description("Delete a projec
|
|
|
3563
3909
|
`);
|
|
3564
3910
|
})
|
|
3565
3911
|
);
|
|
3566
|
-
var envCmd = new
|
|
3912
|
+
var envCmd = new Command13("env").description("Manage project environment variables");
|
|
3567
3913
|
envCmd.command("get").description("List env vars (secret values are masked by the API)").argument("<id>", "Project ID").option("--environment <env>", "Filter by environment (production|preview|development)").action(
|
|
3568
3914
|
action(async (id, opts) => {
|
|
3569
3915
|
const qs = opts.environment ? `?environment=${encodeURIComponent(opts.environment)}` : "";
|
|
@@ -3637,7 +3983,7 @@ envCmd.command("set").description("Merge env vars: upsert KEY=VALUE pairs and/or
|
|
|
3637
3983
|
);
|
|
3638
3984
|
})
|
|
3639
3985
|
);
|
|
3640
|
-
var gitCmd = new
|
|
3986
|
+
var gitCmd = new Command13("git").description("Manage git linkage and auto-deploy");
|
|
3641
3987
|
gitCmd.command("link").description("Link a GitHub repository to a project").argument("<id>", "Project ID").requiredOption("--owner <owner>", "GitHub owner/org").requiredOption("--repo <repo>", "Repository name").option("--branch <branch>", "Branch (defaults to the repo's default branch)").option("--installation-id <id>", "GitHub App installation id", (v) => Number(v)).action(
|
|
3642
3988
|
action(async (id, opts) => {
|
|
3643
3989
|
const result = await apiRequest(
|
|
@@ -3725,7 +4071,7 @@ gitCmd.command("webhook-domain").description("Set or clear the domain that recei
|
|
|
3725
4071
|
`);
|
|
3726
4072
|
})
|
|
3727
4073
|
);
|
|
3728
|
-
var connectCmd = new
|
|
4074
|
+
var connectCmd = new Command13("connect").description("Connect a custom domain to a project").argument("<id>", "Project ID").argument("<domain>", "Custom domain hostname").option("--include-www", "Also connect the www. variant").action(
|
|
3729
4075
|
action(async (id, domain, opts) => {
|
|
3730
4076
|
const result = await apiRequest(
|
|
3731
4077
|
`/projects/${encodeURIComponent(id)}/connect`,
|
|
@@ -3745,7 +4091,7 @@ var connectCmd = new Command12("connect").description("Connect a custom domain t
|
|
|
3745
4091
|
printJson(result.records);
|
|
3746
4092
|
})
|
|
3747
4093
|
);
|
|
3748
|
-
var enableCmd = new
|
|
4094
|
+
var enableCmd = new Command13("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
|
|
3749
4095
|
action(async (id) => {
|
|
3750
4096
|
const result = await apiRequest(
|
|
3751
4097
|
`/projects/${encodeURIComponent(id)}/enable`,
|
|
@@ -3757,7 +4103,7 @@ var enableCmd = new Command12("enable").description("Start a stopped project").a
|
|
|
3757
4103
|
`);
|
|
3758
4104
|
})
|
|
3759
4105
|
);
|
|
3760
|
-
var disableCmd = new
|
|
4106
|
+
var disableCmd = new Command13("disable").description("Stop a running project").argument("<id>", "Project ID").action(
|
|
3761
4107
|
action(async (id) => {
|
|
3762
4108
|
const result = await apiRequest(
|
|
3763
4109
|
`/projects/${encodeURIComponent(id)}/disable`,
|
|
@@ -3770,7 +4116,7 @@ var disableCmd = new Command12("disable").description("Stop a running project").
|
|
|
3770
4116
|
})
|
|
3771
4117
|
);
|
|
3772
4118
|
var SLEEP_MODES = ["auto_sleep", "always_on"];
|
|
3773
|
-
var sleepModeCmd = new
|
|
4119
|
+
var sleepModeCmd = new Command13("sleep-mode").description("Set the project sleep mode").argument("<id>", "Project ID").argument("<mode>", `One of: ${SLEEP_MODES.join(", ")}`).action(
|
|
3774
4120
|
action(async (id, mode) => {
|
|
3775
4121
|
if (!SLEEP_MODES.includes(mode)) {
|
|
3776
4122
|
err(` mode must be one of: ${SLEEP_MODES.join(", ")}`);
|
|
@@ -3788,7 +4134,7 @@ var sleepModeCmd = new Command12("sleep-mode").description("Set the project slee
|
|
|
3788
4134
|
})
|
|
3789
4135
|
);
|
|
3790
4136
|
var TRANSFER_DIRS = ["to-cloud", "to-self-hosted"];
|
|
3791
|
-
var transferCmd = new
|
|
4137
|
+
var transferCmd = new Command13("transfer").description("Promote a project to Openship Cloud, or bring it back (self-hosted only)").argument("<id>", "Project ID").argument("<direction>", `One of: ${TRANSFER_DIRS.join(", ")}`).action(
|
|
3792
4138
|
action(async (id, direction) => {
|
|
3793
4139
|
if (!TRANSFER_DIRS.includes(direction)) {
|
|
3794
4140
|
err(` direction must be one of: ${TRANSFER_DIRS.join(", ")}`);
|
|
@@ -3811,7 +4157,7 @@ var transferCmd = new Command12("transfer").description("Promote a project to Op
|
|
|
3811
4157
|
);
|
|
3812
4158
|
})
|
|
3813
4159
|
);
|
|
3814
|
-
var logsCmd = new
|
|
4160
|
+
var logsCmd = new Command13("logs").description("Show or stream runtime (container) logs").argument("<id>", "Project ID").option("--tail <n>", "Number of recent lines", (v) => Number(v)).option("-f, --follow", "Stream logs until interrupted").action(
|
|
3815
4161
|
action(async (id, opts) => {
|
|
3816
4162
|
const tailQs = opts.tail ? `?tail=${opts.tail}` : "";
|
|
3817
4163
|
if (!opts.follow) {
|
|
@@ -3839,7 +4185,7 @@ var logsCmd = new Command12("logs").description("Show or stream runtime (contain
|
|
|
3839
4185
|
}
|
|
3840
4186
|
})
|
|
3841
4187
|
);
|
|
3842
|
-
var serverLogsCmd = new
|
|
4188
|
+
var serverLogsCmd = new Command13("server-logs").description("Show or stream HTTP request logs (edge/OpenResty)").argument("<id>", "Project ID").option("--limit <n>", "Number of recent entries (max 200)", (v) => Number(v)).option("--domain <domain>", "Restrict to a specific domain").option("-f, --follow", "Stream request logs until interrupted").action(
|
|
3843
4189
|
action(async (id, opts) => {
|
|
3844
4190
|
const base = `/projects/${encodeURIComponent(id)}/server-logs`;
|
|
3845
4191
|
const domainQs = opts.domain ? `domain=${encodeURIComponent(opts.domain)}` : "";
|
|
@@ -3880,14 +4226,14 @@ function safeParse(s) {
|
|
|
3880
4226
|
}
|
|
3881
4227
|
}
|
|
3882
4228
|
function printLogEntry(entry) {
|
|
3883
|
-
const ts = entry.timestamp ?
|
|
4229
|
+
const ts = entry.timestamp ? chalk9.dim(String(entry.timestamp)) : "";
|
|
3884
4230
|
const level = String(entry.level ?? "info");
|
|
3885
|
-
const color = level === "error" ?
|
|
4231
|
+
const color = level === "error" ? chalk9.red : level === "warn" ? chalk9.yellow : chalk9.dim;
|
|
3886
4232
|
const msg = entry.message ?? entry.data ?? "";
|
|
3887
4233
|
process.stdout.write(` ${ts} ${color(level.padEnd(5))} ${String(msg)}
|
|
3888
4234
|
`);
|
|
3889
4235
|
}
|
|
3890
|
-
var projectCommand = new
|
|
4236
|
+
var projectCommand = new Command13("project").alias("projects").description("Manage Openship projects");
|
|
3891
4237
|
projectCommand.addCommand(listCmd2);
|
|
3892
4238
|
projectCommand.addCommand(getCmd);
|
|
3893
4239
|
projectCommand.addCommand(createCmd);
|
|
@@ -3903,9 +4249,9 @@ projectCommand.addCommand(logsCmd);
|
|
|
3903
4249
|
projectCommand.addCommand(serverLogsCmd);
|
|
3904
4250
|
|
|
3905
4251
|
// src/commands/service.ts
|
|
3906
|
-
import { Command as
|
|
3907
|
-
import
|
|
3908
|
-
import { spawnSync as
|
|
4252
|
+
import { Command as Command14 } from "commander";
|
|
4253
|
+
import chalk10 from "chalk";
|
|
4254
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3909
4255
|
import path from "path";
|
|
3910
4256
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
3911
4257
|
import { stdin as input4, stdout as output4 } from "process";
|
|
@@ -3917,14 +4263,14 @@ function requireAuth() {
|
|
|
3917
4263
|
}
|
|
3918
4264
|
function fail(e) {
|
|
3919
4265
|
if (e instanceof ApiError) {
|
|
3920
|
-
err(` ${e.message}` + (e.status ?
|
|
4266
|
+
err(` ${e.message}` + (e.status ? chalk10.dim(` (HTTP ${e.status})`) : ""));
|
|
3921
4267
|
} else {
|
|
3922
4268
|
err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
3923
4269
|
}
|
|
3924
4270
|
process.exit(1);
|
|
3925
4271
|
}
|
|
3926
4272
|
function stackCommand(name) {
|
|
3927
|
-
return new
|
|
4273
|
+
return new Command14(name).requiredOption(
|
|
3928
4274
|
"-p, --project <id|slug|name>",
|
|
3929
4275
|
"Stack (project) id, slug, or name"
|
|
3930
4276
|
);
|
|
@@ -4009,7 +4355,7 @@ var listCmd3 = stackCommand("list").description("List the services in a stack").
|
|
|
4009
4355
|
image: s.image ?? "\u2014",
|
|
4010
4356
|
enabled: s.enabled ? "yes" : "no",
|
|
4011
4357
|
exposed: s.exposed ? "yes" : "no",
|
|
4012
|
-
drift: s.drift ?
|
|
4358
|
+
drift: s.drift ? chalk10.yellow("pending") : "\u2014"
|
|
4013
4359
|
})),
|
|
4014
4360
|
["name", "kind", "image", "enabled", "exposed", "drift"]
|
|
4015
4361
|
);
|
|
@@ -4172,7 +4518,7 @@ function mapComposeService(name, def, baseDir) {
|
|
|
4172
4518
|
var syncCmd = stackCommand("sync").description("Sync a stack's services from a docker-compose file (services not in the file are removed)").argument("<compose-file>", "Path to docker-compose.yml / compose.yaml").option("-y, --yes", "Skip the confirmation prompt").action(async (composeFile, opts) => {
|
|
4173
4519
|
requireAuth();
|
|
4174
4520
|
const abs = path.resolve(composeFile);
|
|
4175
|
-
const proc =
|
|
4521
|
+
const proc = spawnSync3(
|
|
4176
4522
|
"docker",
|
|
4177
4523
|
["compose", "-f", abs, "config", "--format", "json"],
|
|
4178
4524
|
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
|
|
@@ -4266,7 +4612,7 @@ var containersCmd = stackCommand("containers").description("List the stack's act
|
|
|
4266
4612
|
fail(e);
|
|
4267
4613
|
}
|
|
4268
4614
|
});
|
|
4269
|
-
var driftCmd = new
|
|
4615
|
+
var driftCmd = new Command14("drift").description(
|
|
4270
4616
|
"Resolve compose drift on a service (upstream compose changed a value you edited)"
|
|
4271
4617
|
);
|
|
4272
4618
|
function driftActionCommand(action2) {
|
|
@@ -4294,7 +4640,7 @@ function driftActionCommand(action2) {
|
|
|
4294
4640
|
}
|
|
4295
4641
|
driftCmd.addCommand(driftActionCommand("accept"));
|
|
4296
4642
|
driftCmd.addCommand(driftActionCommand("keep"));
|
|
4297
|
-
var envCmd2 = new
|
|
4643
|
+
var envCmd2 = new Command14("env").description("Read and write a service's environment variables");
|
|
4298
4644
|
var envGetCmd = stackCommand("get").description("List a service's environment variables (secrets masked)").argument("<service>", "Service name or id").option("-e, --env <environment>", "Environment: production | preview | development").action(async (service, opts) => {
|
|
4299
4645
|
requireAuth();
|
|
4300
4646
|
try {
|
|
@@ -4377,8 +4723,8 @@ function printLogEntry2(entry) {
|
|
|
4377
4723
|
process.stdout.write(JSON.stringify(entry) + "\n");
|
|
4378
4724
|
return;
|
|
4379
4725
|
}
|
|
4380
|
-
const ts = entry.timestamp ?
|
|
4381
|
-
const line = entry.level === "error" ?
|
|
4726
|
+
const ts = entry.timestamp ? chalk10.dim(entry.timestamp) : "";
|
|
4727
|
+
const line = entry.level === "error" ? chalk10.red(msg) : entry.level === "warn" ? chalk10.yellow(msg) : msg;
|
|
4382
4728
|
process.stdout.write(`${ts ? ts + " " : ""}${line}
|
|
4383
4729
|
`);
|
|
4384
4730
|
}
|
|
@@ -4427,7 +4773,7 @@ var execCmd = stackCommand("exec").description("Open an interactive shell in a s
|
|
|
4427
4773
|
);
|
|
4428
4774
|
process.exit(1);
|
|
4429
4775
|
});
|
|
4430
|
-
var serviceCommand = new
|
|
4776
|
+
var serviceCommand = new Command14("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
|
|
4431
4777
|
serviceCommand.addCommand(listCmd3);
|
|
4432
4778
|
serviceCommand.addCommand(getCmd2);
|
|
4433
4779
|
serviceCommand.addCommand(createCmd2);
|
|
@@ -4443,15 +4789,15 @@ serviceCommand.addCommand(logsCmd2);
|
|
|
4443
4789
|
serviceCommand.addCommand(execCmd);
|
|
4444
4790
|
|
|
4445
4791
|
// src/commands/domain.ts
|
|
4446
|
-
import { Command as
|
|
4447
|
-
import
|
|
4792
|
+
import { Command as Command15 } from "commander";
|
|
4793
|
+
import chalk11 from "chalk";
|
|
4448
4794
|
import ora3 from "ora";
|
|
4449
|
-
function spin(
|
|
4450
|
-
return isJsonMode() ? null : ora3(
|
|
4795
|
+
function spin(text2) {
|
|
4796
|
+
return isJsonMode() ? null : ora3(text2).start();
|
|
4451
4797
|
}
|
|
4452
4798
|
function fail2(e) {
|
|
4453
4799
|
if (e instanceof ApiError) {
|
|
4454
|
-
err(` ${e.message}${e.status ?
|
|
4800
|
+
err(` ${e.message}${e.status ? chalk11.dim(` (${e.status})`) : ""}`);
|
|
4455
4801
|
} else {
|
|
4456
4802
|
err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
4457
4803
|
}
|
|
@@ -4479,7 +4825,7 @@ function printRecords(result) {
|
|
|
4479
4825
|
["type", "host", "value"]
|
|
4480
4826
|
);
|
|
4481
4827
|
}
|
|
4482
|
-
var listCmd4 = new
|
|
4828
|
+
var listCmd4 = new Command15("list").description("List a project's custom domains").requiredOption("-p, --project <id>", "Project ID to list domains for").action(async (opts) => {
|
|
4483
4829
|
try {
|
|
4484
4830
|
const res = await apiRequest(
|
|
4485
4831
|
`/domains?projectId=${encodeURIComponent(opts.project)}`
|
|
@@ -4494,7 +4840,7 @@ var listCmd4 = new Command14("list").description("List a project's custom domain
|
|
|
4494
4840
|
fail2(e);
|
|
4495
4841
|
}
|
|
4496
4842
|
});
|
|
4497
|
-
var addCmd2 = new
|
|
4843
|
+
var addCmd2 = new Command15("add").description("Add a custom domain to a project").argument("<hostname>", "Domain hostname (e.g. app.example.com)").requiredOption("-p, --project <id>", "Project ID to attach the domain to").option("--primary", "Mark this domain as the project's primary", false).action(async (hostname, opts) => {
|
|
4498
4844
|
const sp = spin(`Adding ${hostname}\u2026`);
|
|
4499
4845
|
try {
|
|
4500
4846
|
const res = await apiRequest("/domains", {
|
|
@@ -4513,7 +4859,7 @@ var addCmd2 = new Command14("add").description("Add a custom domain to a project
|
|
|
4513
4859
|
fail2(e);
|
|
4514
4860
|
}
|
|
4515
4861
|
});
|
|
4516
|
-
var previewCmd = new
|
|
4862
|
+
var previewCmd = new Command15("preview").description("Preview the DNS records a hostname would need (no changes saved)").argument("<hostname>", "Domain hostname to preview").action(async (hostname) => {
|
|
4517
4863
|
try {
|
|
4518
4864
|
const res = await apiRequest("/domains/preview", {
|
|
4519
4865
|
method: "POST",
|
|
@@ -4524,7 +4870,7 @@ var previewCmd = new Command14("preview").description("Preview the DNS records a
|
|
|
4524
4870
|
fail2(e);
|
|
4525
4871
|
}
|
|
4526
4872
|
});
|
|
4527
|
-
var verifyCmd = new
|
|
4873
|
+
var verifyCmd = new Command15("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
|
|
4528
4874
|
const sp = spin("Checking DNS records\u2026");
|
|
4529
4875
|
try {
|
|
4530
4876
|
const res = await apiRaw(`/domains/${encodeURIComponent(id)}/verify`, { method: "POST" });
|
|
@@ -4550,7 +4896,7 @@ var verifyCmd = new Command14("verify").description("Run DNS verification for a
|
|
|
4550
4896
|
fail2(e);
|
|
4551
4897
|
}
|
|
4552
4898
|
});
|
|
4553
|
-
var primaryCmd = new
|
|
4899
|
+
var primaryCmd = new Command15("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
|
|
4554
4900
|
const sp = spin("Setting primary\u2026");
|
|
4555
4901
|
try {
|
|
4556
4902
|
const res = await apiRequest(`/domains/${encodeURIComponent(id)}/primary`, {
|
|
@@ -4563,7 +4909,7 @@ var primaryCmd = new Command14("primary").description("Make a domain the project
|
|
|
4563
4909
|
fail2(e);
|
|
4564
4910
|
}
|
|
4565
4911
|
});
|
|
4566
|
-
var recordsCmd = new
|
|
4912
|
+
var recordsCmd = new Command15("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
|
|
4567
4913
|
try {
|
|
4568
4914
|
const res = await apiRequest(`/domains/${encodeURIComponent(id)}/records`);
|
|
4569
4915
|
printRecords(res.data);
|
|
@@ -4581,7 +4927,7 @@ function printSsl(data) {
|
|
|
4581
4927
|
if (data.issuer) info(` issuer: ${data.issuer}`);
|
|
4582
4928
|
if (data.expiresAt) info(` expires: ${data.expiresAt}`);
|
|
4583
4929
|
}
|
|
4584
|
-
var renewCmd = new
|
|
4930
|
+
var renewCmd = new Command15("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
|
|
4585
4931
|
const sp = spin("Renewing certificate\u2026");
|
|
4586
4932
|
try {
|
|
4587
4933
|
const res = await apiRequest(`/domains/${encodeURIComponent(id)}/renew`, {
|
|
@@ -4594,7 +4940,7 @@ var renewCmd = new Command14("renew").description("Renew the SSL certificate for
|
|
|
4594
4940
|
fail2(e);
|
|
4595
4941
|
}
|
|
4596
4942
|
});
|
|
4597
|
-
var verifySslCmd = new
|
|
4943
|
+
var verifySslCmd = new Command15("verify-ssl").description("Recheck that a domain's SSL certificate is issued and valid (no reissue)").argument("<id>", "Domain ID").action(async (id) => {
|
|
4598
4944
|
const sp = spin("Checking certificate\u2026");
|
|
4599
4945
|
try {
|
|
4600
4946
|
const res = await apiRequest(`/domains/${encodeURIComponent(id)}/verify-ssl`, {
|
|
@@ -4609,7 +4955,7 @@ var verifySslCmd = new Command14("verify-ssl").description("Recheck that a domai
|
|
|
4609
4955
|
fail2(e);
|
|
4610
4956
|
}
|
|
4611
4957
|
});
|
|
4612
|
-
var renewAllCmd = new
|
|
4958
|
+
var renewAllCmd = new Command15("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
|
|
4613
4959
|
const sp = spin("Renewing expiring certificates\u2026");
|
|
4614
4960
|
try {
|
|
4615
4961
|
const res = await apiRequest("/domains/renew-all", { method: "POST" });
|
|
@@ -4631,11 +4977,11 @@ var renewAllCmd = new Command14("renew-all").description("Renew SSL for every ne
|
|
|
4631
4977
|
fail2(e);
|
|
4632
4978
|
}
|
|
4633
4979
|
});
|
|
4634
|
-
var domainCommand = new
|
|
4980
|
+
var domainCommand = new Command15("domain").description("Manage custom domains, DNS verification, and SSL certificates").addCommand(listCmd4).addCommand(addCmd2).addCommand(previewCmd).addCommand(verifyCmd).addCommand(primaryCmd).addCommand(recordsCmd).addCommand(renewCmd).addCommand(verifySslCmd).addCommand(renewAllCmd);
|
|
4635
4981
|
|
|
4636
4982
|
// src/commands/server.ts
|
|
4637
|
-
import { Command as
|
|
4638
|
-
import
|
|
4983
|
+
import { Command as Command16 } from "commander";
|
|
4984
|
+
import chalk12 from "chalk";
|
|
4639
4985
|
import ora4 from "ora";
|
|
4640
4986
|
var INSTALLABLE = ["docker", "git", "openresty", "certbot", "rsync"];
|
|
4641
4987
|
function guard(fn) {
|
|
@@ -4667,7 +5013,7 @@ function connBody(o) {
|
|
|
4667
5013
|
sshArgs: o.sshArgs
|
|
4668
5014
|
};
|
|
4669
5015
|
}
|
|
4670
|
-
var server = new
|
|
5016
|
+
var server = new Command16("server").description("Manage self-hosted SSH servers");
|
|
4671
5017
|
server.command("list").alias("ls").description("List servers in the active organization").action(
|
|
4672
5018
|
guard(async () => {
|
|
4673
5019
|
const servers = await apiRequest("/system/servers");
|
|
@@ -4705,31 +5051,31 @@ server.command("rm <id>").alias("remove").description("Delete a server").action(
|
|
|
4705
5051
|
);
|
|
4706
5052
|
server.command("test-connection").alias("test").description("Test an SSH connection without saving it").requiredOption("--host <host>", "SSH host / IP").option("--port <port>", "SSH port", "22").option("--user <user>", "SSH user", "root").option("--auth-method <method>", "Auth method (password|key|agent)").option("--password <password>", "SSH password").option("--key-path <path>", "Path to private key").option("--key-passphrase <passphrase>", "Private key passphrase").option("--jump-host <host>", "SSH jump / bastion host").option("--ssh-args <args>", "Extra raw ssh args").action(
|
|
4707
5053
|
guard(async (o) => {
|
|
4708
|
-
const
|
|
5054
|
+
const spinner3 = isJsonMode() ? null : ora4(`Connecting to ${o.host}\u2026`).start();
|
|
4709
5055
|
try {
|
|
4710
5056
|
const res = await apiRequest("/system/test-connection", {
|
|
4711
5057
|
method: "POST",
|
|
4712
5058
|
body: JSON.stringify(connBody(o))
|
|
4713
5059
|
});
|
|
4714
|
-
|
|
5060
|
+
spinner3?.stop();
|
|
4715
5061
|
if (isJsonMode()) return printJson(res);
|
|
4716
5062
|
if (res.ok) return ok(` ${res.message}`);
|
|
4717
5063
|
err(` ${res.message}`);
|
|
4718
5064
|
process.exit(1);
|
|
4719
5065
|
} catch (e) {
|
|
4720
|
-
|
|
5066
|
+
spinner3?.stop();
|
|
4721
5067
|
throw e;
|
|
4722
5068
|
}
|
|
4723
5069
|
})
|
|
4724
5070
|
);
|
|
4725
5071
|
server.command("check <serverId>").description("Run component health checks against a saved server").option("-c, --component <name...>", "Limit to specific components").action(
|
|
4726
5072
|
guard(async (serverId, o) => {
|
|
4727
|
-
const
|
|
5073
|
+
const spinner3 = isJsonMode() ? null : ora4("Checking components\u2026").start();
|
|
4728
5074
|
const res = await apiRequest("/system/check", {
|
|
4729
5075
|
method: "POST",
|
|
4730
5076
|
body: JSON.stringify({ serverId, components: o.component })
|
|
4731
5077
|
});
|
|
4732
|
-
|
|
5078
|
+
spinner3?.stop();
|
|
4733
5079
|
if (isJsonMode()) return printJson(res);
|
|
4734
5080
|
printTable(
|
|
4735
5081
|
res.components.map((c) => ({
|
|
@@ -4766,8 +5112,8 @@ server.command("install <serverId>").description("Install components on a server
|
|
|
4766
5112
|
printJson({ event: ev.event, ...payload });
|
|
4767
5113
|
} else if (ev.event === "log") {
|
|
4768
5114
|
const p = payload;
|
|
4769
|
-
const line = ` ${
|
|
4770
|
-
process.stderr.write((p.level === "error" ?
|
|
5115
|
+
const line = ` ${chalk12.dim(`[${p.component}]`)} ${p.message ?? ""}`;
|
|
5116
|
+
process.stderr.write((p.level === "error" ? chalk12.red(line) : line) + "\n");
|
|
4771
5117
|
} else if (ev.event === "progress") {
|
|
4772
5118
|
const p = payload;
|
|
4773
5119
|
if (p.component) info(` ${p.component}: ${p.status}`);
|
|
@@ -4785,15 +5131,15 @@ server.command("install <serverId>").description("Install components on a server
|
|
|
4785
5131
|
}
|
|
4786
5132
|
const results = [];
|
|
4787
5133
|
for (const component of components) {
|
|
4788
|
-
const
|
|
5134
|
+
const spinner3 = isJsonMode() ? null : ora4(`Installing ${component}\u2026`).start();
|
|
4789
5135
|
const res = await apiRequest(
|
|
4790
5136
|
"/system/install",
|
|
4791
5137
|
{ method: "POST", body: JSON.stringify({ serverId, component }) }
|
|
4792
5138
|
);
|
|
4793
5139
|
results.push(res);
|
|
4794
|
-
if (isJsonMode())
|
|
4795
|
-
else if (res.success)
|
|
4796
|
-
else
|
|
5140
|
+
if (isJsonMode()) spinner3?.stop();
|
|
5141
|
+
else if (res.success) spinner3?.succeed(`${component} installed${res.version ? ` (${res.version})` : ""}`);
|
|
5142
|
+
else spinner3?.fail(`${component} failed: ${res.error ?? "unknown error"}`);
|
|
4797
5143
|
}
|
|
4798
5144
|
if (isJsonMode()) printJson(results);
|
|
4799
5145
|
})
|
|
@@ -4885,9 +5231,9 @@ function fmtUptime(seconds) {
|
|
|
4885
5231
|
var serverCommand = server;
|
|
4886
5232
|
|
|
4887
5233
|
// src/commands/system.ts
|
|
4888
|
-
import { Command as
|
|
5234
|
+
import { Command as Command17 } from "commander";
|
|
4889
5235
|
import ora5 from "ora";
|
|
4890
|
-
import { readFileSync as readFileSync5, writeFileSync as
|
|
5236
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
4891
5237
|
import { createInterface as createInterface6 } from "readline/promises";
|
|
4892
5238
|
import { stdin as input5, stdout as output5 } from "process";
|
|
4893
5239
|
async function guarded(fn) {
|
|
@@ -4908,8 +5254,8 @@ function report2(obj, human) {
|
|
|
4908
5254
|
if (isJsonMode()) printJson(obj);
|
|
4909
5255
|
else human();
|
|
4910
5256
|
}
|
|
4911
|
-
function spinner(
|
|
4912
|
-
return isJsonMode() ? null : ora5(
|
|
5257
|
+
function spinner(text2) {
|
|
5258
|
+
return isJsonMode() ? null : ora5(text2).start();
|
|
4913
5259
|
}
|
|
4914
5260
|
async function confirm2(message, yes) {
|
|
4915
5261
|
if (yes) return true;
|
|
@@ -4933,7 +5279,7 @@ async function promptHidden(query) {
|
|
|
4933
5279
|
output5.write("\n");
|
|
4934
5280
|
return answer;
|
|
4935
5281
|
}
|
|
4936
|
-
var settingsCommand = new
|
|
5282
|
+
var settingsCommand = new Command17("settings").description("Read or update instance settings");
|
|
4937
5283
|
settingsCommand.command("get").description("Show current instance settings").action(async () => {
|
|
4938
5284
|
await guarded(async () => {
|
|
4939
5285
|
const s = await apiRequest("/system/settings");
|
|
@@ -4972,7 +5318,7 @@ settingsCommand.command("set").description("Update instance-level settings").opt
|
|
|
4972
5318
|
`));
|
|
4973
5319
|
});
|
|
4974
5320
|
});
|
|
4975
|
-
var onboardingCommand = new
|
|
5321
|
+
var onboardingCommand = new Command17("onboarding").description("First-run instance setup");
|
|
4976
5322
|
onboardingCommand.command("apply").description("Configure a fresh instance (fails once already configured)").option("--ssh-host <host>", "SSH host of the target server").option("--ssh-port <n>", "SSH port (default 22)").option("--ssh-user <user>", "SSH user (default root)").option("--ssh-auth-method <method>", "SSH auth method").option("--ssh-password <password>", "SSH password").option("--ssh-key-path <path>", "SSH private key path").option("--ssh-key-passphrase <pass>", "SSH key passphrase").option("--ssh-jump-host <host>", "SSH jump host").option("--ssh-args <args>", "Extra SSH args").option("--server-name <name>", "Display name for the server").option("--auth-mode <mode>", "Initial auth mode: none | local | cloud").option("--tunnel-provider <provider>", "Tunnel provider").option("--tunnel-token <token>", "Tunnel token").option("--default-build-mode <mode>", "Default build mode").option("--default-rollback-window <n>", "Default rollback window").action(async (opts) => {
|
|
4977
5323
|
await guarded(async () => {
|
|
4978
5324
|
const body = {
|
|
@@ -5006,17 +5352,17 @@ onboardingCommand.command("apply").description("Configure a fresh instance (fail
|
|
|
5006
5352
|
}
|
|
5007
5353
|
});
|
|
5008
5354
|
});
|
|
5009
|
-
var upgradeToAuthCommand = new
|
|
5355
|
+
var upgradeToAuthCommand = new Command17("upgrade-to-auth").description("Promote a zero-auth instance to email/password login").option("--name <name>", "Account display name").option("--email <email>", "Account email").option("--password <password>", "Account password (prompted if omitted)").option("--use-own-mail-server", "Warm the self-hosted mail server for auth emails").action(async (opts) => {
|
|
5010
5356
|
await guarded(async () => {
|
|
5011
5357
|
const name = opts.name;
|
|
5012
5358
|
const email = opts.email;
|
|
5013
|
-
let
|
|
5014
|
-
if (!
|
|
5359
|
+
let password2 = opts.password;
|
|
5360
|
+
if (!password2) {
|
|
5015
5361
|
if (!process.stdin.isTTY || isJsonMode()) {
|
|
5016
5362
|
err("\n --password is required in non-interactive mode.\n");
|
|
5017
5363
|
process.exit(1);
|
|
5018
5364
|
}
|
|
5019
|
-
|
|
5365
|
+
password2 = await promptHidden(" New password: ");
|
|
5020
5366
|
}
|
|
5021
5367
|
const res = await apiRequest(
|
|
5022
5368
|
"/system/upgrade-to-auth",
|
|
@@ -5025,7 +5371,7 @@ var upgradeToAuthCommand = new Command16("upgrade-to-auth").description("Promote
|
|
|
5025
5371
|
body: JSON.stringify({
|
|
5026
5372
|
name,
|
|
5027
5373
|
email,
|
|
5028
|
-
password,
|
|
5374
|
+
password: password2,
|
|
5029
5375
|
useOwnMailServer: opts.useOwnMailServer === true
|
|
5030
5376
|
})
|
|
5031
5377
|
}
|
|
@@ -5035,7 +5381,7 @@ var upgradeToAuthCommand = new Command16("upgrade-to-auth").description("Promote
|
|
|
5035
5381
|
`));
|
|
5036
5382
|
});
|
|
5037
5383
|
});
|
|
5038
|
-
var browseCommand = new
|
|
5384
|
+
var browseCommand = new Command17("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
|
|
5039
5385
|
await guarded(async () => {
|
|
5040
5386
|
const qs = path2 ? `?path=${encodeURIComponent(path2)}` : "";
|
|
5041
5387
|
const res = await apiRequest(`/system/browse${qs}`);
|
|
@@ -5064,7 +5410,7 @@ function buildDomain(opts) {
|
|
|
5064
5410
|
err("\n A domain is required: pass --hostname <host> or --slug <slug>.\n");
|
|
5065
5411
|
process.exit(1);
|
|
5066
5412
|
}
|
|
5067
|
-
var migrationCommand = new
|
|
5413
|
+
var migrationCommand = new Command17("migration").description("Team-mode migration lifecycle");
|
|
5068
5414
|
migrationCommand.command("preflight").description("Read-only readiness check for the own-server migration").requiredOption("--server-id <id>", "Target server id").option("--hostname <host>", "Custom domain pointing at the server").option("--slug <slug>", "Free <slug>.opsh.io subdomain").action(async (opts) => {
|
|
5069
5415
|
await guarded(async () => {
|
|
5070
5416
|
const domain = buildDomain(opts);
|
|
@@ -5171,7 +5517,7 @@ migrationCommand.command("switch-back").description("Reverse migration back to s
|
|
|
5171
5517
|
}
|
|
5172
5518
|
});
|
|
5173
5519
|
});
|
|
5174
|
-
var dataTransferCommand = new
|
|
5520
|
+
var dataTransferCommand = new Command17("data-transfer").description(
|
|
5175
5521
|
"Whole-instance export / import (owner-only)"
|
|
5176
5522
|
);
|
|
5177
5523
|
dataTransferCommand.command("export").description("Export the entire instance to a JSON file").option("--passphrase <passphrase>", "Seal secrets under this passphrase").option("--out <file>", "Write the export to this file instead of stdout").action(async (opts) => {
|
|
@@ -5187,7 +5533,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
|
|
|
5187
5533
|
);
|
|
5188
5534
|
spin4?.succeed("Export ready.");
|
|
5189
5535
|
if (opts.out) {
|
|
5190
|
-
|
|
5536
|
+
writeFileSync6(opts.out, JSON.stringify(file));
|
|
5191
5537
|
const tables = Object.keys(file.dump?.tables ?? {}).length;
|
|
5192
5538
|
report2(
|
|
5193
5539
|
{ out: opts.out, tables },
|
|
@@ -5241,11 +5587,11 @@ dataTransferCommand.command("import").description("Import an instance export fil
|
|
|
5241
5587
|
}
|
|
5242
5588
|
});
|
|
5243
5589
|
});
|
|
5244
|
-
var systemCommand = new
|
|
5590
|
+
var systemCommand = new Command17("system").description("Instance settings, onboarding, migration, and data transfer").addCommand(settingsCommand).addCommand(onboardingCommand).addCommand(upgradeToAuthCommand).addCommand(browseCommand).addCommand(migrationCommand).addCommand(dataTransferCommand);
|
|
5245
5591
|
|
|
5246
5592
|
// src/commands/mail.ts
|
|
5247
|
-
import { Command as
|
|
5248
|
-
import
|
|
5593
|
+
import { Command as Command18 } from "commander";
|
|
5594
|
+
import chalk13 from "chalk";
|
|
5249
5595
|
import ora6 from "ora";
|
|
5250
5596
|
import { createInterface as createInterface7 } from "readline/promises";
|
|
5251
5597
|
import { stdin as input6, stdout as output6 } from "process";
|
|
@@ -5264,8 +5610,8 @@ function guard2(fn) {
|
|
|
5264
5610
|
}
|
|
5265
5611
|
};
|
|
5266
5612
|
}
|
|
5267
|
-
function spin2(
|
|
5268
|
-
return isJsonMode() ? null : ora6(
|
|
5613
|
+
function spin2(text2) {
|
|
5614
|
+
return isJsonMode() ? null : ora6(text2).start();
|
|
5269
5615
|
}
|
|
5270
5616
|
function safeParse4(data) {
|
|
5271
5617
|
try {
|
|
@@ -5286,7 +5632,7 @@ function printRecordsObject(records) {
|
|
|
5286
5632
|
if (rows.length === 0) return info(" (no DNS records)");
|
|
5287
5633
|
printTable(rows, ["key", "type", "host", "value"]);
|
|
5288
5634
|
}
|
|
5289
|
-
var stepsCmd = new
|
|
5635
|
+
var stepsCmd = new Command18("steps").description("List the mail setup steps").action(
|
|
5290
5636
|
guard2(async () => {
|
|
5291
5637
|
const res = await apiRequest(
|
|
5292
5638
|
"/mail/steps"
|
|
@@ -5299,7 +5645,7 @@ var stepsCmd = new Command17("steps").description("List the mail setup steps").a
|
|
|
5299
5645
|
info(` ${res.total} steps total.`);
|
|
5300
5646
|
})
|
|
5301
5647
|
);
|
|
5302
|
-
var statusCmd = new
|
|
5648
|
+
var statusCmd = new Command18("status").description("Show the setup progress for a mail server").argument("[serverId]", "Mail server ID (omit for the empty welcome shell)").action(
|
|
5303
5649
|
guard2(async (serverId) => {
|
|
5304
5650
|
const q = serverId ? `?serverId=${encodeURIComponent(serverId)}` : "";
|
|
5305
5651
|
const res = await apiRequest(`/mail/status${q}`);
|
|
@@ -5320,7 +5666,7 @@ var statusCmd = new Command17("status").description("Show the setup progress for
|
|
|
5320
5666
|
}
|
|
5321
5667
|
})
|
|
5322
5668
|
);
|
|
5323
|
-
var serversCmd = new
|
|
5669
|
+
var serversCmd = new Command18("servers").description("List every server the mail stack is installed on").action(
|
|
5324
5670
|
guard2(async () => {
|
|
5325
5671
|
const res = await apiRequest(
|
|
5326
5672
|
"/mail/servers"
|
|
@@ -5341,7 +5687,7 @@ var serversCmd = new Command17("servers").description("List every server the mai
|
|
|
5341
5687
|
);
|
|
5342
5688
|
})
|
|
5343
5689
|
);
|
|
5344
|
-
var scanCmd = new
|
|
5690
|
+
var scanCmd = new Command18("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
|
|
5345
5691
|
guard2(async (serverId) => {
|
|
5346
5692
|
const sp = spin2("Scanning server\u2026");
|
|
5347
5693
|
const res = await apiRequest("/mail/scan", { method: "POST", body: JSON.stringify({ serverId }) });
|
|
@@ -5356,7 +5702,7 @@ var scanCmd = new Command17("scan").description("Probe a server for an existing
|
|
|
5356
5702
|
else info(" Nothing to adopt on this server.");
|
|
5357
5703
|
})
|
|
5358
5704
|
);
|
|
5359
|
-
var adoptCmd = new
|
|
5705
|
+
var adoptCmd = new Command18("adopt").description("Re-adopt an existing mail install whose orchestrator state was lost").argument("<serverId>", "Server ID to adopt").action(
|
|
5360
5706
|
guard2(async (serverId) => {
|
|
5361
5707
|
const sp = spin2("Adopting mail server\u2026");
|
|
5362
5708
|
const res = await apiRequest(
|
|
@@ -5369,7 +5715,7 @@ var adoptCmd = new Command17("adopt").description("Re-adopt an existing mail ins
|
|
|
5369
5715
|
info(` completed: ${res.completed ? "yes" : "no"}`);
|
|
5370
5716
|
})
|
|
5371
5717
|
);
|
|
5372
|
-
var setupCmd = new
|
|
5718
|
+
var setupCmd = new Command18("setup").description("Start or resume the mail setup wizard (streams over SSE)").argument("<serverId>", "Server ID to install the mail stack on").requiredOption("-d, --domain <domain>", "Mail domain (e.g. example.com)").option("--start-step <n>", "Resume from a specific step (1-13)").option("--config <json>", "iRedMail config overrides as a JSON object").action(
|
|
5373
5719
|
guard2(async (serverId, opts) => {
|
|
5374
5720
|
let config;
|
|
5375
5721
|
if (opts.config) {
|
|
@@ -5402,7 +5748,7 @@ var setupCmd = new Command17("setup").description("Start or resume the mail setu
|
|
|
5402
5748
|
break;
|
|
5403
5749
|
case "log": {
|
|
5404
5750
|
const line = ` ${String(p.message ?? "")}`;
|
|
5405
|
-
process.stderr.write((p.level === "error" ?
|
|
5751
|
+
process.stderr.write((p.level === "error" ? chalk13.red(line) : chalk13.dim(line)) + "\n");
|
|
5406
5752
|
break;
|
|
5407
5753
|
}
|
|
5408
5754
|
case "step_done":
|
|
@@ -5438,7 +5784,7 @@ var setupCmd = new Command17("setup").description("Start or resume the mail setu
|
|
|
5438
5784
|
if (failed) process.exit(1);
|
|
5439
5785
|
})
|
|
5440
5786
|
);
|
|
5441
|
-
var cancelCmd = new
|
|
5787
|
+
var cancelCmd = new Command18("cancel").description("Cancel the mail setup currently running").action(
|
|
5442
5788
|
guard2(async () => {
|
|
5443
5789
|
const res = await apiRequest("/mail/setup/cancel", {
|
|
5444
5790
|
method: "POST"
|
|
@@ -5448,7 +5794,7 @@ var cancelCmd = new Command17("cancel").description("Cancel the mail setup curre
|
|
|
5448
5794
|
})
|
|
5449
5795
|
);
|
|
5450
5796
|
function ackCommand(name, path2, description, successMsg) {
|
|
5451
|
-
return new
|
|
5797
|
+
return new Command18(name).description(description).argument("<serverId>", "Mail server ID").action(
|
|
5452
5798
|
guard2(async (serverId) => {
|
|
5453
5799
|
const res = await apiRequest(path2, {
|
|
5454
5800
|
method: "POST",
|
|
@@ -5471,7 +5817,7 @@ var ptrAckCmd = ackCommand(
|
|
|
5471
5817
|
"Acknowledge that reverse DNS (PTR) is configured",
|
|
5472
5818
|
"PTR acknowledged. Re-run `mail setup` with --start-step to continue."
|
|
5473
5819
|
);
|
|
5474
|
-
var resetCmd = new
|
|
5820
|
+
var resetCmd = new Command18("reset").description("Wipe the on-server setup state file (does NOT touch installed daemons)").argument("<serverId>", "Mail server ID").option("-y, --yes", "Skip the confirmation prompt").action(
|
|
5475
5821
|
guard2(async (serverId, opts) => {
|
|
5476
5822
|
if (!opts.yes && !isJsonMode()) {
|
|
5477
5823
|
const rl = createInterface7({ input: input6, output: output6 });
|
|
@@ -5487,7 +5833,7 @@ var resetCmd = new Command17("reset").description("Wipe the on-server setup stat
|
|
|
5487
5833
|
ok(" Setup state reset.");
|
|
5488
5834
|
})
|
|
5489
5835
|
);
|
|
5490
|
-
var forgetCmd = new
|
|
5836
|
+
var forgetCmd = new Command18("forget").description("Stop managing a mail server (drops the DB row; leaves the stack + state intact)").argument("<serverId>", "Mail server ID").action(
|
|
5491
5837
|
guard2(async (serverId) => {
|
|
5492
5838
|
const res = await apiRequest(`/mail/servers/${encodeURIComponent(serverId)}`, {
|
|
5493
5839
|
method: "DELETE"
|
|
@@ -5496,7 +5842,7 @@ var forgetCmd = new Command17("forget").description("Stop managing a mail server
|
|
|
5496
5842
|
ok(` Forgot mail server ${serverId} (re-adopt with \`mail scan\` + \`mail adopt\`).`);
|
|
5497
5843
|
})
|
|
5498
5844
|
);
|
|
5499
|
-
var healthCmd = new
|
|
5845
|
+
var healthCmd = new Command18("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
|
|
5500
5846
|
guard2(async (serverId) => {
|
|
5501
5847
|
const sp = spin2("Checking mail daemons\u2026");
|
|
5502
5848
|
const res = await apiRequest(`/mail/health/${encodeURIComponent(serverId)}`);
|
|
@@ -5513,7 +5859,7 @@ var healthCmd = new Command17("health").description("Show live status of every m
|
|
|
5513
5859
|
);
|
|
5514
5860
|
})
|
|
5515
5861
|
);
|
|
5516
|
-
var logsCmd3 = new
|
|
5862
|
+
var logsCmd3 = new Command18("logs").description("Tail a mail component's journal (snapshot)").argument("<serverId>", "Mail server ID").argument("<component>", "Component key (postfix|dovecot|amavis|clamav|iredapd|postgresql|\u2026)").option("-n, --lines <n>", "Number of lines (max 1000)", "200").action(
|
|
5517
5863
|
guard2(async (serverId, component, opts) => {
|
|
5518
5864
|
const res = await apiRequest(
|
|
5519
5865
|
`/mail/admin/${encodeURIComponent(serverId)}/components/${encodeURIComponent(component)}/logs?lines=${encodeURIComponent(opts.lines)}`
|
|
@@ -5523,38 +5869,38 @@ var logsCmd3 = new Command17("logs").description("Tail a mail component's journa
|
|
|
5523
5869
|
for (const line of res.lines) process.stdout.write(line + "\n");
|
|
5524
5870
|
})
|
|
5525
5871
|
);
|
|
5526
|
-
var postmasterCmd = new
|
|
5872
|
+
var postmasterCmd = new Command18("postmaster").description("Manage the postmaster mailbox");
|
|
5527
5873
|
postmasterCmd.addCommand(
|
|
5528
|
-
new
|
|
5874
|
+
new Command18("set-password").description("Rotate the postmaster password").argument("<serverId>", "Mail server ID").option("--password <password>", "New password (min 12 chars); prompted if omitted").action(
|
|
5529
5875
|
guard2(async (serverId, opts) => {
|
|
5530
|
-
let
|
|
5531
|
-
if (!
|
|
5876
|
+
let password2 = opts.password;
|
|
5877
|
+
if (!password2) {
|
|
5532
5878
|
if (isJsonMode()) {
|
|
5533
5879
|
err(" --password is required in JSON mode.");
|
|
5534
5880
|
process.exit(1);
|
|
5535
5881
|
}
|
|
5536
5882
|
const rl = createInterface7({ input: input6, output: output6 });
|
|
5537
|
-
|
|
5883
|
+
password2 = (await rl.question(" New postmaster password (min 12 chars): ")).trim();
|
|
5538
5884
|
rl.close();
|
|
5539
5885
|
}
|
|
5540
|
-
if (!
|
|
5886
|
+
if (!password2 || password2.length < 12) {
|
|
5541
5887
|
err(" Password must be at least 12 characters.");
|
|
5542
5888
|
process.exit(1);
|
|
5543
5889
|
}
|
|
5544
5890
|
const sp = spin2("Updating postmaster password\u2026");
|
|
5545
5891
|
const res = await apiRequest("/mail/credentials/postmaster", {
|
|
5546
5892
|
method: "POST",
|
|
5547
|
-
body: JSON.stringify({ serverId, password })
|
|
5893
|
+
body: JSON.stringify({ serverId, password: password2 })
|
|
5548
5894
|
});
|
|
5549
5895
|
sp?.succeed("Postmaster password updated.");
|
|
5550
5896
|
if (isJsonMode()) printJson(res);
|
|
5551
5897
|
})
|
|
5552
5898
|
)
|
|
5553
5899
|
);
|
|
5554
|
-
var mailCommand = new
|
|
5900
|
+
var mailCommand = new Command18("mail").description("Self-hosted mail server (iRedMail) setup and admin [self-host]").addCommand(stepsCmd).addCommand(statusCmd).addCommand(serversCmd).addCommand(scanCmd).addCommand(adoptCmd).addCommand(setupCmd).addCommand(cancelCmd).addCommand(dnsAckCmd).addCommand(ptrAckCmd).addCommand(resetCmd).addCommand(forgetCmd).addCommand(healthCmd).addCommand(logsCmd3).addCommand(postmasterCmd);
|
|
5555
5901
|
|
|
5556
5902
|
// src/commands/backup.ts
|
|
5557
|
-
import { Command as
|
|
5903
|
+
import { Command as Command19 } from "commander";
|
|
5558
5904
|
import ora7 from "ora";
|
|
5559
5905
|
import { readFileSync as readFileSync6 } from "fs";
|
|
5560
5906
|
async function guard3(fn) {
|
|
@@ -5600,7 +5946,7 @@ function fmtBytes2(n) {
|
|
|
5600
5946
|
return `${v.toFixed(i === 0 ? 0 : 1)}${units[i]}`;
|
|
5601
5947
|
}
|
|
5602
5948
|
async function followStream(path2, label) {
|
|
5603
|
-
const
|
|
5949
|
+
const spinner3 = isJsonMode() ? null : ora7(`${label}: connecting\u2026`).start();
|
|
5604
5950
|
let status = "unknown";
|
|
5605
5951
|
try {
|
|
5606
5952
|
for await (const ev of sseRequest(path2)) {
|
|
@@ -5615,31 +5961,31 @@ async function followStream(path2, label) {
|
|
|
5615
5961
|
if (type === "snapshot") {
|
|
5616
5962
|
const rec = payload.run ?? payload.restore;
|
|
5617
5963
|
status = rec?.status ?? status;
|
|
5618
|
-
if (
|
|
5964
|
+
if (spinner3) spinner3.text = `${label}: ${status}`;
|
|
5619
5965
|
} else if (type === "transition") {
|
|
5620
5966
|
status = payload.status ?? status;
|
|
5621
5967
|
const bytes = payload.bytesTransferred ?? payload.bytesRestored;
|
|
5622
|
-
if (
|
|
5968
|
+
if (spinner3) spinner3.text = `${label}: ${status}${bytes ? ` (${fmtBytes2(bytes)})` : ""}`;
|
|
5623
5969
|
} else if (type === "progress") {
|
|
5624
5970
|
const artifact = payload.currentArtifact ?? "working";
|
|
5625
5971
|
const bytes = payload.bytesTransferred;
|
|
5626
|
-
if (
|
|
5972
|
+
if (spinner3) spinner3.text = `${label}: ${artifact} ${fmtBytes2(bytes)}`.trimEnd();
|
|
5627
5973
|
} else if (type === "complete") {
|
|
5628
5974
|
status = payload.status ?? status;
|
|
5629
5975
|
const errMsg = payload.errorMessage;
|
|
5630
|
-
if (status === "succeeded")
|
|
5631
|
-
else
|
|
5976
|
+
if (status === "succeeded") spinner3?.succeed(`${label} succeeded`);
|
|
5977
|
+
else spinner3?.fail(`${label} ${status}${errMsg ? `: ${errMsg}` : ""}`);
|
|
5632
5978
|
break;
|
|
5633
5979
|
}
|
|
5634
5980
|
}
|
|
5635
5981
|
} catch (e) {
|
|
5636
|
-
|
|
5982
|
+
spinner3?.stop();
|
|
5637
5983
|
throw e;
|
|
5638
5984
|
}
|
|
5639
|
-
|
|
5985
|
+
spinner3?.stop();
|
|
5640
5986
|
return status;
|
|
5641
5987
|
}
|
|
5642
|
-
var policyCmd = new
|
|
5988
|
+
var policyCmd = new Command19("policy").description("Backup policies (schedules) for a project");
|
|
5643
5989
|
policyCmd.command("list").description("List backup policies for a project").requiredOption("--project <id>", "Project ID").action(
|
|
5644
5990
|
(opts) => guard3(async () => {
|
|
5645
5991
|
const { data } = await apiRequest(
|
|
@@ -5709,7 +6055,7 @@ policyCmd.command("run").description("Trigger a policy's backup now").argument("
|
|
|
5709
6055
|
}
|
|
5710
6056
|
})
|
|
5711
6057
|
);
|
|
5712
|
-
var runCmd = new
|
|
6058
|
+
var runCmd = new Command19("run").description("Backup runs (executions)");
|
|
5713
6059
|
runCmd.command("list").description("List backup runs for a project").requiredOption("--project <id>", "Project ID").option("--service <id>", "Filter to a single service").option("--limit <n>", "Max rows (default 50)").action(
|
|
5714
6060
|
(opts) => guard3(async () => {
|
|
5715
6061
|
const qs = new URLSearchParams();
|
|
@@ -5785,7 +6131,7 @@ runCmd.command("restore").description("Prepare a restore from a run (stages it;
|
|
|
5785
6131
|
}
|
|
5786
6132
|
})
|
|
5787
6133
|
);
|
|
5788
|
-
var restoreCmd = new
|
|
6134
|
+
var restoreCmd = new Command19("restore").description("Manage staged restores");
|
|
5789
6135
|
restoreCmd.command("apply").description("Apply a staged restore (destructive)").argument("<restoreId>", "Restore ID from `backup run restore`").requiredOption("--token <token>", "Confirmation token from prepare").option("--follow", "Stream the restore to completion").action(
|
|
5790
6136
|
(restoreId, opts) => guard3(async () => {
|
|
5791
6137
|
await apiRequest(
|
|
@@ -5828,7 +6174,7 @@ restoreCmd.command("get").description("Show one restore (optionally stream it)")
|
|
|
5828
6174
|
show(data);
|
|
5829
6175
|
})
|
|
5830
6176
|
);
|
|
5831
|
-
var destinationCmd = new
|
|
6177
|
+
var destinationCmd = new Command19("destination").description("Backup destinations (storage targets)");
|
|
5832
6178
|
destinationCmd.command("list").description("List backup destinations").action(
|
|
5833
6179
|
() => guard3(async () => {
|
|
5834
6180
|
const { data } = await apiRequest("/backup-destinations");
|
|
@@ -5884,7 +6230,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
|
|
|
5884
6230
|
);
|
|
5885
6231
|
destinationCmd.command("preflight").description("Verify a destination (write + read + delete a probe object)").argument("<destinationId>", "Destination ID").action(
|
|
5886
6232
|
(destinationId) => guard3(async () => {
|
|
5887
|
-
const
|
|
6233
|
+
const spinner3 = isJsonMode() ? null : ora7("Running preflight\u2026").start();
|
|
5888
6234
|
const { data } = await apiRequest(
|
|
5889
6235
|
`/backup-destinations/${encodeURIComponent(destinationId)}/preflight`,
|
|
5890
6236
|
{ method: "POST", body: JSON.stringify({}) }
|
|
@@ -5893,28 +6239,28 @@ destinationCmd.command("preflight").description("Verify a destination (write + r
|
|
|
5893
6239
|
printJson(data);
|
|
5894
6240
|
return;
|
|
5895
6241
|
}
|
|
5896
|
-
if (data.ok)
|
|
6242
|
+
if (data.ok) spinner3?.succeed("Destination reachable");
|
|
5897
6243
|
else {
|
|
5898
|
-
|
|
6244
|
+
spinner3?.fail(`Preflight failed: ${data.reason ?? "unknown"}`);
|
|
5899
6245
|
process.exit(1);
|
|
5900
6246
|
}
|
|
5901
6247
|
})
|
|
5902
6248
|
);
|
|
5903
|
-
var backupCommand = new
|
|
6249
|
+
var backupCommand = new Command19("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
|
|
5904
6250
|
|
|
5905
6251
|
// src/commands/token.ts
|
|
5906
|
-
import { Command as
|
|
5907
|
-
import
|
|
6252
|
+
import { Command as Command20 } from "commander";
|
|
6253
|
+
import chalk15 from "chalk";
|
|
5908
6254
|
|
|
5909
6255
|
// src/lib/cmd-helpers.ts
|
|
5910
|
-
import
|
|
6256
|
+
import chalk14 from "chalk";
|
|
5911
6257
|
import ora8 from "ora";
|
|
5912
|
-
function spin3(
|
|
5913
|
-
return isJsonMode() ? null : ora8(
|
|
6258
|
+
function spin3(text2) {
|
|
6259
|
+
return isJsonMode() ? null : ora8(text2).start();
|
|
5914
6260
|
}
|
|
5915
6261
|
function fail3(e) {
|
|
5916
6262
|
if (e instanceof ApiError) {
|
|
5917
|
-
err(` ${e.message}${e.status ?
|
|
6263
|
+
err(` ${e.message}${e.status ? chalk14.dim(` (${e.status})`) : ""}`);
|
|
5918
6264
|
} else {
|
|
5919
6265
|
err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
5920
6266
|
}
|
|
@@ -5931,7 +6277,7 @@ function collectGrant(value, acc) {
|
|
|
5931
6277
|
acc.push({ resourceType, resourceId, permissions });
|
|
5932
6278
|
return acc;
|
|
5933
6279
|
}
|
|
5934
|
-
var listCmd5 = new
|
|
6280
|
+
var listCmd5 = new Command20("list").description("List your personal access tokens").action(async () => {
|
|
5935
6281
|
try {
|
|
5936
6282
|
const res = await apiRequest("/tokens");
|
|
5937
6283
|
const rows = res.data ?? [];
|
|
@@ -5956,7 +6302,7 @@ var listCmd5 = new Command19("list").description("List your personal access toke
|
|
|
5956
6302
|
fail3(e);
|
|
5957
6303
|
}
|
|
5958
6304
|
});
|
|
5959
|
-
var createCmd3 = new
|
|
6305
|
+
var createCmd3 = new Command20("create").description("Mint a new personal access token (the secret is shown once)").argument("<name>", "Human-readable token name").option("--read-only", "Reject mutation methods (POST/PUT/PATCH/DELETE)", false).option("--expires <days>", "Expire after N days (1\u2013365); omit for non-expiring", (v) => parseInt(v, 10)).option(
|
|
5960
6306
|
"--grant <type:id:perms>",
|
|
5961
6307
|
"Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
|
|
5962
6308
|
collectGrant,
|
|
@@ -5981,14 +6327,14 @@ var createCmd3 = new Command19("create").description("Mint a new personal access
|
|
|
5981
6327
|
return;
|
|
5982
6328
|
}
|
|
5983
6329
|
info(" Copy this token now \u2014 it will not be shown again:");
|
|
5984
|
-
process.stdout.write(
|
|
6330
|
+
process.stdout.write(chalk15.cyan(` ${res.data.token}
|
|
5985
6331
|
`));
|
|
5986
6332
|
} catch (e) {
|
|
5987
6333
|
sp?.fail("Create failed");
|
|
5988
6334
|
fail3(e);
|
|
5989
6335
|
}
|
|
5990
6336
|
});
|
|
5991
|
-
var revokeCmd = new
|
|
6337
|
+
var revokeCmd = new Command20("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
|
|
5992
6338
|
const sp = spin3("Revoking token\u2026");
|
|
5993
6339
|
try {
|
|
5994
6340
|
await apiRequest(`/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
@@ -6000,11 +6346,11 @@ var revokeCmd = new Command19("revoke").description("Revoke one of your tokens")
|
|
|
6000
6346
|
fail3(e);
|
|
6001
6347
|
}
|
|
6002
6348
|
});
|
|
6003
|
-
var tokenCommand = new
|
|
6349
|
+
var tokenCommand = new Command20("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
|
|
6004
6350
|
|
|
6005
6351
|
// src/commands/api.ts
|
|
6006
|
-
import { Command as
|
|
6007
|
-
var apiCommand = new
|
|
6352
|
+
import { Command as Command21 } from "commander";
|
|
6353
|
+
var apiCommand = new Command21("api").description("Make an authenticated request to any Openship API route (like `gh api`)").argument("<path>", "Path under /api, e.g. /projects or /deployments/<id>").option("-X, --method <method>", "HTTP method (defaults to GET, or POST when --data is given)").option("-d, --data <json>", "Request body as a JSON string").option("-q, --query <kv...>", "Query parameter key=value (repeatable)").action(async (path2, opts) => {
|
|
6008
6354
|
const method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
|
|
6009
6355
|
let url = path2.startsWith("/") ? path2 : `/${path2}`;
|
|
6010
6356
|
if (opts.query?.length) {
|
|
@@ -6019,10 +6365,10 @@ var apiCommand = new Command20("api").description("Make an authenticated request
|
|
|
6019
6365
|
if (opts.data) init.body = opts.data;
|
|
6020
6366
|
try {
|
|
6021
6367
|
const res = await apiRaw(url, init);
|
|
6022
|
-
const
|
|
6023
|
-
let body =
|
|
6368
|
+
const text2 = await res.text();
|
|
6369
|
+
let body = text2;
|
|
6024
6370
|
try {
|
|
6025
|
-
body = JSON.parse(
|
|
6371
|
+
body = JSON.parse(text2);
|
|
6026
6372
|
} catch {
|
|
6027
6373
|
}
|
|
6028
6374
|
if (typeof body === "string") {
|
|
@@ -6038,11 +6384,11 @@ var apiCommand = new Command20("api").description("Make an authenticated request
|
|
|
6038
6384
|
});
|
|
6039
6385
|
|
|
6040
6386
|
// src/commands/install.ts
|
|
6041
|
-
import { Command as
|
|
6042
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
6043
|
-
import { spawn as spawn2, spawnSync as
|
|
6044
|
-
import { homedir as
|
|
6045
|
-
import { join as
|
|
6387
|
+
import { Command as Command22 } from "commander";
|
|
6388
|
+
import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
6389
|
+
import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
|
|
6390
|
+
import { homedir as homedir5 } from "os";
|
|
6391
|
+
import { join as join9 } from "path";
|
|
6046
6392
|
import ora9 from "ora";
|
|
6047
6393
|
function assetForPlatform() {
|
|
6048
6394
|
const { platform, arch } = process;
|
|
@@ -6054,33 +6400,33 @@ function assetForPlatform() {
|
|
|
6054
6400
|
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
|
6055
6401
|
}
|
|
6056
6402
|
function installDmg(dmg) {
|
|
6057
|
-
const homeApps =
|
|
6403
|
+
const homeApps = join9(homedir5(), "Applications");
|
|
6058
6404
|
let dest = homeApps;
|
|
6059
6405
|
try {
|
|
6060
|
-
|
|
6406
|
+
mkdirSync7(homeApps, { recursive: true });
|
|
6061
6407
|
} catch {
|
|
6062
6408
|
dest = "/Applications";
|
|
6063
6409
|
}
|
|
6064
|
-
const attach =
|
|
6410
|
+
const attach = spawnSync4("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
|
|
6065
6411
|
encoding: "utf8"
|
|
6066
6412
|
});
|
|
6067
6413
|
if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
|
|
6068
6414
|
const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
|
|
6069
6415
|
if (!mount) throw new Error("Could not determine the mounted volume");
|
|
6070
|
-
let target =
|
|
6416
|
+
let target = join9(dest, "Openship.app");
|
|
6071
6417
|
try {
|
|
6072
|
-
const appInDmg =
|
|
6073
|
-
if (!
|
|
6074
|
-
|
|
6075
|
-
let copy =
|
|
6418
|
+
const appInDmg = join9(mount, "Openship.app");
|
|
6419
|
+
if (!existsSync9(appInDmg)) throw new Error("Openship.app not found in the disk image");
|
|
6420
|
+
spawnSync4("rm", ["-rf", target]);
|
|
6421
|
+
let copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
|
|
6076
6422
|
if (copy.status !== 0 && dest === homeApps) {
|
|
6077
|
-
target =
|
|
6078
|
-
|
|
6079
|
-
copy =
|
|
6423
|
+
target = join9("/Applications", "Openship.app");
|
|
6424
|
+
spawnSync4("rm", ["-rf", target]);
|
|
6425
|
+
copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
|
|
6080
6426
|
}
|
|
6081
6427
|
if (copy.status !== 0) throw new Error(`ditto copy failed: ${copy.stderr?.trim()}`);
|
|
6082
6428
|
} finally {
|
|
6083
|
-
|
|
6429
|
+
spawnSync4("hdiutil", ["detach", mount, "-quiet"]);
|
|
6084
6430
|
}
|
|
6085
6431
|
return target;
|
|
6086
6432
|
}
|
|
@@ -6089,10 +6435,10 @@ function installAppImage(appImage) {
|
|
|
6089
6435
|
return appImage;
|
|
6090
6436
|
}
|
|
6091
6437
|
function installZip(zip) {
|
|
6092
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
6093
|
-
const target =
|
|
6094
|
-
|
|
6095
|
-
const expand =
|
|
6438
|
+
const localAppData = process.env.LOCALAPPDATA || join9(homedir5(), "AppData", "Local");
|
|
6439
|
+
const target = join9(localAppData, "Programs", "Openship");
|
|
6440
|
+
mkdirSync7(target, { recursive: true });
|
|
6441
|
+
const expand = spawnSync4(
|
|
6096
6442
|
"powershell",
|
|
6097
6443
|
[
|
|
6098
6444
|
"-NoProfile",
|
|
@@ -6107,7 +6453,7 @@ function installZip(zip) {
|
|
|
6107
6453
|
}
|
|
6108
6454
|
function launch(kind, target) {
|
|
6109
6455
|
if (kind === "dmg") {
|
|
6110
|
-
|
|
6456
|
+
spawnSync4("open", [target]);
|
|
6111
6457
|
return;
|
|
6112
6458
|
}
|
|
6113
6459
|
if (kind === "appimage") {
|
|
@@ -6118,11 +6464,11 @@ function launch(kind, target) {
|
|
|
6118
6464
|
child.unref();
|
|
6119
6465
|
return;
|
|
6120
6466
|
}
|
|
6121
|
-
const exe =
|
|
6122
|
-
const path2 =
|
|
6123
|
-
|
|
6467
|
+
const exe = join9(target, "Openship.exe");
|
|
6468
|
+
const path2 = existsSync9(exe) ? exe : target;
|
|
6469
|
+
spawnSync4("cmd", ["/c", "start", "", path2]);
|
|
6124
6470
|
}
|
|
6125
|
-
var installCommand = new
|
|
6471
|
+
var installCommand = new Command22("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) => {
|
|
6126
6472
|
let asset;
|
|
6127
6473
|
try {
|
|
6128
6474
|
asset = assetForPlatform();
|
|
@@ -6130,7 +6476,7 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6130
6476
|
err(e.message);
|
|
6131
6477
|
process.exit(1);
|
|
6132
6478
|
}
|
|
6133
|
-
const spin4 = (
|
|
6479
|
+
const spin4 = (text2) => isJsonMode() ? null : ora9(text2).start();
|
|
6134
6480
|
let tag;
|
|
6135
6481
|
try {
|
|
6136
6482
|
if (opts.version) {
|
|
@@ -6145,13 +6491,13 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6145
6491
|
process.exit(1);
|
|
6146
6492
|
}
|
|
6147
6493
|
const dir = releaseDir(tag);
|
|
6148
|
-
const assetPath =
|
|
6494
|
+
const assetPath = join9(dir, asset.name);
|
|
6149
6495
|
const sidecarPath = `${assetPath}.sha256`;
|
|
6150
6496
|
const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
|
|
6151
6497
|
const sidecarUrl = `${assetUrl2}.sha256`;
|
|
6152
6498
|
let downloaded = false;
|
|
6153
6499
|
let sha;
|
|
6154
|
-
const cachedUsable = !opts.force &&
|
|
6500
|
+
const cachedUsable = !opts.force && existsSync9(assetPath) && (existsSync9(sidecarPath) || opts.verify === false);
|
|
6155
6501
|
if (cachedUsable) {
|
|
6156
6502
|
info(` Using cached ${asset.name} (${tag}).`);
|
|
6157
6503
|
} else {
|
|
@@ -6173,7 +6519,7 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6173
6519
|
const s2 = spin4("Verifying checksum\u2026");
|
|
6174
6520
|
try {
|
|
6175
6521
|
let sidecarBody;
|
|
6176
|
-
if (
|
|
6522
|
+
if (existsSync9(sidecarPath) && !downloaded) {
|
|
6177
6523
|
sidecarBody = readFileSync7(sidecarPath, "utf8");
|
|
6178
6524
|
} else {
|
|
6179
6525
|
sidecarBody = await fetchSidecar(sidecarUrl);
|
|
@@ -6197,8 +6543,8 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6197
6543
|
err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
|
|
6198
6544
|
process.exit(1);
|
|
6199
6545
|
}
|
|
6200
|
-
|
|
6201
|
-
|
|
6546
|
+
mkdirSync7(dir, { recursive: true });
|
|
6547
|
+
writeFileSync7(sidecarPath, sidecarBody);
|
|
6202
6548
|
s2?.succeed("Checksum verified");
|
|
6203
6549
|
} catch (e) {
|
|
6204
6550
|
s2?.fail("Verification failed");
|
|
@@ -6237,15 +6583,15 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6237
6583
|
});
|
|
6238
6584
|
|
|
6239
6585
|
// src/commands/update.ts
|
|
6240
|
-
import { Command as
|
|
6241
|
-
import { spawnSync as
|
|
6586
|
+
import { Command as Command23 } from "commander";
|
|
6587
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
6242
6588
|
function detectPackageManager2(override) {
|
|
6243
6589
|
if (override === "bun" || override === "npm") return override;
|
|
6244
|
-
const hasBun =
|
|
6590
|
+
const hasBun = spawnSync5("bun", ["--version"], { stdio: "ignore" }).status === 0;
|
|
6245
6591
|
return hasBun ? "bun" : "npm";
|
|
6246
6592
|
}
|
|
6247
|
-
var updateCommand = new
|
|
6248
|
-
const current = "0.
|
|
6593
|
+
var updateCommand = new Command23("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").action(async (opts) => {
|
|
6594
|
+
const current = "0.2.0";
|
|
6249
6595
|
let latest;
|
|
6250
6596
|
try {
|
|
6251
6597
|
latest = (await resolveLatestTag()).replace(/^v/, "");
|
|
@@ -6273,32 +6619,35 @@ var updateCommand = new Command22("update").description("Update the Openship CLI
|
|
|
6273
6619
|
const ref = `openship@${latest}`;
|
|
6274
6620
|
const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
|
|
6275
6621
|
info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
|
|
6276
|
-
const res =
|
|
6622
|
+
const res = spawnSync5(pm, argv, { stdio: "inherit" });
|
|
6277
6623
|
if (res.status !== 0) {
|
|
6278
6624
|
err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
|
|
6279
6625
|
process.exitCode = 1;
|
|
6280
6626
|
return;
|
|
6281
6627
|
}
|
|
6628
|
+
const { restarted } = restart();
|
|
6282
6629
|
if (isJsonMode()) {
|
|
6283
|
-
printJson({ updated: true, from: current, to: latest, via: pm });
|
|
6630
|
+
printJson({ updated: true, from: current, to: latest, via: pm, restarted });
|
|
6631
|
+
} else if (restarted) {
|
|
6632
|
+
ok(`Updated to v${latest} and restarted the service \u2014 you're on the new version.`);
|
|
6284
6633
|
} else {
|
|
6285
6634
|
ok(`Updated to v${latest}. Restart the server to run the new version: openship up`);
|
|
6286
6635
|
}
|
|
6287
6636
|
});
|
|
6288
6637
|
|
|
6289
6638
|
// src/commands/cache.ts
|
|
6290
|
-
import { Command as
|
|
6291
|
-
import { existsSync as
|
|
6292
|
-
import { join as
|
|
6639
|
+
import { Command as Command24 } from "commander";
|
|
6640
|
+
import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync8, rmSync as rmSync4, statSync } from "fs";
|
|
6641
|
+
import { join as join10 } from "path";
|
|
6293
6642
|
function listAssets() {
|
|
6294
|
-
if (!
|
|
6643
|
+
if (!existsSync10(RELEASES_DIR)) return [];
|
|
6295
6644
|
const out = [];
|
|
6296
6645
|
for (const tag of readdirSync(RELEASES_DIR)) {
|
|
6297
6646
|
const dir = releaseDir(tag);
|
|
6298
6647
|
if (!statSync(dir).isDirectory()) continue;
|
|
6299
6648
|
for (const name of readdirSync(dir)) {
|
|
6300
6649
|
if (name.endsWith(".sha256")) continue;
|
|
6301
|
-
const path2 =
|
|
6650
|
+
const path2 = join10(dir, name);
|
|
6302
6651
|
const st = statSync(path2);
|
|
6303
6652
|
if (!st.isFile()) continue;
|
|
6304
6653
|
out.push({
|
|
@@ -6306,17 +6655,17 @@ function listAssets() {
|
|
|
6306
6655
|
name,
|
|
6307
6656
|
path: path2,
|
|
6308
6657
|
size: st.size,
|
|
6309
|
-
hasSidecar:
|
|
6658
|
+
hasSidecar: existsSync10(`${path2}.sha256`)
|
|
6310
6659
|
});
|
|
6311
6660
|
}
|
|
6312
6661
|
}
|
|
6313
6662
|
return out;
|
|
6314
6663
|
}
|
|
6315
|
-
var pathCmd = new
|
|
6664
|
+
var pathCmd = new Command24("path").description("Print the cache directory path").action(() => {
|
|
6316
6665
|
if (isJsonMode()) printJson({ path: CACHE_DIR });
|
|
6317
6666
|
else process.stdout.write(CACHE_DIR + "\n");
|
|
6318
6667
|
});
|
|
6319
|
-
var listCmd6 = new
|
|
6668
|
+
var listCmd6 = new Command24("list").alias("ls").description("List cached release assets").action(() => {
|
|
6320
6669
|
const assets = listAssets();
|
|
6321
6670
|
printTable(
|
|
6322
6671
|
assets.map((a) => ({
|
|
@@ -6328,7 +6677,7 @@ var listCmd6 = new Command23("list").alias("ls").description("List cached releas
|
|
|
6328
6677
|
["tag", "asset", "size", "sidecar"]
|
|
6329
6678
|
);
|
|
6330
6679
|
});
|
|
6331
|
-
var verifyCmd2 = new
|
|
6680
|
+
var verifyCmd2 = new Command24("verify").description("Re-hash cached assets and compare to their .sha256 sidecar").argument("[tag]", "Only verify assets under this release tag").action(async (tag) => {
|
|
6332
6681
|
const assets = listAssets().filter((a) => !tag || a.tag === tag);
|
|
6333
6682
|
const results = [];
|
|
6334
6683
|
let bad = 0;
|
|
@@ -6351,30 +6700,478 @@ var verifyCmd2 = new Command23("verify").description("Re-hash cached assets and
|
|
|
6351
6700
|
}
|
|
6352
6701
|
if (bad > 0) process.exit(1);
|
|
6353
6702
|
});
|
|
6354
|
-
var cleanCmd = new
|
|
6703
|
+
var cleanCmd = new Command24("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
|
|
6355
6704
|
const target = tag ? releaseDir(tag) : RELEASES_DIR;
|
|
6356
|
-
if (!
|
|
6705
|
+
if (!existsSync10(target)) {
|
|
6357
6706
|
if (isJsonMode()) printJson({ removed: false, path: target });
|
|
6358
6707
|
else info(` Nothing to clean (${target}).`);
|
|
6359
6708
|
return;
|
|
6360
6709
|
}
|
|
6361
|
-
|
|
6710
|
+
rmSync4(target, { recursive: true, force: true });
|
|
6362
6711
|
if (isJsonMode()) printJson({ removed: true, path: target });
|
|
6363
6712
|
else ok(`
|
|
6364
6713
|
Removed ${target}
|
|
6365
6714
|
`);
|
|
6366
6715
|
});
|
|
6367
|
-
var cacheCommand = new
|
|
6716
|
+
var cacheCommand = new Command24("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
|
|
6368
6717
|
err("Specify a subcommand: path | list | verify | clean");
|
|
6369
6718
|
process.exit(1);
|
|
6370
6719
|
}).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
|
|
6371
6720
|
|
|
6721
|
+
// src/commands/wizard.ts
|
|
6722
|
+
import chalk16 from "chalk";
|
|
6723
|
+
import open from "open";
|
|
6724
|
+
import { createServer } from "http";
|
|
6725
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
6726
|
+
import {
|
|
6727
|
+
intro,
|
|
6728
|
+
outro,
|
|
6729
|
+
text,
|
|
6730
|
+
password,
|
|
6731
|
+
select,
|
|
6732
|
+
confirm as confirm3,
|
|
6733
|
+
spinner as spinner2,
|
|
6734
|
+
note,
|
|
6735
|
+
log,
|
|
6736
|
+
cancel as cancel2,
|
|
6737
|
+
isCancel
|
|
6738
|
+
} from "@clack/prompts";
|
|
6739
|
+
function ensure(value) {
|
|
6740
|
+
if (isCancel(value)) {
|
|
6741
|
+
cancel2("Setup cancelled.");
|
|
6742
|
+
process.exit(0);
|
|
6743
|
+
}
|
|
6744
|
+
return value;
|
|
6745
|
+
}
|
|
6746
|
+
var SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
6747
|
+
async function internalGet(port, path2) {
|
|
6748
|
+
try {
|
|
6749
|
+
const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
|
|
6750
|
+
headers: { "X-Internal-Token": ensureInternalToken() },
|
|
6751
|
+
signal: AbortSignal.timeout(1e4)
|
|
6752
|
+
});
|
|
6753
|
+
if (!res.ok) return null;
|
|
6754
|
+
return await res.json();
|
|
6755
|
+
} catch {
|
|
6756
|
+
return null;
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
async function internalPost(port, path2, body) {
|
|
6760
|
+
try {
|
|
6761
|
+
const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
|
|
6762
|
+
method: "POST",
|
|
6763
|
+
headers: { "Content-Type": "application/json", "X-Internal-Token": ensureInternalToken() },
|
|
6764
|
+
body: JSON.stringify(body),
|
|
6765
|
+
signal: AbortSignal.timeout(3e4)
|
|
6766
|
+
});
|
|
6767
|
+
const data = await res.json().catch(() => ({}));
|
|
6768
|
+
return { ok: res.ok, data };
|
|
6769
|
+
} catch (err2) {
|
|
6770
|
+
return { ok: false, data: { error: err2.message } };
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
async function bootstrapAdmin(apiPort, admin) {
|
|
6774
|
+
const { ok: ok3, data } = await internalPost(apiPort, "/api/system/bootstrap-admin", admin);
|
|
6775
|
+
if (ok3) return { ok: true };
|
|
6776
|
+
if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
|
|
6777
|
+
return { ok: false, message: data?.error || "failed" };
|
|
6778
|
+
}
|
|
6779
|
+
async function waitHealthy(apiPort, seconds = 90) {
|
|
6780
|
+
for (let i = 0; i < seconds; i++) {
|
|
6781
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
6782
|
+
try {
|
|
6783
|
+
await fetch(`http://127.0.0.1:${apiPort}/api/health`, { signal: AbortSignal.timeout(2e3) });
|
|
6784
|
+
return true;
|
|
6785
|
+
} catch {
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
return false;
|
|
6789
|
+
}
|
|
6790
|
+
async function detectPublicIp() {
|
|
6791
|
+
for (const url of ["https://api.ipify.org", "https://ifconfig.me/ip"]) {
|
|
6792
|
+
try {
|
|
6793
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3e3) });
|
|
6794
|
+
if (!res.ok) continue;
|
|
6795
|
+
const ip = (await res.text()).trim();
|
|
6796
|
+
if (/^[0-9.]+$/.test(ip) || ip.includes(":")) return ip;
|
|
6797
|
+
} catch {
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
return null;
|
|
6801
|
+
}
|
|
6802
|
+
var b64url = (buf) => buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6803
|
+
async function connectOpenshipCloud(port) {
|
|
6804
|
+
const already = await internalGet(port, "/api/system/cloud-status");
|
|
6805
|
+
if (already?.connected) {
|
|
6806
|
+
log.success(`Already connected to Openship Cloud${already.user?.email ? ` as ${already.user.email}` : ""}.`);
|
|
6807
|
+
return true;
|
|
6808
|
+
}
|
|
6809
|
+
const capsEnv = await internalGet(port, "/api/health/env");
|
|
6810
|
+
const cloudApiUrl = capsEnv?.cloudApiUrl;
|
|
6811
|
+
if (!cloudApiUrl) {
|
|
6812
|
+
log.error("Couldn't discover the Openship Cloud URL \u2014 free domain unavailable. Use a custom domain instead.");
|
|
6813
|
+
return false;
|
|
6814
|
+
}
|
|
6815
|
+
const verifier = b64url(randomBytes2(32));
|
|
6816
|
+
const challenge = b64url(createHash2("sha256").update(verifier).digest());
|
|
6817
|
+
const state = b64url(randomBytes2(16));
|
|
6818
|
+
const codePromise = new Promise((resolve2) => {
|
|
6819
|
+
const server2 = createServer((req, res2) => {
|
|
6820
|
+
const u = new URL(req.url || "/", "http://127.0.0.1");
|
|
6821
|
+
if (!u.pathname.startsWith("/callback")) {
|
|
6822
|
+
res2.writeHead(404).end();
|
|
6823
|
+
return;
|
|
6824
|
+
}
|
|
6825
|
+
const code2 = u.searchParams.get("code");
|
|
6826
|
+
const gotState = u.searchParams.get("state");
|
|
6827
|
+
res2.writeHead(200, { "Content-Type": "text/html" }).end(
|
|
6828
|
+
"<html><body style='font:16px system-ui;padding:3rem;text-align:center'><h2>Openship Cloud connected</h2><p>You can close this window and return to your terminal.</p></body></html>"
|
|
6829
|
+
);
|
|
6830
|
+
server2.close();
|
|
6831
|
+
resolve2(code2 && gotState === state ? code2 : null);
|
|
6832
|
+
});
|
|
6833
|
+
server2.on("error", () => resolve2(null));
|
|
6834
|
+
server2.listen(0, "127.0.0.1", () => {
|
|
6835
|
+
const cbPort = server2.address().port;
|
|
6836
|
+
const redirect = `http://127.0.0.1:${cbPort}/callback`;
|
|
6837
|
+
const handoff = `${cloudApiUrl.replace(/\/$/, "")}/api/cloud/connect-handoff?redirect=${encodeURIComponent(redirect)}&state=${state}&code_challenge=${challenge}`;
|
|
6838
|
+
note(handoff, "Open this URL to authorize (opening your browser\u2026)");
|
|
6839
|
+
void open(handoff).catch(() => {
|
|
6840
|
+
});
|
|
6841
|
+
});
|
|
6842
|
+
setTimeout(() => {
|
|
6843
|
+
try {
|
|
6844
|
+
server2.close();
|
|
6845
|
+
} catch {
|
|
6846
|
+
}
|
|
6847
|
+
resolve2(null);
|
|
6848
|
+
}, 3e5);
|
|
6849
|
+
});
|
|
6850
|
+
const s = spinner2();
|
|
6851
|
+
s.start("Waiting for Openship Cloud authorization in your browser");
|
|
6852
|
+
const code = await codePromise;
|
|
6853
|
+
if (!code) {
|
|
6854
|
+
s.stop("Openship Cloud wasn't authorized.", 1);
|
|
6855
|
+
return false;
|
|
6856
|
+
}
|
|
6857
|
+
s.message("Linking this instance to Openship Cloud");
|
|
6858
|
+
const res = await internalPost(port, "/api/system/cloud-connect", { code, codeVerifier: verifier });
|
|
6859
|
+
if (!res.ok) {
|
|
6860
|
+
s.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
|
|
6861
|
+
return false;
|
|
6862
|
+
}
|
|
6863
|
+
s.stop("Connected to Openship Cloud.");
|
|
6864
|
+
return true;
|
|
6865
|
+
}
|
|
6866
|
+
async function promptLocalAdmin() {
|
|
6867
|
+
const name = ensure(await text({ message: "Your name", validate: (v) => v?.trim() ? void 0 : "Required" })).trim();
|
|
6868
|
+
const email = ensure(
|
|
6869
|
+
await text({
|
|
6870
|
+
message: "Email",
|
|
6871
|
+
placeholder: "you@example.com",
|
|
6872
|
+
validate: (v) => v?.includes("@") ? void 0 : "Enter a valid email"
|
|
6873
|
+
})
|
|
6874
|
+
).trim().toLowerCase();
|
|
6875
|
+
const pw = ensure(
|
|
6876
|
+
await password({ message: "Password", validate: (v) => v && v.length >= 8 ? void 0 : "At least 8 characters" })
|
|
6877
|
+
);
|
|
6878
|
+
ensure(await password({ message: "Confirm password", validate: (v) => v === pw ? void 0 : "Passwords don't match" }));
|
|
6879
|
+
return { name, email, password: pw };
|
|
6880
|
+
}
|
|
6881
|
+
async function streamProvision(port, sessionId, s) {
|
|
6882
|
+
let ok3 = false;
|
|
6883
|
+
try {
|
|
6884
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/system/self-register/stream?id=${sessionId}`, {
|
|
6885
|
+
headers: { "X-Internal-Token": ensureInternalToken() },
|
|
6886
|
+
signal: AbortSignal.timeout(3e5)
|
|
6887
|
+
});
|
|
6888
|
+
if (!res.ok || !res.body) return false;
|
|
6889
|
+
const reader = res.body.getReader();
|
|
6890
|
+
const decoder = new TextDecoder();
|
|
6891
|
+
let buffer = "";
|
|
6892
|
+
for (; ; ) {
|
|
6893
|
+
const { value, done } = await reader.read();
|
|
6894
|
+
if (done) break;
|
|
6895
|
+
buffer += decoder.decode(value, { stream: true });
|
|
6896
|
+
let sep;
|
|
6897
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
6898
|
+
const frame = buffer.slice(0, sep);
|
|
6899
|
+
buffer = buffer.slice(sep + 2);
|
|
6900
|
+
const event = /event:\s*(.*)/.exec(frame)?.[1]?.trim();
|
|
6901
|
+
const dataRaw = /data:\s*([\s\S]*)/.exec(frame)?.[1]?.trim();
|
|
6902
|
+
if (!event) continue;
|
|
6903
|
+
if (event === "log" && dataRaw) {
|
|
6904
|
+
try {
|
|
6905
|
+
const d = JSON.parse(dataRaw);
|
|
6906
|
+
if (d.message) s.message(String(d.message).replace(/\s+/g, " ").slice(0, 68));
|
|
6907
|
+
} catch {
|
|
6908
|
+
}
|
|
6909
|
+
} else if (event === "complete" && dataRaw) {
|
|
6910
|
+
try {
|
|
6911
|
+
ok3 = JSON.parse(dataRaw).status === "completed";
|
|
6912
|
+
} catch {
|
|
6913
|
+
}
|
|
6914
|
+
} else if (event === "end") {
|
|
6915
|
+
return ok3;
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
} catch {
|
|
6920
|
+
return ok3;
|
|
6921
|
+
}
|
|
6922
|
+
return ok3;
|
|
6923
|
+
}
|
|
6924
|
+
async function runWizard() {
|
|
6925
|
+
intro(`${chalk16.bgCyan(chalk16.black(" Openship "))}${chalk16.dim(" setup")}`);
|
|
6926
|
+
log.message(
|
|
6927
|
+
chalk16.dim(
|
|
6928
|
+
"Deploy Openship on this machine \u2014 a few questions, then it installs itself\nas a service, registers as an app, and prints the URL to log in."
|
|
6929
|
+
)
|
|
6930
|
+
);
|
|
6931
|
+
let publicUrl;
|
|
6932
|
+
let behindProxy = false;
|
|
6933
|
+
let managedEdge = false;
|
|
6934
|
+
let domainPlan = { type: "none" };
|
|
6935
|
+
const reach = ensure(
|
|
6936
|
+
await select({
|
|
6937
|
+
message: "How should this instance be reachable?",
|
|
6938
|
+
initialValue: "private",
|
|
6939
|
+
options: [
|
|
6940
|
+
{ value: "private", label: "Private", hint: "this machine only (localhost)" },
|
|
6941
|
+
{ value: "public", label: "Public", hint: "a server / VPS, reachable from other machines" }
|
|
6942
|
+
]
|
|
6943
|
+
})
|
|
6944
|
+
);
|
|
6945
|
+
if (reach === "public") {
|
|
6946
|
+
const canManage = process.platform === "linux";
|
|
6947
|
+
const domainType = ensure(
|
|
6948
|
+
await select({
|
|
6949
|
+
message: "How do you want a domain + HTTPS?",
|
|
6950
|
+
initialValue: "free",
|
|
6951
|
+
options: [
|
|
6952
|
+
{ value: "free", label: "Free domain", hint: "name.opsh.io via Openship Cloud \u2014 HTTPS handled for you" },
|
|
6953
|
+
...canManage ? [{ value: "custom", label: "Custom domain", hint: "your domain + free Let's Encrypt on this box" }] : [],
|
|
6954
|
+
{ value: "byo", label: "Bring your own", hint: "your domain, behind your own reverse proxy" }
|
|
6955
|
+
]
|
|
6956
|
+
})
|
|
6957
|
+
);
|
|
6958
|
+
if (domainType === "free") {
|
|
6959
|
+
const slug = ensure(
|
|
6960
|
+
await text({
|
|
6961
|
+
message: "Choose your subdomain",
|
|
6962
|
+
placeholder: "my-openship",
|
|
6963
|
+
validate: (v) => v && SLUG_RE.test(v.trim().toLowerCase()) ? void 0 : "Lowercase letters, digits, hyphens"
|
|
6964
|
+
})
|
|
6965
|
+
).trim().toLowerCase();
|
|
6966
|
+
const s2 = spinner2();
|
|
6967
|
+
s2.start("Detecting this server's public IP");
|
|
6968
|
+
const publicHost = await detectPublicIp();
|
|
6969
|
+
s2.stop(publicHost ? `Public IP: ${chalk16.bold(publicHost)}` : "Couldn't detect the public IP automatically.");
|
|
6970
|
+
publicUrl = `https://${slug}.opsh.io`;
|
|
6971
|
+
behindProxy = true;
|
|
6972
|
+
domainPlan = { type: "free", slug, publicHost };
|
|
6973
|
+
} else if (domainType === "custom") {
|
|
6974
|
+
const raw = ensure(
|
|
6975
|
+
await text({
|
|
6976
|
+
message: "Your domain",
|
|
6977
|
+
placeholder: "ops.example.com",
|
|
6978
|
+
validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
|
|
6979
|
+
})
|
|
6980
|
+
);
|
|
6981
|
+
publicUrl = normalizeUrl(raw).replace(/^http:/i, "https:");
|
|
6982
|
+
const hostname = new URL(publicUrl).hostname;
|
|
6983
|
+
managedEdge = true;
|
|
6984
|
+
behindProxy = true;
|
|
6985
|
+
if (typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
6986
|
+
log.warn("Managed HTTPS installs OpenResty + certbot \u2014 that needs root. Re-run with sudo if it can't install.");
|
|
6987
|
+
}
|
|
6988
|
+
const s2 = spinner2();
|
|
6989
|
+
s2.start("Detecting this server's public IP");
|
|
6990
|
+
const ip = await detectPublicIp();
|
|
6991
|
+
s2.stop(ip ? `Public IP: ${chalk16.bold(ip)}` : "Couldn't detect the public IP automatically.");
|
|
6992
|
+
note(
|
|
6993
|
+
`Add a DNS ${chalk16.bold("A record")}:
|
|
6994
|
+
|
|
6995
|
+
${chalk16.cyan(hostname)} \u2192 ${chalk16.cyan(ip ?? "<this server's public IP>")}
|
|
6996
|
+
|
|
6997
|
+
` + chalk16.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
|
|
6998
|
+
"DNS"
|
|
6999
|
+
);
|
|
7000
|
+
ensure(await confirm3({ message: "A record set? (continue either way \u2014 it retries)", initialValue: true }));
|
|
7001
|
+
domainPlan = { type: "custom", hostname };
|
|
7002
|
+
} else {
|
|
7003
|
+
const raw = ensure(
|
|
7004
|
+
await text({
|
|
7005
|
+
message: "Your domain (served behind your proxy)",
|
|
7006
|
+
placeholder: "ops.example.com",
|
|
7007
|
+
validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
|
|
7008
|
+
})
|
|
7009
|
+
);
|
|
7010
|
+
publicUrl = normalizeUrl(raw);
|
|
7011
|
+
behindProxy = true;
|
|
7012
|
+
if (publicUrl.startsWith("http://")) {
|
|
7013
|
+
log.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
|
|
7014
|
+
}
|
|
7015
|
+
domainPlan = { type: "byo", hostname: new URL(publicUrl).hostname };
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
const isCloudDomain = domainPlan.type === "free";
|
|
7019
|
+
const admin = isCloudDomain ? null : await promptLocalAdmin();
|
|
7020
|
+
const s = spinner2();
|
|
7021
|
+
s.start("Installing Openship as a service");
|
|
7022
|
+
let started;
|
|
7023
|
+
try {
|
|
7024
|
+
started = startService(
|
|
7025
|
+
{ publicUrl, trustProxy: behindProxy, managedEdge, acmeEmail: managedEdge ? admin?.email : void 0 },
|
|
7026
|
+
{ quiet: true }
|
|
7027
|
+
);
|
|
7028
|
+
} catch (e) {
|
|
7029
|
+
s.stop("Couldn't install the service.", 1);
|
|
7030
|
+
log.error(e.message);
|
|
7031
|
+
log.info("Run `openship up --foreground` to run it attached and see the error.");
|
|
7032
|
+
process.exit(1);
|
|
7033
|
+
}
|
|
7034
|
+
s.message("Waiting for Openship to come up");
|
|
7035
|
+
if (!await waitHealthy(started.port)) {
|
|
7036
|
+
s.stop("Openship didn't become healthy in time.", 1);
|
|
7037
|
+
log.info("Check logs: `openship logs` (or `openship up --foreground`).");
|
|
7038
|
+
process.exit(1);
|
|
7039
|
+
}
|
|
7040
|
+
if (admin) {
|
|
7041
|
+
s.message("Creating your admin account");
|
|
7042
|
+
const adminRes = await bootstrapAdmin(started.port, admin);
|
|
7043
|
+
if (!adminRes.ok) {
|
|
7044
|
+
s.stop(`Couldn't create the admin account: ${adminRes.message}`, 1);
|
|
7045
|
+
process.exit(1);
|
|
7046
|
+
}
|
|
7047
|
+
s.stop(
|
|
7048
|
+
adminRes.message === "already-exists" ? "An admin already exists \u2014 use your existing login." : `Admin account created for ${admin.email}.`
|
|
7049
|
+
);
|
|
7050
|
+
} else {
|
|
7051
|
+
s.stop("Openship is up.");
|
|
7052
|
+
}
|
|
7053
|
+
let liveUrl = publicUrl ?? `http://localhost:${started.dashPort}`;
|
|
7054
|
+
const port = started.port;
|
|
7055
|
+
if (domainPlan.type === "free") {
|
|
7056
|
+
const linked = await connectOpenshipCloud(port);
|
|
7057
|
+
if (!linked) {
|
|
7058
|
+
log.warn("Openship Cloud wasn't connected \u2014 set up a local admin instead. You can add the free domain later in Settings \u2192 Cloud.");
|
|
7059
|
+
const fb = await promptLocalAdmin();
|
|
7060
|
+
const abr = await bootstrapAdmin(port, fb);
|
|
7061
|
+
if (!abr.ok) {
|
|
7062
|
+
log.error(`Couldn't create the admin account: ${abr.message}`);
|
|
7063
|
+
process.exit(1);
|
|
7064
|
+
}
|
|
7065
|
+
await internalPost(port, "/api/system/self-register", { domainType: "byo" });
|
|
7066
|
+
} else {
|
|
7067
|
+
const s2 = spinner2();
|
|
7068
|
+
s2.start("Registering your free domain with Openship Cloud");
|
|
7069
|
+
const res = await internalPost(port, "/api/system/self-register", {
|
|
7070
|
+
domainType: "free",
|
|
7071
|
+
slug: domainPlan.slug,
|
|
7072
|
+
publicHost: domainPlan.publicHost,
|
|
7073
|
+
dashPort: Number(started.dashPort)
|
|
7074
|
+
});
|
|
7075
|
+
if (res.ok && res.data?.url) {
|
|
7076
|
+
liveUrl = res.data.url;
|
|
7077
|
+
s2.stop(`Free domain live: ${res.data.url}`);
|
|
7078
|
+
} else {
|
|
7079
|
+
s2.stop(`Couldn't register the free domain: ${res.data?.error || "failed"}`, 1);
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
} else if (domainPlan.type === "custom") {
|
|
7083
|
+
let edgeTakeover = false;
|
|
7084
|
+
let edgeMigrate = false;
|
|
7085
|
+
let proceedCustom = true;
|
|
7086
|
+
const pf = await internalPost(port, "/api/system/self-edge/preflight", {});
|
|
7087
|
+
const status = pf.ok ? pf.data?.status : void 0;
|
|
7088
|
+
const importable = pf.ok && Array.isArray(pf.data?.sites) ? pf.data.sites.length : 0;
|
|
7089
|
+
if (status && !status.canProceedClean && status.occupants?.length) {
|
|
7090
|
+
const owner = status.occupants.map((o) => o.command ?? `port ${o.port}`).join(", ");
|
|
7091
|
+
const known = status.classification === "known";
|
|
7092
|
+
const choice = ensure(
|
|
7093
|
+
await select({
|
|
7094
|
+
message: known ? `An existing reverse proxy (${owner}) is serving ports 80/443.` : `Ports 80/443 are in use by ${owner}, which we couldn't identify.`,
|
|
7095
|
+
options: [
|
|
7096
|
+
...importable > 0 ? [{
|
|
7097
|
+
value: "migrate",
|
|
7098
|
+
label: `Migrate ${importable} site${importable === 1 ? "" : "s"} & take over`,
|
|
7099
|
+
hint: "import the existing sites into Openship, then take 80/443"
|
|
7100
|
+
}] : [],
|
|
7101
|
+
{
|
|
7102
|
+
value: "override",
|
|
7103
|
+
label: "Stop it & take over 80/443",
|
|
7104
|
+
hint: known ? "the existing sites stop being served" : "may interrupt a running service"
|
|
7105
|
+
},
|
|
7106
|
+
{ value: "cancel", label: "Cancel \u2014 leave it running" }
|
|
7107
|
+
],
|
|
7108
|
+
// Per product decision: unknown owner pre-selects takeover; a known
|
|
7109
|
+
// proxy defaults to cancel so the user chooses deliberately.
|
|
7110
|
+
initialValue: known ? "cancel" : "override"
|
|
7111
|
+
})
|
|
7112
|
+
);
|
|
7113
|
+
if (choice === "cancel") proceedCustom = false;
|
|
7114
|
+
else if (choice === "migrate") edgeMigrate = true;
|
|
7115
|
+
else edgeTakeover = true;
|
|
7116
|
+
}
|
|
7117
|
+
if (!proceedCustom) {
|
|
7118
|
+
log.warn(
|
|
7119
|
+
"Left the existing proxy on 80/443 running. Registering Openship without managed HTTPS \u2014 front it with your proxy, or re-run setup to take over."
|
|
7120
|
+
);
|
|
7121
|
+
await internalPost(port, "/api/system/self-register", {
|
|
7122
|
+
domainType: "byo",
|
|
7123
|
+
hostname: domainPlan.hostname
|
|
7124
|
+
});
|
|
7125
|
+
liveUrl = `https://${domainPlan.hostname}`;
|
|
7126
|
+
} else {
|
|
7127
|
+
const res = await internalPost(port, "/api/system/self-register", {
|
|
7128
|
+
domainType: "custom",
|
|
7129
|
+
hostname: domainPlan.hostname,
|
|
7130
|
+
dashPort: Number(started.dashPort),
|
|
7131
|
+
acmeEmail: admin?.email,
|
|
7132
|
+
edgeTakeover,
|
|
7133
|
+
edgeMigrate
|
|
7134
|
+
});
|
|
7135
|
+
if (res.ok && res.data?.sessionId) {
|
|
7136
|
+
const s2 = spinner2();
|
|
7137
|
+
s2.start("Issuing HTTPS certificate (OpenResty + Let's Encrypt)");
|
|
7138
|
+
const done = await streamProvision(port, res.data.sessionId, s2);
|
|
7139
|
+
liveUrl = res.data.url ?? liveUrl;
|
|
7140
|
+
if (done) s2.stop(`HTTPS ready: ${liveUrl}`);
|
|
7141
|
+
else s2.stop("HTTPS isn't ready yet \u2014 it retries on reboot; the site serves over HTTP meanwhile.", 1);
|
|
7142
|
+
} else {
|
|
7143
|
+
log.warn(`Couldn't start domain provisioning: ${res.data?.error || "failed"}`);
|
|
7144
|
+
}
|
|
7145
|
+
}
|
|
7146
|
+
} else if (domainPlan.type === "byo") {
|
|
7147
|
+
const res = await internalPost(port, "/api/system/self-register", {
|
|
7148
|
+
domainType: "byo",
|
|
7149
|
+
hostname: domainPlan.hostname
|
|
7150
|
+
});
|
|
7151
|
+
if (res.ok && res.data?.url) liveUrl = res.data.url;
|
|
7152
|
+
} else {
|
|
7153
|
+
await internalPost(port, "/api/system/self-register", { domainType: "byo" });
|
|
7154
|
+
}
|
|
7155
|
+
note(
|
|
7156
|
+
`${chalk16.bold(liveUrl)}
|
|
7157
|
+
|
|
7158
|
+
` + chalk16.dim(`${admin ? `Log in as ${admin.email}` : "Log in with Openship Cloud"}. Openship now appears under your Apps, and runs as a service (restarts on boot).`),
|
|
7159
|
+
"Openship is live"
|
|
7160
|
+
);
|
|
7161
|
+
outro(
|
|
7162
|
+
domainPlan.type === "byo" ? chalk16.dim("Point your reverse proxy at the dashboard port above.") : chalk16.green("Happy shipping.")
|
|
7163
|
+
);
|
|
7164
|
+
}
|
|
7165
|
+
|
|
6372
7166
|
// src/index.ts
|
|
6373
|
-
var program = new
|
|
6374
|
-
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.
|
|
7167
|
+
var program = new Command25();
|
|
7168
|
+
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.0").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
|
|
6375
7169
|
if (thisCommand.opts().json) setJsonMode(true);
|
|
7170
|
+
}).action(async () => {
|
|
7171
|
+
await runWizard();
|
|
6376
7172
|
});
|
|
6377
7173
|
program.addCommand(upCommand);
|
|
7174
|
+
program.addCommand(stopCommand);
|
|
6378
7175
|
program.addCommand(installCommand);
|
|
6379
7176
|
program.addCommand(updateCommand);
|
|
6380
7177
|
program.addCommand(openCommand);
|