/** * Auto-update helpers — pure / filesystem-only pieces of the self-update * (specs/auto-update.md), kept apart from the service so each rule is * testable without timers: * * - where the running copy lives and what kind of install it is (npm global, * user-prefix npm, dev checkout, anything else); * - the `npm install -g` arguments that target that same prefix; * - the on/off decision (setting + env override); * - whether a supervisor will bring the backend back after RESTART_REQUESTED; * - the status file (`crewly update-status`) and the pending-upgrade marker. * * @module services/system/auto-update.utils */ /** What kind of install the running copy is. */ export type InstallKind = 'npm-global' | 'dev-checkout' | 'unmanaged'; /** Where and how the running copy is installed. */ export interface InstallInfo { /** Install kind */ kind: InstallKind; /** Real path of the Crewly package root (null when not found) */ packageRoot: string | null; /** npm prefix the package lives under (npm-global only) */ prefix: string | null; /** Human-readable reason, used in the skip log line */ detail: string; } /** Why auto-update is on or off. */ export type AutoUpdateSwitch = 'enabled' | 'disabled-setting' | 'disabled-env' | 'enabled-env'; /** How the last auto-update attempt ended. */ export type AutoUpdateOutcome = 'up-to-date' | 'deferred-busy' | 'installed-restarting' | 'upgraded' | 'install-failed' | 'verify-failed' | 'restart-unavailable' | 'input-guard-blocked' | 'check-failed' | 'skipped'; /** Persisted status of the auto-updater (`/auto-update-state.json`). */ export interface AutoUpdateState { /** Version this backend is running */ currentVersion: string | null; /** Latest version the registry reported */ latestVersion: string | null; /** ISO time of the last registry check */ lastCheckAt: string | null; /** Whether the updater would act, and why not when it would not */ mode: string; /** How the last attempt ended */ lastResult: { outcome: AutoUpdateOutcome; at: string; version?: string; message?: string; } | null; /** Failed attempts in a row (reset on success) */ consecutiveFailures: number; /** ISO time before which no new attempt is made */ backoffUntil: string | null; /** Target version the owner was already told about (one failure DM per version) */ failureNotifiedVersion: string | null; } /** Marker persisted before an upgrade restart and read on the next boot. */ export interface PendingUpgradeMarker { /** Version the process was running before the restart */ fromVersion: string; /** Version that was installed */ toVersion: string; /** ISO time the marker was written */ at: string; } /** * Resolve symlinks when the path exists. * * @param p - Path * @returns Real path, or the resolved path when it does not exist */ export declare function realOrResolved(p: string): string; /** * Walk up from `startDir` to the directory whose package.json is `crewly`. * * @param startDir - Directory to start from * @returns The package root, or null when none is found */ export declare function findCrewlyPackageRoot(startDir: string): string | null; /** * Package root of the running backend, anchored on the entry script * (`process.argv[1]`, e.g. `/dist/backend/backend/src/index.js`) rather * than the cwd — after `npm install -g` replaces the package directory the * old cwd no longer exists. * * @param argv1 - Entry script path (defaults to process.argv[1]) * @param cwd - Fallback start directory * @returns Real path of the package root, or null */ export declare function resolveRunningPackageRoot(argv1?: string | undefined, cwd?: string): string | null; /** * The npm prefix a package root was installed under, when it is a global * install: `/lib/node_modules/crewly` on POSIX, * `\node_modules\crewly` on Windows. Covers the default global prefix * (nvm, Homebrew, /usr/local) and the user prefix (`/npm-global`) * alike — installing with `--prefix ` replaces exactly the running copy * even when a different `npm` is first on PATH. * * @param packageRoot - Real path of the package root * @param platform - Platform (tests) * @returns The prefix, or null for anything that is not a global install * * @example * ```ts * derivePrefixFromPackageRoot('/usr/local/lib/node_modules/crewly'); // '/usr/local' * derivePrefixFromPackageRoot('/home/me/.crewly/npm-global/lib/node_modules/crewly'); // '/home/me/.crewly/npm-global' * derivePrefixFromPackageRoot('/home/me/proj/node_modules/crewly'); // null (local dependency) * ``` */ export declare function derivePrefixFromPackageRoot(packageRoot: string, platform?: NodeJS.Platform): string | null; /** * Whether a directory is a git working tree root (`.git` directory, or the * `.git` file of a worktree). * * @param dir - Directory * @returns True when `.git` exists there */ export declare function isGitWorkingTree(dir: string): boolean; /** * Classify the running install. * * - `dev-checkout`: the package root is a git working tree (e.g. the owner's * `crewly/` repo running its own build, or an `npm link` that resolves into * it). It must never npm-install over itself. * - `npm-global`: the root sits in `/lib/node_modules/crewly`. * - `unmanaged`: anything else (Docker image at /app, a local dependency, an * npx cache) — nobody owns an in-place upgrade there. * * @param packageRoot - Real path of the package root (null when unknown) * @param platform - Platform (tests) * @returns Install info */ export declare function detectInstall(packageRoot: string | null, platform?: NodeJS.Platform): InstallInfo; /** * `npm` arguments that install `crewly@` over the running copy. * * @param prefix - Prefix from {@link derivePrefixFromPackageRoot} * @param version - Exact target version * @returns Arguments for npm * * @example * ```ts * npmInstallArgs('/usr/local', '1.20.144'); * // ['install', '-g', '--prefix', '/usr/local', 'crewly@1.20.144'] * ``` */ export declare function npmInstallArgs(prefix: string, version: string): string[]; /** * The npm executable that belongs to the running node, when it sits next to * it (nvm, official installers); plain `npm` from PATH otherwise. * * @param execPath - Running node binary (defaults to process.execPath) * @param platform - Platform (tests) * @returns Command to run */ export declare function resolveNpmCommand(execPath?: string, platform?: NodeJS.Platform): string; /** * Version in `/package.json`, read fresh from disk. * * @param packageRoot - Package root * @returns The version, or null when unreadable */ export declare function readInstalledVersion(packageRoot: string): string | null; /** * Whether `latest` is a strictly newer semver than `current` * (numeric major.minor.patch; pre-release tags are ignored). * * @param latest - Candidate version * @param current - Running version * @returns True when latest > current */ export declare function isNewerVersion(latest: string, current: string): boolean; /** * Resolve the on/off switch: the env override wins over the setting. * * @param settingValue - `settings.general.autoUpdate` (undefined = default on) * @param env - Environment * @returns The switch state * * @example * ```ts * resolveAutoUpdateSwitch(true, { CREWLY_AUTO_UPDATE: '0' }); // 'disabled-env' * resolveAutoUpdateSwitch(false, {}); // 'disabled-setting' * ``` */ export declare function resolveAutoUpdateSwitch(settingValue: boolean | undefined, env?: NodeJS.ProcessEnv): AutoUpdateSwitch; /** * Whether a switch state means "on". * * @param state - Switch state * @returns True when enabled */ export declare function isSwitchOn(state: AutoUpdateSwitch): boolean; /** * Read the command line of a process (POSIX only; `/proc` on Linux, `ps` * elsewhere). * * @param pid - Process id * @returns The command line, or null */ export declare function readProcessCommandLine(pid: number): string | null; /** * Whether a supervisor will respawn the backend after it exits with * RESTART_REQUESTED. That supervisor is `crewly start` (foreground, under the * macOS login-item wrapper, under systemd, or under the desktop app). New * CLIs say so in the env; an older CLI parent is recognised by its command line. * * @param env - Environment * @param parentCommandLine - Reader for the parent's command line (tests) * @returns True when a respawning parent is present */ export declare function hasRestartSupervisor(env?: NodeJS.ProcessEnv, parentCommandLine?: () => string | null): boolean; /** * A fresh, empty state. * * @returns Default state */ export declare function emptyAutoUpdateState(): AutoUpdateState; /** * Read the status file; missing or malformed → empty state. * * @param file - Path of the status file * @returns The state */ export declare function readAutoUpdateState(file: string): AutoUpdateState; /** * Write the status file atomically (temp file + rename). * * @param file - Path of the status file * @param state - State to write */ export declare function writeAutoUpdateState(file: string, state: AutoUpdateState): void; /** * Write the pending-upgrade marker. * * @param file - Marker path * @param marker - Contents */ export declare function writePendingMarker(file: string, marker: PendingUpgradeMarker): void; /** * Read and delete the pending-upgrade marker. It is removed even when * malformed, so a crash loop can never repeat the notice. * * @param file - Marker path * @returns The marker, or null when absent or malformed */ export declare function consumePendingMarker(file: string): PendingUpgradeMarker | null; /** * Owner notice after a successful auto-upgrade. * * @param version - New version * @param deviceName - This machine's name * @returns The one-line notice */ export declare function composeUpgradedNotice(version: string, deviceName: string): string; /** * Owner notice after repeated failures. * * @param version - Target version * @param deviceName - This machine's name * @param failures - Failures in a row * @param reason - Last failure reason * @returns The notice text */ export declare function composeFailureNotice(version: string, deviceName: string, failures: number, reason: string): string; /** * Directory to run `npm install -g` from: the first candidate that exists. * * npm calls process.cwd() before it does anything else and dies with * `ENOENT: uv_cwd` (exit 7) when the directory it was started in is gone. * The backend's own cwd is the package root, which every global install * deletes and recreates — so npm must never inherit it. * * @param candidates - Directories in order of preference (e.g. crewlyHome, home, tmp) * @param isDir - Existence check (tests) * @returns The first existing directory, or the filesystem root * * @example * ```ts * resolveInstallCwd(['/root/.crewly', '/root', '/tmp']); // '/root/.crewly' * ``` */ export declare function resolveInstallCwd(candidates: Array, isDir?: (dir: string) => boolean): string; /** * Remove credentials from npm output. * * @param text - Raw output * @returns The output with secrets replaced by `[redacted]` */ export declare function sanitizeNpmOutput(text: string): string; /** * Last `maxLines` non-empty, sanitised lines of npm's output — what goes to * the backend log so the next failure is diagnosable without the box. * * @param outputTail - Combined stdout/stderr tail * @param maxLines - Lines to keep * @returns The sanitised tail */ export declare function installOutputTail(outputTail: string, maxLines?: number): string; /** * One-line reason for a failed install. Node crashes end with a * `Node.js vX` trailer and npm with a log-file pointer, so the last line * alone says nothing; this picks the first line that names the error and * appends the last line when it differs. * * @param code - npm exit code (null when killed) * @param outputTail - Combined stdout/stderr tail * @returns e.g. `npm exited with 7: Error: ENOENT: no such file or directory, uv_cwd (… Node.js v22.23.2)` * * @example * ```ts * describeInstallFailure(7, 'Error: ENOENT: no such file or directory, uv_cwd\n at x\nNode.js v22.23.2'); * // 'npm exited with 7: Error: ENOENT: no such file or directory, uv_cwd (… Node.js v22.23.2)' * ``` */ export declare function describeInstallFailure(code: number | null, outputTail: string): string; /** * process.cwd(), or undefined when the cwd was deleted (npm replaced the * package directory the backend was started in). * * @returns The cwd or undefined */ export declare function safeProcessCwd(): string | undefined; //# sourceMappingURL=auto-update.utils.d.ts.map