openship 0.1.10 → 0.1.11
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 +470 -249
- package/dist/server/index.js +27 -160
- package/package.json +1 -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";
|
|
@@ -2367,9 +2367,9 @@ import chalk5 from "chalk";
|
|
|
2367
2367
|
import ora from "ora";
|
|
2368
2368
|
import { spawn } from "child_process";
|
|
2369
2369
|
import { randomBytes } from "crypto";
|
|
2370
|
-
import { existsSync as
|
|
2371
|
-
import { homedir as
|
|
2372
|
-
import { dirname as dirname2, join as
|
|
2370
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2371
|
+
import { homedir as homedir4 } from "os";
|
|
2372
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
2373
2373
|
import { fileURLToPath } from "url";
|
|
2374
2374
|
|
|
2375
2375
|
// src/lib/dashboard.ts
|
|
@@ -2419,8 +2419,8 @@ async function downloadToFile(url, dest, onProgress) {
|
|
|
2419
2419
|
} finally {
|
|
2420
2420
|
file.end();
|
|
2421
2421
|
}
|
|
2422
|
-
await new Promise((
|
|
2423
|
-
file.on("finish", () =>
|
|
2422
|
+
await new Promise((resolve2, reject2) => {
|
|
2423
|
+
file.on("finish", () => resolve2());
|
|
2424
2424
|
file.on("error", reject2);
|
|
2425
2425
|
});
|
|
2426
2426
|
return { sha256: hash.digest("hex"), size: received };
|
|
@@ -2510,29 +2510,233 @@ async function ensureDashboard(opts = {}) {
|
|
|
2510
2510
|
return { tag, entry, cwd };
|
|
2511
2511
|
}
|
|
2512
2512
|
|
|
2513
|
+
// src/lib/service.ts
|
|
2514
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2515
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
|
|
2516
|
+
import { homedir as homedir3 } from "os";
|
|
2517
|
+
import { join as join4, resolve } from "path";
|
|
2518
|
+
var HOME = homedir3();
|
|
2519
|
+
var OS_DIR = join4(HOME, ".openship");
|
|
2520
|
+
var LOG_DIR = join4(OS_DIR, "logs");
|
|
2521
|
+
var MAC_LABEL = "io.openship.up";
|
|
2522
|
+
var MAC_PLIST = join4(HOME, "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
|
|
2523
|
+
var SYSTEMD_NAME = "openship";
|
|
2524
|
+
var WIN_TASK = "Openship";
|
|
2525
|
+
function selfInvocation() {
|
|
2526
|
+
const runtime = process.execPath;
|
|
2527
|
+
const entry = resolve(process.argv[1] ?? "");
|
|
2528
|
+
return { runtime, args: [entry] };
|
|
2529
|
+
}
|
|
2530
|
+
function upArgs(flags) {
|
|
2531
|
+
const a = ["up", "--foreground"];
|
|
2532
|
+
if (flags.port) a.push("--port", flags.port);
|
|
2533
|
+
if (flags.dataDir) a.push("--data-dir", flags.dataDir);
|
|
2534
|
+
if (flags.dashboardPort) a.push("--dashboard-port", flags.dashboardPort);
|
|
2535
|
+
if (flags.ui === false) a.push("--no-ui");
|
|
2536
|
+
if (flags.uiVersion) a.push("--ui-version", flags.uiVersion);
|
|
2537
|
+
return a;
|
|
2538
|
+
}
|
|
2539
|
+
function runArgv(flags) {
|
|
2540
|
+
const { runtime, args } = selfInvocation();
|
|
2541
|
+
return [runtime, ...args, ...upArgs(flags)];
|
|
2542
|
+
}
|
|
2543
|
+
function run(cmd, args) {
|
|
2544
|
+
const r = spawnSync2(cmd, args, { encoding: "utf8" });
|
|
2545
|
+
return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
2546
|
+
}
|
|
2547
|
+
function isRoot() {
|
|
2548
|
+
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
2549
|
+
}
|
|
2550
|
+
function detectKind() {
|
|
2551
|
+
if (process.platform === "darwin") return "launchd";
|
|
2552
|
+
if (process.platform === "linux") {
|
|
2553
|
+
if (!hasSystemd()) return "unsupported";
|
|
2554
|
+
return isRoot() ? "systemd-system" : "systemd-user";
|
|
2555
|
+
}
|
|
2556
|
+
if (process.platform === "win32") return "schtasks";
|
|
2557
|
+
return "unsupported";
|
|
2558
|
+
}
|
|
2559
|
+
function hasSystemd() {
|
|
2560
|
+
return spawnSync2("sh", ["-c", "command -v systemctl"]).status === 0;
|
|
2561
|
+
}
|
|
2562
|
+
function xmlEscape(s) {
|
|
2563
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2564
|
+
}
|
|
2565
|
+
function plist(flags) {
|
|
2566
|
+
const argv = runArgv(flags);
|
|
2567
|
+
const items = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
2568
|
+
const path2 = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", join4(HOME, ".bun/bin")].join(":");
|
|
2569
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2570
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2571
|
+
<plist version="1.0">
|
|
2572
|
+
<dict>
|
|
2573
|
+
<key>Label</key><string>${MAC_LABEL}</string>
|
|
2574
|
+
<key>ProgramArguments</key>
|
|
2575
|
+
<array>
|
|
2576
|
+
${items}
|
|
2577
|
+
</array>
|
|
2578
|
+
<key>RunAtLoad</key><true/>
|
|
2579
|
+
<key>KeepAlive</key><true/>
|
|
2580
|
+
<key>StandardOutPath</key><string>${join4(LOG_DIR, "up.log")}</string>
|
|
2581
|
+
<key>StandardErrorPath</key><string>${join4(LOG_DIR, "up.err.log")}</string>
|
|
2582
|
+
<key>EnvironmentVariables</key>
|
|
2583
|
+
<dict><key>PATH</key><string>${path2}</string></dict>
|
|
2584
|
+
</dict>
|
|
2585
|
+
</plist>
|
|
2586
|
+
`;
|
|
2587
|
+
}
|
|
2588
|
+
function systemdUnit(flags) {
|
|
2589
|
+
const argv = runArgv(flags);
|
|
2590
|
+
const execStart = argv.map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
|
|
2591
|
+
return `[Unit]
|
|
2592
|
+
Description=Openship control plane
|
|
2593
|
+
After=network-online.target
|
|
2594
|
+
Wants=network-online.target
|
|
2595
|
+
|
|
2596
|
+
[Service]
|
|
2597
|
+
Type=simple
|
|
2598
|
+
ExecStart=${execStart}
|
|
2599
|
+
Restart=always
|
|
2600
|
+
RestartSec=2
|
|
2601
|
+
Environment=NODE_ENV=production
|
|
2602
|
+
|
|
2603
|
+
[Install]
|
|
2604
|
+
WantedBy=default.target
|
|
2605
|
+
`;
|
|
2606
|
+
}
|
|
2607
|
+
function preview(flags) {
|
|
2608
|
+
const kind = detectKind();
|
|
2609
|
+
if (kind === "launchd") return { kind, path: MAC_PLIST, content: plist(flags) };
|
|
2610
|
+
if (kind === "systemd-user") return { kind, path: join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`), content: systemdUnit(flags) };
|
|
2611
|
+
if (kind === "systemd-system") return { kind, path: `/etc/systemd/system/${SYSTEMD_NAME}.service`, content: systemdUnit(flags) };
|
|
2612
|
+
if (kind === "schtasks") return { kind, path: WIN_TASK, content: runArgv(flags).join(" ") };
|
|
2613
|
+
return { kind, path: "", content: "" };
|
|
2614
|
+
}
|
|
2615
|
+
function installAndStart(flags) {
|
|
2616
|
+
mkdirSync4(LOG_DIR, { recursive: true });
|
|
2617
|
+
const kind = detectKind();
|
|
2618
|
+
if (kind === "launchd") {
|
|
2619
|
+
mkdirSync4(join4(HOME, "Library", "LaunchAgents"), { recursive: true });
|
|
2620
|
+
run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
|
|
2621
|
+
writeFileSync3(MAC_PLIST, plist(flags));
|
|
2622
|
+
const uid = String(process.getuid?.() ?? "");
|
|
2623
|
+
let r = run("launchctl", ["bootstrap", `gui/${uid}`, MAC_PLIST]);
|
|
2624
|
+
if (!r.ok) r = run("launchctl", ["load", "-w", MAC_PLIST]);
|
|
2625
|
+
if (!r.ok) throw new Error(`launchctl failed to load the agent: ${r.out}`);
|
|
2626
|
+
return { kind, detail: `launchd agent ${MAC_LABEL} (logs: ${LOG_DIR})` };
|
|
2627
|
+
}
|
|
2628
|
+
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2629
|
+
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
2630
|
+
const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
|
|
2631
|
+
mkdirSync4(unitPath.slice(0, unitPath.lastIndexOf("/")), { recursive: true });
|
|
2632
|
+
writeFileSync3(unitPath, systemdUnit(flags));
|
|
2633
|
+
run("systemctl", [...sysArgs, "daemon-reload"]);
|
|
2634
|
+
const r = run("systemctl", [...sysArgs, "enable", "--now", SYSTEMD_NAME]);
|
|
2635
|
+
if (!r.ok) throw new Error(`systemctl enable failed: ${r.out}`);
|
|
2636
|
+
if (kind === "systemd-user") {
|
|
2637
|
+
run("loginctl", ["enable-linger", process.env.USER ?? ""]);
|
|
2638
|
+
}
|
|
2639
|
+
return { kind, detail: `systemd unit ${SYSTEMD_NAME} (${kind === "systemd-user" ? "--user" : "system"})` };
|
|
2640
|
+
}
|
|
2641
|
+
if (kind === "schtasks") {
|
|
2642
|
+
const tr = runArgv(flags).map((a) => `\\"${a}\\"`).join(" ");
|
|
2643
|
+
const r = run("schtasks", ["/Create", "/TN", WIN_TASK, "/SC", "ONLOGON", "/RL", "HIGHEST", "/TR", tr, "/F"]);
|
|
2644
|
+
if (!r.ok) throw new Error(`schtasks create failed: ${r.out}`);
|
|
2645
|
+
run("schtasks", ["/Run", "/TN", WIN_TASK]);
|
|
2646
|
+
return { kind, detail: `Scheduled Task ${WIN_TASK} (best-effort; runs at logon)` };
|
|
2647
|
+
}
|
|
2648
|
+
throw new Error(
|
|
2649
|
+
"No supported service manager found (need systemd on Linux). Run `openship up --foreground` instead, or use docker compose for always-on."
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
function stop() {
|
|
2653
|
+
const kind = detectKind();
|
|
2654
|
+
if (kind === "launchd") {
|
|
2655
|
+
run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
|
|
2656
|
+
if (existsSync3(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
|
|
2657
|
+
return { kind, detail: `launchd agent ${MAC_LABEL} stopped + removed` };
|
|
2658
|
+
}
|
|
2659
|
+
if (kind === "systemd-user" || kind === "systemd-system") {
|
|
2660
|
+
const sysArgs = kind === "systemd-user" ? ["--user"] : [];
|
|
2661
|
+
run("systemctl", [...sysArgs, "disable", "--now", SYSTEMD_NAME]);
|
|
2662
|
+
const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
|
|
2663
|
+
if (existsSync3(unitPath)) rmSync2(unitPath, { force: true });
|
|
2664
|
+
run("systemctl", [...sysArgs, "daemon-reload"]);
|
|
2665
|
+
return { kind, detail: `systemd unit ${SYSTEMD_NAME} stopped + disabled` };
|
|
2666
|
+
}
|
|
2667
|
+
if (kind === "schtasks") {
|
|
2668
|
+
run("schtasks", ["/End", "/TN", WIN_TASK]);
|
|
2669
|
+
run("schtasks", ["/Delete", "/TN", WIN_TASK, "/F"]);
|
|
2670
|
+
return { kind, detail: `Scheduled Task ${WIN_TASK} stopped + removed` };
|
|
2671
|
+
}
|
|
2672
|
+
return { kind, detail: "no supported service manager \u2014 nothing to stop" };
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2513
2675
|
// src/commands/up.ts
|
|
2514
2676
|
var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
2515
|
-
var SERVER_DIR =
|
|
2516
|
-
var
|
|
2677
|
+
var SERVER_DIR = join5(DIST_DIR, "server");
|
|
2678
|
+
var OS_DIR2 = join5(homedir4(), ".openship");
|
|
2517
2679
|
function ensureAuthSecret() {
|
|
2518
|
-
const path2 =
|
|
2519
|
-
if (
|
|
2520
|
-
|
|
2680
|
+
const path2 = join5(OS_DIR2, "auth-secret");
|
|
2681
|
+
if (existsSync4(path2)) return readFileSync2(path2, "utf8").trim();
|
|
2682
|
+
mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
|
|
2521
2683
|
const secret = randomBytes(32).toString("hex");
|
|
2522
|
-
|
|
2684
|
+
writeFileSync4(path2, secret, { mode: 384 });
|
|
2523
2685
|
return secret;
|
|
2524
2686
|
}
|
|
2525
|
-
var upCommand = new Command4("up").description("
|
|
2526
|
-
|
|
2527
|
-
|
|
2687
|
+
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").action(async (opts) => {
|
|
2688
|
+
if (opts.foreground) return runForeground(opts);
|
|
2689
|
+
startService(opts);
|
|
2690
|
+
});
|
|
2691
|
+
function startService(opts) {
|
|
2692
|
+
const flags = {
|
|
2693
|
+
port: opts.port,
|
|
2694
|
+
dataDir: opts.dataDir,
|
|
2695
|
+
dashboardPort: opts.dashboardPort,
|
|
2696
|
+
ui: opts.ui,
|
|
2697
|
+
uiVersion: opts.uiVersion
|
|
2698
|
+
};
|
|
2699
|
+
if (opts.dryRun) {
|
|
2700
|
+
const p = preview(flags);
|
|
2701
|
+
console.log(
|
|
2702
|
+
chalk5.dim(`
|
|
2703
|
+
service manager: ${p.kind}
|
|
2704
|
+
path: ${p.path}
|
|
2705
|
+
|
|
2706
|
+
`) + p.content + "\n"
|
|
2707
|
+
);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
try {
|
|
2711
|
+
const res = installAndStart(flags);
|
|
2712
|
+
const port = String(opts.port || "4000");
|
|
2713
|
+
const dashPort = String(opts.dashboardPort || "3001");
|
|
2714
|
+
console.log(
|
|
2715
|
+
chalk5.green("\n \u2714 Openship is running as a service.\n") + chalk5.dim(` API: http://localhost:${port}/api
|
|
2716
|
+
`) + (opts.ui !== false ? chalk5.dim(` Dashboard: http://localhost:${dashPort}
|
|
2717
|
+
`) : "") + chalk5.dim(` ${res.detail}
|
|
2718
|
+
`) + chalk5.dim(" Starts on boot and auto-restarts. Stop with `openship stop`.\n")
|
|
2719
|
+
);
|
|
2720
|
+
} catch (e) {
|
|
2721
|
+
console.error(
|
|
2722
|
+
chalk5.red(`
|
|
2723
|
+
Couldn't install the service: ${e.message}
|
|
2724
|
+
`) + chalk5.dim(" Run `openship up --foreground` to run it attached instead.\n")
|
|
2725
|
+
);
|
|
2726
|
+
process.exit(1);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
async function runForeground(opts) {
|
|
2730
|
+
const serverEntry = join5(SERVER_DIR, "index.js");
|
|
2731
|
+
if (!existsSync4(serverEntry)) {
|
|
2528
2732
|
console.error(
|
|
2529
2733
|
chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
|
|
2530
2734
|
);
|
|
2531
2735
|
process.exit(1);
|
|
2532
2736
|
}
|
|
2533
2737
|
const port = String(opts.port || "4000");
|
|
2534
|
-
const dataDir = opts.dataDir ||
|
|
2535
|
-
|
|
2738
|
+
const dataDir = opts.dataDir || join5(OS_DIR2, "data");
|
|
2739
|
+
mkdirSync5(dataDir, { recursive: true });
|
|
2536
2740
|
const env = {
|
|
2537
2741
|
...process.env,
|
|
2538
2742
|
PORT: port,
|
|
@@ -2544,8 +2748,8 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2544
2748
|
OPENSHIP_JOB_RUNNER: "in-process",
|
|
2545
2749
|
OPENSHIP_ALLOW_ZERO_AUTH: "true",
|
|
2546
2750
|
PGLITE_DATA_DIR: dataDir,
|
|
2547
|
-
OPENSHIP_MIGRATIONS_DIR:
|
|
2548
|
-
OPENSHIP_PGLITE_ASSETS_DIR:
|
|
2751
|
+
OPENSHIP_MIGRATIONS_DIR: join5(SERVER_DIR, "migrations"),
|
|
2752
|
+
OPENSHIP_PGLITE_ASSETS_DIR: join5(SERVER_DIR, "pglite"),
|
|
2549
2753
|
BETTER_AUTH_SECRET: ensureAuthSecret()
|
|
2550
2754
|
};
|
|
2551
2755
|
delete env.DATABASE_URL;
|
|
@@ -2606,7 +2810,7 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2606
2810
|
const uiSpinner = ora("Preparing the dashboard\u2026").start();
|
|
2607
2811
|
try {
|
|
2608
2812
|
const bundle = await ensureDashboard({
|
|
2609
|
-
tag: opts.uiVersion || `v${"0.1.
|
|
2813
|
+
tag: opts.uiVersion || `v${"0.1.11"}`,
|
|
2610
2814
|
onProgress: (received, total) => {
|
|
2611
2815
|
if (total) {
|
|
2612
2816
|
uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
|
|
@@ -2685,19 +2889,35 @@ var upCommand = new Command4("up").description("Run the Openship control plane l
|
|
|
2685
2889
|
stopAll();
|
|
2686
2890
|
process.exit(code ?? 0);
|
|
2687
2891
|
});
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// src/commands/stop.ts
|
|
2895
|
+
import { Command as Command5 } from "commander";
|
|
2896
|
+
import chalk6 from "chalk";
|
|
2897
|
+
var stopCommand = new Command5("stop").description("Stop the Openship service (started by `openship up`) \u2014 it won't restart or return on reboot").action(() => {
|
|
2898
|
+
try {
|
|
2899
|
+
const res = stop();
|
|
2900
|
+
console.log(chalk6.green("\n \u2714 Openship stopped.\n") + chalk6.dim(` ${res.detail}
|
|
2901
|
+
`));
|
|
2902
|
+
} catch (e) {
|
|
2903
|
+
console.error(chalk6.red(`
|
|
2904
|
+
Couldn't stop the service: ${e.message}
|
|
2905
|
+
`));
|
|
2906
|
+
process.exit(1);
|
|
2907
|
+
}
|
|
2688
2908
|
});
|
|
2689
2909
|
|
|
2690
2910
|
// src/commands/init.ts
|
|
2691
|
-
import { Command as
|
|
2692
|
-
import { existsSync as
|
|
2911
|
+
import { Command as Command6 } from "commander";
|
|
2912
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2693
2913
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2694
2914
|
import { stdin as input2, stdout as output2 } from "process";
|
|
2695
|
-
import { join as
|
|
2696
|
-
var initCommand = new
|
|
2915
|
+
import { join as join6 } from "path";
|
|
2916
|
+
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
2917
|
const root = opts.dir || process.cwd();
|
|
2698
|
-
const linkDir =
|
|
2699
|
-
const linkPath =
|
|
2700
|
-
if (
|
|
2918
|
+
const linkDir = join6(root, ".openship");
|
|
2919
|
+
const linkPath = join6(linkDir, "project.json");
|
|
2920
|
+
if (existsSync5(linkPath) && !opts.force) {
|
|
2701
2921
|
err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
|
|
2702
2922
|
process.exit(1);
|
|
2703
2923
|
}
|
|
@@ -2750,8 +2970,8 @@ var initCommand = new Command5("init").description("Link the current directory t
|
|
|
2750
2970
|
context: getActiveContext(),
|
|
2751
2971
|
defaults: { environment: opts.environment || "production" }
|
|
2752
2972
|
};
|
|
2753
|
-
|
|
2754
|
-
|
|
2973
|
+
mkdirSync6(linkDir, { recursive: true });
|
|
2974
|
+
writeFileSync5(linkPath, JSON.stringify(link, null, 2) + "\n");
|
|
2755
2975
|
if (isJsonMode()) {
|
|
2756
2976
|
printJson({ path: linkPath, link });
|
|
2757
2977
|
return;
|
|
@@ -2762,7 +2982,7 @@ var initCommand = new Command5("init").description("Link the current directory t
|
|
|
2762
2982
|
});
|
|
2763
2983
|
|
|
2764
2984
|
// src/commands/context.ts
|
|
2765
|
-
import { Command as
|
|
2985
|
+
import { Command as Command7 } from "commander";
|
|
2766
2986
|
function renderContexts() {
|
|
2767
2987
|
const rows = listContexts().map((c) => ({
|
|
2768
2988
|
current: c.current ? "*" : "",
|
|
@@ -2773,8 +2993,8 @@ function renderContexts() {
|
|
|
2773
2993
|
}));
|
|
2774
2994
|
printTable(rows, ["current", "name", "apiUrl", "dashboardUrl", "auth"]);
|
|
2775
2995
|
}
|
|
2776
|
-
var listCmd = new
|
|
2777
|
-
var useCmd = new
|
|
2996
|
+
var listCmd = new Command7("list").alias("ls").description("List configured contexts").action(renderContexts);
|
|
2997
|
+
var useCmd = new Command7("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
|
|
2778
2998
|
try {
|
|
2779
2999
|
setActiveContext(name);
|
|
2780
3000
|
ok(`
|
|
@@ -2785,7 +3005,7 @@ var useCmd = new Command6("use").description("Switch the active context").argume
|
|
|
2785
3005
|
process.exit(1);
|
|
2786
3006
|
}
|
|
2787
3007
|
});
|
|
2788
|
-
var addCmd = new
|
|
3008
|
+
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
3009
|
addContext(name, {
|
|
2790
3010
|
apiUrl: opts.apiUrl,
|
|
2791
3011
|
dashboardUrl: opts.dashboardUrl,
|
|
@@ -2796,7 +3016,7 @@ var addCmd = new Command6("add").description("Create or update a context's endpo
|
|
|
2796
3016
|
Saved context "${name}"${opts.use ? " (now active)" : ""}.
|
|
2797
3017
|
`);
|
|
2798
3018
|
});
|
|
2799
|
-
var rmCmd = new
|
|
3019
|
+
var rmCmd = new Command7("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
|
|
2800
3020
|
try {
|
|
2801
3021
|
removeContext(name);
|
|
2802
3022
|
ok(`
|
|
@@ -2807,15 +3027,15 @@ var rmCmd = new Command6("rm").alias("remove").description("Remove a context (ca
|
|
|
2807
3027
|
process.exit(1);
|
|
2808
3028
|
}
|
|
2809
3029
|
});
|
|
2810
|
-
var contextCommand = new
|
|
3030
|
+
var contextCommand = new Command7("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
|
|
2811
3031
|
ok(` Active context: ${getActiveContext()}`);
|
|
2812
3032
|
renderContexts();
|
|
2813
3033
|
}).addCommand(listCmd).addCommand(useCmd).addCommand(addCmd).addCommand(rmCmd);
|
|
2814
3034
|
|
|
2815
3035
|
// src/commands/status.ts
|
|
2816
|
-
import { Command as
|
|
2817
|
-
import
|
|
2818
|
-
var statusCommand = new
|
|
3036
|
+
import { Command as Command8 } from "commander";
|
|
3037
|
+
import chalk7 from "chalk";
|
|
3038
|
+
var statusCommand = new Command8("status").description("Show the active context's API health and deployment info").action(async () => {
|
|
2819
3039
|
const context = getActiveContext();
|
|
2820
3040
|
const apiUrl = getApiUrl2();
|
|
2821
3041
|
let health;
|
|
@@ -2838,18 +3058,18 @@ var statusCommand = new Command7("status").description("Show the active context'
|
|
|
2838
3058
|
printJson({ context, apiUrl, reachable: true, health, env: envInfo });
|
|
2839
3059
|
return;
|
|
2840
3060
|
}
|
|
2841
|
-
const row = (label, value) => ` ${
|
|
3061
|
+
const row = (label, value) => ` ${chalk7.dim(label.padEnd(14))}${value ?? chalk7.dim("-")}
|
|
2842
3062
|
`;
|
|
2843
3063
|
process.stdout.write(
|
|
2844
|
-
|
|
3064
|
+
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
3065
|
);
|
|
2846
3066
|
});
|
|
2847
3067
|
|
|
2848
3068
|
// src/commands/doctor.ts
|
|
2849
|
-
import { Command as
|
|
2850
|
-
import { existsSync as
|
|
3069
|
+
import { Command as Command9 } from "commander";
|
|
3070
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2851
3071
|
import { execFileSync } from "child_process";
|
|
2852
|
-
import
|
|
3072
|
+
import chalk8 from "chalk";
|
|
2853
3073
|
function bunVersion() {
|
|
2854
3074
|
const embedded = process.versions.bun;
|
|
2855
3075
|
if (embedded) return embedded;
|
|
@@ -2859,9 +3079,9 @@ function bunVersion() {
|
|
|
2859
3079
|
return null;
|
|
2860
3080
|
}
|
|
2861
3081
|
}
|
|
2862
|
-
var doctorCommand = new
|
|
3082
|
+
var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
|
|
2863
3083
|
const checks = [];
|
|
2864
|
-
const hasConfig =
|
|
3084
|
+
const hasConfig = existsSync6(CONFIG_PATH);
|
|
2865
3085
|
checks.push({
|
|
2866
3086
|
name: "config",
|
|
2867
3087
|
status: hasConfig ? "pass" : "warn",
|
|
@@ -2897,10 +3117,10 @@ var doctorCommand = new Command8("doctor").description("Diagnose the CLI setup (
|
|
|
2897
3117
|
if (isJsonMode()) {
|
|
2898
3118
|
printJson({ context, apiUrl, reachable, checks });
|
|
2899
3119
|
} else {
|
|
2900
|
-
const glyph = { pass:
|
|
2901
|
-
process.stdout.write(
|
|
3120
|
+
const glyph = { pass: chalk8.green("\u2713"), warn: chalk8.yellow("!"), fail: chalk8.red("\u2717") };
|
|
3121
|
+
process.stdout.write(chalk8.bold("\n Openship doctor\n\n"));
|
|
2902
3122
|
for (const c of checks) {
|
|
2903
|
-
process.stdout.write(` ${glyph[c.status]} ${
|
|
3123
|
+
process.stdout.write(` ${glyph[c.status]} ${chalk8.bold(c.name.padEnd(8))} ${c.detail}
|
|
2904
3124
|
`);
|
|
2905
3125
|
}
|
|
2906
3126
|
process.stdout.write("\n");
|
|
@@ -2909,20 +3129,20 @@ var doctorCommand = new Command8("doctor").description("Diagnose the CLI setup (
|
|
|
2909
3129
|
});
|
|
2910
3130
|
|
|
2911
3131
|
// src/commands/deploy.ts
|
|
2912
|
-
import { Command as
|
|
3132
|
+
import { Command as Command10 } from "commander";
|
|
2913
3133
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2914
3134
|
import ora2 from "ora";
|
|
2915
3135
|
|
|
2916
3136
|
// src/lib/project-link.ts
|
|
2917
|
-
import { readFileSync as readFileSync3, existsSync as
|
|
2918
|
-
import { join as
|
|
2919
|
-
var LINK_REL =
|
|
3137
|
+
import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
|
|
3138
|
+
import { join as join7, dirname as dirname3, parse } from "path";
|
|
3139
|
+
var LINK_REL = join7(".openship", "project.json");
|
|
2920
3140
|
function findProjectLinkPath(from = process.cwd()) {
|
|
2921
3141
|
let dir = from;
|
|
2922
3142
|
const root = parse(dir).root;
|
|
2923
3143
|
for (; ; ) {
|
|
2924
|
-
const candidate =
|
|
2925
|
-
if (
|
|
3144
|
+
const candidate = join7(dir, LINK_REL);
|
|
3145
|
+
if (existsSync7(candidate)) return candidate;
|
|
2926
3146
|
if (dir === root) return null;
|
|
2927
3147
|
dir = dirname3(dir);
|
|
2928
3148
|
}
|
|
@@ -2939,21 +3159,21 @@ function readProjectLink(from) {
|
|
|
2939
3159
|
|
|
2940
3160
|
// src/lib/folder-deploy.ts
|
|
2941
3161
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2942
|
-
import { readFileSync as readFileSync4, existsSync as
|
|
3162
|
+
import { readFileSync as readFileSync4, existsSync as existsSync8, rmSync as rmSync3 } from "fs";
|
|
2943
3163
|
import { tmpdir } from "os";
|
|
2944
|
-
import { join as
|
|
3164
|
+
import { join as join8, basename } from "path";
|
|
2945
3165
|
function detectPackageManager(dir) {
|
|
2946
|
-
if (
|
|
2947
|
-
if (
|
|
2948
|
-
if (
|
|
2949
|
-
if (
|
|
3166
|
+
if (existsSync8(join8(dir, "bun.lockb")) || existsSync8(join8(dir, "bun.lock"))) return "bun";
|
|
3167
|
+
if (existsSync8(join8(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
3168
|
+
if (existsSync8(join8(dir, "yarn.lock"))) return "yarn";
|
|
3169
|
+
if (existsSync8(join8(dir, "package.json"))) return "npm";
|
|
2950
3170
|
return void 0;
|
|
2951
3171
|
}
|
|
2952
3172
|
function detectStack(dir) {
|
|
2953
|
-
if (
|
|
2954
|
-
if (
|
|
2955
|
-
if (
|
|
2956
|
-
if (
|
|
3173
|
+
if (existsSync8(join8(dir, "go.mod"))) return "go";
|
|
3174
|
+
if (existsSync8(join8(dir, "Cargo.toml"))) return "rust";
|
|
3175
|
+
if (existsSync8(join8(dir, "requirements.txt")) || existsSync8(join8(dir, "pyproject.toml"))) return "python";
|
|
3176
|
+
if (existsSync8(join8(dir, "package.json"))) return "node";
|
|
2957
3177
|
return void 0;
|
|
2958
3178
|
}
|
|
2959
3179
|
async function deployFolder(opts) {
|
|
@@ -2970,7 +3190,7 @@ async function deployFolder(opts) {
|
|
|
2970
3190
|
throw new Error(session.error || "Failed to open upload session");
|
|
2971
3191
|
}
|
|
2972
3192
|
step("Packaging folder");
|
|
2973
|
-
const tarball =
|
|
3193
|
+
const tarball = join8(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
|
|
2974
3194
|
execFileSync2(
|
|
2975
3195
|
"tar",
|
|
2976
3196
|
[
|
|
@@ -2996,7 +3216,7 @@ async function deployFolder(opts) {
|
|
|
2996
3216
|
if (!res.ok) throw new Error(`upload failed (HTTP ${res.status})`);
|
|
2997
3217
|
} finally {
|
|
2998
3218
|
try {
|
|
2999
|
-
|
|
3219
|
+
rmSync3(tarball, { force: true });
|
|
3000
3220
|
} catch {
|
|
3001
3221
|
}
|
|
3002
3222
|
}
|
|
@@ -3170,7 +3390,7 @@ function git(args) {
|
|
|
3170
3390
|
return void 0;
|
|
3171
3391
|
}
|
|
3172
3392
|
}
|
|
3173
|
-
var deployCommand = new
|
|
3393
|
+
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
3394
|
const link = readProjectLink();
|
|
3175
3395
|
const env = opts.env;
|
|
3176
3396
|
if (env !== "production" && env !== "preview") {
|
|
@@ -3252,9 +3472,9 @@ var deployCommand = new Command9("deploy").description("Trigger a deployment for
|
|
|
3252
3472
|
});
|
|
3253
3473
|
|
|
3254
3474
|
// src/commands/deployment.ts
|
|
3255
|
-
import { Command as
|
|
3475
|
+
import { Command as Command11 } from "commander";
|
|
3256
3476
|
import { createInterface as createInterface3 } from "readline";
|
|
3257
|
-
function
|
|
3477
|
+
function run2(fn) {
|
|
3258
3478
|
return async (...args) => {
|
|
3259
3479
|
try {
|
|
3260
3480
|
await fn(...args);
|
|
@@ -3274,12 +3494,12 @@ function shortSha(v) {
|
|
|
3274
3494
|
async function confirm(question) {
|
|
3275
3495
|
if (!process.stdin.isTTY) return true;
|
|
3276
3496
|
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
3277
|
-
const answer = await new Promise((
|
|
3497
|
+
const answer = await new Promise((resolve2) => rl.question(`${question} [y/N] `, resolve2));
|
|
3278
3498
|
rl.close();
|
|
3279
3499
|
return /^y(es)?$/i.test(answer.trim());
|
|
3280
3500
|
}
|
|
3281
|
-
var list = new
|
|
3282
|
-
|
|
3501
|
+
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(
|
|
3502
|
+
run2(async (opts) => {
|
|
3283
3503
|
const projectId = opts.project || readProjectLink()?.projectId;
|
|
3284
3504
|
const params = new URLSearchParams();
|
|
3285
3505
|
if (projectId) params.set("projectId", projectId);
|
|
@@ -3301,8 +3521,8 @@ var list = new Command10("list").description("List deployments (org-wide, or sco
|
|
|
3301
3521
|
printTable(rows, ["id", "status", "env", "branch", "commit", "active", "created"]);
|
|
3302
3522
|
})
|
|
3303
3523
|
);
|
|
3304
|
-
var get = new
|
|
3305
|
-
|
|
3524
|
+
var get = new Command11("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
|
|
3525
|
+
run2(async (id) => {
|
|
3306
3526
|
const res = await apiRequest(`/deployments/${id}`);
|
|
3307
3527
|
const d = res.data ?? {};
|
|
3308
3528
|
if (isJsonMode()) return printJson(d);
|
|
@@ -3322,20 +3542,20 @@ var get = new Command10("get").description("Show a single deployment").argument(
|
|
|
3322
3542
|
);
|
|
3323
3543
|
})
|
|
3324
3544
|
);
|
|
3325
|
-
var info2 = new
|
|
3326
|
-
|
|
3545
|
+
var info2 = new Command11("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
|
|
3546
|
+
run2(async (id) => {
|
|
3327
3547
|
const res = await apiRequest(`/deployments/${id}/info`);
|
|
3328
3548
|
printJson(res.data ?? res);
|
|
3329
3549
|
})
|
|
3330
3550
|
);
|
|
3331
|
-
var usage = new
|
|
3332
|
-
|
|
3551
|
+
var usage = new Command11("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
|
|
3552
|
+
run2(async (id) => {
|
|
3333
3553
|
const res = await apiRequest(`/deployments/${id}/usage`);
|
|
3334
3554
|
printJson(res.data ?? res);
|
|
3335
3555
|
})
|
|
3336
3556
|
);
|
|
3337
|
-
var redeploy = new
|
|
3338
|
-
|
|
3557
|
+
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(
|
|
3558
|
+
run2(async (id, opts) => {
|
|
3339
3559
|
const res = await apiRequest(`/deployments/${id}/redeploy`, {
|
|
3340
3560
|
method: "POST",
|
|
3341
3561
|
body: JSON.stringify({ useExistingCommit: opts.useExistingCommit === true })
|
|
@@ -3343,14 +3563,14 @@ var redeploy = new Command10("redeploy").description("Redeploy from an existing
|
|
|
3343
3563
|
report(res, `Redeploy triggered for ${id}`);
|
|
3344
3564
|
})
|
|
3345
3565
|
);
|
|
3346
|
-
var rollback = new
|
|
3347
|
-
|
|
3566
|
+
var rollback = new Command11("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
|
|
3567
|
+
run2(async (id) => {
|
|
3348
3568
|
const res = await apiRequest(`/deployments/${id}/rollback`, { method: "POST" });
|
|
3349
3569
|
report(res, `Rolled back to ${id}`);
|
|
3350
3570
|
})
|
|
3351
3571
|
);
|
|
3352
|
-
var pin = new
|
|
3353
|
-
|
|
3572
|
+
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(
|
|
3573
|
+
run2(async (id, opts) => {
|
|
3354
3574
|
const pinned = !opts.off;
|
|
3355
3575
|
const res = await apiRequest(`/deployments/${id}/pin`, {
|
|
3356
3576
|
method: "POST",
|
|
@@ -3359,32 +3579,32 @@ var pin = new Command10("pin").description("Pin (or unpin) a deployment's rollba
|
|
|
3359
3579
|
report(res, `${pinned ? "Pinned" : "Unpinned"} ${id}`);
|
|
3360
3580
|
})
|
|
3361
3581
|
);
|
|
3362
|
-
var cancel = new
|
|
3363
|
-
|
|
3582
|
+
var cancel = new Command11("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
|
|
3583
|
+
run2(async (id) => {
|
|
3364
3584
|
const res = await apiRequest(`/deployments/${id}/cancel`, { method: "POST" });
|
|
3365
3585
|
report(res, `Cancelled ${id}`);
|
|
3366
3586
|
})
|
|
3367
3587
|
);
|
|
3368
|
-
var restart = new
|
|
3369
|
-
|
|
3588
|
+
var restart = new Command11("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
|
|
3589
|
+
run2(async (id) => {
|
|
3370
3590
|
const res = await apiRequest(`/deployments/${id}/restart`, { method: "POST" });
|
|
3371
3591
|
report(res, `Restarted ${id}`);
|
|
3372
3592
|
})
|
|
3373
3593
|
);
|
|
3374
|
-
var reject = new
|
|
3375
|
-
|
|
3594
|
+
var reject = new Command11("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
|
|
3595
|
+
run2(async (id) => {
|
|
3376
3596
|
const res = await apiRequest(`/deployments/${id}/reject`, { method: "POST" });
|
|
3377
3597
|
report(res, `Rejected ${id}`);
|
|
3378
3598
|
})
|
|
3379
3599
|
);
|
|
3380
|
-
var keep = new
|
|
3381
|
-
|
|
3600
|
+
var keep = new Command11("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
|
|
3601
|
+
run2(async (id) => {
|
|
3382
3602
|
const res = await apiRequest(`/deployments/${id}/keep`, { method: "POST" });
|
|
3383
3603
|
report(res, `Kept ${id}`);
|
|
3384
3604
|
})
|
|
3385
3605
|
);
|
|
3386
|
-
var rm = new
|
|
3387
|
-
|
|
3606
|
+
var rm = new Command11("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
|
|
3607
|
+
run2(async (id, opts) => {
|
|
3388
3608
|
if (!opts.yes && !isJsonMode() && !await confirm(`Delete deployment ${id}?`)) {
|
|
3389
3609
|
err("Aborted.");
|
|
3390
3610
|
process.exit(1);
|
|
@@ -3393,8 +3613,8 @@ var rm = new Command10("rm").description("Delete a deployment").argument("<id>",
|
|
|
3393
3613
|
report(res, `Deleted ${id}`);
|
|
3394
3614
|
})
|
|
3395
3615
|
);
|
|
3396
|
-
var sslStatus = new
|
|
3397
|
-
|
|
3616
|
+
var sslStatus = new Command11("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
|
|
3617
|
+
run2(async (domain) => {
|
|
3398
3618
|
const res = await apiRequest("/deployments/ssl/status", {
|
|
3399
3619
|
method: "POST",
|
|
3400
3620
|
body: JSON.stringify({ domain })
|
|
@@ -3402,8 +3622,8 @@ var sslStatus = new Command10("status").description("Check SSL certificate statu
|
|
|
3402
3622
|
printJson(res);
|
|
3403
3623
|
})
|
|
3404
3624
|
);
|
|
3405
|
-
var sslRenew = new
|
|
3406
|
-
|
|
3625
|
+
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(
|
|
3626
|
+
run2(async (domain, opts) => {
|
|
3407
3627
|
const res = await apiRequest("/deployments/ssl/renew", {
|
|
3408
3628
|
method: "POST",
|
|
3409
3629
|
body: JSON.stringify({ domain, includeWww: opts.www === true })
|
|
@@ -3411,12 +3631,12 @@ var sslRenew = new Command10("renew").description("Renew (issue) an SSL certific
|
|
|
3411
3631
|
report(res, `SSL renewal requested for ${domain}`);
|
|
3412
3632
|
})
|
|
3413
3633
|
);
|
|
3414
|
-
var ssl = new
|
|
3415
|
-
var deploymentCommand = new
|
|
3634
|
+
var ssl = new Command11("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
|
|
3635
|
+
var deploymentCommand = new Command11("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(restart).addCommand(reject).addCommand(keep).addCommand(rm).addCommand(ssl);
|
|
3416
3636
|
|
|
3417
3637
|
// src/commands/logs.ts
|
|
3418
|
-
import { Command as
|
|
3419
|
-
var logsCommand = new
|
|
3638
|
+
import { Command as Command12 } from "commander";
|
|
3639
|
+
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
3640
|
if (opts.follow) {
|
|
3421
3641
|
try {
|
|
3422
3642
|
const result = await streamDeploymentLogs(deploymentId);
|
|
@@ -3448,8 +3668,8 @@ var logsCommand = new Command11("logs").description("View or stream a deployment
|
|
|
3448
3668
|
});
|
|
3449
3669
|
|
|
3450
3670
|
// src/commands/project.ts
|
|
3451
|
-
import { Command as
|
|
3452
|
-
import
|
|
3671
|
+
import { Command as Command13 } from "commander";
|
|
3672
|
+
import chalk9 from "chalk";
|
|
3453
3673
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
3454
3674
|
import { stdin as input3, stdout as output3 } from "process";
|
|
3455
3675
|
function action(fn) {
|
|
@@ -3480,12 +3700,12 @@ function printProject(project) {
|
|
|
3480
3700
|
];
|
|
3481
3701
|
for (const [k, v] of fields) {
|
|
3482
3702
|
if (v === null || v === void 0 || v === "") continue;
|
|
3483
|
-
process.stdout.write(` ${
|
|
3703
|
+
process.stdout.write(` ${chalk9.dim(k.padEnd(12))} ${String(v)}
|
|
3484
3704
|
`);
|
|
3485
3705
|
}
|
|
3486
3706
|
}
|
|
3487
3707
|
var ENVIRONMENTS = ["production", "preview", "development"];
|
|
3488
|
-
var listCmd2 = new
|
|
3708
|
+
var listCmd2 = new Command13("list").alias("ls").description("List projects in the active organization").action(
|
|
3489
3709
|
action(async () => {
|
|
3490
3710
|
const rows = [];
|
|
3491
3711
|
for await (const p of paginate("/projects")) {
|
|
@@ -3500,7 +3720,7 @@ var listCmd2 = new Command12("list").alias("ls").description("List projects in t
|
|
|
3500
3720
|
printTable(rows, ["id", "name", "slug", "repo", "source"]);
|
|
3501
3721
|
})
|
|
3502
3722
|
);
|
|
3503
|
-
var getCmd = new
|
|
3723
|
+
var getCmd = new Command13("get").description("Show a single project").argument("<id>", "Project ID").action(
|
|
3504
3724
|
action(async (id) => {
|
|
3505
3725
|
const { data } = await apiRequest(
|
|
3506
3726
|
`/projects/${encodeURIComponent(id)}`
|
|
@@ -3508,7 +3728,7 @@ var getCmd = new Command12("get").description("Show a single project").argument(
|
|
|
3508
3728
|
printProject(data);
|
|
3509
3729
|
})
|
|
3510
3730
|
);
|
|
3511
|
-
var createCmd = new
|
|
3731
|
+
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
3732
|
"--type <type>",
|
|
3513
3733
|
"Project type: app | docker | services | monorepo"
|
|
3514
3734
|
).action(
|
|
@@ -3532,12 +3752,12 @@ var createCmd = new Command12("create").description("Create a project").required
|
|
|
3532
3752
|
printProject(data);
|
|
3533
3753
|
})
|
|
3534
3754
|
);
|
|
3535
|
-
var deleteCmd = new
|
|
3755
|
+
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
3756
|
action(async (id, opts) => {
|
|
3537
3757
|
if (!opts.yes) {
|
|
3538
3758
|
const rl = createInterface4({ input: input3, output: output3 });
|
|
3539
3759
|
const answer = await rl.question(
|
|
3540
|
-
|
|
3760
|
+
chalk9.yellow(` Delete project ${id}? This cannot be undone. `) + "(y/N) "
|
|
3541
3761
|
);
|
|
3542
3762
|
rl.close();
|
|
3543
3763
|
if (answer.trim().toLowerCase() !== "y") {
|
|
@@ -3563,7 +3783,7 @@ var deleteCmd = new Command12("delete").alias("rm").description("Delete a projec
|
|
|
3563
3783
|
`);
|
|
3564
3784
|
})
|
|
3565
3785
|
);
|
|
3566
|
-
var envCmd = new
|
|
3786
|
+
var envCmd = new Command13("env").description("Manage project environment variables");
|
|
3567
3787
|
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
3788
|
action(async (id, opts) => {
|
|
3569
3789
|
const qs = opts.environment ? `?environment=${encodeURIComponent(opts.environment)}` : "";
|
|
@@ -3637,7 +3857,7 @@ envCmd.command("set").description("Merge env vars: upsert KEY=VALUE pairs and/or
|
|
|
3637
3857
|
);
|
|
3638
3858
|
})
|
|
3639
3859
|
);
|
|
3640
|
-
var gitCmd = new
|
|
3860
|
+
var gitCmd = new Command13("git").description("Manage git linkage and auto-deploy");
|
|
3641
3861
|
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
3862
|
action(async (id, opts) => {
|
|
3643
3863
|
const result = await apiRequest(
|
|
@@ -3725,7 +3945,7 @@ gitCmd.command("webhook-domain").description("Set or clear the domain that recei
|
|
|
3725
3945
|
`);
|
|
3726
3946
|
})
|
|
3727
3947
|
);
|
|
3728
|
-
var connectCmd = new
|
|
3948
|
+
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
3949
|
action(async (id, domain, opts) => {
|
|
3730
3950
|
const result = await apiRequest(
|
|
3731
3951
|
`/projects/${encodeURIComponent(id)}/connect`,
|
|
@@ -3745,7 +3965,7 @@ var connectCmd = new Command12("connect").description("Connect a custom domain t
|
|
|
3745
3965
|
printJson(result.records);
|
|
3746
3966
|
})
|
|
3747
3967
|
);
|
|
3748
|
-
var enableCmd = new
|
|
3968
|
+
var enableCmd = new Command13("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
|
|
3749
3969
|
action(async (id) => {
|
|
3750
3970
|
const result = await apiRequest(
|
|
3751
3971
|
`/projects/${encodeURIComponent(id)}/enable`,
|
|
@@ -3757,7 +3977,7 @@ var enableCmd = new Command12("enable").description("Start a stopped project").a
|
|
|
3757
3977
|
`);
|
|
3758
3978
|
})
|
|
3759
3979
|
);
|
|
3760
|
-
var disableCmd = new
|
|
3980
|
+
var disableCmd = new Command13("disable").description("Stop a running project").argument("<id>", "Project ID").action(
|
|
3761
3981
|
action(async (id) => {
|
|
3762
3982
|
const result = await apiRequest(
|
|
3763
3983
|
`/projects/${encodeURIComponent(id)}/disable`,
|
|
@@ -3770,7 +3990,7 @@ var disableCmd = new Command12("disable").description("Stop a running project").
|
|
|
3770
3990
|
})
|
|
3771
3991
|
);
|
|
3772
3992
|
var SLEEP_MODES = ["auto_sleep", "always_on"];
|
|
3773
|
-
var sleepModeCmd = new
|
|
3993
|
+
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
3994
|
action(async (id, mode) => {
|
|
3775
3995
|
if (!SLEEP_MODES.includes(mode)) {
|
|
3776
3996
|
err(` mode must be one of: ${SLEEP_MODES.join(", ")}`);
|
|
@@ -3788,7 +4008,7 @@ var sleepModeCmd = new Command12("sleep-mode").description("Set the project slee
|
|
|
3788
4008
|
})
|
|
3789
4009
|
);
|
|
3790
4010
|
var TRANSFER_DIRS = ["to-cloud", "to-self-hosted"];
|
|
3791
|
-
var transferCmd = new
|
|
4011
|
+
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
4012
|
action(async (id, direction) => {
|
|
3793
4013
|
if (!TRANSFER_DIRS.includes(direction)) {
|
|
3794
4014
|
err(` direction must be one of: ${TRANSFER_DIRS.join(", ")}`);
|
|
@@ -3811,7 +4031,7 @@ var transferCmd = new Command12("transfer").description("Promote a project to Op
|
|
|
3811
4031
|
);
|
|
3812
4032
|
})
|
|
3813
4033
|
);
|
|
3814
|
-
var logsCmd = new
|
|
4034
|
+
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
4035
|
action(async (id, opts) => {
|
|
3816
4036
|
const tailQs = opts.tail ? `?tail=${opts.tail}` : "";
|
|
3817
4037
|
if (!opts.follow) {
|
|
@@ -3839,7 +4059,7 @@ var logsCmd = new Command12("logs").description("Show or stream runtime (contain
|
|
|
3839
4059
|
}
|
|
3840
4060
|
})
|
|
3841
4061
|
);
|
|
3842
|
-
var serverLogsCmd = new
|
|
4062
|
+
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
4063
|
action(async (id, opts) => {
|
|
3844
4064
|
const base = `/projects/${encodeURIComponent(id)}/server-logs`;
|
|
3845
4065
|
const domainQs = opts.domain ? `domain=${encodeURIComponent(opts.domain)}` : "";
|
|
@@ -3880,14 +4100,14 @@ function safeParse(s) {
|
|
|
3880
4100
|
}
|
|
3881
4101
|
}
|
|
3882
4102
|
function printLogEntry(entry) {
|
|
3883
|
-
const ts = entry.timestamp ?
|
|
4103
|
+
const ts = entry.timestamp ? chalk9.dim(String(entry.timestamp)) : "";
|
|
3884
4104
|
const level = String(entry.level ?? "info");
|
|
3885
|
-
const color = level === "error" ?
|
|
4105
|
+
const color = level === "error" ? chalk9.red : level === "warn" ? chalk9.yellow : chalk9.dim;
|
|
3886
4106
|
const msg = entry.message ?? entry.data ?? "";
|
|
3887
4107
|
process.stdout.write(` ${ts} ${color(level.padEnd(5))} ${String(msg)}
|
|
3888
4108
|
`);
|
|
3889
4109
|
}
|
|
3890
|
-
var projectCommand = new
|
|
4110
|
+
var projectCommand = new Command13("project").alias("projects").description("Manage Openship projects");
|
|
3891
4111
|
projectCommand.addCommand(listCmd2);
|
|
3892
4112
|
projectCommand.addCommand(getCmd);
|
|
3893
4113
|
projectCommand.addCommand(createCmd);
|
|
@@ -3903,9 +4123,9 @@ projectCommand.addCommand(logsCmd);
|
|
|
3903
4123
|
projectCommand.addCommand(serverLogsCmd);
|
|
3904
4124
|
|
|
3905
4125
|
// src/commands/service.ts
|
|
3906
|
-
import { Command as
|
|
3907
|
-
import
|
|
3908
|
-
import { spawnSync as
|
|
4126
|
+
import { Command as Command14 } from "commander";
|
|
4127
|
+
import chalk10 from "chalk";
|
|
4128
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3909
4129
|
import path from "path";
|
|
3910
4130
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
3911
4131
|
import { stdin as input4, stdout as output4 } from "process";
|
|
@@ -3917,14 +4137,14 @@ function requireAuth() {
|
|
|
3917
4137
|
}
|
|
3918
4138
|
function fail(e) {
|
|
3919
4139
|
if (e instanceof ApiError) {
|
|
3920
|
-
err(` ${e.message}` + (e.status ?
|
|
4140
|
+
err(` ${e.message}` + (e.status ? chalk10.dim(` (HTTP ${e.status})`) : ""));
|
|
3921
4141
|
} else {
|
|
3922
4142
|
err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
3923
4143
|
}
|
|
3924
4144
|
process.exit(1);
|
|
3925
4145
|
}
|
|
3926
4146
|
function stackCommand(name) {
|
|
3927
|
-
return new
|
|
4147
|
+
return new Command14(name).requiredOption(
|
|
3928
4148
|
"-p, --project <id|slug|name>",
|
|
3929
4149
|
"Stack (project) id, slug, or name"
|
|
3930
4150
|
);
|
|
@@ -4009,7 +4229,7 @@ var listCmd3 = stackCommand("list").description("List the services in a stack").
|
|
|
4009
4229
|
image: s.image ?? "\u2014",
|
|
4010
4230
|
enabled: s.enabled ? "yes" : "no",
|
|
4011
4231
|
exposed: s.exposed ? "yes" : "no",
|
|
4012
|
-
drift: s.drift ?
|
|
4232
|
+
drift: s.drift ? chalk10.yellow("pending") : "\u2014"
|
|
4013
4233
|
})),
|
|
4014
4234
|
["name", "kind", "image", "enabled", "exposed", "drift"]
|
|
4015
4235
|
);
|
|
@@ -4172,7 +4392,7 @@ function mapComposeService(name, def, baseDir) {
|
|
|
4172
4392
|
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
4393
|
requireAuth();
|
|
4174
4394
|
const abs = path.resolve(composeFile);
|
|
4175
|
-
const proc =
|
|
4395
|
+
const proc = spawnSync3(
|
|
4176
4396
|
"docker",
|
|
4177
4397
|
["compose", "-f", abs, "config", "--format", "json"],
|
|
4178
4398
|
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
|
|
@@ -4266,7 +4486,7 @@ var containersCmd = stackCommand("containers").description("List the stack's act
|
|
|
4266
4486
|
fail(e);
|
|
4267
4487
|
}
|
|
4268
4488
|
});
|
|
4269
|
-
var driftCmd = new
|
|
4489
|
+
var driftCmd = new Command14("drift").description(
|
|
4270
4490
|
"Resolve compose drift on a service (upstream compose changed a value you edited)"
|
|
4271
4491
|
);
|
|
4272
4492
|
function driftActionCommand(action2) {
|
|
@@ -4294,7 +4514,7 @@ function driftActionCommand(action2) {
|
|
|
4294
4514
|
}
|
|
4295
4515
|
driftCmd.addCommand(driftActionCommand("accept"));
|
|
4296
4516
|
driftCmd.addCommand(driftActionCommand("keep"));
|
|
4297
|
-
var envCmd2 = new
|
|
4517
|
+
var envCmd2 = new Command14("env").description("Read and write a service's environment variables");
|
|
4298
4518
|
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
4519
|
requireAuth();
|
|
4300
4520
|
try {
|
|
@@ -4377,8 +4597,8 @@ function printLogEntry2(entry) {
|
|
|
4377
4597
|
process.stdout.write(JSON.stringify(entry) + "\n");
|
|
4378
4598
|
return;
|
|
4379
4599
|
}
|
|
4380
|
-
const ts = entry.timestamp ?
|
|
4381
|
-
const line = entry.level === "error" ?
|
|
4600
|
+
const ts = entry.timestamp ? chalk10.dim(entry.timestamp) : "";
|
|
4601
|
+
const line = entry.level === "error" ? chalk10.red(msg) : entry.level === "warn" ? chalk10.yellow(msg) : msg;
|
|
4382
4602
|
process.stdout.write(`${ts ? ts + " " : ""}${line}
|
|
4383
4603
|
`);
|
|
4384
4604
|
}
|
|
@@ -4427,7 +4647,7 @@ var execCmd = stackCommand("exec").description("Open an interactive shell in a s
|
|
|
4427
4647
|
);
|
|
4428
4648
|
process.exit(1);
|
|
4429
4649
|
});
|
|
4430
|
-
var serviceCommand = new
|
|
4650
|
+
var serviceCommand = new Command14("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
|
|
4431
4651
|
serviceCommand.addCommand(listCmd3);
|
|
4432
4652
|
serviceCommand.addCommand(getCmd2);
|
|
4433
4653
|
serviceCommand.addCommand(createCmd2);
|
|
@@ -4443,15 +4663,15 @@ serviceCommand.addCommand(logsCmd2);
|
|
|
4443
4663
|
serviceCommand.addCommand(execCmd);
|
|
4444
4664
|
|
|
4445
4665
|
// src/commands/domain.ts
|
|
4446
|
-
import { Command as
|
|
4447
|
-
import
|
|
4666
|
+
import { Command as Command15 } from "commander";
|
|
4667
|
+
import chalk11 from "chalk";
|
|
4448
4668
|
import ora3 from "ora";
|
|
4449
4669
|
function spin(text) {
|
|
4450
4670
|
return isJsonMode() ? null : ora3(text).start();
|
|
4451
4671
|
}
|
|
4452
4672
|
function fail2(e) {
|
|
4453
4673
|
if (e instanceof ApiError) {
|
|
4454
|
-
err(` ${e.message}${e.status ?
|
|
4674
|
+
err(` ${e.message}${e.status ? chalk11.dim(` (${e.status})`) : ""}`);
|
|
4455
4675
|
} else {
|
|
4456
4676
|
err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
4457
4677
|
}
|
|
@@ -4479,7 +4699,7 @@ function printRecords(result) {
|
|
|
4479
4699
|
["type", "host", "value"]
|
|
4480
4700
|
);
|
|
4481
4701
|
}
|
|
4482
|
-
var listCmd4 = new
|
|
4702
|
+
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
4703
|
try {
|
|
4484
4704
|
const res = await apiRequest(
|
|
4485
4705
|
`/domains?projectId=${encodeURIComponent(opts.project)}`
|
|
@@ -4494,7 +4714,7 @@ var listCmd4 = new Command14("list").description("List a project's custom domain
|
|
|
4494
4714
|
fail2(e);
|
|
4495
4715
|
}
|
|
4496
4716
|
});
|
|
4497
|
-
var addCmd2 = new
|
|
4717
|
+
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
4718
|
const sp = spin(`Adding ${hostname}\u2026`);
|
|
4499
4719
|
try {
|
|
4500
4720
|
const res = await apiRequest("/domains", {
|
|
@@ -4513,7 +4733,7 @@ var addCmd2 = new Command14("add").description("Add a custom domain to a project
|
|
|
4513
4733
|
fail2(e);
|
|
4514
4734
|
}
|
|
4515
4735
|
});
|
|
4516
|
-
var previewCmd = new
|
|
4736
|
+
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
4737
|
try {
|
|
4518
4738
|
const res = await apiRequest("/domains/preview", {
|
|
4519
4739
|
method: "POST",
|
|
@@ -4524,7 +4744,7 @@ var previewCmd = new Command14("preview").description("Preview the DNS records a
|
|
|
4524
4744
|
fail2(e);
|
|
4525
4745
|
}
|
|
4526
4746
|
});
|
|
4527
|
-
var verifyCmd = new
|
|
4747
|
+
var verifyCmd = new Command15("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
|
|
4528
4748
|
const sp = spin("Checking DNS records\u2026");
|
|
4529
4749
|
try {
|
|
4530
4750
|
const res = await apiRaw(`/domains/${encodeURIComponent(id)}/verify`, { method: "POST" });
|
|
@@ -4550,7 +4770,7 @@ var verifyCmd = new Command14("verify").description("Run DNS verification for a
|
|
|
4550
4770
|
fail2(e);
|
|
4551
4771
|
}
|
|
4552
4772
|
});
|
|
4553
|
-
var primaryCmd = new
|
|
4773
|
+
var primaryCmd = new Command15("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
|
|
4554
4774
|
const sp = spin("Setting primary\u2026");
|
|
4555
4775
|
try {
|
|
4556
4776
|
const res = await apiRequest(`/domains/${encodeURIComponent(id)}/primary`, {
|
|
@@ -4563,7 +4783,7 @@ var primaryCmd = new Command14("primary").description("Make a domain the project
|
|
|
4563
4783
|
fail2(e);
|
|
4564
4784
|
}
|
|
4565
4785
|
});
|
|
4566
|
-
var recordsCmd = new
|
|
4786
|
+
var recordsCmd = new Command15("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
|
|
4567
4787
|
try {
|
|
4568
4788
|
const res = await apiRequest(`/domains/${encodeURIComponent(id)}/records`);
|
|
4569
4789
|
printRecords(res.data);
|
|
@@ -4581,7 +4801,7 @@ function printSsl(data) {
|
|
|
4581
4801
|
if (data.issuer) info(` issuer: ${data.issuer}`);
|
|
4582
4802
|
if (data.expiresAt) info(` expires: ${data.expiresAt}`);
|
|
4583
4803
|
}
|
|
4584
|
-
var renewCmd = new
|
|
4804
|
+
var renewCmd = new Command15("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
|
|
4585
4805
|
const sp = spin("Renewing certificate\u2026");
|
|
4586
4806
|
try {
|
|
4587
4807
|
const res = await apiRequest(`/domains/${encodeURIComponent(id)}/renew`, {
|
|
@@ -4594,7 +4814,7 @@ var renewCmd = new Command14("renew").description("Renew the SSL certificate for
|
|
|
4594
4814
|
fail2(e);
|
|
4595
4815
|
}
|
|
4596
4816
|
});
|
|
4597
|
-
var verifySslCmd = new
|
|
4817
|
+
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
4818
|
const sp = spin("Checking certificate\u2026");
|
|
4599
4819
|
try {
|
|
4600
4820
|
const res = await apiRequest(`/domains/${encodeURIComponent(id)}/verify-ssl`, {
|
|
@@ -4609,7 +4829,7 @@ var verifySslCmd = new Command14("verify-ssl").description("Recheck that a domai
|
|
|
4609
4829
|
fail2(e);
|
|
4610
4830
|
}
|
|
4611
4831
|
});
|
|
4612
|
-
var renewAllCmd = new
|
|
4832
|
+
var renewAllCmd = new Command15("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
|
|
4613
4833
|
const sp = spin("Renewing expiring certificates\u2026");
|
|
4614
4834
|
try {
|
|
4615
4835
|
const res = await apiRequest("/domains/renew-all", { method: "POST" });
|
|
@@ -4631,11 +4851,11 @@ var renewAllCmd = new Command14("renew-all").description("Renew SSL for every ne
|
|
|
4631
4851
|
fail2(e);
|
|
4632
4852
|
}
|
|
4633
4853
|
});
|
|
4634
|
-
var domainCommand = new
|
|
4854
|
+
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
4855
|
|
|
4636
4856
|
// src/commands/server.ts
|
|
4637
|
-
import { Command as
|
|
4638
|
-
import
|
|
4857
|
+
import { Command as Command16 } from "commander";
|
|
4858
|
+
import chalk12 from "chalk";
|
|
4639
4859
|
import ora4 from "ora";
|
|
4640
4860
|
var INSTALLABLE = ["docker", "git", "openresty", "certbot", "rsync"];
|
|
4641
4861
|
function guard(fn) {
|
|
@@ -4667,7 +4887,7 @@ function connBody(o) {
|
|
|
4667
4887
|
sshArgs: o.sshArgs
|
|
4668
4888
|
};
|
|
4669
4889
|
}
|
|
4670
|
-
var server = new
|
|
4890
|
+
var server = new Command16("server").description("Manage self-hosted SSH servers");
|
|
4671
4891
|
server.command("list").alias("ls").description("List servers in the active organization").action(
|
|
4672
4892
|
guard(async () => {
|
|
4673
4893
|
const servers = await apiRequest("/system/servers");
|
|
@@ -4766,8 +4986,8 @@ server.command("install <serverId>").description("Install components on a server
|
|
|
4766
4986
|
printJson({ event: ev.event, ...payload });
|
|
4767
4987
|
} else if (ev.event === "log") {
|
|
4768
4988
|
const p = payload;
|
|
4769
|
-
const line = ` ${
|
|
4770
|
-
process.stderr.write((p.level === "error" ?
|
|
4989
|
+
const line = ` ${chalk12.dim(`[${p.component}]`)} ${p.message ?? ""}`;
|
|
4990
|
+
process.stderr.write((p.level === "error" ? chalk12.red(line) : line) + "\n");
|
|
4771
4991
|
} else if (ev.event === "progress") {
|
|
4772
4992
|
const p = payload;
|
|
4773
4993
|
if (p.component) info(` ${p.component}: ${p.status}`);
|
|
@@ -4885,9 +5105,9 @@ function fmtUptime(seconds) {
|
|
|
4885
5105
|
var serverCommand = server;
|
|
4886
5106
|
|
|
4887
5107
|
// src/commands/system.ts
|
|
4888
|
-
import { Command as
|
|
5108
|
+
import { Command as Command17 } from "commander";
|
|
4889
5109
|
import ora5 from "ora";
|
|
4890
|
-
import { readFileSync as readFileSync5, writeFileSync as
|
|
5110
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
4891
5111
|
import { createInterface as createInterface6 } from "readline/promises";
|
|
4892
5112
|
import { stdin as input5, stdout as output5 } from "process";
|
|
4893
5113
|
async function guarded(fn) {
|
|
@@ -4933,7 +5153,7 @@ async function promptHidden(query) {
|
|
|
4933
5153
|
output5.write("\n");
|
|
4934
5154
|
return answer;
|
|
4935
5155
|
}
|
|
4936
|
-
var settingsCommand = new
|
|
5156
|
+
var settingsCommand = new Command17("settings").description("Read or update instance settings");
|
|
4937
5157
|
settingsCommand.command("get").description("Show current instance settings").action(async () => {
|
|
4938
5158
|
await guarded(async () => {
|
|
4939
5159
|
const s = await apiRequest("/system/settings");
|
|
@@ -4972,7 +5192,7 @@ settingsCommand.command("set").description("Update instance-level settings").opt
|
|
|
4972
5192
|
`));
|
|
4973
5193
|
});
|
|
4974
5194
|
});
|
|
4975
|
-
var onboardingCommand = new
|
|
5195
|
+
var onboardingCommand = new Command17("onboarding").description("First-run instance setup");
|
|
4976
5196
|
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
5197
|
await guarded(async () => {
|
|
4978
5198
|
const body = {
|
|
@@ -5006,7 +5226,7 @@ onboardingCommand.command("apply").description("Configure a fresh instance (fail
|
|
|
5006
5226
|
}
|
|
5007
5227
|
});
|
|
5008
5228
|
});
|
|
5009
|
-
var upgradeToAuthCommand = new
|
|
5229
|
+
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
5230
|
await guarded(async () => {
|
|
5011
5231
|
const name = opts.name;
|
|
5012
5232
|
const email = opts.email;
|
|
@@ -5035,7 +5255,7 @@ var upgradeToAuthCommand = new Command16("upgrade-to-auth").description("Promote
|
|
|
5035
5255
|
`));
|
|
5036
5256
|
});
|
|
5037
5257
|
});
|
|
5038
|
-
var browseCommand = new
|
|
5258
|
+
var browseCommand = new Command17("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
|
|
5039
5259
|
await guarded(async () => {
|
|
5040
5260
|
const qs = path2 ? `?path=${encodeURIComponent(path2)}` : "";
|
|
5041
5261
|
const res = await apiRequest(`/system/browse${qs}`);
|
|
@@ -5064,7 +5284,7 @@ function buildDomain(opts) {
|
|
|
5064
5284
|
err("\n A domain is required: pass --hostname <host> or --slug <slug>.\n");
|
|
5065
5285
|
process.exit(1);
|
|
5066
5286
|
}
|
|
5067
|
-
var migrationCommand = new
|
|
5287
|
+
var migrationCommand = new Command17("migration").description("Team-mode migration lifecycle");
|
|
5068
5288
|
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
5289
|
await guarded(async () => {
|
|
5070
5290
|
const domain = buildDomain(opts);
|
|
@@ -5171,7 +5391,7 @@ migrationCommand.command("switch-back").description("Reverse migration back to s
|
|
|
5171
5391
|
}
|
|
5172
5392
|
});
|
|
5173
5393
|
});
|
|
5174
|
-
var dataTransferCommand = new
|
|
5394
|
+
var dataTransferCommand = new Command17("data-transfer").description(
|
|
5175
5395
|
"Whole-instance export / import (owner-only)"
|
|
5176
5396
|
);
|
|
5177
5397
|
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 +5407,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
|
|
|
5187
5407
|
);
|
|
5188
5408
|
spin4?.succeed("Export ready.");
|
|
5189
5409
|
if (opts.out) {
|
|
5190
|
-
|
|
5410
|
+
writeFileSync6(opts.out, JSON.stringify(file));
|
|
5191
5411
|
const tables = Object.keys(file.dump?.tables ?? {}).length;
|
|
5192
5412
|
report2(
|
|
5193
5413
|
{ out: opts.out, tables },
|
|
@@ -5241,11 +5461,11 @@ dataTransferCommand.command("import").description("Import an instance export fil
|
|
|
5241
5461
|
}
|
|
5242
5462
|
});
|
|
5243
5463
|
});
|
|
5244
|
-
var systemCommand = new
|
|
5464
|
+
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
5465
|
|
|
5246
5466
|
// src/commands/mail.ts
|
|
5247
|
-
import { Command as
|
|
5248
|
-
import
|
|
5467
|
+
import { Command as Command18 } from "commander";
|
|
5468
|
+
import chalk13 from "chalk";
|
|
5249
5469
|
import ora6 from "ora";
|
|
5250
5470
|
import { createInterface as createInterface7 } from "readline/promises";
|
|
5251
5471
|
import { stdin as input6, stdout as output6 } from "process";
|
|
@@ -5286,7 +5506,7 @@ function printRecordsObject(records) {
|
|
|
5286
5506
|
if (rows.length === 0) return info(" (no DNS records)");
|
|
5287
5507
|
printTable(rows, ["key", "type", "host", "value"]);
|
|
5288
5508
|
}
|
|
5289
|
-
var stepsCmd = new
|
|
5509
|
+
var stepsCmd = new Command18("steps").description("List the mail setup steps").action(
|
|
5290
5510
|
guard2(async () => {
|
|
5291
5511
|
const res = await apiRequest(
|
|
5292
5512
|
"/mail/steps"
|
|
@@ -5299,7 +5519,7 @@ var stepsCmd = new Command17("steps").description("List the mail setup steps").a
|
|
|
5299
5519
|
info(` ${res.total} steps total.`);
|
|
5300
5520
|
})
|
|
5301
5521
|
);
|
|
5302
|
-
var statusCmd = new
|
|
5522
|
+
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
5523
|
guard2(async (serverId) => {
|
|
5304
5524
|
const q = serverId ? `?serverId=${encodeURIComponent(serverId)}` : "";
|
|
5305
5525
|
const res = await apiRequest(`/mail/status${q}`);
|
|
@@ -5320,7 +5540,7 @@ var statusCmd = new Command17("status").description("Show the setup progress for
|
|
|
5320
5540
|
}
|
|
5321
5541
|
})
|
|
5322
5542
|
);
|
|
5323
|
-
var serversCmd = new
|
|
5543
|
+
var serversCmd = new Command18("servers").description("List every server the mail stack is installed on").action(
|
|
5324
5544
|
guard2(async () => {
|
|
5325
5545
|
const res = await apiRequest(
|
|
5326
5546
|
"/mail/servers"
|
|
@@ -5341,7 +5561,7 @@ var serversCmd = new Command17("servers").description("List every server the mai
|
|
|
5341
5561
|
);
|
|
5342
5562
|
})
|
|
5343
5563
|
);
|
|
5344
|
-
var scanCmd = new
|
|
5564
|
+
var scanCmd = new Command18("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
|
|
5345
5565
|
guard2(async (serverId) => {
|
|
5346
5566
|
const sp = spin2("Scanning server\u2026");
|
|
5347
5567
|
const res = await apiRequest("/mail/scan", { method: "POST", body: JSON.stringify({ serverId }) });
|
|
@@ -5356,7 +5576,7 @@ var scanCmd = new Command17("scan").description("Probe a server for an existing
|
|
|
5356
5576
|
else info(" Nothing to adopt on this server.");
|
|
5357
5577
|
})
|
|
5358
5578
|
);
|
|
5359
|
-
var adoptCmd = new
|
|
5579
|
+
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
5580
|
guard2(async (serverId) => {
|
|
5361
5581
|
const sp = spin2("Adopting mail server\u2026");
|
|
5362
5582
|
const res = await apiRequest(
|
|
@@ -5369,7 +5589,7 @@ var adoptCmd = new Command17("adopt").description("Re-adopt an existing mail ins
|
|
|
5369
5589
|
info(` completed: ${res.completed ? "yes" : "no"}`);
|
|
5370
5590
|
})
|
|
5371
5591
|
);
|
|
5372
|
-
var setupCmd = new
|
|
5592
|
+
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
5593
|
guard2(async (serverId, opts) => {
|
|
5374
5594
|
let config;
|
|
5375
5595
|
if (opts.config) {
|
|
@@ -5402,7 +5622,7 @@ var setupCmd = new Command17("setup").description("Start or resume the mail setu
|
|
|
5402
5622
|
break;
|
|
5403
5623
|
case "log": {
|
|
5404
5624
|
const line = ` ${String(p.message ?? "")}`;
|
|
5405
|
-
process.stderr.write((p.level === "error" ?
|
|
5625
|
+
process.stderr.write((p.level === "error" ? chalk13.red(line) : chalk13.dim(line)) + "\n");
|
|
5406
5626
|
break;
|
|
5407
5627
|
}
|
|
5408
5628
|
case "step_done":
|
|
@@ -5438,7 +5658,7 @@ var setupCmd = new Command17("setup").description("Start or resume the mail setu
|
|
|
5438
5658
|
if (failed) process.exit(1);
|
|
5439
5659
|
})
|
|
5440
5660
|
);
|
|
5441
|
-
var cancelCmd = new
|
|
5661
|
+
var cancelCmd = new Command18("cancel").description("Cancel the mail setup currently running").action(
|
|
5442
5662
|
guard2(async () => {
|
|
5443
5663
|
const res = await apiRequest("/mail/setup/cancel", {
|
|
5444
5664
|
method: "POST"
|
|
@@ -5448,7 +5668,7 @@ var cancelCmd = new Command17("cancel").description("Cancel the mail setup curre
|
|
|
5448
5668
|
})
|
|
5449
5669
|
);
|
|
5450
5670
|
function ackCommand(name, path2, description, successMsg) {
|
|
5451
|
-
return new
|
|
5671
|
+
return new Command18(name).description(description).argument("<serverId>", "Mail server ID").action(
|
|
5452
5672
|
guard2(async (serverId) => {
|
|
5453
5673
|
const res = await apiRequest(path2, {
|
|
5454
5674
|
method: "POST",
|
|
@@ -5471,7 +5691,7 @@ var ptrAckCmd = ackCommand(
|
|
|
5471
5691
|
"Acknowledge that reverse DNS (PTR) is configured",
|
|
5472
5692
|
"PTR acknowledged. Re-run `mail setup` with --start-step to continue."
|
|
5473
5693
|
);
|
|
5474
|
-
var resetCmd = new
|
|
5694
|
+
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
5695
|
guard2(async (serverId, opts) => {
|
|
5476
5696
|
if (!opts.yes && !isJsonMode()) {
|
|
5477
5697
|
const rl = createInterface7({ input: input6, output: output6 });
|
|
@@ -5487,7 +5707,7 @@ var resetCmd = new Command17("reset").description("Wipe the on-server setup stat
|
|
|
5487
5707
|
ok(" Setup state reset.");
|
|
5488
5708
|
})
|
|
5489
5709
|
);
|
|
5490
|
-
var forgetCmd = new
|
|
5710
|
+
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
5711
|
guard2(async (serverId) => {
|
|
5492
5712
|
const res = await apiRequest(`/mail/servers/${encodeURIComponent(serverId)}`, {
|
|
5493
5713
|
method: "DELETE"
|
|
@@ -5496,7 +5716,7 @@ var forgetCmd = new Command17("forget").description("Stop managing a mail server
|
|
|
5496
5716
|
ok(` Forgot mail server ${serverId} (re-adopt with \`mail scan\` + \`mail adopt\`).`);
|
|
5497
5717
|
})
|
|
5498
5718
|
);
|
|
5499
|
-
var healthCmd = new
|
|
5719
|
+
var healthCmd = new Command18("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
|
|
5500
5720
|
guard2(async (serverId) => {
|
|
5501
5721
|
const sp = spin2("Checking mail daemons\u2026");
|
|
5502
5722
|
const res = await apiRequest(`/mail/health/${encodeURIComponent(serverId)}`);
|
|
@@ -5513,7 +5733,7 @@ var healthCmd = new Command17("health").description("Show live status of every m
|
|
|
5513
5733
|
);
|
|
5514
5734
|
})
|
|
5515
5735
|
);
|
|
5516
|
-
var logsCmd3 = new
|
|
5736
|
+
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
5737
|
guard2(async (serverId, component, opts) => {
|
|
5518
5738
|
const res = await apiRequest(
|
|
5519
5739
|
`/mail/admin/${encodeURIComponent(serverId)}/components/${encodeURIComponent(component)}/logs?lines=${encodeURIComponent(opts.lines)}`
|
|
@@ -5523,9 +5743,9 @@ var logsCmd3 = new Command17("logs").description("Tail a mail component's journa
|
|
|
5523
5743
|
for (const line of res.lines) process.stdout.write(line + "\n");
|
|
5524
5744
|
})
|
|
5525
5745
|
);
|
|
5526
|
-
var postmasterCmd = new
|
|
5746
|
+
var postmasterCmd = new Command18("postmaster").description("Manage the postmaster mailbox");
|
|
5527
5747
|
postmasterCmd.addCommand(
|
|
5528
|
-
new
|
|
5748
|
+
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
5749
|
guard2(async (serverId, opts) => {
|
|
5530
5750
|
let password = opts.password;
|
|
5531
5751
|
if (!password) {
|
|
@@ -5551,10 +5771,10 @@ postmasterCmd.addCommand(
|
|
|
5551
5771
|
})
|
|
5552
5772
|
)
|
|
5553
5773
|
);
|
|
5554
|
-
var mailCommand = new
|
|
5774
|
+
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
5775
|
|
|
5556
5776
|
// src/commands/backup.ts
|
|
5557
|
-
import { Command as
|
|
5777
|
+
import { Command as Command19 } from "commander";
|
|
5558
5778
|
import ora7 from "ora";
|
|
5559
5779
|
import { readFileSync as readFileSync6 } from "fs";
|
|
5560
5780
|
async function guard3(fn) {
|
|
@@ -5639,7 +5859,7 @@ async function followStream(path2, label) {
|
|
|
5639
5859
|
spinner2?.stop();
|
|
5640
5860
|
return status;
|
|
5641
5861
|
}
|
|
5642
|
-
var policyCmd = new
|
|
5862
|
+
var policyCmd = new Command19("policy").description("Backup policies (schedules) for a project");
|
|
5643
5863
|
policyCmd.command("list").description("List backup policies for a project").requiredOption("--project <id>", "Project ID").action(
|
|
5644
5864
|
(opts) => guard3(async () => {
|
|
5645
5865
|
const { data } = await apiRequest(
|
|
@@ -5709,7 +5929,7 @@ policyCmd.command("run").description("Trigger a policy's backup now").argument("
|
|
|
5709
5929
|
}
|
|
5710
5930
|
})
|
|
5711
5931
|
);
|
|
5712
|
-
var runCmd = new
|
|
5932
|
+
var runCmd = new Command19("run").description("Backup runs (executions)");
|
|
5713
5933
|
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
5934
|
(opts) => guard3(async () => {
|
|
5715
5935
|
const qs = new URLSearchParams();
|
|
@@ -5785,7 +6005,7 @@ runCmd.command("restore").description("Prepare a restore from a run (stages it;
|
|
|
5785
6005
|
}
|
|
5786
6006
|
})
|
|
5787
6007
|
);
|
|
5788
|
-
var restoreCmd = new
|
|
6008
|
+
var restoreCmd = new Command19("restore").description("Manage staged restores");
|
|
5789
6009
|
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
6010
|
(restoreId, opts) => guard3(async () => {
|
|
5791
6011
|
await apiRequest(
|
|
@@ -5828,7 +6048,7 @@ restoreCmd.command("get").description("Show one restore (optionally stream it)")
|
|
|
5828
6048
|
show(data);
|
|
5829
6049
|
})
|
|
5830
6050
|
);
|
|
5831
|
-
var destinationCmd = new
|
|
6051
|
+
var destinationCmd = new Command19("destination").description("Backup destinations (storage targets)");
|
|
5832
6052
|
destinationCmd.command("list").description("List backup destinations").action(
|
|
5833
6053
|
() => guard3(async () => {
|
|
5834
6054
|
const { data } = await apiRequest("/backup-destinations");
|
|
@@ -5900,21 +6120,21 @@ destinationCmd.command("preflight").description("Verify a destination (write + r
|
|
|
5900
6120
|
}
|
|
5901
6121
|
})
|
|
5902
6122
|
);
|
|
5903
|
-
var backupCommand = new
|
|
6123
|
+
var backupCommand = new Command19("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
|
|
5904
6124
|
|
|
5905
6125
|
// src/commands/token.ts
|
|
5906
|
-
import { Command as
|
|
5907
|
-
import
|
|
6126
|
+
import { Command as Command20 } from "commander";
|
|
6127
|
+
import chalk15 from "chalk";
|
|
5908
6128
|
|
|
5909
6129
|
// src/lib/cmd-helpers.ts
|
|
5910
|
-
import
|
|
6130
|
+
import chalk14 from "chalk";
|
|
5911
6131
|
import ora8 from "ora";
|
|
5912
6132
|
function spin3(text) {
|
|
5913
6133
|
return isJsonMode() ? null : ora8(text).start();
|
|
5914
6134
|
}
|
|
5915
6135
|
function fail3(e) {
|
|
5916
6136
|
if (e instanceof ApiError) {
|
|
5917
|
-
err(` ${e.message}${e.status ?
|
|
6137
|
+
err(` ${e.message}${e.status ? chalk14.dim(` (${e.status})`) : ""}`);
|
|
5918
6138
|
} else {
|
|
5919
6139
|
err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
5920
6140
|
}
|
|
@@ -5931,7 +6151,7 @@ function collectGrant(value, acc) {
|
|
|
5931
6151
|
acc.push({ resourceType, resourceId, permissions });
|
|
5932
6152
|
return acc;
|
|
5933
6153
|
}
|
|
5934
|
-
var listCmd5 = new
|
|
6154
|
+
var listCmd5 = new Command20("list").description("List your personal access tokens").action(async () => {
|
|
5935
6155
|
try {
|
|
5936
6156
|
const res = await apiRequest("/tokens");
|
|
5937
6157
|
const rows = res.data ?? [];
|
|
@@ -5956,7 +6176,7 @@ var listCmd5 = new Command19("list").description("List your personal access toke
|
|
|
5956
6176
|
fail3(e);
|
|
5957
6177
|
}
|
|
5958
6178
|
});
|
|
5959
|
-
var createCmd3 = new
|
|
6179
|
+
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
6180
|
"--grant <type:id:perms>",
|
|
5961
6181
|
"Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
|
|
5962
6182
|
collectGrant,
|
|
@@ -5981,14 +6201,14 @@ var createCmd3 = new Command19("create").description("Mint a new personal access
|
|
|
5981
6201
|
return;
|
|
5982
6202
|
}
|
|
5983
6203
|
info(" Copy this token now \u2014 it will not be shown again:");
|
|
5984
|
-
process.stdout.write(
|
|
6204
|
+
process.stdout.write(chalk15.cyan(` ${res.data.token}
|
|
5985
6205
|
`));
|
|
5986
6206
|
} catch (e) {
|
|
5987
6207
|
sp?.fail("Create failed");
|
|
5988
6208
|
fail3(e);
|
|
5989
6209
|
}
|
|
5990
6210
|
});
|
|
5991
|
-
var revokeCmd = new
|
|
6211
|
+
var revokeCmd = new Command20("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
|
|
5992
6212
|
const sp = spin3("Revoking token\u2026");
|
|
5993
6213
|
try {
|
|
5994
6214
|
await apiRequest(`/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
@@ -6000,11 +6220,11 @@ var revokeCmd = new Command19("revoke").description("Revoke one of your tokens")
|
|
|
6000
6220
|
fail3(e);
|
|
6001
6221
|
}
|
|
6002
6222
|
});
|
|
6003
|
-
var tokenCommand = new
|
|
6223
|
+
var tokenCommand = new Command20("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
|
|
6004
6224
|
|
|
6005
6225
|
// src/commands/api.ts
|
|
6006
|
-
import { Command as
|
|
6007
|
-
var apiCommand = new
|
|
6226
|
+
import { Command as Command21 } from "commander";
|
|
6227
|
+
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
6228
|
const method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
|
|
6009
6229
|
let url = path2.startsWith("/") ? path2 : `/${path2}`;
|
|
6010
6230
|
if (opts.query?.length) {
|
|
@@ -6038,11 +6258,11 @@ var apiCommand = new Command20("api").description("Make an authenticated request
|
|
|
6038
6258
|
});
|
|
6039
6259
|
|
|
6040
6260
|
// 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
|
|
6261
|
+
import { Command as Command22 } from "commander";
|
|
6262
|
+
import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
6263
|
+
import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
|
|
6264
|
+
import { homedir as homedir5 } from "os";
|
|
6265
|
+
import { join as join9 } from "path";
|
|
6046
6266
|
import ora9 from "ora";
|
|
6047
6267
|
function assetForPlatform() {
|
|
6048
6268
|
const { platform, arch } = process;
|
|
@@ -6054,33 +6274,33 @@ function assetForPlatform() {
|
|
|
6054
6274
|
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
|
6055
6275
|
}
|
|
6056
6276
|
function installDmg(dmg) {
|
|
6057
|
-
const homeApps =
|
|
6277
|
+
const homeApps = join9(homedir5(), "Applications");
|
|
6058
6278
|
let dest = homeApps;
|
|
6059
6279
|
try {
|
|
6060
|
-
|
|
6280
|
+
mkdirSync7(homeApps, { recursive: true });
|
|
6061
6281
|
} catch {
|
|
6062
6282
|
dest = "/Applications";
|
|
6063
6283
|
}
|
|
6064
|
-
const attach =
|
|
6284
|
+
const attach = spawnSync4("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
|
|
6065
6285
|
encoding: "utf8"
|
|
6066
6286
|
});
|
|
6067
6287
|
if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
|
|
6068
6288
|
const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
|
|
6069
6289
|
if (!mount) throw new Error("Could not determine the mounted volume");
|
|
6070
|
-
let target =
|
|
6290
|
+
let target = join9(dest, "Openship.app");
|
|
6071
6291
|
try {
|
|
6072
|
-
const appInDmg =
|
|
6073
|
-
if (!
|
|
6074
|
-
|
|
6075
|
-
let copy =
|
|
6292
|
+
const appInDmg = join9(mount, "Openship.app");
|
|
6293
|
+
if (!existsSync9(appInDmg)) throw new Error("Openship.app not found in the disk image");
|
|
6294
|
+
spawnSync4("rm", ["-rf", target]);
|
|
6295
|
+
let copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
|
|
6076
6296
|
if (copy.status !== 0 && dest === homeApps) {
|
|
6077
|
-
target =
|
|
6078
|
-
|
|
6079
|
-
copy =
|
|
6297
|
+
target = join9("/Applications", "Openship.app");
|
|
6298
|
+
spawnSync4("rm", ["-rf", target]);
|
|
6299
|
+
copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
|
|
6080
6300
|
}
|
|
6081
6301
|
if (copy.status !== 0) throw new Error(`ditto copy failed: ${copy.stderr?.trim()}`);
|
|
6082
6302
|
} finally {
|
|
6083
|
-
|
|
6303
|
+
spawnSync4("hdiutil", ["detach", mount, "-quiet"]);
|
|
6084
6304
|
}
|
|
6085
6305
|
return target;
|
|
6086
6306
|
}
|
|
@@ -6089,10 +6309,10 @@ function installAppImage(appImage) {
|
|
|
6089
6309
|
return appImage;
|
|
6090
6310
|
}
|
|
6091
6311
|
function installZip(zip) {
|
|
6092
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
6093
|
-
const target =
|
|
6094
|
-
|
|
6095
|
-
const expand =
|
|
6312
|
+
const localAppData = process.env.LOCALAPPDATA || join9(homedir5(), "AppData", "Local");
|
|
6313
|
+
const target = join9(localAppData, "Programs", "Openship");
|
|
6314
|
+
mkdirSync7(target, { recursive: true });
|
|
6315
|
+
const expand = spawnSync4(
|
|
6096
6316
|
"powershell",
|
|
6097
6317
|
[
|
|
6098
6318
|
"-NoProfile",
|
|
@@ -6107,7 +6327,7 @@ function installZip(zip) {
|
|
|
6107
6327
|
}
|
|
6108
6328
|
function launch(kind, target) {
|
|
6109
6329
|
if (kind === "dmg") {
|
|
6110
|
-
|
|
6330
|
+
spawnSync4("open", [target]);
|
|
6111
6331
|
return;
|
|
6112
6332
|
}
|
|
6113
6333
|
if (kind === "appimage") {
|
|
@@ -6118,11 +6338,11 @@ function launch(kind, target) {
|
|
|
6118
6338
|
child.unref();
|
|
6119
6339
|
return;
|
|
6120
6340
|
}
|
|
6121
|
-
const exe =
|
|
6122
|
-
const path2 =
|
|
6123
|
-
|
|
6341
|
+
const exe = join9(target, "Openship.exe");
|
|
6342
|
+
const path2 = existsSync9(exe) ? exe : target;
|
|
6343
|
+
spawnSync4("cmd", ["/c", "start", "", path2]);
|
|
6124
6344
|
}
|
|
6125
|
-
var installCommand = new
|
|
6345
|
+
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
6346
|
let asset;
|
|
6127
6347
|
try {
|
|
6128
6348
|
asset = assetForPlatform();
|
|
@@ -6145,13 +6365,13 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6145
6365
|
process.exit(1);
|
|
6146
6366
|
}
|
|
6147
6367
|
const dir = releaseDir(tag);
|
|
6148
|
-
const assetPath =
|
|
6368
|
+
const assetPath = join9(dir, asset.name);
|
|
6149
6369
|
const sidecarPath = `${assetPath}.sha256`;
|
|
6150
6370
|
const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
|
|
6151
6371
|
const sidecarUrl = `${assetUrl2}.sha256`;
|
|
6152
6372
|
let downloaded = false;
|
|
6153
6373
|
let sha;
|
|
6154
|
-
const cachedUsable = !opts.force &&
|
|
6374
|
+
const cachedUsable = !opts.force && existsSync9(assetPath) && (existsSync9(sidecarPath) || opts.verify === false);
|
|
6155
6375
|
if (cachedUsable) {
|
|
6156
6376
|
info(` Using cached ${asset.name} (${tag}).`);
|
|
6157
6377
|
} else {
|
|
@@ -6173,7 +6393,7 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6173
6393
|
const s2 = spin4("Verifying checksum\u2026");
|
|
6174
6394
|
try {
|
|
6175
6395
|
let sidecarBody;
|
|
6176
|
-
if (
|
|
6396
|
+
if (existsSync9(sidecarPath) && !downloaded) {
|
|
6177
6397
|
sidecarBody = readFileSync7(sidecarPath, "utf8");
|
|
6178
6398
|
} else {
|
|
6179
6399
|
sidecarBody = await fetchSidecar(sidecarUrl);
|
|
@@ -6197,8 +6417,8 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6197
6417
|
err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
|
|
6198
6418
|
process.exit(1);
|
|
6199
6419
|
}
|
|
6200
|
-
|
|
6201
|
-
|
|
6420
|
+
mkdirSync7(dir, { recursive: true });
|
|
6421
|
+
writeFileSync7(sidecarPath, sidecarBody);
|
|
6202
6422
|
s2?.succeed("Checksum verified");
|
|
6203
6423
|
} catch (e) {
|
|
6204
6424
|
s2?.fail("Verification failed");
|
|
@@ -6237,15 +6457,15 @@ var installCommand = new Command21("install").description("Download and install
|
|
|
6237
6457
|
});
|
|
6238
6458
|
|
|
6239
6459
|
// src/commands/update.ts
|
|
6240
|
-
import { Command as
|
|
6241
|
-
import { spawnSync as
|
|
6460
|
+
import { Command as Command23 } from "commander";
|
|
6461
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
6242
6462
|
function detectPackageManager2(override) {
|
|
6243
6463
|
if (override === "bun" || override === "npm") return override;
|
|
6244
|
-
const hasBun =
|
|
6464
|
+
const hasBun = spawnSync5("bun", ["--version"], { stdio: "ignore" }).status === 0;
|
|
6245
6465
|
return hasBun ? "bun" : "npm";
|
|
6246
6466
|
}
|
|
6247
|
-
var updateCommand = new
|
|
6248
|
-
const current = "0.1.
|
|
6467
|
+
var updateCommand = new Command23("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").action(async (opts) => {
|
|
6468
|
+
const current = "0.1.11";
|
|
6249
6469
|
let latest;
|
|
6250
6470
|
try {
|
|
6251
6471
|
latest = (await resolveLatestTag()).replace(/^v/, "");
|
|
@@ -6273,7 +6493,7 @@ var updateCommand = new Command22("update").description("Update the Openship CLI
|
|
|
6273
6493
|
const ref = `openship@${latest}`;
|
|
6274
6494
|
const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
|
|
6275
6495
|
info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
|
|
6276
|
-
const res =
|
|
6496
|
+
const res = spawnSync5(pm, argv, { stdio: "inherit" });
|
|
6277
6497
|
if (res.status !== 0) {
|
|
6278
6498
|
err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
|
|
6279
6499
|
process.exitCode = 1;
|
|
@@ -6287,18 +6507,18 @@ var updateCommand = new Command22("update").description("Update the Openship CLI
|
|
|
6287
6507
|
});
|
|
6288
6508
|
|
|
6289
6509
|
// src/commands/cache.ts
|
|
6290
|
-
import { Command as
|
|
6291
|
-
import { existsSync as
|
|
6292
|
-
import { join as
|
|
6510
|
+
import { Command as Command24 } from "commander";
|
|
6511
|
+
import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync8, rmSync as rmSync4, statSync } from "fs";
|
|
6512
|
+
import { join as join10 } from "path";
|
|
6293
6513
|
function listAssets() {
|
|
6294
|
-
if (!
|
|
6514
|
+
if (!existsSync10(RELEASES_DIR)) return [];
|
|
6295
6515
|
const out = [];
|
|
6296
6516
|
for (const tag of readdirSync(RELEASES_DIR)) {
|
|
6297
6517
|
const dir = releaseDir(tag);
|
|
6298
6518
|
if (!statSync(dir).isDirectory()) continue;
|
|
6299
6519
|
for (const name of readdirSync(dir)) {
|
|
6300
6520
|
if (name.endsWith(".sha256")) continue;
|
|
6301
|
-
const path2 =
|
|
6521
|
+
const path2 = join10(dir, name);
|
|
6302
6522
|
const st = statSync(path2);
|
|
6303
6523
|
if (!st.isFile()) continue;
|
|
6304
6524
|
out.push({
|
|
@@ -6306,17 +6526,17 @@ function listAssets() {
|
|
|
6306
6526
|
name,
|
|
6307
6527
|
path: path2,
|
|
6308
6528
|
size: st.size,
|
|
6309
|
-
hasSidecar:
|
|
6529
|
+
hasSidecar: existsSync10(`${path2}.sha256`)
|
|
6310
6530
|
});
|
|
6311
6531
|
}
|
|
6312
6532
|
}
|
|
6313
6533
|
return out;
|
|
6314
6534
|
}
|
|
6315
|
-
var pathCmd = new
|
|
6535
|
+
var pathCmd = new Command24("path").description("Print the cache directory path").action(() => {
|
|
6316
6536
|
if (isJsonMode()) printJson({ path: CACHE_DIR });
|
|
6317
6537
|
else process.stdout.write(CACHE_DIR + "\n");
|
|
6318
6538
|
});
|
|
6319
|
-
var listCmd6 = new
|
|
6539
|
+
var listCmd6 = new Command24("list").alias("ls").description("List cached release assets").action(() => {
|
|
6320
6540
|
const assets = listAssets();
|
|
6321
6541
|
printTable(
|
|
6322
6542
|
assets.map((a) => ({
|
|
@@ -6328,7 +6548,7 @@ var listCmd6 = new Command23("list").alias("ls").description("List cached releas
|
|
|
6328
6548
|
["tag", "asset", "size", "sidecar"]
|
|
6329
6549
|
);
|
|
6330
6550
|
});
|
|
6331
|
-
var verifyCmd2 = new
|
|
6551
|
+
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
6552
|
const assets = listAssets().filter((a) => !tag || a.tag === tag);
|
|
6333
6553
|
const results = [];
|
|
6334
6554
|
let bad = 0;
|
|
@@ -6351,30 +6571,31 @@ var verifyCmd2 = new Command23("verify").description("Re-hash cached assets and
|
|
|
6351
6571
|
}
|
|
6352
6572
|
if (bad > 0) process.exit(1);
|
|
6353
6573
|
});
|
|
6354
|
-
var cleanCmd = new
|
|
6574
|
+
var cleanCmd = new Command24("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
|
|
6355
6575
|
const target = tag ? releaseDir(tag) : RELEASES_DIR;
|
|
6356
|
-
if (!
|
|
6576
|
+
if (!existsSync10(target)) {
|
|
6357
6577
|
if (isJsonMode()) printJson({ removed: false, path: target });
|
|
6358
6578
|
else info(` Nothing to clean (${target}).`);
|
|
6359
6579
|
return;
|
|
6360
6580
|
}
|
|
6361
|
-
|
|
6581
|
+
rmSync4(target, { recursive: true, force: true });
|
|
6362
6582
|
if (isJsonMode()) printJson({ removed: true, path: target });
|
|
6363
6583
|
else ok(`
|
|
6364
6584
|
Removed ${target}
|
|
6365
6585
|
`);
|
|
6366
6586
|
});
|
|
6367
|
-
var cacheCommand = new
|
|
6587
|
+
var cacheCommand = new Command24("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
|
|
6368
6588
|
err("Specify a subcommand: path | list | verify | clean");
|
|
6369
6589
|
process.exit(1);
|
|
6370
6590
|
}).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
|
|
6371
6591
|
|
|
6372
6592
|
// 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.1.
|
|
6593
|
+
var program = new Command25();
|
|
6594
|
+
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.1.11").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
|
|
6375
6595
|
if (thisCommand.opts().json) setJsonMode(true);
|
|
6376
6596
|
});
|
|
6377
6597
|
program.addCommand(upCommand);
|
|
6598
|
+
program.addCommand(stopCommand);
|
|
6378
6599
|
program.addCommand(installCommand);
|
|
6379
6600
|
program.addCommand(updateCommand);
|
|
6380
6601
|
program.addCommand(openCommand);
|