first commit
This commit is contained in:
78
docs/devtools/tooling/installer/src/host/detect.md
Normal file
78
docs/devtools/tooling/installer/src/host/detect.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# `detect`
|
||||
|
||||
## Purpose
|
||||
|
||||
Runs a plugin's optional `detect` stage inside an OpenTelemetry span. The
|
||||
plugin reports what is already present in the target — existing skill
|
||||
directories, conflicting files, configuration to migrate — so the host
|
||||
can show that information to the user before prompts run, and so plugins
|
||||
can tailor their plans accordingly. Detect is read-only by contract: it
|
||||
inspects `ctx.fs` but never mutates state.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps the plugin call in `installer.detect` span carrying `plugin.id`,
|
||||
`plugin.version`, and `dry_run` so traces correlate with apply.
|
||||
- No-op when the plugin omits `detect` — returns `{}` without opening a
|
||||
span, so plugins that don't need detection pay zero observability
|
||||
overhead.
|
||||
- Surfaces plugin-thrown errors unchanged — the host's outer `try/catch`
|
||||
is responsible for wrapping these in `INSTALLER.PLAN_FAILED` if they
|
||||
occur post-detect, but detect-stage exceptions propagate raw so plugins
|
||||
can model their own detection-time error codes.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```ts
|
||||
import { runDetect } from "@suaveplan/installer";
|
||||
|
||||
const result = await runDetect(myPlugin, ctx);
|
||||
if (result.alreadyInstalled) {
|
||||
ctx.cli.write("Plugin is already installed");
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
```ts
|
||||
import type { InstallerPlugin } from "@suaveplan/installer";
|
||||
|
||||
const plugin: InstallerPlugin<undefined, void> = {
|
||||
id: "auditor",
|
||||
version: "0.1.0",
|
||||
detect: async (ctx) => {
|
||||
const skillsDir = ctx.fs.pathOf(ctx.cwd, ".suaveplan/skills");
|
||||
const exists = await ctx.fs.exists(skillsDir);
|
||||
if (!exists) {
|
||||
return {
|
||||
alreadyInstalled: false,
|
||||
notes: ["fresh target"],
|
||||
};
|
||||
}
|
||||
const entries = await ctx.fs.listDir(skillsDir);
|
||||
return {
|
||||
alreadyInstalled: true,
|
||||
conflicts: entries.filter((e) => e !== "index.json"),
|
||||
notes: [`${entries.length} entries already present`],
|
||||
};
|
||||
},
|
||||
plan: async () => [],
|
||||
apply: async () => undefined,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `runDetect<TAnswers, THandle>(plugin, ctx): Promise<DetectResult>`
|
||||
- `DetectResult` — `{ alreadyInstalled?: boolean, conflicts?: string[], notes?: string[] }`. All fields optional.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The span is opened only when `plugin.detect` exists; this is a small
|
||||
optimisation but it matters when the host is called in a tight loop
|
||||
(e.g. `list` discovers many plugins). The default empty `DetectResult`
|
||||
(`{}`) ensures downstream stages never see `undefined` and lets the
|
||||
`alreadyInstalled` boolean be used in CLI rendering without an extra
|
||||
guard. Detect doesn't receive answers — those don't exist yet — so any
|
||||
configuration-dependent detection must happen lazily during plan
|
||||
instead.
|
||||
Reference in New Issue
Block a user