#!/usr/bin/env node /** * The homebridge-plugin-utils CLI, exposed to consumers via the `bin` field in `package.json`. A single cohesive module: the content-hash helper, the `prepareUi`, * `prepareDocs`, and `prepareChrome` transforms, the `runCli` dispatcher, and the entry-point execution all live here with no inter-file relative VALUE imports. * * That single-file shape is deliberate, not incidental. A bin is invoked through an `npm`-managed symlink in `node_modules/.bin`; if the entry imported a sibling * module by relative path AT LOAD TIME, that import would resolve against the symlink's directory under symlink-preserving or copied-package layouts and fail. With * every runtime edge pointing only at `node:` builtins, there is nothing to fracture - the CLI runs identically whether reached directly, through a symlink, through * a `file:` dependency, or under `--preserve-symlinks`. The lone `import type` of the renderer's signatures is fully erased by the compiler and emits no runtime edge, * so it carries the SSOT types without reintroducing a load-time relative dependency; when `prepareDocs` actually needs the renderer it reaches it through a computed * dynamic import the dispatch site supplies, the same indirection the `hblog` bin uses. * * The module is simultaneously the executable (run via the bin) and a side-effect-free library surface (`prepareUi` / `prepareDocs` / `prepareChrome` / `runCli` / * `USAGE`) that the test suite imports. The entry block at the bottom only executes when this module is the program entry point, detected by comparing canonicalized * real paths - see its comment for why a raw path comparison is insufficient. * * @module */ import type { parseDocChromeManifest, parseProjectEntries, renderDevBadges, renderDocIndex, renderMasthead, renderProjects } from "../docChrome.ts"; import type { renderFeatureOptionsReference, spliceMarkedRegion } from "../featureOptions-docs.ts"; /** * Usage banner shown on no command or an unknown command. Kept as an exported constant so tests assert against its text without * coupling to formatting details and so README documentation can reference it via import rather than duplicate. */ export declare const USAGE: string; /** * Mirror HBPU's compiled browser-side webUI into a plugin's homebridge-ui/public/lib directory under a content-hashed, version-named subdirectory. The subdir name * combines the package's semver version with a short content hash of `dist/ui/` (e.g., `2.0.0-abc1234567890def`), so the browser's HTTP cache invalidates * structurally on any content change rather than via per-file query-string hacks: the plugin's `index.html` reads the small manifest written alongside, then * injects a trailing-slash importmap entry that prefixes every `./lib/` import with the hashed-versioned path. Transitive imports inherit the prefix through * relative-URL resolution, so cache-busting reaches files the importmap never names. * * The content hash is what makes the maintainer-iteration use case work. A semver-only subdir name would stay constant across the maintainer's * edit/rebuild/test cycle (version doesn't bump on every save), and the browser would happily serve cached copies of the stale URLs. Hashing the tree means any * source change produces a different subdir name, which produces different URLs, which forces fresh fetches. Same code path serves the published-release case: a * release ships with a fixed hash, and every consumer fetching it sees the same URL space and caches aggressively within it. * * Operates repeatably. A re-run against unchanged source content produces the same subdir name + same contents + same manifest. Stale prior-build subdirs are * removed in the same pass; non-version-shaped entries in the destination are left untouched so a plugin's own files (assets, sibling tooling, README copies) * survive across runs. * * @param args * @param args.dest - The plugin's destination directory (typically `homebridge-ui/public/lib`). Created if missing. * @param args.sourceRoot - Path to HBPU's package root (the directory containing `package.json` and `dist/`). The entry block resolves this from the CLI's own * real path; tests pass a tmpdir populated with a synthetic HBPU layout. * * @throws When the source has not been built (`dist/ui/` missing), the source `package.json` lacks a `version` field, or the source `dist/ui` path exists but is not a * directory. */ export declare function prepareUi({ dest, sourceRoot }: { dest: string; sourceRoot: string; }): Promise; /** * Regenerate a plugin's Feature Options reference by projecting its live catalog through HBPU's shared renderer and splicing the result into the plugin's doc, in * place between the shared `FEATURE OPTIONS:BEGIN` / `END` markers. This centralizes the read/splice/atomic-write orchestration so a plugin's `build-docs` script * only needs a single line invoking this subcommand instead of a bespoke `*-gendocs.ts` shim. * * Pure-by-injection on `render` and `splice`: the renderer and the splice helper are passed in rather than imported statically, so this function is unit-testable * against the real `featureOptions-docs.ts` exports without a built `dist/`, and the CLI's single-file no-static-relative-import discipline is preserved (the dispatch * site reaches the renderer through a computed dynamic import). The catalog is loaded by dynamic import of its absolute path resolved to a `file:` URL, since a bare * absolute path is not a valid ESM specifier on every platform. The module's required exports are validated up front so a mis-shaped catalog fails with a * diagnostic naming the offending module and export rather than a downstream type error inside the renderer. * * The same catalog module may OPTIONALLY export scope hooks - `describeCategoryScope` and `describeOptionScope` - that the renderer threads through to contribute * plugin-private scope prose (a device-scope line under a category heading, a suffix on an option's description cell). These are the annotated-plugin extension point * (Protect/Access); a zero-config plugin (ratgdo) exports neither and documents every option unconditionally. Each hook is validated INDEPENDENTLY: if a module exports * one under either name it MUST be a function, else this throws a framed diagnostic naming the module and the offending export; an absent hook is simply omitted, and * the renderer already treats an absent hook as "omit cleanly". Because the hooks arrive through the dynamic-imported catalog namespace (never a static relative * import), they preserve the bin's symlink-safe load-time edge discipline exactly as the catalog arrays do. * * The write is atomic: the new contents are staged in a sibling `.tmp` file and renamed over the doc. The rename is atomic on a single filesystem, so a crash * mid-write can never leave a half-spliced doc behind - the file is either the prior content or the complete new content, never a truncated splice. * * @param args * @param args.catalogModulePath - Absolute path to the plugin's compiled catalog module exporting `featureOptionCategories` (an array) and `featureOptions` (an * object). Resolved to a `file:` URL before the dynamic import. * @param args.docPath - Absolute path to the doc whose marked region is replaced (typically the plugin's `docs/FeatureOptions.md`). * @param args.render - The injected {@link renderFeatureOptionsReference} from `featureOptions-docs.ts`. * @param args.splice - The injected {@link spliceMarkedRegion} from `featureOptions-docs.ts`. * * @throws When the catalog module lacks `featureOptionCategories` (or it is not an array) or `featureOptions` (or it is not a non-null object), when it exports a * present-but-non-function `describeCategoryScope` or `describeOptionScope`, and propagates the splice's own framed errors when the doc's marker pair is absent * or ambiguous. */ export declare function prepareDocs({ catalogModulePath, docPath, render, splice }: { catalogModulePath: string; docPath: string; render: typeof renderFeatureOptionsReference; splice: typeof spliceMarkedRegion; }): Promise; interface DocChromeModule { readonly DEV_BADGES_BEGIN: string; readonly DEV_BADGES_END: string; readonly DOCUMENTATION_BEGIN: string; readonly DOCUMENTATION_END: string; readonly MASTHEAD_BEGIN: string; readonly MASTHEAD_END: string; readonly PROJECTS_BEGIN: string; readonly PROJECTS_END: string; readonly parseDocChromeManifest: typeof parseDocChromeManifest; readonly parseProjectEntries: typeof parseProjectEntries; readonly renderDevBadges: typeof renderDevBadges; readonly renderDocIndex: typeof renderDocIndex; readonly renderMasthead: typeof renderMasthead; readonly renderProjects: typeof renderProjects; } /** * Stamp a plugin's documentation-chrome regions - the masthead, the documentation index, the dashboard badges, and the project list - across its README, every content * doc, and its webUI Support tab, from one per-plugin manifest. This is the multi-region, multi-file counterpart to {@link prepareDocs}: where that regenerates one * feature-options region in one doc, this regenerates several named regions in many files, so a plugin's masthead and navigation cannot drift between the surfaces that * repeat them. * * Pure-by-injection like {@link prepareDocs}: the `docChrome` renderers and validator arrive through the injected `chrome` namespace and the splice helper through * `splice`, so this function is unit-testable against the real exports without a built `dist/`, and the CLI's single-file no-static-relative-import discipline is * preserved. `fetchImpl` is injected (defaulting to the global `fetch`) so the remote project-source path is testable without network access. * * The manifest is loaded (a typed module or a static JSON file), validated with a field-naming diagnostic, and its optional project source resolved to inline data. The * per-file edit plan is then built - the README carries the masthead, the documentation index, and the dashboard badges; each content doc carries the masthead (unless * its entry opts out) and a self-omitting footer index; the webUI, when present, carries the documentation index and the project list. Every region is spliced against * its own marker pair through the shared, ambiguity-rejecting splice primitive. * * The write is all-or-nothing across files in the realistic failure mode. Splicing happens entirely in memory first, so a missing or ambiguous marker in any file * aborts the run before a single write. The writes then proceed in two phases - every file's new content is staged in a sibling `.tmp`, and only once all temps exist * are they renamed over their targets - so a staging failure leaves every original untouched and each promotion is an atomic rename. * * @param args * @param args.chrome - The injected `docChrome` module namespace (its renderers, marker constants, and validators). * @param args.fetchImpl - The `fetch` implementation used to resolve a remote project source. Defaults to the global `fetch`; tests inject a fake. * @param args.manifestPath - Absolute path to the plugin's manifest - a compiled module or a `.json` file. * @param args.pluginRoot - Absolute path to the plugin root that the manifest's surface and file references resolve against. * @param args.splice - The injected {@link spliceMarkedRegion} from `featureOptions-docs.ts`. * * @throws When the manifest is mis-shaped, when a resolved project source is malformed, when a target file cannot be read, or when any region's marker pair is absent or * ambiguous - propagating the splice's own framed errors so the dispatch site frames them uniformly. */ export declare function prepareChrome({ chrome, fetchImpl, manifestPath, pluginRoot, splice }: { chrome: DocChromeModule; fetchImpl?: typeof fetch; manifestPath: string; pluginRoot: string; splice: typeof spliceMarkedRegion; }): Promise; /** * Run the CLI against a synthetic argv vector. Returns the process exit code that the entry block propagates. Pure-by-injection: takes its `cwd`, `stderr`, and * `sourceRoot` as arguments rather than reading them from globals, so tests exercise the full dispatch path against a captured stderr, a tmpdir source root, and a * tmpdir working directory without ever touching `process.exit` or the real filesystem outside the tmpdir scope. * * @param args * @param args.argv - Positional and flag arguments (typically `process.argv.slice(2)`). * @param args.cwd - The working directory that relative subcommand path arguments resolve against. Production passes `process.cwd()`; tests pass a tmpdir. * @param args.sourceRoot - Path to HBPU's package root (resolved from the CLI's real path by the entry block; tests pass a tmpdir). * @param args.stderr - Stream-like sink for usage and error output. Production passes `process.stderr`; tests pass a captured-output collector. * * @returns The exit code: `0` on success or on an explicit no-arg invocation showing the usage banner, `1` on misuse or subcommand failure. */ export declare function runCli({ argv, cwd, sourceRoot, stderr }: { argv: readonly string[]; cwd: string; sourceRoot: string; stderr: { write: (chunk: string) => unknown; }; }): Promise; export {}; //# sourceMappingURL=index.d.ts.map