mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
feat: add Windows PowerShell support — remove bash dependency for local execution (#2727)
Replace hardcoded "bash" shell references with platform-aware utilities so spawn works natively from PowerShell on Windows without WSL or Git Bash. - New shared/shell.ts: isWindows(), getLocalShell(), getInstallScriptUrl(), getInstallCmd(), getWhichCommand() with platform override for testability - local/local.ts: use getLocalShell() for runLocal() and interactiveSession() - commands/run.ts: spawnScript/runScriptHeadless use getLocalShell() - commands/update.ts: Windows downloads install.ps1, runs via PowerShell - update-check.ts: Windows auto-update uses install.ps1; "where" replaces "which" - shared/orchestrate.ts: PowerShell-compatible .spawnrc setup for local Windows - Remote SSH commands unchanged — remote servers are always Linux Closes #2726 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
This commit is contained in:
parent
ba94f681b3
commit
66b16d8651
8 changed files with 306 additions and 54 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.20.11",
|
||||
"version": "0.21.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
92
packages/cli/src/__tests__/shell.test.ts
Normal file
92
packages/cli/src/__tests__/shell.test.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Tests for shared/shell.ts — platform-aware shell execution utilities.
|
||||
*
|
||||
* Uses platform parameter overrides for testability since process.platform is read-only.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { getInstallCmd, getInstallScriptUrl, getLocalShell, getWhichCommand, isWindows } from "../shared/shell";
|
||||
|
||||
const CDN = "https://example.com";
|
||||
|
||||
describe("isWindows", () => {
|
||||
it("returns true for win32", () => {
|
||||
expect(isWindows("win32")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for darwin", () => {
|
||||
expect(isWindows("darwin")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for linux", () => {
|
||||
expect(isWindows("linux")).toBe(false);
|
||||
});
|
||||
|
||||
it("uses process.platform when no override", () => {
|
||||
expect(isWindows()).toBe(process.platform === "win32");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLocalShell", () => {
|
||||
it("returns powershell on Windows", () => {
|
||||
const [shell, flag] = getLocalShell("win32");
|
||||
expect(shell).toBe("powershell.exe");
|
||||
expect(flag).toBe("-Command");
|
||||
});
|
||||
|
||||
it("returns bash on macOS", () => {
|
||||
const [shell, flag] = getLocalShell("darwin");
|
||||
expect(shell).toBe("bash");
|
||||
expect(flag).toBe("-c");
|
||||
});
|
||||
|
||||
it("returns bash on Linux", () => {
|
||||
const [shell, flag] = getLocalShell("linux");
|
||||
expect(shell).toBe("bash");
|
||||
expect(flag).toBe("-c");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInstallScriptUrl", () => {
|
||||
it("returns .ps1 URL on Windows", () => {
|
||||
expect(getInstallScriptUrl(CDN, "win32")).toBe(`${CDN}/cli/install.ps1`);
|
||||
});
|
||||
|
||||
it("returns .sh URL on macOS", () => {
|
||||
expect(getInstallScriptUrl(CDN, "darwin")).toBe(`${CDN}/cli/install.sh`);
|
||||
});
|
||||
|
||||
it("returns .sh URL on Linux", () => {
|
||||
expect(getInstallScriptUrl(CDN, "linux")).toBe(`${CDN}/cli/install.sh`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInstallCmd", () => {
|
||||
it("returns irm | iex on Windows", () => {
|
||||
const cmd = getInstallCmd(CDN, "win32");
|
||||
expect(cmd).toContain("irm");
|
||||
expect(cmd).toContain("iex");
|
||||
expect(cmd).toContain("install.ps1");
|
||||
});
|
||||
|
||||
it("returns curl | bash on macOS", () => {
|
||||
const cmd = getInstallCmd(CDN, "darwin");
|
||||
expect(cmd).toContain("curl");
|
||||
expect(cmd).toContain("bash");
|
||||
expect(cmd).toContain("install.sh");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWhichCommand", () => {
|
||||
it("returns 'where' on Windows", () => {
|
||||
expect(getWhichCommand("win32")).toBe("where");
|
||||
});
|
||||
|
||||
it("returns 'which' on macOS", () => {
|
||||
expect(getWhichCommand("darwin")).toBe("which");
|
||||
});
|
||||
|
||||
it("returns 'which' on Linux", () => {
|
||||
expect(getWhichCommand("linux")).toBe("which");
|
||||
});
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@ import { generateSpawnId, getActiveServers, loadHistory, saveSpawnRecord } from
|
|||
import { loadManifest, RAW_BASE, REPO, SPAWN_CDN } from "../manifest.js";
|
||||
import { validateIdentifier, validatePrompt, validateScriptContent } from "../security.js";
|
||||
import { asyncTryCatch, isFileError, tryCatch, tryCatchIf } from "../shared/result.js";
|
||||
import { getLocalShell } from "../shared/shell.js";
|
||||
import { prepareStdinForHandoff, toKebabCase } from "../shared/ui.js";
|
||||
import { promptSetupOptions, promptSpawnName } from "./interactive.js";
|
||||
import { handleRecordAction } from "./list.js";
|
||||
|
|
@ -482,13 +483,14 @@ function handleUserInterrupt(errMsg: string, dashboardUrl?: string): void {
|
|||
process.exit(130);
|
||||
}
|
||||
|
||||
// ── Bash execution ───────────────────────────────────────────────────────────
|
||||
// ── Script execution ─────────────────────────────────────────────────────────
|
||||
|
||||
function spawnBash(script: string, env: Record<string, string | undefined>): void {
|
||||
function spawnScript(script: string, env: Record<string, string | undefined>): void {
|
||||
const [shell, flag] = getLocalShell();
|
||||
const result = spawnSync(
|
||||
"bash",
|
||||
shell,
|
||||
[
|
||||
"-c",
|
||||
flag,
|
||||
script,
|
||||
],
|
||||
{
|
||||
|
|
@ -543,7 +545,7 @@ function runBash(script: string, prompt?: string, debug?: boolean, spawnName?: s
|
|||
// gets a pristine file descriptor (prevents silent hangs / early exit)
|
||||
prepareStdinForHandoff();
|
||||
|
||||
spawnBash(script, env);
|
||||
spawnScript(script, env);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -700,8 +702,8 @@ function headlessError(
|
|||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
/** Run a bash script in headless mode (all output to stderr, no interactive session) */
|
||||
function runBashHeadless(script: string, prompt?: string, debug?: boolean, spawnName?: string): Promise<number> {
|
||||
/** Run a script in headless mode (all output to stderr, no interactive session) */
|
||||
function runScriptHeadless(script: string, prompt?: string, debug?: boolean, spawnName?: string): Promise<number> {
|
||||
validateScriptContent(script);
|
||||
|
||||
const env = {
|
||||
|
|
@ -720,11 +722,12 @@ function runBashHeadless(script: string, prompt?: string, debug?: boolean, spawn
|
|||
env.SPAWN_NAME_KEBAB = toKebabCase(spawnName);
|
||||
}
|
||||
|
||||
const [shell, flag] = getLocalShell();
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const child = spawn(
|
||||
"bash",
|
||||
shell,
|
||||
[
|
||||
"-c",
|
||||
flag,
|
||||
script,
|
||||
],
|
||||
{
|
||||
|
|
@ -885,7 +888,7 @@ export async function cmdRunHeadless(agent: string, cloud: string, opts: Headles
|
|||
console.error(`[headless] Executing ${resolvedAgent} on ${resolvedCloud}...`);
|
||||
}
|
||||
|
||||
const exitCode = await runBashHeadless(scriptContent, prompt, debug, spawnName);
|
||||
const exitCode = await runScriptHeadless(scriptContent, prompt, debug, spawnName);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
headlessError(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import { execFileSync } from "node:child_process";
|
||||
import { unlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import * as p from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { RAW_BASE, SPAWN_CDN, VERSION_URL } from "../manifest.js";
|
||||
import { parseJsonWith } from "../shared/parse.js";
|
||||
import { asyncTryCatch, tryCatch } from "../shared/result.js";
|
||||
import { asyncTryCatch, isFileError, tryCatch, tryCatchIf } from "../shared/result.js";
|
||||
import { getInstallCmd, getInstallScriptUrl, isWindows } from "../shared/shell.js";
|
||||
import { getErrorMessage, PkgVersionSchema, VERSION } from "./shared.js";
|
||||
|
||||
const INSTALL_URL = `${SPAWN_CDN}/cli/install.sh`;
|
||||
const INSTALL_CMD = `curl --proto '=https' -fsSL ${INSTALL_URL} | bash`;
|
||||
const INSTALL_URL = getInstallScriptUrl(SPAWN_CDN);
|
||||
const INSTALL_CMD = getInstallCmd(SPAWN_CDN);
|
||||
|
||||
async function fetchRemoteVersion(): Promise<string> {
|
||||
// Primary: plain-text version file from GitHub release artifact (static URL)
|
||||
|
|
@ -41,9 +44,49 @@ async function fetchRemoteVersion(): Promise<string> {
|
|||
return data.version;
|
||||
}
|
||||
|
||||
function defaultRunUpdate(): void {
|
||||
// Two-step: fetch with --proto '=https', then execute via bash -c
|
||||
// Prevents protocol downgrade on hostile networks (matches update-check.ts pattern)
|
||||
function runWindowsUpdate(): void {
|
||||
const scriptContent = execFileSync(
|
||||
"curl",
|
||||
[
|
||||
"--proto",
|
||||
"=https",
|
||||
"-fsSL",
|
||||
INSTALL_URL,
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
stdio: [
|
||||
"pipe",
|
||||
"pipe",
|
||||
"inherit",
|
||||
],
|
||||
},
|
||||
);
|
||||
// Write to temp file and execute via PowerShell (avoids string escaping issues)
|
||||
const tmpFile = `${tmpdir()}\\spawn-install-${Date.now()}.ps1`;
|
||||
writeFileSync(tmpFile, scriptContent ?? "");
|
||||
const execResult = tryCatch(() =>
|
||||
execFileSync(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
tmpFile,
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
),
|
||||
);
|
||||
// Best-effort cleanup of temp file
|
||||
tryCatchIf(isFileError, () => unlinkSync(tmpFile));
|
||||
if (!execResult.ok) {
|
||||
throw execResult.error;
|
||||
}
|
||||
}
|
||||
|
||||
function runUnixUpdate(): void {
|
||||
const scriptContent = execFileSync(
|
||||
"curl",
|
||||
[
|
||||
|
|
@ -73,6 +116,14 @@ function defaultRunUpdate(): void {
|
|||
);
|
||||
}
|
||||
|
||||
function defaultRunUpdate(): void {
|
||||
if (isWindows()) {
|
||||
runWindowsUpdate();
|
||||
} else {
|
||||
runUnixUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
async function performUpdate(runUpdate: () => void = defaultRunUpdate): Promise<void> {
|
||||
const r = tryCatch(() => runUpdate());
|
||||
if (r.ok) {
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@
|
|||
import { copyFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { getUserHome } from "../shared/paths";
|
||||
import { getLocalShell } from "../shared/shell";
|
||||
import { spawnInteractive } from "../shared/ssh";
|
||||
|
||||
// ─── Execution ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Run a shell command locally and wait for it to finish. */
|
||||
export async function runLocal(cmd: string): Promise<void> {
|
||||
const [shell, flag] = getLocalShell();
|
||||
const proc = Bun.spawn(
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
shell,
|
||||
flag,
|
||||
cmd,
|
||||
],
|
||||
{
|
||||
|
|
@ -54,9 +56,10 @@ export function downloadFile(remotePath: string, localPath: string): void {
|
|||
|
||||
/** Launch an interactive shell session locally. */
|
||||
export async function interactiveSession(cmd: string): Promise<number> {
|
||||
const [shell, flag] = getLocalShell();
|
||||
return spawnInteractive([
|
||||
"bash",
|
||||
"-c",
|
||||
shell,
|
||||
flag,
|
||||
cmd,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { generateEnvConfig } from "./agents";
|
|||
import { getOrPromptApiKey } from "./oauth";
|
||||
import { getSpawnPreferencesPath } from "./paths";
|
||||
import { asyncTryCatch, asyncTryCatchIf, isFileError, isOperationalError, tryCatchIf } from "./result.js";
|
||||
import { isWindows } from "./shell";
|
||||
import { startSshTunnel } from "./ssh";
|
||||
import { ensureSshKeys, getSshKeyOpts } from "./ssh-keys";
|
||||
import {
|
||||
|
|
@ -199,21 +200,19 @@ export async function runOrchestration(
|
|||
// 9. Inject environment variables via .spawnrc
|
||||
logStep("Setting up environment variables...");
|
||||
const envB64 = Buffer.from(envContent).toString("base64");
|
||||
|
||||
// On Windows local execution, use PowerShell-compatible env setup.
|
||||
// Remote servers (SSH) are always Linux, so bash commands are correct for all non-local clouds.
|
||||
const isLocalWindows = cloud.cloudName === "local" && isWindows();
|
||||
const envSetupCmd = isLocalWindows
|
||||
? `$bytes = [Convert]::FromBase64String('${envB64}'); ` + `[IO.File]::WriteAllBytes("$HOME/.spawnrc", $bytes)`
|
||||
: `printf '%s' '${envB64}' | base64 -d > ~/.spawnrc && chmod 600 ~/.spawnrc; ` +
|
||||
"for _rc in ~/.bashrc ~/.profile ~/.bash_profile ~/.zshrc; do " +
|
||||
`grep -q 'source ~/.spawnrc' "$_rc" 2>/dev/null || echo '[ -f ~/.spawnrc ] && source ~/.spawnrc' >> "$_rc"; ` +
|
||||
"done";
|
||||
|
||||
const envResult = await asyncTryCatch(() =>
|
||||
withRetry(
|
||||
"env setup",
|
||||
() =>
|
||||
wrapSshCall(
|
||||
cloud.runner.runServer(
|
||||
`printf '%s' '${envB64}' | base64 -d > ~/.spawnrc && chmod 600 ~/.spawnrc; ` +
|
||||
"for _rc in ~/.bashrc ~/.profile ~/.bash_profile ~/.zshrc; do " +
|
||||
`grep -q 'source ~/.spawnrc' "$_rc" 2>/dev/null || echo '[ -f ~/.spawnrc ] && source ~/.spawnrc' >> "$_rc"; ` +
|
||||
"done",
|
||||
),
|
||||
),
|
||||
2,
|
||||
5,
|
||||
),
|
||||
withRetry("env setup", () => wrapSshCall(cloud.runner.runServer(envSetupCmd)), 2, 5),
|
||||
);
|
||||
if (!envResult.ok) {
|
||||
logWarn("Environment setup had errors");
|
||||
|
|
|
|||
71
packages/cli/src/shared/shell.ts
Normal file
71
packages/cli/src/shared/shell.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// shared/shell.ts — Platform-aware shell execution utilities
|
||||
// Enables spawn CLI to work natively on Windows (PowerShell) without requiring bash.
|
||||
|
||||
/**
|
||||
* Check if the current platform is Windows.
|
||||
* Accepts an optional override for testability (process.platform is read-only).
|
||||
*/
|
||||
export function isWindows(platform?: string): boolean {
|
||||
return (platform ?? process.platform) === "win32";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local shell executable and its command flag for the current platform.
|
||||
* - Windows: ["powershell.exe", "-Command"]
|
||||
* - macOS/Linux: ["bash", "-c"]
|
||||
*
|
||||
* Accepts an optional platform override for testability.
|
||||
*/
|
||||
export function getLocalShell(platform?: string): [
|
||||
string,
|
||||
string,
|
||||
] {
|
||||
if (isWindows(platform)) {
|
||||
return [
|
||||
"powershell.exe",
|
||||
"-Command",
|
||||
];
|
||||
}
|
||||
return [
|
||||
"bash",
|
||||
"-c",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the install script URL for the current platform.
|
||||
* - Windows: install.ps1
|
||||
* - macOS/Linux: install.sh
|
||||
*/
|
||||
export function getInstallScriptUrl(cdnBase: string, platform?: string): string {
|
||||
if (isWindows(platform)) {
|
||||
return `${cdnBase}/cli/install.ps1`;
|
||||
}
|
||||
return `${cdnBase}/cli/install.sh`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command to display for manual update instructions.
|
||||
* - Windows: PowerShell download + execute
|
||||
* - macOS/Linux: curl | bash
|
||||
*/
|
||||
export function getInstallCmd(cdnBase: string, platform?: string): string {
|
||||
if (isWindows(platform)) {
|
||||
const url = `${cdnBase}/cli/install.ps1`;
|
||||
return `irm ${url} | iex`;
|
||||
}
|
||||
const url = `${cdnBase}/cli/install.sh`;
|
||||
return `curl --proto '=https' -fsSL ${url} | bash`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command name to locate executables on the current platform.
|
||||
* - Windows: "where"
|
||||
* - macOS/Linux: "which"
|
||||
*/
|
||||
export function getWhichCommand(platform?: string): string {
|
||||
if (isWindows(platform)) {
|
||||
return "where";
|
||||
}
|
||||
return "which";
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import type { ExecFileSyncOptions } from "node:child_process";
|
|||
|
||||
import { execFileSync as nodeExecFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { getErrorMessage, hasStatus } from "@openrouter/spawn-shared";
|
||||
import pc from "picocolors";
|
||||
|
|
@ -11,6 +12,7 @@ import { RAW_BASE, SPAWN_CDN, VERSION_URL } from "./manifest.js";
|
|||
import { PkgVersionSchema, parseJsonWith } from "./shared/parse";
|
||||
import { getUpdateFailedPath } from "./shared/paths";
|
||||
import { asyncTryCatchIf, isFileError, isNetworkError, tryCatch, tryCatchIf, unwrapOr } from "./shared/result";
|
||||
import { getInstallCmd, getInstallScriptUrl, getWhichCommand, isWindows } from "./shared/shell";
|
||||
import { logDebug, logWarn } from "./shared/ui";
|
||||
|
||||
const VERSION = pkg.version;
|
||||
|
|
@ -140,14 +142,17 @@ function printUpdateBanner(latestVersion: string): void {
|
|||
/**
|
||||
* Find the spawn binary to re-exec after an update.
|
||||
*
|
||||
* Prefers `which spawn` (PATH resolution) over process.argv[1] because the
|
||||
* installer may place the new binary in a different directory than where the
|
||||
* currently running binary lives, causing re-exec to run the stale old binary.
|
||||
* Prefers PATH resolution over process.argv[1] because the installer may place
|
||||
* the new binary in a different directory than where the currently running
|
||||
* binary lives, causing re-exec to run the stale old binary.
|
||||
*
|
||||
* Uses `where` on Windows, `which` on macOS/Linux.
|
||||
*/
|
||||
function findUpdatedBinary(): string {
|
||||
const whichCmd = getWhichCommand();
|
||||
const r = tryCatch(() =>
|
||||
executor.execFileSync(
|
||||
"which",
|
||||
whichCmd,
|
||||
[
|
||||
"spawn",
|
||||
],
|
||||
|
|
@ -161,7 +166,8 @@ function findUpdatedBinary(): string {
|
|||
},
|
||||
),
|
||||
);
|
||||
const found = r.ok && r.data ? r.data.toString().trim() : "";
|
||||
// `where` on Windows may return multiple lines; take the first
|
||||
const found = r.ok && r.data ? r.data.toString().trim().split("\n")[0].trim() : "";
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
|
@ -200,11 +206,11 @@ function reExecWithArgs(): void {
|
|||
function performAutoUpdate(latestVersion: string): void {
|
||||
printUpdateBanner(latestVersion);
|
||||
|
||||
// Hardcoded CDN URL — no variable interpolation, eliminates CWE-78 concern entirely
|
||||
const installUrl = `${SPAWN_CDN}/cli/install.sh`;
|
||||
const installUrl = getInstallScriptUrl(SPAWN_CDN);
|
||||
const installCmd = getInstallCmd(SPAWN_CDN);
|
||||
|
||||
const updateResult = tryCatch(() => {
|
||||
// Two-step approach: fetch script bytes with curl, then execute via bash -c
|
||||
// Fetch script bytes with curl (available on all modern platforms)
|
||||
const scriptBytes = executor.execFileSync(
|
||||
"curl",
|
||||
[
|
||||
|
|
@ -223,16 +229,43 @@ function performAutoUpdate(latestVersion: string): void {
|
|||
},
|
||||
);
|
||||
const scriptContent = scriptBytes ? scriptBytes.toString() : "";
|
||||
executor.execFileSync(
|
||||
"bash",
|
||||
[
|
||||
"-c",
|
||||
scriptContent,
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
|
||||
if (isWindows()) {
|
||||
// Windows: write to temp file and execute via PowerShell
|
||||
const tmpFile = path.join(tmpdir(), `spawn-install-${Date.now()}.ps1`);
|
||||
fs.writeFileSync(tmpFile, scriptContent);
|
||||
const psResult = tryCatch(() =>
|
||||
executor.execFileSync(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
tmpFile,
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
),
|
||||
);
|
||||
// Best-effort cleanup of temp file
|
||||
tryCatchIf(isFileError, () => fs.unlinkSync(tmpFile));
|
||||
if (!psResult.ok) {
|
||||
throw psResult.error;
|
||||
}
|
||||
} else {
|
||||
// macOS/Linux: execute via bash -c
|
||||
executor.execFileSync(
|
||||
"bash",
|
||||
[
|
||||
"-c",
|
||||
scriptContent,
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (updateResult.ok) {
|
||||
|
|
@ -246,7 +279,7 @@ function performAutoUpdate(latestVersion: string): void {
|
|||
console.error(pc.red(pc.bold(`${CROSS_MARK} Auto-update failed`)));
|
||||
console.error(pc.dim(" Please update manually:"));
|
||||
console.error();
|
||||
console.error(pc.cyan(` curl -fsSL ${installUrl} | bash`));
|
||||
console.error(pc.cyan(` ${installCmd}`));
|
||||
console.error();
|
||||
// Continue with original command despite update failure
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue