Commit current desktop and CLI diff
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Image, Text, View } from "react-native";
|
||||
import { ActivityIndicator, Alert, Text, View } from "react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import * as QRCode from "qrcode";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
RotateCw,
|
||||
Copy,
|
||||
FileText,
|
||||
Smartphone,
|
||||
Activity,
|
||||
} from "lucide-react-native";
|
||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||
@@ -24,7 +22,6 @@ import { isVersionMismatch } from "@/desktop/updates/desktop-updates";
|
||||
import {
|
||||
getCliDaemonStatus,
|
||||
getDesktopDaemonLogs,
|
||||
getDesktopDaemonPairing,
|
||||
getDesktopDaemonStatus,
|
||||
restartDesktopDaemon,
|
||||
shouldUseDesktopDaemon,
|
||||
@@ -32,7 +29,6 @@ import {
|
||||
stopDesktopDaemon,
|
||||
type DesktopDaemonLogs,
|
||||
type DesktopDaemonStatus,
|
||||
type DesktopPairingOffer,
|
||||
} from "@/desktop/daemon/desktop-daemon";
|
||||
|
||||
export interface LocalDaemonSectionProps {
|
||||
@@ -52,10 +48,6 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
|
||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||
const [daemonLogs, setDaemonLogs] = useState<DesktopDaemonLogs | null>(null);
|
||||
const [isLogsModalOpen, setIsLogsModalOpen] = useState(false);
|
||||
const [isPairingModalOpen, setIsPairingModalOpen] = useState(false);
|
||||
const [isLoadingPairing, setIsLoadingPairing] = useState(false);
|
||||
const [pairingOffer, setPairingOffer] = useState<DesktopPairingOffer | null>(null);
|
||||
const [pairingStatusMessage, setPairingStatusMessage] = useState<string | null>(null);
|
||||
const [cliStatusOutput, setCliStatusOutput] = useState<string | null>(null);
|
||||
const [isCliStatusModalOpen, setIsCliStatusModalOpen] = useState(false);
|
||||
const [isLoadingCliStatus, setIsLoadingCliStatus] = useState(false);
|
||||
@@ -238,46 +230,6 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
|
||||
setIsLogsModalOpen(true);
|
||||
}, [daemonLogs]);
|
||||
|
||||
const handleOpenPairingModal = useCallback(() => {
|
||||
if (isLoadingPairing) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPairingModalOpen(true);
|
||||
setIsLoadingPairing(true);
|
||||
setPairingStatusMessage(null);
|
||||
|
||||
void getDesktopDaemonPairing()
|
||||
.then((pairing) => {
|
||||
setPairingOffer(pairing);
|
||||
if (!pairing.relayEnabled || !pairing.url) {
|
||||
setPairingStatusMessage("Relay pairing is not available.");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setPairingOffer(null);
|
||||
setPairingStatusMessage(`Unable to load pairing offer: ${message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingPairing(false);
|
||||
});
|
||||
}, [isLoadingPairing]);
|
||||
|
||||
const handleCopyPairingLink = useCallback(() => {
|
||||
if (!pairingOffer?.url) {
|
||||
return;
|
||||
}
|
||||
void Clipboard.setStringAsync(pairingOffer.url)
|
||||
.then(() => {
|
||||
Alert.alert("Copied", "Pairing link copied.");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[Settings] Failed to copy pairing link", error);
|
||||
Alert.alert("Error", "Unable to copy pairing link.");
|
||||
});
|
||||
}, [pairingOffer?.url]);
|
||||
|
||||
const handleOpenCliStatus = useCallback(async () => {
|
||||
setIsLoadingCliStatus(true);
|
||||
try {
|
||||
@@ -418,20 +370,6 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>Pair device</Text>
|
||||
<Text style={settingsStyles.rowHint}>Connect your phone to this computer.</Text>
|
||||
</View>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<Smartphone size={theme.iconSize.sm} color={theme.colors.foreground} />}
|
||||
onPress={handleOpenPairingModal}
|
||||
>
|
||||
Pair device
|
||||
</Button>
|
||||
</View>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>Full status</Text>
|
||||
@@ -460,20 +398,6 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<AdaptiveModalSheet
|
||||
visible={isPairingModalOpen}
|
||||
onClose={() => setIsPairingModalOpen(false)}
|
||||
title="Pair device"
|
||||
testID="managed-daemon-pairing-dialog"
|
||||
>
|
||||
<PairingOfferDialogContent
|
||||
isLoading={isLoadingPairing}
|
||||
pairingOffer={pairingOffer}
|
||||
statusMessage={pairingStatusMessage}
|
||||
onCopyLink={handleCopyPairingLink}
|
||||
/>
|
||||
</AdaptiveModalSheet>
|
||||
|
||||
<AdaptiveModalSheet
|
||||
visible={isLogsModalOpen}
|
||||
onClose={() => setIsLogsModalOpen(false)}
|
||||
@@ -516,107 +440,6 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
|
||||
|
||||
const ADVANCED_DAEMON_SETTINGS_URL = "https://paseo.sh/docs/configuration";
|
||||
|
||||
function PairingOfferDialogContent(input: {
|
||||
isLoading: boolean;
|
||||
pairingOffer: DesktopPairingOffer | null;
|
||||
statusMessage: string | null;
|
||||
onCopyLink: () => void;
|
||||
}) {
|
||||
const { isLoading, pairingOffer, statusMessage, onCopyLink } = input;
|
||||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||
const [qrError, setQrError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
if (!pairingOffer?.url) {
|
||||
setQrDataUrl(null);
|
||||
setQrError(null);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
setQrError(null);
|
||||
setQrDataUrl(null);
|
||||
|
||||
void QRCode.toDataURL(pairingOffer.url, {
|
||||
errorCorrectionLevel: "M",
|
||||
margin: 1,
|
||||
width: 480,
|
||||
})
|
||||
.then((dataUrl) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setQrDataUrl(dataUrl);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setQrError(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pairingOffer?.url]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.pairingState}>
|
||||
<ActivityIndicator size="small" />
|
||||
<Text style={settingsStyles.rowHint}>Loading pairing offer…</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (statusMessage) {
|
||||
return (
|
||||
<View style={styles.modalBody}>
|
||||
<Text style={settingsStyles.rowHint}>{statusMessage}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!pairingOffer?.url) {
|
||||
return (
|
||||
<View style={styles.modalBody}>
|
||||
<Text style={settingsStyles.rowHint}>Pairing offer unavailable.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.modalBody}>
|
||||
<Text style={settingsStyles.rowHint}>
|
||||
Scan this QR code in Paseo, or copy the pairing link below.
|
||||
</Text>
|
||||
<View style={styles.qrCard}>
|
||||
{qrDataUrl ? (
|
||||
<Image source={{ uri: qrDataUrl }} style={styles.qrImage} resizeMode="contain" />
|
||||
) : qrError ? (
|
||||
<Text style={settingsStyles.rowHint}>QR unavailable: {qrError}</Text>
|
||||
) : (
|
||||
<ActivityIndicator size="small" />
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.linkSection}>
|
||||
<Text style={styles.linkLabel}>Pairing link</Text>
|
||||
<Text style={styles.linkText} selectable>
|
||||
{pairingOffer.url}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.modalActions}>
|
||||
<Button variant="outline" size="sm" onPress={onCopyLink}>
|
||||
Copy link
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
actionGroup: {
|
||||
flexDirection: "row",
|
||||
@@ -659,40 +482,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
gap: theme.spacing[3],
|
||||
paddingBottom: theme.spacing[2],
|
||||
},
|
||||
pairingState: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[6],
|
||||
},
|
||||
qrCard: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
aspectRatio: 1,
|
||||
alignSelf: "stretch",
|
||||
padding: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
qrImage: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
linkSection: {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
linkLabel: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
linkText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
lineHeight: 18,
|
||||
},
|
||||
logOutput: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
Shield,
|
||||
Puzzle,
|
||||
Blocks,
|
||||
Smartphone,
|
||||
} from "lucide-react-native";
|
||||
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
|
||||
import type { HostProfile, HostConnection } from "@/types/host-connection";
|
||||
@@ -54,6 +55,7 @@ import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-mod
|
||||
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
|
||||
import { IntegrationsSection } from "@/desktop/components/integrations-section";
|
||||
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
|
||||
import { PairDeviceSection } from "@/desktop/components/pair-device-section";
|
||||
import { isElectronRuntime } from "@/desktop/host";
|
||||
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
|
||||
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
|
||||
@@ -82,7 +84,8 @@ type SettingsSectionId =
|
||||
| "diagnostics"
|
||||
| "about"
|
||||
| "permissions"
|
||||
| "daemon";
|
||||
| "daemon"
|
||||
| "pair-device";
|
||||
|
||||
interface SettingsSectionDef {
|
||||
id: SettingsSectionId;
|
||||
@@ -101,6 +104,7 @@ function getSettingsSections(context: { isDesktopApp: boolean }): SettingsSectio
|
||||
if (context.isDesktopApp) {
|
||||
sections.push(
|
||||
{ id: "integrations", label: "Integrations", icon: Puzzle },
|
||||
{ id: "pair-device", label: "Pair device", icon: Smartphone },
|
||||
{ id: "daemon", label: "Daemon", icon: Settings },
|
||||
);
|
||||
}
|
||||
@@ -647,6 +651,8 @@ function SettingsSectionContent({
|
||||
return isDesktopApp ? <IntegrationsSection /> : null;
|
||||
case "permissions":
|
||||
return isDesktopApp ? <DesktopPermissionsSection /> : null;
|
||||
case "pair-device":
|
||||
return isDesktopApp ? <PairDeviceSection /> : null;
|
||||
case "daemon":
|
||||
return isDesktopApp ? (
|
||||
<LocalDaemonSection appVersion={appVersion} showLifecycleControls={isLocalDaemon} />
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Command } from "commander";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
getOrCreateServerId,
|
||||
findExecutable,
|
||||
quoteWindowsCommand,
|
||||
applyProviderEnv,
|
||||
} from "@getpaseo/server";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
import { tryConnectToDaemon } from "../../utils/client.js";
|
||||
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
|
||||
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
||||
@@ -171,31 +173,33 @@ const PROVIDER_BINARIES: { label: string; binary: string }[] = [
|
||||
{ label: "OpenCode", binary: "opencode" },
|
||||
];
|
||||
|
||||
function checkProviderBinary(binary: string): { path: string | null; version: string | null } {
|
||||
const binaryPath = findExecutable(binary);
|
||||
async function checkProviderBinary(binary: string): Promise<{ path: string | null; version: string | null }> {
|
||||
const binaryPath = await findExecutable(binary);
|
||||
if (!binaryPath) {
|
||||
return { path: null, version: null };
|
||||
}
|
||||
const env = applyProviderEnv(process.env);
|
||||
try {
|
||||
const output = execFileSync(quoteWindowsCommand(binaryPath), ["--version"], {
|
||||
const { stdout } = await execFileAsync(binaryPath, ["--version"], {
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env,
|
||||
shell: process.platform === "win32",
|
||||
}).trim();
|
||||
return { path: binaryPath, version: output || null };
|
||||
windowsHide: true,
|
||||
});
|
||||
return { path: binaryPath, version: stdout.trim() || null };
|
||||
} catch {
|
||||
return { path: binaryPath, version: null };
|
||||
}
|
||||
}
|
||||
|
||||
function checkProviderBinaries(): ProviderBinaryStatus[] {
|
||||
return PROVIDER_BINARIES.map(({ label, binary }) => {
|
||||
const result = checkProviderBinary(binary);
|
||||
return { label, ...result };
|
||||
});
|
||||
async function checkProviderBinaries(): Promise<ProviderBinaryStatus[]> {
|
||||
const results = await Promise.all(
|
||||
PROVIDER_BINARIES.map(async ({ label, binary }) => {
|
||||
const result = await checkProviderBinary(binary);
|
||||
return { label, ...result };
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
function resolveOwnerLabel(uid: number | undefined, hostname: string | undefined): string | null {
|
||||
@@ -295,7 +299,7 @@ export async function runStatusCommand(
|
||||
note = appendNote(note, `serverId unavailable: ${shortenMessage(normalizeError(error))}`);
|
||||
}
|
||||
|
||||
const providers = checkProviderBinaries();
|
||||
const providers = await checkProviderBinaries();
|
||||
|
||||
const daemonStatus: DaemonStatus = {
|
||||
serverId,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import assert from "node:assert";
|
||||
import { createE2ETestContext, type TestDaemonContext } from "../helpers/test-daemon.ts";
|
||||
|
||||
interface E2EContext extends TestDaemonContext {
|
||||
paseo: (
|
||||
args: string[],
|
||||
opts?: { timeout?: number; cwd?: string },
|
||||
) => Promise<{
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
let ctx: E2EContext;
|
||||
|
||||
async function setup(): Promise<void> {
|
||||
ctx = await createE2ETestContext({ timeout: 45_000 });
|
||||
}
|
||||
|
||||
async function cleanup(): Promise<void> {
|
||||
if (ctx) {
|
||||
await ctx.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function test_invalid_opencode_model_does_not_report_completed_while_still_running() {
|
||||
const result = await ctx.paseo(
|
||||
["run", "--provider", "opencode/adklasldkdas", "hello"],
|
||||
{ timeout: 45_000 },
|
||||
);
|
||||
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
const agentId = output.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0];
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
assert(
|
||||
output.toLowerCase().includes("error") || output.toLowerCase().includes("failed"),
|
||||
`expected invalid model failure output\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(agentId, `expected run output to include an agent id\nstdout:\n${result.stdout}`);
|
||||
|
||||
const inspect = await ctx.paseo(["inspect", agentId], { timeout: 15_000 });
|
||||
assert.strictEqual(inspect.exitCode, 0, `inspect failed\nstdout:\n${inspect.stdout}\nstderr:\n${inspect.stderr}`);
|
||||
|
||||
const runReportedCompleted = result.stdout.includes("completed");
|
||||
const inspectStillRunning = inspect.stdout.includes("Status running");
|
||||
|
||||
assert(
|
||||
!(runReportedCompleted && inspectStillRunning),
|
||||
`run reported completed while inspect still showed running\nrun stdout:\n${result.stdout}\ninspect stdout:\n${inspect.stdout}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await setup();
|
||||
await test_invalid_opencode_model_does_not_report_completed_while_still_running();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,9 +1,9 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { type ChildProcess } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { app, ipcMain } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import { resolvePaseoHome } from "@getpaseo/server";
|
||||
import { resolvePaseoHome, spawnProcess } from "@getpaseo/server";
|
||||
import {
|
||||
copyAttachmentFileToManagedStorage,
|
||||
deleteManagedAttachmentFile,
|
||||
@@ -266,15 +266,11 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
|
||||
args: invocation.args,
|
||||
});
|
||||
|
||||
const child: ChildProcess = spawn(
|
||||
invocation.command,
|
||||
invocation.args,
|
||||
{
|
||||
detached: true,
|
||||
env: { ...invocation.env, PASEO_DESKTOP_MANAGED: "1" },
|
||||
stdio: ["ignore", "ignore", "ignore"],
|
||||
},
|
||||
);
|
||||
const child: ChildProcess = spawnProcess(invocation.command, invocation.args, {
|
||||
detached: true,
|
||||
env: { ...invocation.env, PASEO_DESKTOP_MANAGED: "1" },
|
||||
stdio: ["ignore", "ignore", "ignore"],
|
||||
});
|
||||
|
||||
logDesktopDaemonLifecycle("detached spawn returned", {
|
||||
childPid: child.pid ?? null,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { spawnProcess } from "@getpaseo/server";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { app } from "electron";
|
||||
@@ -234,6 +235,7 @@ export function runCliPassthroughCommand(args: string[]): number {
|
||||
const result = spawnSync(invocation.command, invocation.args, {
|
||||
env: invocation.env,
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
@@ -252,7 +254,7 @@ function spawnAsync(
|
||||
options: { env: NodeJS.ProcessEnv },
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
const child = spawnProcess(command, args, {
|
||||
env: options.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
@@ -260,10 +262,10 @@ function spawnAsync(
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout.on("data", (data: Buffer) => {
|
||||
child.stdout!.on("data", (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
child.stderr.on("data", (data: Buffer) => {
|
||||
child.stderr!.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
|
||||
@@ -200,7 +200,20 @@ export async function installCli(): Promise<InstallStatus> {
|
||||
if (await pathOrSymlinkExists(targetPath)) {
|
||||
await fs.unlink(targetPath);
|
||||
}
|
||||
await fs.copyFile(installSourcePath, targetPath);
|
||||
// Generate a thin .cmd trampoline that delegates to the bundled shim.
|
||||
// Only the app install path is baked in — internal details (asar layout,
|
||||
// entrypoint scripts) live in the bundled shim and update with the app.
|
||||
const cmdContent = [
|
||||
"@echo off",
|
||||
`set "BUNDLED_CLI=${shimPath}"`,
|
||||
`if not exist "%BUNDLED_CLI%" (`,
|
||||
` echo Paseo CLI not found at %BUNDLED_CLI% — is Paseo installed? 1>&2`,
|
||||
` exit /b 1`,
|
||||
`)`,
|
||||
`call "%BUNDLED_CLI%" %*`,
|
||||
`exit /b %errorlevel%`,
|
||||
].join("\r\n");
|
||||
await fs.writeFile(targetPath, cmdContent, "utf-8");
|
||||
} else {
|
||||
if (await pathOrSymlinkExists(targetPath)) {
|
||||
await fs.unlink(targetPath);
|
||||
|
||||
Reference in New Issue
Block a user