Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions packages/cli/bin/finalrun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,10 @@ program
.command('upgrade')
.description('Upgrade the finalrun CLI by re-running the install script')
.option('--version <version>', 'Pin to a specific version (default: latest GitHub release)')
.option('--ci', 'Install only the binary (skip runtime tarball + prompts)')
.action(async (options: UpgradeCommandOptions) => {
await runCommand(async () => {
await runUpgrade({
version: options.version,
ci: options.ci === true,
});
});
});
Expand Down Expand Up @@ -365,7 +363,6 @@ interface InternalReportServerOptions {

interface UpgradeCommandOptions {
version?: string;
ci?: boolean;
}

async function runTestCommand(params: {
Expand Down
57 changes: 16 additions & 41 deletions packages/cli/src/upgradeCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
//
// We don't reimplement the install logic here; the install scripts
// (install.sh on macOS/Linux, install.ps1 on Windows) handle binary
// download, PATH wiring, and (interactively) the runtime tarball +
// host tools. This subcommand just spawns the right one.
// download, PATH wiring, runtime tarball, and host tools. This subcommand
// just spawns the right one with the same defaults a fresh user would get.
//
// Users who want a binary-only refresh (no runtime tarball, no prompts)
// should run the install script directly with --ci, or set CI=1 /
// FINALRUN_NON_INTERACTIVE=1 — install.sh honors those env vars itself.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawn } from 'node:child_process';
import { resolveCliPackageVersion } from './runtimePaths.js';

const INSTALL_SH_URL =
'https://raw.githubusercontent.com/final-run/finalrun-agent/main/scripts/install.sh';
Expand All @@ -18,53 +18,40 @@ const INSTALL_PS1_URL =

export interface UpgradeOptions {
version?: string;
/** Pass --ci to the installer (binary only, skip runtime tarball + prompts). */
ci?: boolean;
}

export async function runUpgrade(options: UpgradeOptions): Promise<void> {
// Mode detection: if the user explicitly passed --ci, honor it. Otherwise,
// mirror the user's previous install footprint — if they don't have the
// runtime tarball installed today, they probably want a binary-only
// upgrade too. If they DO have the runtime tarball, we want the installer
// to refresh it (default, no --ci flag).
let useCiFlag = options.ci === true;
if (!useCiFlag && !hasInstalledRuntime()) {
useCiFlag = true;
}

const targetLabel = options.version ? `v${options.version}` : 'latest';
const modeLabel = useCiFlag ? 'binary-only (--ci)' : 'full setup';
console.log(`Upgrading finalrun to ${targetLabel} (${modeLabel})...`);
console.log(`Upgrading finalrun to ${targetLabel}...`);
console.log('');

// Strip FINALRUN_* env vars from the inherited environment before spawning
// the installer. The current process may have been started with debugging
// overrides (FINALRUN_RUNTIME_ROOT, FINALRUN_ASSET_DIR, FINALRUN_CLOUD_URL,
// FINALRUN_CACHE_DIR, etc.) — those are runtime concerns for THIS binary
// and shouldn't influence where the installer puts the next version.
// FINALRUN_DIR is the one knob users intentionally pin install location
// with, so we preserve it. FINALRUN_VERSION we set explicitly when --version
// was passed; otherwise we drop it so the installer resolves "latest".
//
// The vars that DO mean something to the installer get explicitly carried
// over: FINALRUN_DIR (install root), FINALRUN_NON_INTERACTIVE (binary-only
// / no-prompt mode — install.sh and install.ps1 both honor this).
// FINALRUN_VERSION we set explicitly when --version was passed; otherwise
// we drop it so the installer resolves "latest".
const preservedDir = process.env['FINALRUN_DIR'];
const preservedNonInteractive = process.env['FINALRUN_NON_INTERACTIVE'];
const env: NodeJS.ProcessEnv = {};
for (const [key, value] of Object.entries(process.env)) {
if (!key.startsWith('FINALRUN_')) {
env[key] = value;
}
}
if (preservedDir) env['FINALRUN_DIR'] = preservedDir;
if (preservedNonInteractive) env['FINALRUN_NON_INTERACTIVE'] = preservedNonInteractive;
if (options.version) env['FINALRUN_VERSION'] = options.version;

let shell: string;
let shellArgs: string[];

if (process.platform === 'win32') {
// PowerShell's `irm | iex` reads the script as an in-memory string and
// can't forward arguments to it (no equivalent of bash's `-s --`). The
// CI flag travels via env var instead — install.ps1 honors FINALRUN_-
// NON_INTERACTIVE the same way install.sh does.
if (useCiFlag) env['FINALRUN_NON_INTERACTIVE'] = '1';
shell = 'powershell.exe';
shellArgs = [
'-NoProfile',
Expand All @@ -73,11 +60,9 @@ export async function runUpgrade(options: UpgradeOptions): Promise<void> {
`irm ${INSTALL_PS1_URL} | iex`,
];
} else {
// curl -fsSL <url> | bash [-s -- --ci]
// Implemented as `bash -c` so the pipe stays correctly inside one shell.
const flagPart = useCiFlag ? ' -s -- --ci' : '';
shell = 'bash';
shellArgs = ['-c', `curl -fsSL ${INSTALL_SH_URL} | bash${flagPart}`];
shellArgs = ['-c', `curl -fsSL ${INSTALL_SH_URL} | bash`];
}

await new Promise<void>((resolve, reject) => {
Expand All @@ -99,13 +84,3 @@ export async function runUpgrade(options: UpgradeOptions): Promise<void> {
});
});
}

function hasInstalledRuntime(): boolean {
const version = resolveCliPackageVersion();
const explicit = process.env['FINALRUN_RUNTIME_ROOT']?.trim();
if (explicit) {
return fs.existsSync(path.join(explicit, 'manifest.json'));
}
const finalrunDir = process.env['FINALRUN_DIR']?.trim() || path.join(os.homedir(), '.finalrun');
return fs.existsSync(path.join(finalrunDir, 'runtime', version, 'manifest.json'));
}