@join-pantheon/cli 0.32.1 → 0.33.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts.js +1 -1
- package/dist/board.js +1417 -0
- package/dist/claude-config.js +189 -0
- package/dist/cli.js +351 -101
- package/dist/client.js +16 -0
- package/dist/command-state.js +2 -1
- package/dist/dbus.js +92 -13
- package/dist/doctor.js +36 -5
- package/dist/folder-guard.js +89 -0
- package/dist/git-safe.js +3 -3
- package/dist/host-env.js +47 -1
- package/dist/install.js +76 -10
- package/dist/installer.js +2 -1
- package/dist/ledger.js +2 -2
- package/dist/machine.js +174 -3
- package/dist/managed.js +27 -1
- package/dist/notice-linux.js +501 -0
- package/dist/notice-windows.js +323 -0
- package/dist/notifier.js +12 -3
- package/dist/quickstart.js +28 -11
- package/dist/relay.js +64 -9
- package/dist/service.js +455 -29
- package/dist/setup-guide.js +22 -5
- package/dist/store.js +18 -2
- package/dist/uninstall.js +1 -1
- package/dist/upgrade.js +249 -4
- package/dist/uri-handler.js +335 -0
- package/dist/win-host.js +993 -0
- package/dist/worker-policy.js +3 -0
- package/dist/worker.js +11 -11
- package/helper/macos/prebuilt/manifest.json +2 -2
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/windows/pantheon-host.cs +170 -0
- package/helper/windows/pantheon-notice.ps1 +127 -0
- package/helper/windows/pantheon-tray.ps1 +731 -0
- package/helper/windows/pantheon.ico +0 -0
- package/installer/install.ps1 +238 -19
- package/installer/install.sh +36 -1
- package/package.json +2 -2
package/dist/ledger.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, existsSync, openSync, readSync, writeSync, fsyncSync, closeSync, fstatSync, copyFileSync, truncateSync, chmodSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { load } from "./config.js";
|
|
4
|
-
import { fsyncDir, renameRetrying, writeFileAtomic } from "./store.js";
|
|
4
|
+
import { fsyncDir, renameRetrying, syncFd, writeFileAtomic } from "./store.js";
|
|
5
5
|
export class SpaceFull extends Error {
|
|
6
6
|
bytes;
|
|
7
7
|
cap;
|
|
@@ -107,7 +107,7 @@ export class Ledger {
|
|
|
107
107
|
const fd = openSync(this.file, "a", 0o600);
|
|
108
108
|
try {
|
|
109
109
|
writeSync(fd, line);
|
|
110
|
-
|
|
110
|
+
syncFd(fd);
|
|
111
111
|
}
|
|
112
112
|
finally {
|
|
113
113
|
closeSync(fd);
|
package/dist/machine.js
CHANGED
|
@@ -3,7 +3,8 @@ import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, re
|
|
|
3
3
|
import { homedir, hostname, networkInterfaces, platform } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { CONFIG_DIR } from "./config.js";
|
|
6
|
-
import { borrowedHome, disableService, existingUnitPath, plistPath, serviceInstalled, serviceLogFile, unitPath, unitPaths, uninstallService } from "./service.js";
|
|
6
|
+
import { borrowedHome, disableService, existingUnitPath, plistPath, serviceInstalled, serviceLogFile, stopWindowsHost, unitPath, unitPaths, uninstallService } from "./service.js";
|
|
7
|
+
import { LEGACY_TASK_NAME, hostAlive, hostChildRunning, hostFiles, isHostKind, readHostState, recordedTaskName } from "./win-host.js";
|
|
7
8
|
import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir } from "./install.js";
|
|
8
9
|
const sh = (cmd) => {
|
|
9
10
|
try {
|
|
@@ -100,7 +101,33 @@ function fromSystemd(kind) {
|
|
|
100
101
|
...(s.lastExit ? { lastExit: s.lastExit } : {}),
|
|
101
102
|
};
|
|
102
103
|
}
|
|
104
|
+
export function elapsed(ms) {
|
|
105
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
106
|
+
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
|
|
107
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
108
|
+
return `${d ? `${d}-` : ""}${d || h ? `${two(h)}:` : ""}${two(m)}:${two(sec)}`;
|
|
109
|
+
}
|
|
110
|
+
export function hostProc(kind, o = {}) {
|
|
111
|
+
if ((o.platform ?? platform()) !== "win32" || !isHostKind(kind))
|
|
112
|
+
return {};
|
|
113
|
+
const home = o.home ?? CONFIG_DIR;
|
|
114
|
+
if (!existsSync(hostFiles(home).definition(kind)))
|
|
115
|
+
return {};
|
|
116
|
+
const now = o.now ?? Date.now();
|
|
117
|
+
const st = readHostState(home);
|
|
118
|
+
const up = hostAlive(st, now, o.alive);
|
|
119
|
+
const run = hostChildRunning(kind, home, now, o.alive);
|
|
120
|
+
const r = up ? st.children[kind] : undefined;
|
|
121
|
+
return {
|
|
122
|
+
managedBy: "host", label: `task ${recordedTaskName(home) ?? LEGACY_TASK_NAME}`, logPath: hostFiles(home).childLog(kind),
|
|
123
|
+
...(run ? { running: true, pid: run.pid, ...(run.since ? { since: elapsed(now - run.since) } : {}) } : { running: false }),
|
|
124
|
+
...(r?.lastExit && r.lastExit !== "0" ? { lastExit: r.lastExit } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
103
127
|
const fromManager = (kind) => {
|
|
128
|
+
const h = hostProc(kind);
|
|
129
|
+
if (h.managedBy)
|
|
130
|
+
return h;
|
|
104
131
|
const l = fromLaunchd(kind);
|
|
105
132
|
return l.managedBy ? l : fromSystemd(kind);
|
|
106
133
|
};
|
|
@@ -438,6 +465,7 @@ const defaultStopIo = (port = 8787) => ({
|
|
|
438
465
|
borrowed: () => borrowedHome(),
|
|
439
466
|
loadedFrom: (kind) => managerDefinitionPath(kind),
|
|
440
467
|
kill: killPid,
|
|
468
|
+
...(platform() === "win32" ? { stopHost: () => stopWindowsHost() } : {}),
|
|
441
469
|
});
|
|
442
470
|
export const borrowedHomeLine = (kind) => `${kind === "services" ? "services" : `${kind}:`} left alone — HOME (${homedir()}) is not your own home (a test, a sandbox or another account's folder), so the service manager's jobs here are the real user's, not this home's. Nothing was stopped; run this from your own login to stop your services`;
|
|
443
471
|
export const isPantheonCommand = (cmd) => /pantheon/i.test(cmd) || /\bcli\.js\s+(relay|start)\b/.test(cmd);
|
|
@@ -500,7 +528,9 @@ export function stopProcResult(kind, io = defaultStopIo()) {
|
|
|
500
528
|
else {
|
|
501
529
|
const how = platform() === "darwin"
|
|
502
530
|
? `launchctl disable gui/${process.getuid?.() ?? 0}/dev.pantheon.${kind}`
|
|
503
|
-
:
|
|
531
|
+
: platform() === "win32"
|
|
532
|
+
? `pantheon service uninstall ${kind}`
|
|
533
|
+
: `systemctl --user disable --now pantheon-${kind}`;
|
|
504
534
|
lines.push(`${kind}: could not disable the service (${e.message.split("\n")[0]}) — it may start again at login; try: ${how}`);
|
|
505
535
|
}
|
|
506
536
|
}
|
|
@@ -511,7 +541,7 @@ export function stopProcResult(kind, io = defaultStopIo()) {
|
|
|
511
541
|
const stillUp = !!after.running && !k?.killed;
|
|
512
542
|
if (disabled) {
|
|
513
543
|
lines.unshift(`${kind}: ${stillUp ? "disabled, but a process is still running (see below)" : p.running || k?.killed ? "stopped, and will not start again at login" : "was not running; disabled, and will not start again at login"}`);
|
|
514
|
-
lines.push(` start it again with: pantheon service install ${kind}`);
|
|
544
|
+
lines.push(` start it again with: pantheon service ${p.managedBy === "host" ? "start" : "install"} ${kind}`);
|
|
515
545
|
}
|
|
516
546
|
return { lines, stopped: (p.running && disabled && !stillUp) || !!k?.killed };
|
|
517
547
|
}
|
|
@@ -576,6 +606,11 @@ export function stopMachine(what, opts, io = defaultStopIo(opts.port), hosts = h
|
|
|
576
606
|
lines.push(`worker: ${k}`);
|
|
577
607
|
}
|
|
578
608
|
}
|
|
609
|
+
if (what === "all" && io.stopHost) {
|
|
610
|
+
const h = io.stopHost();
|
|
611
|
+
if (h.length)
|
|
612
|
+
lines.push(...h.map((l) => `host: ${l.trim().replace(/^the Pantheon host /, "")}`), " it starts again at your next logon (services stay paused), or now: pantheon service start worker");
|
|
613
|
+
}
|
|
579
614
|
if (!relayStopped)
|
|
580
615
|
return { lines };
|
|
581
616
|
const teams = opts.teams ?? (opts.teamRelay ? [{ name: "", relay: opts.teamRelay }] : []);
|
|
@@ -670,6 +705,142 @@ export function removeHomeLeftovers(home, keep, opts = {}) {
|
|
|
670
705
|
}
|
|
671
706
|
return { removed, left };
|
|
672
707
|
}
|
|
708
|
+
export const PANTHEON_HOME_ENTRIES = [
|
|
709
|
+
{ name: "config.json", what: "your key and team tokens", identity: true },
|
|
710
|
+
{ name: "config.json.bak", what: "the previous config.json", identity: true },
|
|
711
|
+
{ name: "keys.json", what: "the teammates' keys this machine pinned", identity: true },
|
|
712
|
+
{ name: "nonces.json", what: "the signed requests this machine has seen", identity: true },
|
|
713
|
+
{ name: "continued.json", what: "what this machine already handled", identity: true },
|
|
714
|
+
{ name: "cursors.json", what: "where each agent session last read", identity: true },
|
|
715
|
+
{ name: "worker-cursor.json", what: "where the worker last read", identity: true },
|
|
716
|
+
{ name: "profile.json", what: "your profile", identity: true },
|
|
717
|
+
{ name: "worker-policy.json", what: "the worker's wake policy", identity: true },
|
|
718
|
+
{ name: "spend.json", what: "the spend ledger", identity: true },
|
|
719
|
+
{ name: "personas", what: "the worker's personas", dir: true, identity: true },
|
|
720
|
+
{ name: "worker-state.json", what: "the worker's last state" },
|
|
721
|
+
{ name: "worker-run.json", what: "the worker's record of a run in flight" },
|
|
722
|
+
{ name: "run-slot.json", what: "the worker's run slot" },
|
|
723
|
+
{ name: "worker.pid", what: "the worker's pid file" },
|
|
724
|
+
{ name: /^notifier-state(-.+)?\.json$/, sample: "notifier-state-team%2Fone.json", what: "the alerts' state" },
|
|
725
|
+
{ name: "update-check.json", what: "the update check's cache" },
|
|
726
|
+
{ name: "uri-key", what: "the key pantheon:// links are signed with" },
|
|
727
|
+
{ name: "board.json", what: "the board's state" },
|
|
728
|
+
{ name: "hook-intent.json", what: "the hook intent" },
|
|
729
|
+
{ name: "hook-guard.json", what: "the hook guard" },
|
|
730
|
+
{ name: /^(worker|relay|notifier|menubar)\.log(\.1)?$/, sample: "worker.log", what: "service logs an older version wrote here" },
|
|
731
|
+
{ name: /^\.write-check\..+$/, sample: ".write-check.1234.5678", what: "a leftover write probe" },
|
|
732
|
+
{ name: "bin", what: "the menu bar's and alerts' files", dir: true },
|
|
733
|
+
{ name: "logs", what: "the service logs", dir: true },
|
|
734
|
+
{ name: "host", what: "the Windows host's queue, state and markers", dir: true },
|
|
735
|
+
{ name: "mcp", what: "the agent windows' server state", dir: true },
|
|
736
|
+
{ name: "run-homes", what: "the woken runs' homes", dir: true },
|
|
737
|
+
{ name: "worktrees", what: "the worker's worktrees", dir: true },
|
|
738
|
+
{ name: "no-hooks", what: "the empty hooks folder woken runs' git uses", dir: true },
|
|
739
|
+
{ name: "lfs-hooks", what: "the LFS-only hooks folder woken runs' git uses", dir: true },
|
|
740
|
+
{ name: "runtime", what: "Pantheon's own Node", dir: true, kept: "managed" },
|
|
741
|
+
{ name: "data", what: "the relay's ledger", dir: true, kept: "it is your team's history and may be its only copy; delete it yourself once no relay runs from it" },
|
|
742
|
+
{ name: /^(\.DS_Store|Thumbs\.db|desktop\.ini)$/i, sample: ".DS_Store", what: "the file browser's folder cache" },
|
|
743
|
+
];
|
|
744
|
+
const LOCK_OR_TMP = /^(.+?)(\.lock|(\.[0-9a-f]+)+\.tmp)$/i;
|
|
745
|
+
export function homeEntryFor(name) {
|
|
746
|
+
const direct = (n) => PANTHEON_HOME_ENTRIES.find((e) => typeof e.name === "string" ? e.name === n : e.name.test(n)) ?? null;
|
|
747
|
+
const e = direct(name);
|
|
748
|
+
if (e)
|
|
749
|
+
return e;
|
|
750
|
+
const m = LOCK_OR_TMP.exec(name);
|
|
751
|
+
const base = m ? direct(m[1]) : null;
|
|
752
|
+
return base && !base.dir ? base : null;
|
|
753
|
+
}
|
|
754
|
+
const pause = (ms) => { try {
|
|
755
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
756
|
+
}
|
|
757
|
+
catch { } };
|
|
758
|
+
const realSweepFs = {
|
|
759
|
+
list: (d) => readdirSync(d),
|
|
760
|
+
exists: (p) => existsSync(p),
|
|
761
|
+
remove: (p) => rmSync(p, { recursive: true, force: true }),
|
|
762
|
+
removeDir: (d) => rmdirSync(d),
|
|
763
|
+
};
|
|
764
|
+
export function sweepPantheonHome(home, o = {}) {
|
|
765
|
+
const fs = o.fs ?? realSweepFs;
|
|
766
|
+
const tries = o.tries ?? 5, waitMs = o.waitMs ?? 200;
|
|
767
|
+
const os = o.os ?? platform();
|
|
768
|
+
const removed = [], left = [];
|
|
769
|
+
if (!fs.exists(home))
|
|
770
|
+
return { removed, left, gone: true };
|
|
771
|
+
const spare = new Set((o.spare ?? []).map((p) => resolve(p)));
|
|
772
|
+
const attempt = (p, rm) => {
|
|
773
|
+
let why = "";
|
|
774
|
+
for (let i = 0; i < tries; i++) {
|
|
775
|
+
try {
|
|
776
|
+
rm(p);
|
|
777
|
+
}
|
|
778
|
+
catch (e) {
|
|
779
|
+
why = String(e.code ?? e.message.split("\n")[0]);
|
|
780
|
+
}
|
|
781
|
+
if (!fs.exists(p))
|
|
782
|
+
return null;
|
|
783
|
+
if (i < tries - 1)
|
|
784
|
+
pause(waitMs * (i + 1));
|
|
785
|
+
}
|
|
786
|
+
return why || "still there";
|
|
787
|
+
};
|
|
788
|
+
const lockedHint = os === "win32" ? "; a Pantheon process (an agent window, the menu bar) may still have it open — close it, then delete it" : "; delete it yourself";
|
|
789
|
+
const gone = [], foreign = [], keptForIdentity = [];
|
|
790
|
+
let names = [];
|
|
791
|
+
try {
|
|
792
|
+
names = fs.list(home);
|
|
793
|
+
}
|
|
794
|
+
catch (e) {
|
|
795
|
+
return { removed, left: [`${home} — could not be read (${e.message.split("\n")[0]}), so nothing in it was removed`], gone: false };
|
|
796
|
+
}
|
|
797
|
+
for (const name of names.sort()) {
|
|
798
|
+
const p = join(home, name);
|
|
799
|
+
const e = homeEntryFor(name);
|
|
800
|
+
if (!e) {
|
|
801
|
+
foreign.push(name);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (spare.has(resolve(p)))
|
|
805
|
+
continue;
|
|
806
|
+
if (e.kept === "managed")
|
|
807
|
+
continue;
|
|
808
|
+
if (e.kept) {
|
|
809
|
+
left.push(`${e.what}, ${p} — kept: ${e.kept}`);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (o.keepIdentity && e.identity) {
|
|
813
|
+
keptForIdentity.push(name);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const why = attempt(p, fs.remove);
|
|
817
|
+
if (why === null)
|
|
818
|
+
gone.push(name);
|
|
819
|
+
else
|
|
820
|
+
left.push(`${e.what}, ${p} — could not remove (${why})${lockedHint}`);
|
|
821
|
+
}
|
|
822
|
+
if (gone.length)
|
|
823
|
+
removed.push(`Pantheon's own files in ${home}: ${gone.join(", ")}`);
|
|
824
|
+
if (keptForIdentity.length)
|
|
825
|
+
left.push(`what goes with your identity in ${home}: ${keptForIdentity.join(", ")} (--keep-identity)`);
|
|
826
|
+
let rest = [];
|
|
827
|
+
try {
|
|
828
|
+
rest = fs.list(home);
|
|
829
|
+
}
|
|
830
|
+
catch { }
|
|
831
|
+
if (!rest.length) {
|
|
832
|
+
const why = attempt(home, fs.removeDir);
|
|
833
|
+
if (why === null) {
|
|
834
|
+
removed.push(`${home} itself, now empty`);
|
|
835
|
+
return { removed, left, gone: true };
|
|
836
|
+
}
|
|
837
|
+
left.push(`${home} — empty, but could not remove it (${why})${lockedHint}`);
|
|
838
|
+
return { removed, left, gone: false };
|
|
839
|
+
}
|
|
840
|
+
if (foreign.length)
|
|
841
|
+
left.push(`${home}, for what is not Pantheon's: ${foreign.join(", ")} — Pantheon never writes ${foreign.length === 1 ? "it" : "these"}, so ${foreign.length === 1 ? "it was" : "they were"} not touched`);
|
|
842
|
+
return { removed, left, gone: false };
|
|
843
|
+
}
|
|
673
844
|
export const accountStillExists = (email) => `your account${email ? ` ${email}` : ""} still exists — this machine is signed out. Sign back in with: pantheon login${email ? ` --email ${email}` : ""} (pantheon signup would fail: the email is taken). To delete the account itself: pantheon account delete, before leaving`;
|
|
674
845
|
const LOG_KINDS = ["worker", "relay", "notifier", "menubar"];
|
|
675
846
|
export function uninstallMachine(opts, io) {
|
package/dist/managed.js
CHANGED
|
@@ -23,6 +23,7 @@ export function managedLayout(execPath = process.execPath, os = platform()) {
|
|
|
23
23
|
root, nodeDir, node: p, npm: win32.join(nodeDir, "npm.cmd"), npmCli: win32.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"), binDir,
|
|
24
24
|
launcher: win32.join(binDir, "pantheon.cmd"), pkgDir,
|
|
25
25
|
entry: win32.join(pkgDir, "bin", "pantheon.js"), cli: win32.join(pkgDir, "dist", "cli.js"),
|
|
26
|
+
prevDir: win32.join(runtime, "cli.prev"),
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
const p = posix.normalize(execPath);
|
|
@@ -42,8 +43,33 @@ export function managedLayout(execPath = process.execPath, os = platform()) {
|
|
|
42
43
|
root, nodeDir, node: p, npm: posix.join(bin, "npm"), npmCli: posix.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"), binDir,
|
|
43
44
|
launcher: posix.join(binDir, "pantheon"), pkgDir,
|
|
44
45
|
entry: posix.join(pkgDir, "bin", "pantheon.js"), cli: posix.join(pkgDir, "dist", "cli.js"),
|
|
46
|
+
prevDir: posix.join(runtime, "cli.prev"),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
49
|
+
const shq = (p) => `'${p.replace(/'/g, `'\\''`)}'`;
|
|
50
|
+
export function launcherRepairLines(m, os = platform()) {
|
|
51
|
+
if (os === "win32") {
|
|
52
|
+
return [
|
|
53
|
+
"rem An upgrade cut off mid-swap leaves the previous version beside the runtime: put it back.",
|
|
54
|
+
'if not exist "%~dp0..\\runtime\\node\\node_modules\\@join-pantheon\\cli\\" if exist "%~dp0..\\runtime\\cli.prev\\bin\\pantheon.js" move "%~dp0..\\runtime\\cli.prev" "%~dp0..\\runtime\\node\\node_modules\\@join-pantheon\\cli" >nul 2>&1',
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
"# An upgrade cut off mid-swap leaves the previous version beside the runtime: put it back.",
|
|
59
|
+
`[ -e ${shq(m.pkgDir)} ] || { [ -f ${shq(posix.join(m.prevDir, "bin", "pantheon.js"))} ] && mv ${shq(m.prevDir)} ${shq(m.pkgDir)} 2>/dev/null; }`,
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
export function launcherWithRepair(current, m, os = platform()) {
|
|
63
|
+
if (current.includes("cli.prev"))
|
|
64
|
+
return null;
|
|
65
|
+
const nl = os === "win32" ? "\r\n" : "\n";
|
|
66
|
+
const lines = current.split(nl);
|
|
67
|
+
const at = os === "win32" ? lines.findIndex((l) => l.startsWith('"%~dp0..\\runtime\\node\\node.exe"')) : lines.findIndex((l) => l.startsWith("exec "));
|
|
68
|
+
if (at < 0)
|
|
69
|
+
return null;
|
|
70
|
+
lines.splice(at, 0, ...launcherRepairLines(m, os));
|
|
71
|
+
return lines.join(nl);
|
|
72
|
+
}
|
|
47
73
|
export function childNodeFlags(execArgv = process.execArgv, layout = managedLayout()) {
|
|
48
74
|
return execArgv.includes("--use-system-ca") || layout ? ["--use-system-ca"] : [];
|
|
49
75
|
}
|
|
@@ -178,7 +204,7 @@ export function installerLine(relay, shell, args = []) {
|
|
|
178
204
|
if (shell === "ps" && !args.length)
|
|
179
205
|
return `irm ${base}/install.ps1 | iex`;
|
|
180
206
|
const set = args.length ? `Set-Item Env:PANTHEON_ARGS '${args.join(" ")}'; ` : "";
|
|
181
|
-
return `powershell -NoProfile -
|
|
207
|
+
return `powershell -NoProfile -Command "${set}irm ${base}/install.ps1 | iex"`;
|
|
182
208
|
}
|
|
183
209
|
export function installerLineHere(relay, os = platform()) {
|
|
184
210
|
return installerLine(relay, os === "win32" ? "ps" : "sh") ?? installerLine(HOSTED_INSTALLER_BASE, os === "win32" ? "ps" : "sh");
|