/** * Install and uninstall plugins in the running profile. * * Two things must both happen, and they are separate mechanisms: * * - PERSISTENCE — pnpm writes the dependency and `reconcileBundles` adds the * package to `dsh.profile.bundles`, which is what the NEXT cold boot * composes from. * - LIVE MOUNT — `loader.create()` mounts the plugin's rows into the running * tree right now. The Loader root is in-memory (its `write()` is a no-op), * so these rows are never persisted and cannot collide with the bundle * layer the next boot inserts. * * Doing only the first would require a restart; doing only the second would * lose the plugin on restart. */ import { type CatalogEntry, type MutationResponse } from './types.ts'; /** The Loader surface this plugin uses, kept structural to avoid a hard dependency. */ export interface LoaderLike { create(options: { name: string; config?: unknown; }): Promise; remove(id: string): Promise; entries(): Iterable<{ id: string; options: { name?: string; group?: boolean | null; }; }>; } /** Construction inputs for {@link Installer}. */ export interface InstallerOptions { readonly profileDir: string; readonly allowInstall: boolean; readonly loader: LoaderLike; readonly warn: (line: string) => void; } /** * Whether a pnpm package spec is safe to pass as an argument. * * Specs are re-derived host-side from the catalog and never taken from the * client, so this is defence in depth. The leading-dash check is the load * bearing one: an argument starting with `-` would be read by pnpm as a flag * rather than a package. * @param spec - the candidate spec. * @returns true when the spec may be handed to pnpm. */ export declare function isSafeSpec(spec: string): boolean; /** Runs pnpm and mounts plugin rows for one profile. */ export declare class Installer { private readonly options; /** Serializes every mutation: two concurrent pnpm runs would race the lockfile. */ private queue; constructor(options: InstallerOptions); /** * Install a catalog entry into the profile. * @param entry - the entry, as held by the host's own catalog copy. * @returns the outcome, including whether the page must be reloaded. */ install(entry: CatalogEntry): Promise; /** * Remove a package from the profile. * @param packageName - the installed dependency name. * @returns the outcome. */ uninstall(packageName: string): Promise; /** * List the profile's installed dependencies with their versions. * @returns package name to installed version. */ installed(): Map; /** * Mount every row a newly installed bundle contributes. * @param packageName - the installed bundle. * @returns true when the bundle ships a browser half. */ private mount; /** * Remove every live entry a package contributed. * * Entries are matched by module name rather than by ids remembered at * install time, so a plugin mounted by a previous cold boot unmounts here * exactly like one this process installed. * @param packageName - the package whose rows should go. */ private unmount; /** * Run one pnpm command in the profile directory. * * `ctx.subprocess` is deliberately not used: it scrubs environment variables * matching /KEY|PASSWORD|SECRET|TOKEN/i, which would strip the registry * credentials pnpm needs for private packages. * @param args - pnpm arguments. * @returns exit status and combined output. */ private runPnpm; /** * Turn a pnpm failure into an actionable sentence. * @param result - the failed invocation. * @param spec - the spec that was being installed or removed. * @returns a message naming the likely fix. */ private explainPnpmFailure; /** * Queue a mutation behind any already running one. * @param task - the mutation to run. * @returns the mutation's result. */ private serialize; }