From 6bac4822006184c90d1552bd66007377462c87a4 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Mar 2026 19:40:31 +0200 Subject: [PATCH 01/11] fix: make install hidden command work when running install script --- scripts/install/dev-test-install.sh | 145 +++++++++++++++++++++++++ scripts/install/install.sh | 16 ++- src/commands/cli-management/install.ts | 91 ++++++++++++++-- 3 files changed, 239 insertions(+), 13 deletions(-) create mode 100644 scripts/install/dev-test-install.sh diff --git a/scripts/install/dev-test-install.sh b/scripts/install/dev-test-install.sh new file mode 100644 index 000000000..ab0ea2f7f --- /dev/null +++ b/scripts/install/dev-test-install.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script should be used like so: `/bin/cat dev-test-install.sh | bash` + +# Reset +Color_Off='' + +# Regular Colors +Red='' +Green='' +Dim='' # White + +# Bold +Bold_White='' +Bold_Green='' + +if [[ -t 1 ]]; then + # Reset + Color_Off='\033[0m' # Text Reset + + # Regular Colors + Red='\033[0;31m' # Red + Green='\033[0;32m' # Green + Dim='\033[0;2m' # White + + # Bold + Bold_Green='\033[1;32m' # Bold Green + Bold_White='\033[1m' # Bold White +fi + +error() { + echo -e "${Red}error${Color_Off}:" "$@" >&2 + exit 1 +} + +info() { + echo -e "${Dim}$@ ${Color_Off}" +} + +info_bold() { + echo -e "${Bold_White}$@ ${Color_Off}" +} + +success() { + echo -e "${Green}$@ ${Color_Off}" +} + +platform=$(uname -ms) + +case $platform in +'Darwin x86_64') + target=darwin-x64 + ;; +'Darwin arm64') + target=darwin-arm64 + ;; +'Linux aarch64' | 'Linux arm64') + target=linux-arm64 + ;; +'MINGW64'*) + target=windows-x64 + ;; +'Linux x86_64' | *) + target=linux-x64 + ;; +esac + +case "$target" in +'linux'*) + if [ -f /etc/alpine-release ]; then + target="$target-musl" + fi + ;; +esac + +# If AVX2 isn't supported, use the -baseline build +case "$target" in +'darwin-x64'*) + if [[ $(sysctl -a | grep machdep.cpu | grep AVX2) == '' ]]; then + target="$target-baseline" + fi + ;; +'linux-x64'*) + # If AVX2 isn't supported, use the -baseline build + if [[ $(cat /proc/cpuinfo | grep avx2) = '' ]]; then + target="$target-baseline" + fi + ;; +esac + +install_env=APIFY_CLI_INSTALL +install_dir=${!install_env:-$HOME/.apify} +bin_dir=$install_dir/bin + +if [[ ! -d $bin_dir ]]; then + mkdir -p "$bin_dir" || + error "Failed to create install directory \"$bin_dir\"" +fi + +# Ensure we are in the apify-cli root by checking for ./package.json +if [[ ! -f ./package.json ]]; then + error "Not in the apify-cli root" + exit 1 +fi + +echo "Install directory: $install_dir" +echo "Bin directory: $bin_dir" + +# Ensure we have bun installed +if ! command -v bun &> /dev/null; then + error "bun could not be found. Please install it from https://bun.sh/docs/installation" + exit 1 +fi + +# Check package.json for the version +version=$(jq -r '.version' package.json) +echo "Version: $version" + +info "Installing dependencies" +yarn + +info "Building bundles" +yarn insert-cli-metadata && yarn build-bundles && git checkout origin/master -- src/lib/hooks/useCLIMetadata.ts + +info "Installing bundles" + +executable_names=("apify" "actor") + +for executable_name in "${executable_names[@]}"; do + output_filename="${executable_name}" + + info "Installing $executable_name bundle for version $version and target $target" + + cp "bundles/$executable_name-$version-$target" "$bin_dir/$output_filename" + chmod +x "$bin_dir/$output_filename" +done + +success "Installed bundles successfully" + +if ! [ -t 0 ] && [ -r /dev/tty ]; then + PROVIDED_INSTALL_DIR="$install_dir" FINAL_BIN_DIR="$bin_dir" APIFY_OPEN_TTY=1 "$bin_dir/apify" install +else + PROVIDED_INSTALL_DIR="$install_dir" FINAL_BIN_DIR="$bin_dir" "$bin_dir/apify" install +fi diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 6bc5785ab..9618ff0b4 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -101,10 +101,6 @@ if [[ $target = darwin-x64 ]]; then fi fi -GITHUB=${GITHUB-"https://github.com"} - -github_repo="$GITHUB/apify/apify-cli" - # If AVX2 isn't supported, use the -baseline build case "$target" in 'darwin-x64'*) @@ -120,6 +116,9 @@ case "$target" in ;; esac +GITHUB=${GITHUB-"https://github.com"} +github_repo="$GITHUB/apify/apify-cli" + # Function to fetch latest version from GitHub API fetch_latest_version() { local api_url="https://api.github.com/repos/apify/apify-cli/releases/latest" @@ -206,4 +205,11 @@ success "Apify and Actor CLI $version were installed successfully!" info "The binaries are located at $Bold_Green$(tildify "$bin_dir/apify") ${Dim}and $Bold_Green$(tildify "$bin_dir/actor")" # Invoke the CLI to handle shell integrations nicely -PROVIDED_INSTALL_DIR="$install_dir" FINAL_BIN_DIR="$bin_dir" "$bin_dir/apify" install +# When running the script via `curl xxx | bash`, stdin is the script that gets consumed by bash. +# If stdin is not a tty and we have a readable /dev/tty, tell Node.js to open /dev/tty itself +# (shell-level redirects don't support raw mode properly for Node.js/Inquirer). +if ! [ -t 0 ] && [ -r /dev/tty ]; then + PROVIDED_INSTALL_DIR="$install_dir" FINAL_BIN_DIR="$bin_dir" APIFY_OPEN_TTY=1 "$bin_dir/apify" install +else + PROVIDED_INSTALL_DIR="$install_dir" FINAL_BIN_DIR="$bin_dir" "$bin_dir/apify" install +fi diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index ab981afd6..e8b07a0db 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -1,7 +1,8 @@ import assert from 'node:assert'; -import { existsSync } from 'node:fs'; +import { existsSync, openSync } from 'node:fs'; import { mkdir, readFile, symlink, unlink, writeFile } from 'node:fs/promises'; import { basename, join } from 'node:path'; +import { ReadStream } from 'node:tty'; import chalk from 'chalk'; @@ -97,6 +98,66 @@ export class InstallCommand extends ApifyCommand { info({ message: chalk.gray(`Symlinked apify, actor, and apify-cli to ${localBinDirectory}`) }); } + /** + * Prompt using /dev/tty directly, bypassing Inquirer (whose internal readline + * cannot be closed and hangs the process when given a custom input stream). + */ + private async confirmFromTty(message: string): Promise { + let ttyStream: ReadStream | undefined; + + const prompt = `${chalk.green('?')} ${chalk.bold(message)} ${chalk.dim('(Y/n)')} `; + + const writeDone = (answer: string) => { + // Clear the current line and rewrite with the final answer, like Inquirer does + process.stdout.write(`\r\x1b[2K${chalk.green('?')} ${chalk.bold(message)} ${chalk.cyan(answer)}\n`); + }; + + try { + cliDebugPrint('[install] opening /dev/tty for raw mode'); + const fd = openSync('/dev/tty', 'r'); + ttyStream = new ReadStream(fd); + + process.stdout.write(prompt); + + ttyStream.setRawMode(true); + ttyStream.resume(); + + const result = await new Promise((resolve) => { + const onData = (data: Buffer) => { + const key = data.toString(); + + if (key === 'y' || key === 'Y' || key === '\r' || key === '\n') { + ttyStream!.removeListener('data', onData); + writeDone('Yes'); + resolve(true); + } else if (key === 'n' || key === 'N') { + ttyStream!.removeListener('data', onData); + writeDone('No'); + resolve(false); + } else if (key === '\u0003' || key === '\u0004') { + // Ctrl+C or Ctrl+D + ttyStream!.removeListener('data', onData); + process.stdout.write('\n'); + resolve(false); + } + }; + + ttyStream!.on('data', onData); + }); + + return result; + } catch (error) { + cliDebugPrint('[install] failed to open /dev/tty for raw mode', error); + return false; + } finally { + if (ttyStream) { + ttyStream.setRawMode(false); + ttyStream.pause(); + ttyStream.destroy(); + } + } + } + private async promptAddToShell() { const installDir = process.env.PROVIDED_INSTALL_DIR; @@ -109,12 +170,24 @@ export class InstallCommand extends ApifyCommand { simpleLog({ message: '' }); - const allowedToAutomaticallyDo = await useYesNoConfirm({ - message: - 'Should the CLI handle adding itself to your shell automatically? (If you say no, you will receive the lines to add to your shell config file)', - // For now, no stdin -> always false - providedConfirmFromStdin: false, - }); + const confirmMessage = + 'Should the CLI handle adding itself to your shell automatically? (If you say no, you will receive the lines to add to your shell config file)'; + + let allowedToAutomaticallyDo: boolean; + + if (process.env.APIFY_OPEN_TTY && process.platform !== 'win32') { + // When running via `curl | bash`, Inquirer's readline interface cannot be + // properly cleaned up (it never closes), which hangs the process. + // Instead, we open /dev/tty directly and read a single keypress ourselves. + allowedToAutomaticallyDo = await this.confirmFromTty(confirmMessage); + } else { + cliDebugPrint('[install] opening /dev/tty for raw mode not requested, falling back to normal flow'); + allowedToAutomaticallyDo = await useYesNoConfirm({ + message: confirmMessage, + // For now, no stdin -> always false + providedConfirmFromStdin: false, + }); + } const shell = basename(process.env.SHELL ?? 'sh'); const quotedInstallDir = `"${installDir.replaceAll('"', '\\"')}"`; @@ -168,6 +241,8 @@ export class InstallCommand extends ApifyCommand { // We don't use a path as we don't know the shell configFile = '~/.bashrc'; + // But never automatically add to the file as we don't know the shell + allowedToAutomaticallyDo = false; break; } @@ -175,7 +250,7 @@ export class InstallCommand extends ApifyCommand { simpleLog({ message: '' }); if (allowedToAutomaticallyDo && configFile) { - const oldContent = await readFile(configFile, 'utf-8'); + const oldContent = await readFile(configFile, 'utf-8').catch(() => ''); const newContent = `${oldContent}\n\n# apify cli\n${linesToAdd.join('\n')}`; From 02f5b7eb842947a5c09fa336482184f5fdda1241 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Mar 2026 19:50:06 +0200 Subject: [PATCH 02/11] fix: ensure apify install works even without env vars --- src/commands/cli-management/install.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index e8b07a0db..b9e0b0dde 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -15,6 +15,9 @@ import { cliDebugPrint } from '../../lib/utils/cliDebugPrint.js'; const pathToInstallMarker = (installPath: string) => join(installPath, '.install-marker'); +const defaultInstallDir = join(process.env.HOME!, '.apify'); +const defaultBinDir = join(defaultInstallDir, 'bin'); + export class InstallCommand extends ApifyCommand { static override name = 'install' as const; @@ -159,14 +162,14 @@ export class InstallCommand extends ApifyCommand { } private async promptAddToShell() { - const installDir = process.env.PROVIDED_INSTALL_DIR; + const installDir = process.env.PROVIDED_INSTALL_DIR ?? defaultInstallDir; if (!installDir) { warning({ message: chalk.gray(`Install directory not found, cannot add to shell`) }); return; } - const binDir = process.env.FINAL_BIN_DIR!; + const binDir = process.env.FINAL_BIN_DIR ?? defaultBinDir; simpleLog({ message: '' }); @@ -252,7 +255,7 @@ export class InstallCommand extends ApifyCommand { if (allowedToAutomaticallyDo && configFile) { const oldContent = await readFile(configFile, 'utf-8').catch(() => ''); - const newContent = `${oldContent}\n\n# apify cli\n${linesToAdd.join('\n')}`; + const newContent = `${oldContent}\n\n# apify cli\n${linesToAdd.join('\n')}\n`; await writeFile(configFile, newContent); From b6533b6997d60679090fc2eb264320bfad6e9468 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Mar 2026 19:50:59 +0200 Subject: [PATCH 03/11] fix: paths --- src/commands/cli-management/install.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index b9e0b0dde..18b590497 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -15,8 +15,8 @@ import { cliDebugPrint } from '../../lib/utils/cliDebugPrint.js'; const pathToInstallMarker = (installPath: string) => join(installPath, '.install-marker'); -const defaultInstallDir = join(process.env.HOME!, '.apify'); -const defaultBinDir = join(defaultInstallDir, 'bin'); +const defaultInstallDir = process.env.APIFY_CLI_INSTALL ?? join(process.env.HOME!, '.apify'); +const defaultBinDir = process.env.FINAL_BIN_DIR ?? join(defaultInstallDir, 'bin'); export class InstallCommand extends ApifyCommand { static override name = 'install' as const; From 74ed71cb55a9d6da6f77b60581b4ca9cee216d7e Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Fri, 6 Mar 2026 23:25:32 +0200 Subject: [PATCH 04/11] chore: suggested changes 1 --- scripts/install/dev-test-install.sh | 16 ++- scripts/install/install.sh | 5 +- src/commands/cli-management/install.ts | 167 ++++++++++++++++--------- src/lib/utils.ts | 54 ++++++++ 4 files changed, 180 insertions(+), 62 deletions(-) diff --git a/scripts/install/dev-test-install.sh b/scripts/install/dev-test-install.sh index ab0ea2f7f..e88c4d3c7 100644 --- a/scripts/install/dev-test-install.sh +++ b/scripts/install/dev-test-install.sh @@ -136,7 +136,21 @@ for executable_name in "${executable_names[@]}"; do chmod +x "$bin_dir/$output_filename" done -success "Installed bundles successfully" +tildify() { + if [[ $1 = $HOME/* ]]; then + local replacement=\~/ + + echo "${1/$HOME\//$replacement}" + else + echo "$1" + fi +} + +success "Apify and Actor CLI were installed successfully!" +echo '' +echo -e " ${Dim}Version: $Green$version" +echo -e " ${Dim}Location: $Color_Off$Bold_White$(tildify "$bin_dir/apify") ${Dim}and $Color_Off$Bold_White$(tildify "$bin_dir/actor")" +echo '' if ! [ -t 0 ] && [ -r /dev/tty ]; then PROVIDED_INSTALL_DIR="$install_dir" FINAL_BIN_DIR="$bin_dir" APIFY_OPEN_TTY=1 "$bin_dir/apify" install diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 9618ff0b4..956f7457a 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -199,10 +199,11 @@ tildify() { fi } +success "Apify and Actor CLI were installed successfully!" echo '' +echo -e " ${Dim}Version: $Green$version" +echo -e " ${Dim}Location: $Color_Off$Bold_White$(tildify "$bin_dir/apify") ${Dim}and $Color_Off$Bold_White$(tildify "$bin_dir/actor")" echo '' -success "Apify and Actor CLI $version were installed successfully!" -info "The binaries are located at $Bold_Green$(tildify "$bin_dir/apify") ${Dim}and $Bold_Green$(tildify "$bin_dir/actor")" # Invoke the CLI to handle shell integrations nicely # When running the script via `curl xxx | bash`, stdin is the script that gets consumed by bash. diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index 18b590497..fc763eb00 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -1,22 +1,22 @@ import assert from 'node:assert'; -import { existsSync, openSync } from 'node:fs'; +import { closeSync, existsSync, openSync } from 'node:fs'; import { mkdir, readFile, symlink, unlink, writeFile } from 'node:fs/promises'; -import { basename, join } from 'node:path'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; import { ReadStream } from 'node:tty'; import chalk from 'chalk'; +import which from 'which'; import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; import { useCLIMetadata } from '../../lib/hooks/useCLIMetadata.js'; import { useYesNoConfirm } from '../../lib/hooks/user-confirmations/useYesNoConfirm.js'; -import { info, simpleLog, success, warning } from '../../lib/outputs.js'; -import { tildify } from '../../lib/utils.js'; +import { error, info, simpleLog, success, warning } from '../../lib/outputs.js'; +import { detectShell, shellConfigFile, tildify } from '../../lib/utils.js'; import { cliDebugPrint } from '../../lib/utils/cliDebugPrint.js'; const pathToInstallMarker = (installPath: string) => join(installPath, '.install-marker'); - -const defaultInstallDir = process.env.APIFY_CLI_INSTALL ?? join(process.env.HOME!, '.apify'); -const defaultBinDir = process.env.FINAL_BIN_DIR ?? join(defaultInstallDir, 'bin'); +const HOMEDIR = () => process.env.HOME ?? homedir(); export class InstallCommand extends ApifyCommand { static override name = 'install' as const; @@ -44,7 +44,12 @@ export class InstallCommand extends ApifyCommand { await this.symlinkToLocalBin(installPath); - await this.promptAddToShell(); + // We don't want any errors bubbled up to prevent the command from finalizing + try { + await this.promptAddToShell(); + } catch (err: any) { + error({ message: err.message || 'Failed to automatically handle shell integration' }); + } await writeFile(installMarkerPath, version); @@ -56,12 +61,12 @@ export class InstallCommand extends ApifyCommand { } private async symlinkToLocalBin(installPath: string) { - const userHomeDirectory = process.env.HOME; + const userHomeDirectory = HOMEDIR(); - cliDebugPrint('[install] user home directory', userHomeDirectory); + cliDebugPrint('[install -> symlinkToLocalBin] user home directory', userHomeDirectory); if (!userHomeDirectory) { - cliDebugPrint('[install] user home directory not found'); + cliDebugPrint('[install -> symlinkToLocalBin] user home directory not found'); warning({ message: chalk.gray(`User home directory not found, cannot symlink to ~/.local/bin`) }); @@ -71,9 +76,7 @@ export class InstallCommand extends ApifyCommand { const localBinDirectory = join(userHomeDirectory, '.local', 'bin'); // Make sure the directory exists - if (!existsSync(localBinDirectory)) { - await mkdir(localBinDirectory, { recursive: true }); - } + await mkdir(localBinDirectory, { recursive: true }); const fileNames = ['apify', 'actor', 'apify-cli']; @@ -98,7 +101,7 @@ export class InstallCommand extends ApifyCommand { cliDebugPrint('[install] symlink created for item', file, symlinkPath); } - info({ message: chalk.gray(`Symlinked apify, actor, and apify-cli to ${localBinDirectory}`) }); + info({ message: chalk.gray(`Symlinked apify, actor, and apify-cli to ${tildify(localBinDirectory)}`) }); } /** @@ -106,6 +109,7 @@ export class InstallCommand extends ApifyCommand { * cannot be closed and hangs the process when given a custom input stream). */ private async confirmFromTty(message: string): Promise { + let fd: number | undefined; let ttyStream: ReadStream | undefined; const prompt = `${chalk.green('?')} ${chalk.bold(message)} ${chalk.dim('(Y/n)')} `; @@ -117,7 +121,7 @@ export class InstallCommand extends ApifyCommand { try { cliDebugPrint('[install] opening /dev/tty for raw mode'); - const fd = openSync('/dev/tty', 'r'); + fd = openSync('/dev/tty', 'r'); ttyStream = new ReadStream(fd); process.stdout.write(prompt); @@ -158,10 +162,36 @@ export class InstallCommand extends ApifyCommand { ttyStream.pause(); ttyStream.destroy(); } + + if (fd !== undefined) { + try { + closeSync(fd); + } catch { + // Like in C, if close fails, tough luck + } + } } } private async promptAddToShell() { + // On windows our install script automatically handles path handling + if (process.platform === 'win32') { + return; + } + + // Check if we can already resolve the CLI from PATH + const apifyCliPath = await which('apify', { nothrow: true }); + if (apifyCliPath) { + info({ message: chalk.gray(`Apify and Actor CLIs are already in PATH, skipping shell integration`) }); + return; + } + + const userHomeDirectory = HOMEDIR(); + + cliDebugPrint('[install -> promptAddToShell] user home directory', userHomeDirectory); + + const defaultInstallDir = process.env.APIFY_CLI_INSTALL ?? join(userHomeDirectory, '.apify'); + const defaultBinDir = process.env.FINAL_BIN_DIR ?? join(defaultInstallDir, 'bin'); const installDir = process.env.PROVIDED_INSTALL_DIR ?? defaultInstallDir; if (!installDir) { @@ -173,12 +203,11 @@ export class InstallCommand extends ApifyCommand { simpleLog({ message: '' }); - const confirmMessage = - 'Should the CLI handle adding itself to your shell automatically? (If you say no, you will receive the lines to add to your shell config file)'; + const confirmMessage = 'Should the CLI handle adding itself to your shell automatically?'; let allowedToAutomaticallyDo: boolean; - if (process.env.APIFY_OPEN_TTY && process.platform !== 'win32') { + if (process.env.APIFY_OPEN_TTY) { // When running via `curl | bash`, Inquirer's readline interface cannot be // properly cleaned up (it never closes), which hangs the process. // Instead, we open /dev/tty directly and read a single keypress ourselves. @@ -192,83 +221,103 @@ export class InstallCommand extends ApifyCommand { }); } - const shell = basename(process.env.SHELL ?? 'sh'); + const shell = detectShell(); + const configFile = shellConfigFile(userHomeDirectory, shell); const quotedInstallDir = `"${installDir.replaceAll('"', '\\"')}"`; const linesToAdd = []; - let configFile = ''; + let showOneLiner = true; switch (shell) { - case 'bash': { + case 'bash': + case 'zsh': { linesToAdd.push(`export APIFY_CLI_INSTALL=${quotedInstallDir}`); linesToAdd.push(`export PATH="$APIFY_CLI_INSTALL/bin:$PATH"`); - const configFiles = [join(process.env.HOME!, '.bashrc'), join(process.env.HOME!, '.bash_profile')]; - - if (process.env.XDG_CONFIG_HOME) { - configFiles.push( - join(process.env.XDG_CONFIG_HOME, '.bashrc'), - join(process.env.XDG_CONFIG_HOME, '.bash_profile'), - join(process.env.XDG_CONFIG_HOME, 'bashrc'), - join(process.env.XDG_CONFIG_HOME, 'bash_profile'), - ); - } - - // Find the first likely match for the config file [because bash loves having a lot of alternatives] - for (const maybeConfigFile of configFiles) { - if (existsSync(maybeConfigFile)) { - configFile = maybeConfigFile; - break; - } - } - break; } - case 'zsh': - linesToAdd.push(`export APIFY_CLI_INSTALL=${quotedInstallDir}`); - linesToAdd.push(`export PATH="$APIFY_CLI_INSTALL/bin:$PATH"`); - - configFile = join(process.env.HOME!, '.zshrc'); - - break; case 'fish': { linesToAdd.push(`set --export APIFY_CLI_INSTALL ${quotedInstallDir}`); linesToAdd.push(`set --export PATH ${binDir} $PATH`); - configFile = join(process.env.HOME!, '.config', 'fish', 'config.fish'); break; } - default: + default: { + // We don't know the shell, so we just show it to the user linesToAdd.push(`export APIFY_CLI_INSTALL=${quotedInstallDir}`); linesToAdd.push(`export PATH="$APIFY_CLI_INSTALL/bin:$PATH"`); - // We don't use a path as we don't know the shell - configFile = '~/.bashrc'; - // But never automatically add to the file as we don't know the shell + // Never automatically add to the file as we don't know the shell allowedToAutomaticallyDo = false; + // And don't show the one-liner because we don't know the shell + showOneLiner = false; break; + } } simpleLog({ message: '' }); if (allowedToAutomaticallyDo && configFile) { - const oldContent = await readFile(configFile, 'utf-8').catch(() => ''); + const oldContent = await readFile(configFile, 'utf-8').catch((err) => { + if (err.code === 'ENOENT') { + // File doesn't exist, that's fine + return ''; + } + + throw new Error( + `Failed to read config file "${tildify(configFile)}". Received error code: ${err.code}; ${err.message}`, + ); + }); const newContent = `${oldContent}\n\n# apify cli\n${linesToAdd.join('\n')}\n`; - await writeFile(configFile, newContent); + try { + await writeFile(configFile, newContent); + } catch (err: any) { + if (err.code === 'EACCES') { + throw new Error( + `Failed to write to config file "${tildify(configFile)}", as the CLI does not have permission to write to it.`, + ); + } + + throw new Error( + `Failed to write to config file "${tildify(configFile)}". Received error code: ${err.code}; ${err.message}`, + ); + } - info({ message: chalk.gray(`Added "${tildify(binDir)}" to $PATH in ${tildify(configFile)}`) }); - } else { info({ message: [ + chalk.gray(`Added "${tildify(binDir)}" to your PATH in ${tildify(configFile)}.`), chalk.gray( - `Manually add the following lines to your shell config file (${tildify(configFile)} or similar):`, + ` You may need to run ${chalk.white.bold(`source ${tildify(configFile)}`)} to reload your shell.`, ), - ...linesToAdd.map((line) => chalk.white.bold(` ${line}`)), ].join('\n'), }); + + return; + } + + if (showOneLiner) { + const oneLiner = `echo '${linesToAdd.join('\\n')}' >> "${tildify(configFile)}" && source "${tildify(configFile)}"`; + + info({ + message: [ + // + chalk.gray(`The Apify & Actor CLIs are not in your PATH. Run:`), + '', + chalk.white.bold(` ${oneLiner}`), + ].join('\n'), + }); + + return; } + + info({ + message: [ + chalk.gray(`Manually add the following lines to ${tildify(configFile)} or similar:`), + ...linesToAdd.map((line) => chalk.white.bold(` ${line}`)), + ].join('\n'), + }); } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e5425426e..d933d8d4f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -711,3 +711,57 @@ export const tildify = (path: string) => { return path; }; + +export function detectShell() { + const shell = process.env.SHELL ?? ''; + + if (shell.includes('zsh')) { + return 'zsh'; + } + + if (shell.includes('bash')) { + return 'bash'; + } + + if (shell.includes('fish')) { + return 'fish'; + } + + return 'unknown'; +} + +export function shellConfigFile(userHomeDirectory: string, shell: ReturnType) { + // eslint-disable-next-line default-case -- We do not want to add a shell and it fall through to default case + switch (shell) { + case 'bash': { + const configFiles = [join(userHomeDirectory, '.bashrc'), join(userHomeDirectory, '.bash_profile')]; + + if (process.env.XDG_CONFIG_HOME) { + configFiles.push( + join(process.env.XDG_CONFIG_HOME, '.bashrc'), + join(process.env.XDG_CONFIG_HOME, '.bash_profile'), + join(process.env.XDG_CONFIG_HOME, 'bashrc'), + join(process.env.XDG_CONFIG_HOME, 'bash_profile'), + ); + } + + for (const maybeConfigFile of configFiles) { + if (existsSync(maybeConfigFile)) { + return maybeConfigFile; + } + } + + return `~/.bashrc`; + } + case 'zsh': { + const zshBaseDir = process.env.ZDORDIR || homedir(); + return join(zshBaseDir, '.zshrc'); + } + case 'fish': { + return join(userHomeDirectory, '.config', 'fish', 'config.fish'); + } + case 'unknown': { + return `your shell config file`; + } + } +} From 2a741228207fe8a92fec662019c1639c1bd524f9 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Fri, 6 Mar 2026 23:25:57 +0200 Subject: [PATCH 05/11] chore: lint --- src/commands/cli-management/install.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index fc763eb00..97b92d660 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -153,8 +153,8 @@ export class InstallCommand extends ApifyCommand { }); return result; - } catch (error) { - cliDebugPrint('[install] failed to open /dev/tty for raw mode', error); + } catch (err) { + cliDebugPrint('[install] failed to open /dev/tty for raw mode', err); return false; } finally { if (ttyStream) { From aed2cd60bca59bf518f4c3e81369572344f60704 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Tue, 10 Mar 2026 21:21:59 +0200 Subject: [PATCH 06/11] fix: review nits --- scripts/install/dev-test-install.sh | 1 - src/commands/cli-management/install.ts | 2 +- src/lib/utils.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/install/dev-test-install.sh b/scripts/install/dev-test-install.sh index e88c4d3c7..c30027003 100644 --- a/scripts/install/dev-test-install.sh +++ b/scripts/install/dev-test-install.sh @@ -101,7 +101,6 @@ fi # Ensure we are in the apify-cli root by checking for ./package.json if [[ ! -f ./package.json ]]; then error "Not in the apify-cli root" - exit 1 fi echo "Install directory: $install_dir" diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index 97b92d660..58be91f38 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -299,7 +299,7 @@ export class InstallCommand extends ApifyCommand { } if (showOneLiner) { - const oneLiner = `echo '${linesToAdd.join('\\n')}' >> "${tildify(configFile)}" && source "${tildify(configFile)}"`; + const oneLiner = `echo '${linesToAdd.join('\\n')}' >> "${configFile}" && source "${configFile}"`; info({ message: [ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d933d8d4f..db903bb60 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -754,7 +754,7 @@ export function shellConfigFile(userHomeDirectory: string, shell: ReturnType Date: Tue, 10 Mar 2026 22:19:32 +0200 Subject: [PATCH 07/11] fix: reviews and bundles --- scripts/build-cli-bundles.ts | 60 +++++++++++++++++++++----- scripts/install/dev-test-install.sh | 8 +--- scripts/install/install.sh | 6 --- src/commands/cli-management/install.ts | 30 +++++++++++-- src/lib/utils.ts | 10 +++-- 5 files changed, 83 insertions(+), 31 deletions(-) diff --git a/scripts/build-cli-bundles.ts b/scripts/build-cli-bundles.ts index 06c3f4679..f42516aa0 100644 --- a/scripts/build-cli-bundles.ts +++ b/scripts/build-cli-bundles.ts @@ -7,10 +7,11 @@ At the time of writing (~2025-05-31), the bundles are created using Bun as the r When node stabilizes SEA (https://nodejs.org/api/single-executable-applications.html) [and supports ESM -.-], this code can be adapted to build it using that instead (but cross-platform will be a CI experience) */ +import { readFileSync } from 'node:fs'; import { readFile, rm, writeFile } from 'node:fs/promises'; import { basename } from 'node:path'; -import { $, fileURLToPath } from 'bun'; +import { $, type Build, build, fileURLToPath } from 'bun'; import { version } from '../package.json' with { type: 'json' }; @@ -30,13 +31,11 @@ const targets = (() => { 'bun-darwin-arm64-baseline', 'bun-linux-x64-musl', 'bun-linux-arm64-musl', - 'bun-linux-x64-musl-baseline', - 'bun-linux-arm64-musl-baseline', - ]; + ] satisfies Build.CompileTarget[]; } if (process.platform === 'win32') { - return ['bun-windows-x64', 'bun-windows-x64-baseline']; + return ['bun-windows-x64', 'bun-windows-x64-baseline'] satisfies Build.CompileTarget[]; } return [ @@ -50,9 +49,7 @@ const targets = (() => { 'bun-darwin-arm64-baseline', 'bun-linux-x64-musl', 'bun-linux-arm64-musl', - 'bun-linux-x64-musl-baseline', - 'bun-linux-arm64-musl-baseline', - ]; + ] satisfies Build.CompileTarget[]; })(); const entryPoints = [ @@ -77,6 +74,35 @@ await writeFile(metadataFile, newContent); for (const entryPoint of entryPoints) { const cliName = basename(entryPoint, '.ts'); + const lines = readFileSync(entryPoint, 'utf-8').split('\n'); + lines.splice(1, 0, 'import "proxy-agent";'); + + // Step 1: create one fat JS file with node resolver to ensure no imports point to non-node export conditions + const result = await build({ + entrypoints: [entryPoint], + files: { + [entryPoint]: lines.join('\n'), + }, + outdir: fileURLToPath(new URL(`../bundles/fat-clis`, import.meta.url)), + conditions: 'node', + target: 'bun', + sourcemap: 'none', + }); + + const entrypointResultFilePath = result.outputs[0]!.path; + + // Fix apify client js (it now lazy loads proxy-agent, which makes bun skip it from the bundle) + { + const entrypointResultFileContent = await result.outputs[0]!.text(); + + const newEntrypointResultFileContent = entrypointResultFileContent.replace( + `(0, utils_1.dynamicNodeImport)("proxy-agent")`, + `Promise.resolve().then(() => import_proxy_agent)`, + ); + + await writeFile(entrypointResultFilePath, newEntrypointResultFileContent); + } + for (const target of targets) { // eslint-disable-next-line prefer-const -- somehow it cannot tell that os and arch cannot be "const" while the rest are let let [, os, arch, musl, baseline] = target.split('-'); @@ -88,6 +114,7 @@ for (const entryPoint of entryPoints) { // If we are building on Windows ARM64, even though the target is x64, we mark it as "arm64" (there are some weird errors when compiling on x64 // and running on arm64). Hopefully bun will get arm64 native builds + // TODO: Vlad remove this in a subsequent PR as Bun now has native arm64 windows builds if (os === 'windows' && process.platform === 'win32') { const systemType = await $`pwsh -c "(Get-CimInstance Win32_ComputerSystem).SystemType"`.text(); @@ -108,8 +135,21 @@ for (const entryPoint of entryPoints) { const outFile = fileURLToPath(new URL(`../bundles/${fileName}`, import.meta.url)); console.log(`Building ${cliName} for ${target} (result: ${fileName})...`); - // TODO: --sourcemap crashes for w/e reason and --bytecode doesn't support ESM (TLA to be exact) - await $`bun build --compile --minify --target=${target} --outfile=${outFile} ${entryPoint}`; + + // Step 2: create the final executable bundle + await build({ + entrypoints: [entrypointResultFilePath], + compile: { + outfile: outFile, + target, + }, + format: 'esm', + minify: { + identifiers: true, + keepNames: true, + }, + bytecode: true, + }); // Remove the arch override await writeFile(metadataFile, newContent); diff --git a/scripts/install/dev-test-install.sh b/scripts/install/dev-test-install.sh index c30027003..132c14d63 100644 --- a/scripts/install/dev-test-install.sh +++ b/scripts/install/dev-test-install.sh @@ -120,7 +120,7 @@ info "Installing dependencies" yarn info "Building bundles" -yarn insert-cli-metadata && yarn build-bundles && git checkout origin/master -- src/lib/hooks/useCLIMetadata.ts +yarn insert-cli-metadata && yarn build-bundles && git checkout -- src/lib/hooks/useCLIMetadata.ts info "Installing bundles" @@ -145,12 +145,6 @@ tildify() { fi } -success "Apify and Actor CLI were installed successfully!" -echo '' -echo -e " ${Dim}Version: $Green$version" -echo -e " ${Dim}Location: $Color_Off$Bold_White$(tildify "$bin_dir/apify") ${Dim}and $Color_Off$Bold_White$(tildify "$bin_dir/actor")" -echo '' - if ! [ -t 0 ] && [ -r /dev/tty ]; then PROVIDED_INSTALL_DIR="$install_dir" FINAL_BIN_DIR="$bin_dir" APIFY_OPEN_TTY=1 "$bin_dir/apify" install else diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 956f7457a..187c3434f 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -199,12 +199,6 @@ tildify() { fi } -success "Apify and Actor CLI were installed successfully!" -echo '' -echo -e " ${Dim}Version: $Green$version" -echo -e " ${Dim}Location: $Color_Off$Bold_White$(tildify "$bin_dir/apify") ${Dim}and $Color_Off$Bold_White$(tildify "$bin_dir/actor")" -echo '' - # Invoke the CLI to handle shell integrations nicely # When running the script via `curl xxx | bash`, stdin is the script that gets consumed by bash. # If stdin is not a tty and we have a readable /dev/tty, tell Node.js to open /dev/tty itself diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index 58be91f38..60b457eaa 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -55,6 +55,17 @@ export class InstallCommand extends ApifyCommand { cliDebugPrint('[install] install marker written to', installMarkerPath); + simpleLog({ + message: [ + chalk.green('Apify and Actor CLI were installed successfully!'), + '', + chalk.gray(` Version: ${chalk.green(version)}`), + chalk.gray( + ` Location: ${chalk.bold.white(tildify(join(installPath, 'apify')))} and ${chalk.bold.white(tildify(join(installPath, 'actor')))}`, + ), + ].join('\n'), + }); + simpleLog({ message: '' }); success({ message: 'To get started, run:' }); simpleLog({ message: chalk.white.bold(' apify --help\n actor --help') }); @@ -180,8 +191,17 @@ export class InstallCommand extends ApifyCommand { } // Check if we can already resolve the CLI from PATH - const apifyCliPath = await which('apify', { nothrow: true }); - if (apifyCliPath) { + const [apifyCliPath, actorCliPath] = await Promise.allSettled([ + which('apify', { nothrow: true }), + which('actor', { nothrow: true }), + ]); + + if ( + apifyCliPath.status === 'fulfilled' && + actorCliPath.status === 'fulfilled' && + apifyCliPath.value && + actorCliPath.value + ) { info({ message: chalk.gray(`Apify and Actor CLIs are already in PATH, skipping shell integration`) }); return; } @@ -298,8 +318,10 @@ export class InstallCommand extends ApifyCommand { return; } + const resolvedConfigFile = configFile ?? 'your shell config file'; + if (showOneLiner) { - const oneLiner = `echo '${linesToAdd.join('\\n')}' >> "${configFile}" && source "${configFile}"`; + const oneLiner = `echo -e '${linesToAdd.join('\\n')}' >> "${resolvedConfigFile}" && source "${resolvedConfigFile}"`; info({ message: [ @@ -315,7 +337,7 @@ export class InstallCommand extends ApifyCommand { info({ message: [ - chalk.gray(`Manually add the following lines to ${tildify(configFile)} or similar:`), + chalk.gray(`Manually add the following lines to ${resolvedConfigFile} or similar:`), ...linesToAdd.map((line) => chalk.white.bold(` ${line}`)), ].join('\n'), }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index db903bb60..088a9d290 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -48,6 +48,7 @@ import { } from './consts.js'; import { deleteFile, ensureFolderExistsSync, rimrafPromised } from './files.js'; import type { AuthJSON } from './types.js'; +import { cliDebugPrint } from './utils/cliDebugPrint.js'; // Export AJV properly: https://github.com/ajv-validator/ajv/issues/2132 // Welcome to the state of JavaScript/TypeScript and CJS/ESM interop. @@ -174,7 +175,8 @@ export async function getLoggedClient(token?: string, apiBaseUrl?: string) { let userInfo; try { userInfo = await apifyClient.user('me').get(); - } catch { + } catch (err) { + cliDebugPrint('[getLoggedClient] error getting user info', { error: err, apiBaseUrl }); return null; } @@ -730,7 +732,7 @@ export function detectShell() { return 'unknown'; } -export function shellConfigFile(userHomeDirectory: string, shell: ReturnType) { +export function shellConfigFile(userHomeDirectory: string, shell: ReturnType): string | null { // eslint-disable-next-line default-case -- We do not want to add a shell and it fall through to default case switch (shell) { case 'bash': { @@ -751,7 +753,7 @@ export function shellConfigFile(userHomeDirectory: string, shell: ReturnType Date: Tue, 10 Mar 2026 22:31:40 +0200 Subject: [PATCH 08/11] chore: trim install script of unused variables --- scripts/install/dev-test-install.sh | 28 -------------------------- scripts/install/install.sh | 31 ----------------------------- 2 files changed, 59 deletions(-) diff --git a/scripts/install/dev-test-install.sh b/scripts/install/dev-test-install.sh index 132c14d63..aba0b6028 100644 --- a/scripts/install/dev-test-install.sh +++ b/scripts/install/dev-test-install.sh @@ -8,25 +8,15 @@ Color_Off='' # Regular Colors Red='' -Green='' Dim='' # White -# Bold -Bold_White='' -Bold_Green='' - if [[ -t 1 ]]; then # Reset Color_Off='\033[0m' # Text Reset # Regular Colors Red='\033[0;31m' # Red - Green='\033[0;32m' # Green Dim='\033[0;2m' # White - - # Bold - Bold_Green='\033[1;32m' # Bold Green - Bold_White='\033[1m' # Bold White fi error() { @@ -38,14 +28,6 @@ info() { echo -e "${Dim}$@ ${Color_Off}" } -info_bold() { - echo -e "${Bold_White}$@ ${Color_Off}" -} - -success() { - echo -e "${Green}$@ ${Color_Off}" -} - platform=$(uname -ms) case $platform in @@ -135,16 +117,6 @@ for executable_name in "${executable_names[@]}"; do chmod +x "$bin_dir/$output_filename" done -tildify() { - if [[ $1 = $HOME/* ]]; then - local replacement=\~/ - - echo "${1/$HOME\//$replacement}" - else - echo "$1" - fi -} - if ! [ -t 0 ] && [ -r /dev/tty ]; then PROVIDED_INSTALL_DIR="$install_dir" FINAL_BIN_DIR="$bin_dir" APIFY_OPEN_TTY=1 "$bin_dir/apify" install else diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 187c3434f..750406de7 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -19,25 +19,15 @@ Color_Off='' # Regular Colors Red='' -Green='' Dim='' # White -# Bold -Bold_White='' -Bold_Green='' - if [[ -t 1 ]]; then # Reset Color_Off='\033[0m' # Text Reset # Regular Colors Red='\033[0;31m' # Red - Green='\033[0;32m' # Green Dim='\033[0;2m' # White - - # Bold - Bold_Green='\033[1;32m' # Bold Green - Bold_White='\033[1m' # Bold White fi error() { @@ -49,14 +39,6 @@ info() { echo -e "${Dim}$@ ${Color_Off}" } -info_bold() { - echo -e "${Bold_White}$@ ${Color_Off}" -} - -success() { - echo -e "${Green}$@ ${Color_Off}" -} - if [[ $# -gt 1 ]]; then error 'Too many arguments, only 1 is allowed. The first can be a specific tag of Apify CLI to install. (e.g. "0.28.0")' fi @@ -116,9 +98,6 @@ case "$target" in ;; esac -GITHUB=${GITHUB-"https://github.com"} -github_repo="$GITHUB/apify/apify-cli" - # Function to fetch latest version from GitHub API fetch_latest_version() { local api_url="https://api.github.com/repos/apify/apify-cli/releases/latest" @@ -189,16 +168,6 @@ for executable_name in "${executable_names[@]}"; do fi done -tildify() { - if [[ $1 = $HOME/* ]]; then - local replacement=\~/ - - echo "${1/$HOME\//$replacement}" - else - echo "$1" - fi -} - # Invoke the CLI to handle shell integrations nicely # When running the script via `curl xxx | bash`, stdin is the script that gets consumed by bash. # If stdin is not a tty and we have a readable /dev/tty, tell Node.js to open /dev/tty itself From 191046215f565b59b189bdd0e015f4860c221d30 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 11 Mar 2026 12:44:41 +0200 Subject: [PATCH 09/11] chore: small reorder and fix bun closeSync error --- src/commands/cli-management/install.ts | 33 ++++++++++++++++++-------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index 60b457eaa..964873e27 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -1,5 +1,5 @@ import assert from 'node:assert'; -import { closeSync, existsSync, openSync } from 'node:fs'; +import { existsSync, openSync } from 'node:fs'; import { mkdir, readFile, symlink, unlink, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import { join } from 'node:path'; @@ -42,8 +42,6 @@ export class InstallCommand extends ApifyCommand { return; } - await this.symlinkToLocalBin(installPath); - // We don't want any errors bubbled up to prevent the command from finalizing try { await this.promptAddToShell(); @@ -51,12 +49,17 @@ export class InstallCommand extends ApifyCommand { error({ message: err.message || 'Failed to automatically handle shell integration' }); } + simpleLog({ message: '' }); + + await this.symlinkToLocalBin(installPath); + await writeFile(installMarkerPath, version); cliDebugPrint('[install] install marker written to', installMarkerPath); simpleLog({ message: [ + '', chalk.green('Apify and Actor CLI were installed successfully!'), '', chalk.gray(` Version: ${chalk.green(version)}`), @@ -72,6 +75,11 @@ export class InstallCommand extends ApifyCommand { } private async symlinkToLocalBin(installPath: string) { + // On windows our install script automatically handles path handling + if (process.platform === 'win32') { + return; + } + const userHomeDirectory = HOMEDIR(); cliDebugPrint('[install -> symlinkToLocalBin] user home directory', userHomeDirectory); @@ -174,13 +182,16 @@ export class InstallCommand extends ApifyCommand { ttyStream.destroy(); } - if (fd !== undefined) { - try { - closeSync(fd); - } catch { - // Like in C, if close fails, tough luck - } - } + // Keeping this code here if we will need it again, + // but it looks like in Bun it automatically closes the file descriptor [possibly ttyStream.destroy() does this] + + // if (fd !== undefined) { + // try { + // closeSync(fd); + // } catch { + // // Like in C, if close fails, tough luck + // } + // } } } @@ -202,6 +213,8 @@ export class InstallCommand extends ApifyCommand { apifyCliPath.value && actorCliPath.value ) { + cliDebugPrint('[install -> promptAddToShell] already in PATH', { apifyCliPath, actorCliPath }); + info({ message: chalk.gray(`Apify and Actor CLIs are already in PATH, skipping shell integration`) }); return; } From ff54ca837d6f0966659cf95a5a2c30ee6f540a3f Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 11 Mar 2026 15:30:47 +0200 Subject: [PATCH 10/11] chore: copilot review --- scripts/build-cli-bundles.ts | 5 +++++ scripts/install/install.sh | 2 +- src/commands/cli-management/install.ts | 31 ++++++++++---------------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/scripts/build-cli-bundles.ts b/scripts/build-cli-bundles.ts index f42516aa0..5ae25d03d 100644 --- a/scripts/build-cli-bundles.ts +++ b/scripts/build-cli-bundles.ts @@ -31,6 +31,9 @@ const targets = (() => { 'bun-darwin-arm64-baseline', 'bun-linux-x64-musl', 'bun-linux-arm64-musl', + // TODO: when adding native windows arm64 builds, remove these too + 'bun-linux-x64-musl-baseline' as never, + 'bun-linux-arm64-musl-baseline' as never, ] satisfies Build.CompileTarget[]; } @@ -49,6 +52,8 @@ const targets = (() => { 'bun-darwin-arm64-baseline', 'bun-linux-x64-musl', 'bun-linux-arm64-musl', + 'bun-linux-x64-musl-baseline' as never, + 'bun-linux-arm64-musl-baseline' as never, ] satisfies Build.CompileTarget[]; })(); diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 750406de7..9717f2487 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -79,7 +79,7 @@ if [[ $target = darwin-x64 ]]; then # redirect stderr to devnull to avoid error message when not running in Rosetta if [[ $(sysctl -n sysctl.proc_translated 2>/dev/null) = 1 ]]; then target=darwin-arm64 - info "Your shell is running in Rosetta 2. Downloading Apify CLI for $target instead" + info "Your shell is running in Rosetta 2. Downloading Apify and Actor CLI for $target instead" fi fi diff --git a/src/commands/cli-management/install.ts b/src/commands/cli-management/install.ts index 964873e27..7069b967c 100644 --- a/src/commands/cli-management/install.ts +++ b/src/commands/cli-management/install.ts @@ -2,7 +2,7 @@ import assert from 'node:assert'; import { existsSync, openSync } from 'node:fs'; import { mkdir, readFile, symlink, unlink, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; import { ReadStream } from 'node:tty'; import chalk from 'chalk'; @@ -42,16 +42,18 @@ export class InstallCommand extends ApifyCommand { return; } - // We don't want any errors bubbled up to prevent the command from finalizing - try { - await this.promptAddToShell(); - } catch (err: any) { - error({ message: err.message || 'Failed to automatically handle shell integration' }); - } + if (process.platform !== 'win32') { + await this.symlinkToLocalBin(installPath); - simpleLog({ message: '' }); + // We don't want any errors bubbled up to prevent the command from finalizing + try { + await this.promptAddToShell(); + } catch (err: any) { + error({ message: err.message || 'Failed to automatically handle shell integration' }); + } - await this.symlinkToLocalBin(installPath); + simpleLog({ message: '' }); + } await writeFile(installMarkerPath, version); @@ -75,11 +77,6 @@ export class InstallCommand extends ApifyCommand { } private async symlinkToLocalBin(installPath: string) { - // On windows our install script automatically handles path handling - if (process.platform === 'win32') { - return; - } - const userHomeDirectory = HOMEDIR(); cliDebugPrint('[install -> symlinkToLocalBin] user home directory', userHomeDirectory); @@ -196,11 +193,6 @@ export class InstallCommand extends ApifyCommand { } private async promptAddToShell() { - // On windows our install script automatically handles path handling - if (process.platform === 'win32') { - return; - } - // Check if we can already resolve the CLI from PATH const [apifyCliPath, actorCliPath] = await Promise.allSettled([ which('apify', { nothrow: true }), @@ -306,6 +298,7 @@ export class InstallCommand extends ApifyCommand { const newContent = `${oldContent}\n\n# apify cli\n${linesToAdd.join('\n')}\n`; try { + await mkdir(dirname(configFile), { recursive: true }); await writeFile(configFile, newContent); } catch (err: any) { if (err.code === 'EACCES') { From 64fcac537541d7d7374d095add2b9cd687dc64e7 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 11 Mar 2026 15:31:13 +0200 Subject: [PATCH 11/11] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/install/dev-test-install.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/install/dev-test-install.sh b/scripts/install/dev-test-install.sh index aba0b6028..48a9ce1df 100644 --- a/scripts/install/dev-test-install.sh +++ b/scripts/install/dev-test-install.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# This script should be used like so: `/bin/cat dev-test-install.sh | bash` +# This script should be used from the repo root like so: `cat scripts/install/dev-test-install.sh | bash` # Reset Color_Off='' @@ -94,6 +94,11 @@ if ! command -v bun &> /dev/null; then exit 1 fi +# Ensure we have jq installed +if ! command -v jq &> /dev/null; then + error "jq could not be found. Please install it from https://stedolan.github.io/jq/" + exit 1 +fi # Check package.json for the version version=$(jq -r '.version' package.json) echo "Version: $version"