Files
skills/docs/devtools/tooling/installer/src/host/detect.md
2026-05-29 08:20:56 +01:00

2.7 KiB

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

import { runDetect } from "@suaveplan/installer";

const result = await runDetect(myPlugin, ctx);
if (result.alreadyInstalled) {
    ctx.cli.write("Plugin is already installed");
}

Advanced Example

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.