* fix(plugin-manager): bound pnpm runs so a silent child cannot hold the profile lock A pnpm child that prints its completion line and then never exits holds the profile write lock until the host is restarted, so every other session and host on that profile fails after its own lock timeout (issue #4981). execa resolves its promise only once the piped stdio has ended, so a run now completes on the process's own exit and drains the pipes under a bound instead of awaiting their end. A service run whose captured output stays silent for idleTimeoutMs is terminated, reports timedOut alongside its exit status, and is not retried on the next registry; a run with inherited descriptors captures nothing and is left unbounded for its operator. * test(plugin-manager): type the mocked child and await the drain descendants The inherited-descriptor fixture no longer needs its legacy `as unknown` cast, because the mocked child is typed for the fields the operations read; the retired baseline entry is pruned (verify-no-unknown-casts). The drain specs wait for the killed descendant to exit before removing its directory: Windows refuses to delete a directory a live process still holds. * test(plugin-manager): stop the drain descendant deterministically The descendant that holds the pipes now ends on a stop file the teardown writes, and on its own after a minute, so the fixture leaves no process behind when a kill cannot be delivered. Windows stops it through taskkill, and the spec waits for the exit before removing the directory it ran in. * test(plugin-manager): run the drain descendant from its own script file Windows cannot carry a nested `-e` script through another spawn argument: the descendant failed to start there, so the fixture asserted on a process that was already gone. The descendant now runs from a script file the fixture writes. * test(plugin-manager): cover the bounded drain everywhere and stage the real descendant on POSIX Windows does not stage a descendant that keeps the inherited pipes open past the direct child's exit, so the real-process cases run on POSIX only. The mocked child now models execa's own completion rule — its promise settles only once both pipes end — which covers the bounded drain on every platform and still fails without the raw-exit race. * test(plugin-manager): cover the drain failure path without a child process An output consumer failure that the bounded drain cuts short is staged with the mocked child as well, so the per-file coverage gate also holds where the real-process cases are skipped. * fix(plugin-manager): terminate the whole tree of a stalled run A lifecycle script is a child of pnpm, so signalling pnpm alone left an approved postinstall writing into the profile after the operation reported a timeout and released the lock. A stalled run now stops its process group (or Windows process tree) and waits for it to disappear before the caller restores the files. The cut tail's close is told apart from a reading failure by where it happened, so a failure from before the cut still surfaces. * docs(plugin-manager): regenerate the config catalog after the rebase The English catalog gains `idleTimeoutMs` from the regenerator, and the Chinese counterpart carries the same interface line so the pair stays consistent. * fix(plugin-manager): terminate a run's tree through execa, keeping exit cleanup Spawning a service run with `detached` made execa skip the handler that kills the child when the manager exits, so a crashed manager left the pnpm tree running in the profile. `killDescendants` spawns the run as its own process group while keeping that cleanup, and it addresses the tree on every kill path, including cancellation. The module now only decides the probe target and waits for quiescence.
308 lines
14 KiB
Markdown
308 lines
14 KiB
Markdown
# Profile management
|
|
|
|
English | [中文](boot.zh.md)
|
|
|
|
The [boot package group](../../packages/boot/README.md) owns launcher-provided profile access and the plugin manager. [Plugin Manager](../../packages/boot/plugin-manager/README.md) documents persistence, reload and package-operation behavior.
|
|
|
|
## Management records
|
|
|
|
`PluginEntryId` identifies one Loader entry; callers obtain it from `listPlugins` rather than constructing a patch id.
|
|
|
|
`PluginInfo` carries module identity, effective enablement, fiber phase and optional display `meta`, plus a unique `patchId` or a `readOnlyReason`.
|
|
|
|
`BundleInfo` carries the package name, optional installed version, selected enablement, removal availability and optional resolution error. Its optional `meta` and each `BundleRowInfo.meta` contain display text or a metadata diagnostic; Clients select a language at render time.
|
|
|
|
`InstallBundleOptions.enabled` defaults to true. False installs without selecting the bundle layer. `approvedBuilds` grants persistent script permission to the supplied pending package names before installation. `registry` names the registry asked first; absent, the configured one.
|
|
|
|
`PluginRegistries` carries the configured first registry, `null` for the one pnpm's own configuration names, the fallbacks asked after it, and `resolved`, the URL pnpm's own configuration names or `null` while unread. `InspectOptions.registry` names the registry a lookup asks first.
|
|
|
|
`ChangeResult.changed` reports a disk edit independently of `application`: `applied`, `restart-required`, `overridden` or `failed`. Optional `error` carries a localizable code and external diagnostic. `packageResult` records the pnpm exit code, bounded output, truncation flag and complete diagnostic log path, plus `timedOut` when the manager terminated a run that stopped printing. A terminated run is classified `timeout` whatever exit status the signal left behind, so installation and removal report failure instead of success and no further registry is asked. `pendingBuilds` lists undecided packages across the profile; `approvedBuilds` records the names granted permission by this operation; `registries` lists the registries an installation asked, in order; `failedAt` says whether the last failed run could not reach the registry it asked or the host a git or tarball spec is fetched from.
|
|
|
|
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
|
|
|
<a id="cordis-surface"></a>
|
|
|
|
## Cordis API
|
|
|
|
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
|
|
|
<a id="ctxconfigeditor--configeditor"></a>
|
|
|
|
### `ctx.configEditor` — `ConfigEditor`
|
|
|
|
Persist complete raw configs and apply them through the normal Loader path.
|
|
|
|
```ts cordis-catalog
|
|
/** Addressable profile rows; nested Includes have independent configuration ownership.
|
|
* @returns Active entries with unique profile patch ids.
|
|
*/
|
|
entries(): Entry[]
|
|
|
|
/** Read inherited and explicit profile values for the active entries.
|
|
* @returns Detached layer values alongside their Loader entries.
|
|
*/
|
|
configuration(): Array<{ entry: Entry; inherited: Record<string, unknown>; override: Record<string, unknown> }>
|
|
|
|
/** Validate, persist, and reconcile a plugin's next config; ordinary fields keep normal lifecycle rules.
|
|
* @param entry Current Loader entry, also used to detect replacement during the write.
|
|
* @param change Derive a raw config from the current entry and its inherited layer.
|
|
* @returns Fulfillment after Loader reconciliation completes.
|
|
*/
|
|
async edit( entry: Entry, change: (current: Record<string, unknown>, inherited: Record<string, unknown>) => Record<string, unknown>, ): Promise<void>
|
|
```
|
|
|
|
Source: [`packages/boot/config-editor/src/index.ts`](../../packages/boot/config-editor/src/index.ts)
|
|
|
|
<a id="ctxhmr--hmr"></a>
|
|
|
|
### `ctx.hmr` — `Hmr`
|
|
|
|
Hot reload service with Cordis-compatible module configuration and events.
|
|
|
|
```ts cordis-catalog
|
|
/** Serialize a caller-owned mutation with all automatic reload paths.
|
|
* @param operation Work that must not overlap module or configuration replacement.
|
|
* @returns The operation result after its asynchronous work completes.
|
|
*/
|
|
runExclusive<T>(operation: () => Promise<T>): Promise<T>
|
|
|
|
/** Watch a configuration path through the same queue as module replacement.
|
|
* @param filename Absolute path, which may not exist yet.
|
|
* @param refresh Rebuilds configuration from its current files and awaits Loader completion.
|
|
* @returns Disposer closing this registration and waiting for its pending refresh.
|
|
*/
|
|
async watchConfig(filename: string, refresh: () => Promise<void>): Promise<() => Promise<void>>
|
|
|
|
/** Read direct module dependency URLs from the active Node loader.
|
|
* @param url Module URL.
|
|
* @returns Linked module URLs, or an empty list for an uncached module.
|
|
*/
|
|
async getLinked(url: string): Promise<string[]>
|
|
```
|
|
|
|
Source: [`packages/boot/hmr/src/index.ts`](../../packages/boot/hmr/src/index.ts)
|
|
|
|
<a id="ctxpluginmanager--pluginmanager"></a>
|
|
|
|
### `ctx.pluginManager` — `PluginManager`
|
|
|
|
Manage profile files and apply their declared reload lifecycle.
|
|
|
|
```ts cordis-catalog
|
|
/** Read current plugins, including why a row cannot be changed through the profile patch.
|
|
* @returns Current runtime entries with persistent patch targets.
|
|
*/
|
|
@Remote async listPlugins(): Promise<PluginInfo[]>
|
|
|
|
/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
|
|
* A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
|
|
* @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
|
|
* whether the installation offers the bundle, and removal availability.
|
|
*/
|
|
@Remote listBundles(): Promise<BundleInfo[]>
|
|
|
|
/** Read the registries this manager asks: the configured first one, its fallbacks in order, and what pnpm's own configuration names.
|
|
* @returns The registries in pnpm's comparison form; null is the one pnpm's own configuration names, `resolved` as pnpm reads it now.
|
|
*/
|
|
@Remote async registries(): Promise<PluginRegistries>
|
|
|
|
/** Read what a spec names before installing it.
|
|
* @param spec One package spec: a registry name, an absolute path, a git address, or a tarball.
|
|
* @param options The registry asked first.
|
|
* @param signal Ends a registry lookup early.
|
|
* @returns The package the spec names, or why it is refused.
|
|
*/
|
|
@Remote async inspect(spec: string, options?: InspectOptions, signal?: AbortSignal): Promise<PluginSpecInspection>
|
|
|
|
/** Persist a plugin entry's desired enablement and apply it on live profiles.
|
|
* @param id Loader entry identity returned by listPlugins.
|
|
* @param enabled Whether the plugin should run.
|
|
* @returns Saved and runtime outcomes, including higher-priority overrides.
|
|
*/
|
|
@Remote setPluginEnabled(id: PluginEntryId, enabled: boolean): Promise<ChangeResult>
|
|
|
|
/** Select or remove a bundle layer while retaining installed dependencies.
|
|
* @param name Bundle package name.
|
|
* @param enabled Whether the bundle contributes its patch layer.
|
|
* @returns Persisted and runtime outcomes.
|
|
*/
|
|
@Remote setBundleEnabled(name: string, enabled: boolean): Promise<ChangeResult>
|
|
|
|
/**
|
|
* Install a package using the same pnpm implementation as dsh plugin. GitHub
|
|
* repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts;
|
|
* only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run
|
|
* that fails, is cancelled, or adds a package without a bundle patch restores
|
|
* `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
|
|
* @param spec One package spec, including local paths relative to the invocation directory.
|
|
* @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
|
|
* the pending build scripts to allow for this profile before pnpm runs, and the registry asked first.
|
|
* @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
|
|
*/
|
|
@Remote installBundle(spec: string, options?: InstallBundleOptions): Promise<ChangeResult>
|
|
|
|
/** Recover the result of an active installation without cancelling it.
|
|
* @param requestId The id supplied when installation started.
|
|
* @returns The installation's outcome after it settles, or null if no active request has that id.
|
|
* Completed results are not retained; null establishes neither success nor cancellation.
|
|
*/
|
|
@Remote async waitForInstall(requestId: PluginInstallRequestId): Promise<ChangeResult | null>
|
|
|
|
/** Stop an installation this manager owns and wait until its files are back.
|
|
* @param requestId The id the installation was started with.
|
|
* @returns `cancelled` once the Git check or pnpm exited and the files are restored, `too-late` once the bundle is being
|
|
* applied, `not-running` for any other id.
|
|
*/
|
|
@Remote async cancelInstall(requestId: PluginInstallRequestId): Promise<PluginInstallCancellation>
|
|
|
|
/** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.
|
|
* @param name Installed dependency name.
|
|
* @returns Removal diagnostics and the remaining profile state.
|
|
*/
|
|
@Remote removeBundle(name: string): Promise<ChangeResult>
|
|
```
|
|
|
|
Source: [`packages/boot/plugin-manager/src/index.ts`](../../packages/boot/plugin-manager/src/index.ts)
|
|
|
|
<a id="ctxpluginregistryprobe--pluginregistryprobe"></a>
|
|
|
|
### `ctx.pluginRegistryProbe` — `PluginRegistryProbe`
|
|
|
|
Compares public registry responses on the Host; the Client owns the initial selection.
|
|
|
|
```ts cordis-catalog
|
|
/**
|
|
* Race npm and npmmirror HTTPS ping responses through the Host's fetch proxy.
|
|
* Concurrent readers share a probe; a winner cancels and awaits the other request.
|
|
* @returns the first registry with a successful response, or null when disabled or neither responds successfully; results are cached.
|
|
* @throws rejects when the service has been unloaded.
|
|
*/
|
|
@Remote async fastest(): Promise<string | null>
|
|
```
|
|
|
|
Source: [`packages/client/ui-plugin-manager/src/index.ts`](../../packages/client/ui-plugin-manager/src/index.ts)
|
|
|
|
<a id="ctxprofilecontext--profilecontext"></a>
|
|
|
|
### `ctx.profileContext` — `ProfileContext`
|
|
|
|
Current profile facts; scheduling and mutation belong to their callers.
|
|
|
|
Source: [`packages/boot/app-boot/src/profile-context.ts`](../../packages/boot/app-boot/src/profile-context.ts)
|
|
|
|
<a id="app-boot-events"></a>
|
|
|
|
### `app-boot/*` events
|
|
|
|
<a id="app-bootconfig-reload--emit"></a>
|
|
|
|
#### `app-boot/config-reload` — emit
|
|
|
|
Profile patches were reconciled into the running Loader tree: every entry update settled and no new inactive entry was introduced. Carries no diff; listeners re-read Loader entries.
|
|
|
|
```ts cordis-catalog
|
|
/**
|
|
* Profile patches were reconciled into the running Loader tree: every entry update settled and no new
|
|
* inactive entry was introduced. Carries no diff; listeners re-read Loader entries.
|
|
* @mode emit
|
|
*/
|
|
'app-boot/config-reload'(): void
|
|
```
|
|
|
|
Source: [`packages/boot/app-boot/src/index.ts`](../../packages/boot/app-boot/src/index.ts)
|
|
|
|
<a id="hmr-events"></a>
|
|
|
|
### `hmr/*` events
|
|
|
|
<a id="hmrchange--emit"></a>
|
|
|
|
#### `hmr/change` — emit
|
|
|
|
A watched file has no module or configuration handler.
|
|
|
|
```ts cordis-catalog
|
|
/** A watched file has no module or configuration handler.
|
|
* @mode emit
|
|
* @param url Canonical file URL.
|
|
*/
|
|
'hmr/change'(url: string): void
|
|
```
|
|
|
|
Source: [`packages/boot/hmr/src/index.ts`](../../packages/boot/hmr/src/index.ts)
|
|
|
|
<a id="hmrreload--emit"></a>
|
|
|
|
#### `hmr/reload` — emit
|
|
|
|
Module replacements have finished loading.
|
|
|
|
```ts cordis-catalog
|
|
/** Module replacements have finished loading.
|
|
* @mode emit
|
|
* @param reloads Replaced plugins and their module locations.
|
|
*/
|
|
'hmr/reload'(reloads: Map<Plugin, Reload>): void
|
|
```
|
|
|
|
Source: [`packages/boot/hmr/src/index.ts`](../../packages/boot/hmr/src/index.ts)
|
|
|
|
<a id="plugin-manager-events"></a>
|
|
|
|
### `plugin-manager/*` events
|
|
|
|
<a id="plugin-managerchanged--emit"></a>
|
|
|
|
#### `plugin-manager/changed` — emit
|
|
|
|
The profile's plugins, bundles, or composition changed: a manager operation completed. A patch generation applied outside the manager, by HMR's watcher after a CLI or hand edit, announces nothing here.
|
|
|
|
```ts cordis-catalog
|
|
/**
|
|
* The profile's plugins, bundles, or composition changed: a manager
|
|
* operation completed. A patch generation applied outside the manager,
|
|
* by HMR's watcher after a CLI or hand edit, announces nothing here.
|
|
* @mode emit
|
|
* @param change - what changed.
|
|
*/
|
|
'plugin-manager/changed'(change: PluginChange): void
|
|
```
|
|
|
|
Source: [`packages/boot/plugin-manager/src/types.ts`](../../packages/boot/plugin-manager/src/types.ts)
|
|
|
|
<a id="plugin-managerinstall-log--emit"></a>
|
|
|
|
#### `plugin-manager/install-log` — emit
|
|
|
|
One chunk of a pnpm run's output, streamed as the run produces it.
|
|
|
|
```ts cordis-catalog
|
|
/**
|
|
* One chunk of a pnpm run's output, streamed as the run produces it.
|
|
* @mode emit
|
|
* @param chunk - the chunk and the run it belongs to.
|
|
*/
|
|
'plugin-manager/install-log'(chunk: PluginInstallLogChunk): void
|
|
```
|
|
|
|
Source: [`packages/boot/plugin-manager/src/types.ts`](../../packages/boot/plugin-manager/src/types.ts)
|
|
|
|
<a id="plugin-managerinstall-state--emit"></a>
|
|
|
|
#### `plugin-manager/install-state` — emit
|
|
|
|
An installation moved between its Host phases. `installing` is announced once per registry the installation asks, with the attempt's registry and position; `cancelling` and `applying` once.
|
|
|
|
```ts cordis-catalog
|
|
/**
|
|
* An installation moved between its Host phases. `installing` is announced once per registry the
|
|
* installation asks, with the attempt's registry and position; `cancelling` and `applying` once.
|
|
* @mode emit
|
|
* @param progress - the installation's request id and phase, with the attempt while installing.
|
|
*/
|
|
'plugin-manager/install-state'(progress: PluginInstallProgress): void
|
|
```
|
|
|
|
Source: [`packages/boot/plugin-manager/src/types.ts`](../../packages/boot/plugin-manager/src/types.ts)
|
|
<!-- END GENERATED cordis-surface -->
|