UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

483 lines 20.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UpgradeCmd = void 0; exports.resolveUpgradeAction = resolveUpgradeAction; const child_process_1 = require("child_process"); const command_1 = require("../cli/command"); const functions_1 = require("../util/functions"); const format_1 = require("../format/format"); const options_1 = require("./options"); const version_1 = require("../util/version"); const install_method_1 = require("../upgrade/install-method"); const binary_upgrade_1 = require("../upgrade/binary-upgrade"); // ANCHOR - Constants /** Where to point users when no automated upgrade path exists for their install. */ const DOWNLOAD_DOCS_URL = 'https://docs.controlplane.com/reference/cli'; /** The manual-action explanation for a project-local (package.json / npx) install. */ const LOCAL_INSTALL_REASON = "cpln appears to be installed as a project dependency (or was run via npx); a global upgrade would not affect the copy in use. Update the version in the project's package.json instead, or upgrade a global install directly (e.g. npm install -g @controlplane/cli@latest)."; /** The manual-action explanation for a source checkout. */ const DEV_BUILD_REASON = 'This is a development build of cpln; there is nothing to upgrade.'; // ANCHOR - Options /** * Adds the `cpln upgrade` flags controlling inspection and forcing. * * @param {Argv} yargs - The yargs instance to configure. * @returns {Argv} The configured yargs instance. */ function withUpgradeOptions(yargs) { const group = 'Upgrade options:'; return yargs.options({ check: { group, alias: 'c', boolean: true, describe: 'Report the installed and latest versions without upgrading', }, force: { group, boolean: true, describe: 'Upgrade even when already on the latest version', }, }); } // ANCHOR - Command /** * Upgrades the CLI in place. This command must work on a machine that has never * logged in, so it writes through the injected out/err streams and never touches * this.session — the session getter throws when no profile is configured. */ class UpgradeCmd extends command_1.Command { constructor(overrides = {}) { super(); this.command = 'upgrade'; this.aliases = ['update']; this.describe = 'Upgrade the cpln CLI to the latest version'; this.deps = { ...this.defaultDeps(), ...overrides }; } // Public Methods // builder(yargs) { return (0, functions_1.pipe)( // withUpgradeOptions, options_1.withFormatOptions, options_1.withDebugOptions)(yargs); } async handle(args) { // Infer how this CLI was installed; it dictates how it can be upgraded const probe = this.deps.probe(); const method = (0, install_method_1.detectInstallMethod)(probe); // --check only reports; it never touches anything if (args.check) { await this.runCheck(method, probe, args); return; } // A source checkout is upgraded by pulling, not by this command if (method === 'source') { this.end(DEV_BUILD_REASON); } const action = resolveUpgradeAction(method, probe); await this.runAction(action, method, probe, Boolean(args.force)); } // Private Methods // /** * Executes the resolved upgrade action for the detected install method. * * @param {UpgradeAction} action - The method-specific action to perform. * @param {InstallMethod} method - The detected install method, used for messaging. * @param {InstallProbe} probe - The environment probe, reused for the binary path. * @param {boolean} force - Whether to upgrade even when already current. * @returns {Promise<void>} */ async runAction(action, method, probe, force) { // No automated path exists; automation must be able to detect the no-op if (action.kind === 'manual') { this.fail(`${action.reason}\n\nSee ${DOWNLOAD_DOCS_URL} for installation instructions.`); } if (action.kind === 'managed') { await this.runManaged(action, method, force); return; } await this.runBinary(action.artifact, probe, force); } /** * Handles an npm/Homebrew upgrade by delegating to the owning package manager. * * @param {ManagedAction} action - The managed action, carrying the command and any preflight. * @param {InstallMethod} method - The detected install method, used for messaging. * @param {boolean} force - Whether to skip the up-to-date gate. * @returns {Promise<void>} */ async runManaged(action, method, force) { // Only the npm channel is gated on the registry dist-tag; `brew upgrade` performs its own check if (method === 'npm' && !force) { await this.gateNpmChannel(); } this.runPreflight(action, method); const rendered = (0, install_method_1.renderCommand)(action.command); this.deps.out(`Running '${rendered}'...`); const result = this.deps.spawn(action.command); // A non-zero status, a null status (killed by a signal), or a spawn error all mean failure if (result.error || result.status !== 0) { const cause = result.signal ? `was terminated by ${result.signal}` : 'failed'; this.fail(`ERROR: '${rendered}' ${cause}. Run it manually to upgrade; you may need elevated permissions (e.g. sudo) or to ensure ${action.command.program} is installed and on your PATH.`); } this.deps.out(`✓ cpln upgrade completed.`); } /** * Ends the command early when the npm channel is already current, or fails when * the registry cannot be reached. * * @returns {Promise<void>} */ async gateNpmChannel() { const latestVersion = await this.deps.fetchLatestVersion(); if (latestVersion === undefined) { this.fail('ERROR: Unable to determine the latest version. Check your connection, or re-run with --force to upgrade anyway.'); } if (!(0, version_1.isNewerVersion)(latestVersion, version_1.About.npm)) { this.end(`cpln is already up to date (${version_1.About.npm}).`); } } /** * Runs a managed action's preflight step (e.g. `brew update`). A failed refresh * is non-fatal; the upgrade proceeds with the currently available version. * * @param {ManagedAction} action - The managed action, possibly carrying a preflight. * @param {InstallMethod} method - The detected install method, used for messaging. * @returns {void} */ runPreflight(action, method) { if (!action.preflight) { return; } const rendered = (0, install_method_1.renderCommand)(action.preflight); this.deps.out(`Refreshing ${method} ('${rendered}')...`); const refresh = this.deps.spawn(action.preflight); if (refresh.error || refresh.status !== 0) { this.deps.out(`Warning: '${rendered}' did not complete; continuing with the currently available version.`); } } /** * Handles a standalone-binary upgrade by downloading, verifying, and replacing it. * * The release bucket — not the npm registry — is the authoritative source for this * channel, so both the up-to-date check and the download resolve from it. * * @param {BinaryArtifact} artifact - The release archive selected for this platform. * @param {InstallProbe} probe - The environment probe carrying the executable path. * @param {boolean} force - Whether to reinstall even when already current. * @returns {Promise<void>} */ async runBinary(artifact, probe, force) { const baseUrl = (0, binary_upgrade_1.getBaseUrl)(probe.env); const tag = await this.resolveReleaseTag(baseUrl, force); // Download, verify, and atomically replace the installed executables this.deps.out(`Downloading the latest cpln release...`); let replaced; try { replaced = await (0, binary_upgrade_1.performBinaryUpgrade)({ baseUrl, execPath: probe.execPath, artifact, tag, download: this.deps.download, fileOps: this.deps.fileOps, }); } catch (error) { this.fail(binaryFailureMessage(error, probe.execPath)); } const fileCount = `${replaced.length} file${replaced.length === 1 ? '' : 's'}`; this.deps.out(`✓ cpln upgrade completed (replaced ${fileCount}).`); } /** * Resolves the release tag to install, ending early when this build is already * current or newer than the published release. * * @param {string} baseUrl - The release-artifact base URL. * @param {boolean} force - Whether to bypass the up-to-date and ordering gates. * @returns {Promise<string>} The tag to install. */ async resolveReleaseTag(baseUrl, force) { let tag; try { tag = await (0, binary_upgrade_1.fetchLatestTag)(baseUrl, this.deps.download); } catch (error) { const detail = error instanceof Error ? error.message : 'Unknown error'; this.fail(`ERROR: Unable to reach the release server: ${detail}`); } if (force) { return tag; } // The tag embeds the build's git revision, so a matching revision means current if (version_1.About.version !== 'dev' && tag.endsWith(`-${version_1.About.version}`)) { this.end(`cpln is already up to date.`); } // Never step backwards onto an older release (e.g. a stale or rolled-back pointer) if (isOlderRelease(tag)) { this.end(`The published release is older than this build; nothing to upgrade. Re-run with --force to install it anyway.`); } return tag; } /** * Reports the version/install-method status, honoring the requested output format. * * @param {InstallMethod} method - The detected install method. * @param {InstallProbe} probe - The environment probe, used for the release bucket URL. * @param {Arguments<UpgradeOptions & FormatOptions>} args - The parsed command-line arguments. * @returns {Promise<void>} */ async runCheck(method, probe, args) { const status = await this.buildStatus(method, probe); // Structured formats serialize the status object; everything else gets the human report if (args.output && format_1.STRUCTURED_FORMATS.includes(args.output)) { this.deps.out((0, format_1.format)(status, args)); return; } this.deps.out(buildCheckReport(status).join('\n')); } /** * Builds the current-versus-latest picture, consulting the source of truth for * the channel: the release bucket for a standalone binary, the npm registry * otherwise. * * @param {InstallMethod} method - The detected install method. * @param {InstallProbe} probe - The environment probe, used for the release bucket URL. * @returns {Promise<UpgradeStatus>} The status to report. */ async buildStatus(method, probe) { if (method === 'binary') { let latestRelease; try { latestRelease = await (0, binary_upgrade_1.fetchLatestTag)((0, binary_upgrade_1.getBaseUrl)(probe.env), this.deps.download); } catch (_a) { latestRelease = undefined; } return { method, currentVersion: version_1.About.npm, currentRelease: installedRelease(), latestRelease, selfUpdatable: resolveBinaryAction().kind === 'binary', updateAvailable: latestRelease !== undefined && isNewerRelease(latestRelease), }; } const latestVersion = await this.deps.fetchLatestVersion(); return { method, currentVersion: version_1.About.npm, latestVersion, updateAvailable: (0, version_1.isNewerVersion)(latestVersion, version_1.About.npm), }; } /** * Builds the default collaborators backed by the real environment. * * @returns {UpgradeDeps} The production dependencies. */ defaultDeps() { return { probe: install_method_1.probeInstall, fetchLatestVersion: version_1.getLatestPackageVersion, download: binary_upgrade_1.httpDownloader, fileOps: binary_upgrade_1.realFileOps, spawn: runCommand, out: (message) => (0, format_1.write)(this.env.out, message), err: (message) => (0, format_1.write)(this.env.err, message), exit: (code) => process.exit(code), }; } /** * Prints a message to stdout and exits with code 0. * * @param {string} message - The message to print. * @returns {never} */ end(message) { this.deps.out(message); return this.deps.exit(0); } /** * Prints an error message to stderr and exits with code 1. * * @param {string} message - The message to print. * @returns {never} */ fail(message) { this.deps.err(message); return this.deps.exit(1); } } exports.UpgradeCmd = UpgradeCmd; // ANCHOR - Functions /** * Resolves the concrete upgrade action for the detected install method. * * @param {InstallMethod} method - The detected install method. * @param {InstallProbe} probe - The environment probe (for package-manager detection). * @returns {UpgradeAction} The action to perform. */ function resolveUpgradeAction(method, probe) { switch (method) { case 'homebrew': // Refresh the tap first so a stale formula cannot report a false "already up to date" return { kind: 'managed', command: (0, install_method_1.homebrewUpgradeCommand)(), preflight: (0, install_method_1.homebrewRefreshCommand)() }; case 'npm': return { kind: 'managed', command: (0, install_method_1.packageManagerUpgradeCommand)((0, install_method_1.detectPackageManager)(probe.scriptPath)) }; case 'binary': return resolveBinaryAction(); case 'local': return { kind: 'manual', reason: LOCAL_INSTALL_REASON }; case 'source': return { kind: 'manual', reason: DEV_BUILD_REASON }; default: { const exhaustive = method; throw new Error(`Unhandled install method: ${exhaustive}`); } } } /** * Resolves the binary-channel action for the running platform. * * @returns {UpgradeAction} A binary action, or a manual action when no standalone * binary is published for this platform. */ function resolveBinaryAction() { const platform = process.platform; const artifact = (0, binary_upgrade_1.resolveBinaryArtifact)({ platform, arch: process.arch, isMusl: (0, binary_upgrade_1.isMuslLinux)(platform), }); if (!artifact) { return { kind: 'manual', reason: 'No self-updatable cpln binary is published for your platform.' }; } return { kind: 'binary', artifact }; } /** * Runs a command with its output streamed to the user's terminal. * * @param {RunnableCommand} command - The command to run. * @returns {SpawnOutcome} The observed result. */ function runCommand(command) { // Windows exposes npm/pnpm/yarn/brew as shims that spawn only resolves via a shell const result = (0, child_process_1.spawnSync)(command.program, command.args, { stdio: 'inherit', shell: process.platform === 'win32', }); return { status: result.status, signal: result.signal, error: result.error }; } /** * Returns the release tag of the running build ({epoch}-{gitsha}). * * @returns {string} The installed release tag. */ function installedRelease() { return `${version_1.About.epoch}-${version_1.About.version}`; } /** * Determines whether a published release tag is strictly older than the running * build, using the orderable epoch prefix both carry. * * @param {string} tag - The published release tag. * @returns {boolean} True when the tag predates the installed build. */ function isOlderRelease(tag) { const tagEpoch = (0, binary_upgrade_1.parseTagEpoch)(tag); const currentEpoch = Number(version_1.About.epoch); return tagEpoch !== undefined && Number.isFinite(currentEpoch) && currentEpoch > 0 && tagEpoch < currentEpoch; } /** * Determines whether a published release tag names a different, non-older build. * * @param {string} tag - The published release tag. * @returns {boolean} True when installing the tag would move this build forward. */ function isNewerRelease(tag) { if (version_1.About.version !== 'dev' && tag.endsWith(`-${version_1.About.version}`)) { return false; } return !isOlderRelease(tag); } /** * Builds the user-facing error for a failed binary self-replace, adding a * permissions hint when the install location is not writable. * * @param {unknown} error - The error that aborted the upgrade. * @param {string} execPath - The path of the installed executable. * @returns {string} The complete failure message. */ function binaryFailureMessage(error, execPath) { const detail = error instanceof Error ? error.message : 'Unknown error'; const code = error === null || error === void 0 ? void 0 : error.code; const hint = code === 'EACCES' || code === 'EPERM' ? ` cpln is installed at ${execPath}; re-run with elevated permissions (e.g. sudo) or ensure that directory is writable.` : ''; return `ERROR: Binary upgrade failed: ${detail}${hint}`; } /** * Builds the human-readable report lines for `--check`. * * @param {UpgradeStatus} status - The version picture to report. * @returns {string[]} The report lines. */ function buildCheckReport(status) { var _a, _b; const lines = [`Installed version: ${status.currentVersion}`]; // The binary channel is versioned by release tag, not by the npm dist-tag if (status.method === 'binary') { lines.push(`Installed release: ${status.currentRelease}`); lines.push(`Latest release: ${(_a = status.latestRelease) !== null && _a !== void 0 ? _a : 'unknown'}`); } else { lines.push(`Latest version: ${(_b = status.latestVersion) !== null && _b !== void 0 ? _b : 'unknown'}`); } lines.push(`Install method: ${status.method}`); lines.push(buildCheckVerdict(status)); return lines; } /** * Builds the human-readable verdict line for `--check`. * * @param {UpgradeStatus} status - The version picture to summarize. * @returns {string} The verdict sentence. */ function buildCheckVerdict(status) { if (status.method === 'source' || status.currentVersion === 'dev') { return 'This is a development build; there is nothing to upgrade.'; } if (status.method === 'local') { return "cpln is installed as a project dependency. Update the version in the project's package.json."; } if (status.method === 'binary') { return buildBinaryVerdict(status); } if (status.latestVersion === undefined) { return 'Unable to determine the latest version. Check your connection and try again.'; } if (status.updateAvailable) { return `A newer version is available: ${status.latestVersion}. Run 'cpln upgrade' to install it.`; } return 'You are on the latest version.'; } /** * Builds the binary-channel verdict line for `--check`. * * @param {UpgradeStatus} status - The version picture to summarize. * @returns {string} The verdict sentence. */ function buildBinaryVerdict(status) { if (status.latestRelease === undefined) { return 'Unable to determine the latest release. Check your connection and try again.'; } if (!status.updateAvailable) { return 'You are on the latest release.'; } // Never recommend a command that cannot succeed on this platform if (status.selfUpdatable === false) { return `A newer release is available, but no self-updatable cpln binary is published for your platform. See ${DOWNLOAD_DOCS_URL} for installation instructions.`; } return `A newer release is available. Run 'cpln upgrade' to install it.`; } //# sourceMappingURL=upgrade.js.map