/** * Plugin install/uninstall/discovery — the shared engine behind both the * `/plugin` slash command (human-driven) and the model-facing lifecycle tools * (`SearchPlugins`, `InstallPlugin`, ...). Keeping it in one place means the two * surfaces can never drift on where plugins live or how sources resolve. * * Trust model (see docs/plugin-system-spec.md): *adding* a marketplace is the * human trust boundary; *installing* from an already-trusted marketplace is the * model's discretion (package-manager model), and must stay transparent and * reversible. This module performs the mechanical install/remove; the gating * (announce, injection carve-out) lives with the callers. */ import type { MarketplacePlatform } from "./formats/types.js"; import { type PluginInstallScope } from "./locations.js"; import type { NormalizedPlugin } from "./manifest.js"; import { type MarketplacePluginSource, type MarketplaceRecord } from "./marketplace.js"; /** The default marketplace record — always present so source-level trust is meaningful out of the box. */ export declare function defaultMarketplaceRecord(): MarketplaceRecord; /** * Curated well-known marketplaces, trusted out of the box. Shipping an entry * here is a maintainer-level trust decision (the human half of the "adding a * marketplace is the human trust boundary" rule) — keep this list short and * high-trust. Indices are cloned lazily into the marketplace cache on first * search and refreshed on a TTL thereafter. Refreshing the *index* only changes * what is discoverable; installed plugins are still never auto-updated, and * plugin sources inside the official index are sha-pinned. */ export declare const WELL_KNOWN_MARKETPLACES: ReadonlyArray<{ name: string; url: string; ref?: string; }>; /** * Whether {@link ensureWellKnownMarketplaces} would actually reach the network. * * Exists so an interactive caller can say "Refreshing marketplace indices…" * before a multi-second clone and stay silent when the answer is already on * disk. Deliberately does not probe the remote — the ref-mismatch case needs * network to detect, so this under-reports rather than paying for a round trip * to decide whether to print a line. */ export declare function wellKnownMarketplacesAreStale(agentDir?: string): boolean; export interface EnsureMarketplacesOptions { /** Refresh regardless of the TTL. */ force?: boolean; /** * Marketplaces to ensure; defaults to {@link WELL_KNOWN_MARKETPLACES}. * * A seam, and a deliberate one. The ref-pinning rules below — discard a cache * on the wrong branch, fall back when a pin is retired, and do not loop * between those two — are only exercised against a remote whose branches can * be moved and deleted, which no real marketplace will do on demand. Without * this the whole path could only be checked by hand against GitHub, and a * check that needs the network is a check that rots. */ marketplaces?: ReadonlyArray<{ name: string; url: string; ref?: string; }>; } /** * Make sure every well-known marketplace index is cached and reasonably fresh. * * Clones what is missing and pulls what has gone stale. Without the TTL the * indices were fetched exactly once and never again, so a plugin added upstream * stayed permanently invisible — the inherit half of the system quietly stopped * inheriting. Offline is non-fatal: each failure is returned and search degrades * to whatever is already on disk. * * The returned strings are notes for the caller to surface, not fatal errors — * a retired ref that fell back to the default branch reports here too, because * the consequence (entries that install with no capabilities) is otherwise * invisible and inexplicable. */ export declare function ensureWellKnownMarketplaces(agentDir?: string, options?: EnsureMarketplacesOptions): Promise; /** * Refresh every cached index — well-known and user-added — ignoring the TTL. * The explicit half of the freshness story, for when a user knows the upstream * changed and does not want to wait out the interval. */ export declare function refreshMarketplaces(cwd: string, agentDir?: string, options?: EnsureMarketplacesOptions): Promise<{ refreshed: string[]; errors: string[]; }>; /** * All marketplace records in effect: the bundled default first (curated, * trusted), then user-added ones from `.agents/` (falling back to legacy * `.hoocode/`). Deduplicated by directory. */ export declare function readMarketplaceRecords(cwd: string, agentDir?: string): MarketplaceRecord[]; /** A plugin offered by some registered marketplace (not necessarily installed). */ export interface AvailablePlugin { name: string; description?: string; /** Relative path, git URL, `npm:`, or structured source object. */ source: MarketplacePluginSource; /** Resolved source kind, for display and gating. `npm`/`archive` are listed but not installable. */ sourceKind: "local" | "git" | "git-subdir" | "npm" | "archive"; marketplaceName: string; marketplaceRoot: string; /** Platforms this entry targets (per-entry hint, else the marketplace's). */ supportPlatform: MarketplacePlatform[]; } /** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */ export declare function listAvailablePlugins(cwd: string, agentDir?: string): AvailablePlugin[]; /** Find a single available plugin by exact name. */ export declare function findAvailablePlugin(cwd: string, name: string, agentDir?: string): AvailablePlugin | undefined; /** All currently installed plugins (project + global plugin dirs). */ export declare function listInstalledPlugins(cwd: string, agentDir?: string): NormalizedPlugin[]; /** * Whether `name` is already installed, by either name it can go by. * * A marketplace entry name and the plugin's own manifest `name` are allowed to * differ — both vendors say so outright — and installs land in a directory named * for the *entry*, while discovery reports the *manifest* id. Matching ids alone * meant a plugin installed as `42crunch-api-security-testing` (manifest id * `api-security-testing`) never counted as installed, so every InstallPlugin * call for the catalog name re-cloned it. */ export declare function isPluginInstalled(cwd: string, name: string, agentDir?: string): boolean; export interface InstallOptions { /** * Where the plugin lands. Defaults to `user`. The human path (`/plugin * install`) asks; the autonomous path passes the `pluginInstallScope` setting. */ scope?: PluginInstallScope; } export interface InstallOutcome { installed: boolean; /** Install destination directory (when installed). */ dest?: string; /** The scope the plugin was installed at. */ scope?: PluginInstallScope; /** * The plugin's own manifest id, when it differs from the marketplace entry * name it was installed by. This is the id `ListPlugins` reports, so the * caller can name both rather than leaving the model to reconcile them. */ id?: string; /** Platforms the installed plugin supports (read back from disk). */ supportPlatform?: MarketplacePlatform[]; /** Human-readable summary suitable for a tool result or notification. */ message: string; } /** * Install an available plugin by name into the consumption home for * `options.scope` — `~/.agents/plugins/` for `user` (the default), or * `/.agents/plugins/` for `project`. * * `user` is the default for a reason worth keeping in view: a plugin is portable * and reusable across projects, so installing into the repo hides the capability * from every other checkout and puts content into `git status` that has nothing * to do with the change being made. `project` inverts both of those on purpose — * it is how a team pins a plugin to a repository — so it is chosen, never * defaulted into. * * Copies local sources; clones git sources. Transparent + reversible by * construction — the plugin lands in a named directory and * {@link uninstallPlugin} removes it from either scope. Callers activate the * result (live activation via AgentSession.activatePlugin, or a reload). */ export declare function installAvailablePlugin(cwd: string, name: string, agentDir?: string, options?: InstallOptions): Promise; export interface UninstallOutcome { removed: boolean; message: string; } /** * Remove an installed or authored plugin from every location it could occupy: * the consumption home, both production homes, and the legacy project-local * directories that older versions wrote into. * * `name` is matched two ways, because a plugin has two names: the marketplace * entry name it was installed under (which is what the directory is called) and * the manifest id `ListPlugins` reports. Resolving only the first left a plugin * whose names differ impossible to remove through the tool surface — the id the * model was shown was the one uninstall did not accept. * * Removal by id stays inside {@link pluginHomeRoots}, so a plugin discovered in * `/.claude/skills` — repository content the team committed, which hoocode * did not install — is never deleted out of the working tree. */ export declare function uninstallPlugin(cwd: string, name: string, agentDir?: string): UninstallOutcome; //# sourceMappingURL=install.d.ts.map