homebridge-plugin-utils 1.34.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +128 -0
- package/dist/backpressure.js +273 -0
- package/dist/backpressure.js.map +1 -0
- package/dist/cli/index.d.ts +95 -0
- package/dist/cli/index.js +355 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -257
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +70 -4
- package/dist/ffmpeg/fmp4.js +100 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
- package/dist/ffmpeg/mp4-assembler.js +387 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +143 -91
- package/dist/ffmpeg/process.js +406 -265
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -560
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +144 -0
- package/dist/ffmpeg/recording-process-double.js +168 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +473 -244
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -1
- package/dist/ffmpeg/settings.js +20 -2
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -148
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +677 -0
- package/dist/ui/webUi.mjs +192 -87
- package/dist/util.d.ts +597 -45
- package/dist/util.js +787 -68
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { renderFeatureOptionsReference, spliceMarkedRegion } from "../featureOptions-docs.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Usage banner shown on no command or an unknown command. Kept as an exported constant so tests assert against its text without
|
|
5
|
+
* coupling to formatting details and so README documentation can reference it via import rather than duplicate.
|
|
6
|
+
*/
|
|
7
|
+
export declare const USAGE: string;
|
|
8
|
+
/**
|
|
9
|
+
* 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
|
|
10
|
+
* 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
|
|
11
|
+
* 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
|
|
12
|
+
* injects a trailing-slash importmap entry that prefixes every `./lib/` import with the hashed-versioned path. Transitive imports inherit the prefix through
|
|
13
|
+
* relative-URL resolution, so cache-busting reaches files the importmap never names.
|
|
14
|
+
*
|
|
15
|
+
* The content hash is the load-bearing piece for the maintainer-iteration use case. A semver-only subdir name would stay constant across the maintainer's
|
|
16
|
+
* 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
|
|
17
|
+
* source change produces a different subdir name, which produces different URLs, which forces fresh fetches. Same code path serves the published-release case: a
|
|
18
|
+
* release ships with a fixed hash, and every consumer fetching it sees the same URL space and caches aggressively within it.
|
|
19
|
+
*
|
|
20
|
+
* Operates idempotently. A re-run against unchanged source content produces the same subdir name + same contents + same manifest. Stale prior-build subdirs are
|
|
21
|
+
* 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)
|
|
22
|
+
* survive across runs.
|
|
23
|
+
*
|
|
24
|
+
* @param args
|
|
25
|
+
* @param args.dest - The plugin's destination directory (typically `homebridge-ui/public/lib`). Created if missing.
|
|
26
|
+
* @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
|
|
27
|
+
* real path; tests pass a tmpdir populated with a synthetic HBPU layout.
|
|
28
|
+
*
|
|
29
|
+
* @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
|
|
30
|
+
* directory.
|
|
31
|
+
*/
|
|
32
|
+
export declare function prepareUi({ dest, sourceRoot }: {
|
|
33
|
+
dest: string;
|
|
34
|
+
sourceRoot: string;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* 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
|
|
38
|
+
* place between the shared `FEATURE OPTIONS:BEGIN` / `END` markers. This centralizes the read/splice/atomic-write orchestration so a plugin's `build-docs` script
|
|
39
|
+
* only needs a single line invoking this subcommand instead of a bespoke `*-gendocs.ts` shim.
|
|
40
|
+
*
|
|
41
|
+
* 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
|
|
42
|
+
* 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
|
|
43
|
+
* 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
|
|
44
|
+
* 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
|
|
45
|
+
* diagnostic naming the offending module and export rather than a downstream type error inside the renderer.
|
|
46
|
+
*
|
|
47
|
+
* The same catalog module may OPTIONALLY export scope hooks - `describeCategoryScope` and `describeOptionScope` - that the renderer threads through to contribute
|
|
48
|
+
* 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
|
|
49
|
+
* (Protect/Access); a zero-config plugin (ratgdo) exports neither and documents every option unconditionally. Each hook is validated INDEPENDENTLY: if a module exports
|
|
50
|
+
* 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
|
|
51
|
+
* the renderer already treats an absent hook as "omit cleanly". Because the hooks arrive through the dynamic-imported catalog namespace (never a static relative
|
|
52
|
+
* import), they preserve the bin's symlink-safe load-time edge discipline exactly as the catalog arrays do.
|
|
53
|
+
*
|
|
54
|
+
* 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
|
|
55
|
+
* 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.
|
|
56
|
+
*
|
|
57
|
+
* @param args
|
|
58
|
+
* @param args.catalogModulePath - Absolute path to the plugin's compiled catalog module exporting `featureOptionCategories` (an array) and `featureOptions` (an
|
|
59
|
+
* object). Resolved to a `file:` URL before the dynamic import.
|
|
60
|
+
* @param args.docPath - Absolute path to the doc whose marked region is replaced (typically the plugin's `docs/FeatureOptions.md`).
|
|
61
|
+
* @param args.render - The injected {@link renderFeatureOptionsReference} from `featureOptions-docs.ts`.
|
|
62
|
+
* @param args.splice - The injected {@link spliceMarkedRegion} from `featureOptions-docs.ts`.
|
|
63
|
+
*
|
|
64
|
+
* @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
|
|
65
|
+
* present-but-non-function `describeCategoryScope` or `describeOptionScope`, and propagates the splice's own framed errors when the doc's marker pair is absent
|
|
66
|
+
* or ambiguous.
|
|
67
|
+
*/
|
|
68
|
+
export declare function prepareDocs({ catalogModulePath, docPath, render, splice }: {
|
|
69
|
+
catalogModulePath: string;
|
|
70
|
+
docPath: string;
|
|
71
|
+
render: typeof renderFeatureOptionsReference;
|
|
72
|
+
splice: typeof spliceMarkedRegion;
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* 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
|
|
76
|
+
* `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
|
|
77
|
+
* tmpdir working directory without ever touching `process.exit` or the real filesystem outside the tmpdir scope.
|
|
78
|
+
*
|
|
79
|
+
* @param args
|
|
80
|
+
* @param args.argv - Positional and flag arguments (typically `process.argv.slice(2)`).
|
|
81
|
+
* @param args.cwd - The working directory that relative subcommand path arguments resolve against. Production passes `process.cwd()`; tests pass a tmpdir.
|
|
82
|
+
* @param args.sourceRoot - Path to HBPU's package root (resolved from the CLI's real path by the entry block; tests pass a tmpdir).
|
|
83
|
+
* @param args.stderr - Stream-like sink for usage and error output. Production passes `process.stderr`; tests pass a captured-output collector.
|
|
84
|
+
*
|
|
85
|
+
* @returns The exit code: `0` on success or on an explicit no-arg invocation showing the usage banner, `1` on misuse or subcommand failure.
|
|
86
|
+
*/
|
|
87
|
+
export declare function runCli({ argv, cwd, sourceRoot, stderr }: {
|
|
88
|
+
argv: readonly string[];
|
|
89
|
+
cwd: string;
|
|
90
|
+
sourceRoot: string;
|
|
91
|
+
stderr: {
|
|
92
|
+
write: (chunk: string) => unknown;
|
|
93
|
+
};
|
|
94
|
+
}): Promise<number>;
|
|
95
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* cli/index.ts: The homebridge-plugin-utils command-line interface.
|
|
5
|
+
*/
|
|
6
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
7
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
8
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
9
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return path;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* 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`
|
|
16
|
+
* and `prepareDocs` transforms, the `runCli` dispatcher, and the entry-point execution all live here with no inter-file relative VALUE imports.
|
|
17
|
+
*
|
|
18
|
+
* 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
|
|
19
|
+
* 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
|
|
20
|
+
* every runtime edge pointing only at `node:` builtins, there is nothing to fracture - the CLI runs identically whether reached directly, through a symlink, through
|
|
21
|
+
* 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,
|
|
22
|
+
* 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
|
|
23
|
+
* dynamic import the dispatch site supplies, the same indirection the `hblog` bin uses.
|
|
24
|
+
*
|
|
25
|
+
* The module is simultaneously the executable (run via the bin) and a side-effect-free library surface (`prepareUi` / `prepareDocs` / `runCli` / `USAGE`) that the
|
|
26
|
+
* 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
|
|
27
|
+
* its comment for why a raw path comparison is insufficient.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
32
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import { parseArgs } from "node:util";
|
|
36
|
+
import { realpathSync } from "node:fs";
|
|
37
|
+
// Semver-shaped subdir names that {@link prepareUi} owns. Only entries matching this pattern are candidates for the stale-build sweep; anything else in the
|
|
38
|
+
// destination is left alone. Pattern matches `MAJOR.MINOR.PATCH` plus the optional pre-release (`-...`) and build-metadata (`+...`) segments semver permits, so a
|
|
39
|
+
// plugin author who happens to name a subdir `assets` or `i18n` is never at risk of having it removed. The hash-suffixed shape this CLI writes (e.g.,
|
|
40
|
+
// `2.0.0-abc1234567890def`) falls into the pre-release segment, so the same pattern catches stale released-version directories from prior tooling and stale
|
|
41
|
+
// hash-suffixed directories from prior runs alike.
|
|
42
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
43
|
+
// Truncated SHA-256 length used for the content-hash segment of the subdir name. 16 hex chars (64 bits) put the birthday-paradox collision threshold near ~2^32
|
|
44
|
+
// distinct builds (a chance collision between any specific pair is 2^-64), more than sufficient for cache-busting. Matches the truncation convention modern bundlers
|
|
45
|
+
// (Vite, Next.js, esbuild, webpack) use for the same purpose: short enough to scan visually in a path, long enough that a chance collision between two builds is
|
|
46
|
+
// astronomically unlikely.
|
|
47
|
+
const HASH_LENGTH = 16;
|
|
48
|
+
/**
|
|
49
|
+
* Usage banner shown on no command or an unknown command. Kept as an exported constant so tests assert against its text without
|
|
50
|
+
* coupling to formatting details and so README documentation can reference it via import rather than duplicate.
|
|
51
|
+
*/
|
|
52
|
+
export const USAGE = "Usage: homebridge-plugin-utils <command> [options]\n\n" +
|
|
53
|
+
"Commands:\n prepare-ui <destination> Mirror HBPU's webUI into the plugin's lib directory.\n" +
|
|
54
|
+
" prepare-docs <catalog-module> [--doc <path>] Generate the Feature Options reference into the plugin's docs.\n";
|
|
55
|
+
/**
|
|
56
|
+
* Compute a deterministic content hash over `root`'s file tree. Walks every file in lexicographic order of relative POSIX path so two runs against the same content
|
|
57
|
+
* produce the same hash regardless of underlying filesystem ordering or the absolute path the tree sits at. Mixes both the relative path and the file bytes into
|
|
58
|
+
* the hash so renames produce a different output even when content bytes are unchanged - that is the correct cache-bust semantic, since a rename is a visible
|
|
59
|
+
* difference to any consumer importing the file by name.
|
|
60
|
+
*
|
|
61
|
+
* The null-byte (`\0`) delimiters between path and content prevent the synthetic boundary-blurring case where a long path concatenated with a short content could
|
|
62
|
+
* share its hash input with a shorter path concatenated with a longer content. SHA-256's preimage resistance covers anything more contrived, but the explicit
|
|
63
|
+
* delimiter is the discipline that makes the hash input unambiguous on its face.
|
|
64
|
+
*
|
|
65
|
+
* Returns the first {@link HASH_LENGTH} hex characters of the hash. Truncation is deliberate: full SHA-256 (64 hex chars) would bloat the URL path and the
|
|
66
|
+
* filesystem display without earning meaningful collision resistance for this use case.
|
|
67
|
+
*
|
|
68
|
+
* @param root - The directory whose tree contents are hashed.
|
|
69
|
+
*
|
|
70
|
+
* @returns The truncated hex hash, suitable for use as a path segment.
|
|
71
|
+
*/
|
|
72
|
+
async function computeContentHash(root) {
|
|
73
|
+
// Node 22's recursive readdir plus `withFileTypes: true` gives us a flat Dirent[] for the entire tree in one syscall, eliminating the manual stack-walk that
|
|
74
|
+
// older Node versions required. `parentPath` is the non-deprecated way to recover each entry's directory (the `path` alias was deprecated in Node 20.12 /
|
|
75
|
+
// 21.4); using it here keeps the implementation aligned with current Node idioms.
|
|
76
|
+
const entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
77
|
+
const filePaths = entries
|
|
78
|
+
.filter((entry) => entry.isFile())
|
|
79
|
+
.map((entry) => join(entry.parentPath, entry.name))
|
|
80
|
+
.toSorted();
|
|
81
|
+
// Read every file's bytes in parallel, then feed them into the hasher sequentially. Parallel reads keep the I/O latency bound to the slowest single file rather
|
|
82
|
+
// than the sum of all files; the sequential hash update is required because the hasher is stateful and must consume bytes in the canonical sorted order to
|
|
83
|
+
// produce the same digest every run.
|
|
84
|
+
const fileBytes = await Promise.all(filePaths.map(async (path) => ({ bytes: await readFile(path), path })));
|
|
85
|
+
const hasher = createHash("sha256");
|
|
86
|
+
for (const { bytes, path } of fileBytes) {
|
|
87
|
+
// Normalize separators to POSIX form so a hash computed on a Windows build matches a hash computed on Linux for the same tree contents. The relative path
|
|
88
|
+
// anchors the hash to the tree structure (not the absolute location), so the same source tree built into `/tmp/a/dist/ui` and `/var/build/b/dist/ui` produces
|
|
89
|
+
// an identical hash.
|
|
90
|
+
const rel = relative(root, path).split(sep).join("/");
|
|
91
|
+
hasher.update(rel);
|
|
92
|
+
hasher.update("\0");
|
|
93
|
+
hasher.update(bytes);
|
|
94
|
+
hasher.update("\0");
|
|
95
|
+
}
|
|
96
|
+
return hasher.digest("hex").slice(0, HASH_LENGTH);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 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
|
|
100
|
+
* 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
|
|
101
|
+
* 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
|
|
102
|
+
* injects a trailing-slash importmap entry that prefixes every `./lib/` import with the hashed-versioned path. Transitive imports inherit the prefix through
|
|
103
|
+
* relative-URL resolution, so cache-busting reaches files the importmap never names.
|
|
104
|
+
*
|
|
105
|
+
* The content hash is the load-bearing piece for the maintainer-iteration use case. A semver-only subdir name would stay constant across the maintainer's
|
|
106
|
+
* 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
|
|
107
|
+
* source change produces a different subdir name, which produces different URLs, which forces fresh fetches. Same code path serves the published-release case: a
|
|
108
|
+
* release ships with a fixed hash, and every consumer fetching it sees the same URL space and caches aggressively within it.
|
|
109
|
+
*
|
|
110
|
+
* Operates idempotently. A re-run against unchanged source content produces the same subdir name + same contents + same manifest. Stale prior-build subdirs are
|
|
111
|
+
* 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)
|
|
112
|
+
* survive across runs.
|
|
113
|
+
*
|
|
114
|
+
* @param args
|
|
115
|
+
* @param args.dest - The plugin's destination directory (typically `homebridge-ui/public/lib`). Created if missing.
|
|
116
|
+
* @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
|
|
117
|
+
* real path; tests pass a tmpdir populated with a synthetic HBPU layout.
|
|
118
|
+
*
|
|
119
|
+
* @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
|
|
120
|
+
* directory.
|
|
121
|
+
*/
|
|
122
|
+
export async function prepareUi({ dest, sourceRoot }) {
|
|
123
|
+
const sourceUiDir = join(sourceRoot, "dist", "ui");
|
|
124
|
+
const sourcePackageJson = join(sourceRoot, "package.json");
|
|
125
|
+
// Validate the source side first so failures name the corrective action rather than the operation we couldn't perform. The two ways the source can be wrong are
|
|
126
|
+
// structurally distinct: `dist/ui` missing means HBPU hasn't been built; `package.json` missing or version-less means the package is malformed. Both surface as
|
|
127
|
+
// typed errors with the path that diagnosed the problem.
|
|
128
|
+
let sourceUiStat;
|
|
129
|
+
try {
|
|
130
|
+
sourceUiStat = await stat(sourceUiDir);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new Error("HBPU has not been built: " + sourceUiDir + " is missing. Run `npm run build` in HBPU first.");
|
|
134
|
+
}
|
|
135
|
+
if (!sourceUiStat.isDirectory()) {
|
|
136
|
+
throw new Error("HBPU source path is not a directory: " + sourceUiDir + ".");
|
|
137
|
+
}
|
|
138
|
+
const packageManifestRaw = await readFile(sourcePackageJson, "utf8");
|
|
139
|
+
const packageManifest = JSON.parse(packageManifestRaw);
|
|
140
|
+
const version = packageManifest.version;
|
|
141
|
+
if (!version) {
|
|
142
|
+
throw new Error("HBPU package.json at " + sourcePackageJson + " has no version field.");
|
|
143
|
+
}
|
|
144
|
+
// Compute the content hash before any destination work begins so the source side is fully validated and characterized before we touch the plugin's directory.
|
|
145
|
+
// The subdir name combines version + hash: version stays the human-readable, operator-recognizable segment; hash supplies the change-detection that makes the
|
|
146
|
+
// cache invalidation structurally complete across both maintainer-iteration and published-release use cases.
|
|
147
|
+
const hash = await computeContentHash(sourceUiDir);
|
|
148
|
+
const subdir = version + "-" + hash;
|
|
149
|
+
// Resolve the destination to an absolute path up front so every subsequent path operation reads against the same canonical base. The destination is created via
|
|
150
|
+
// `mkdir({ recursive: true })` rather than relying on `cp` to lazily create it, so the readdir-based stale-build sweep below can run against a known-extant
|
|
151
|
+
// directory on every invocation (including the first run against a never-before-existing path).
|
|
152
|
+
const absDest = resolve(dest);
|
|
153
|
+
await mkdir(absDest, { recursive: true });
|
|
154
|
+
const versionedDest = join(absDest, subdir);
|
|
155
|
+
// Idempotent reset of the versioned subdir. Wipe first so the copy step always starts from a known-empty target; the same-content idempotency property is upheld
|
|
156
|
+
// by the hash being a deterministic function of the source, so re-running with unchanged content produces the same subdir name + same contents + same manifest,
|
|
157
|
+
// even though the implementation re-does the work. `force: true` swallows ENOENT so a first run against an empty destination doesn't surface the missing-target
|
|
158
|
+
// as an error.
|
|
159
|
+
await rm(versionedDest, { force: true, recursive: true });
|
|
160
|
+
await cp(sourceUiDir, versionedDest, { recursive: true });
|
|
161
|
+
// Destination manifest sits at the top of `dest`, NOT inside the versioned subdir. The plugin's `index.html` reads it (with `cache: "no-store"`) to discover
|
|
162
|
+
// which subdir to load from. Every manifest field serves a distinct purpose: `version` for human-readable identification, `hash` for change-detection, `subdir`
|
|
163
|
+
// for the canonical "where to load from" answer that consumers reference directly without reconstructing the join. The join convention stays here in the
|
|
164
|
+
// producer; if we ever change it (different delimiter, different segment order), no consumer breaks. Pretty-printed JSON because the file is human-read often
|
|
165
|
+
// enough to matter (debugging sessions, README screenshots, support requests).
|
|
166
|
+
await writeFile(join(absDest, "manifest.json"), JSON.stringify({ hash, subdir, version }, null, 2) + "\n");
|
|
167
|
+
// Sweep stale versioned subdirs from prior runs. The semver-shape filter is the discipline that keeps the sweep narrow: anything in the destination that doesn't
|
|
168
|
+
// look like a version we own (plugin's own files, sibling tools, hand-curated content) survives untouched. The current subdir is skipped here since we just
|
|
169
|
+
// (re)created it above; the loop targets only OLDER builds left over from prior runs (different version, different hash, or both).
|
|
170
|
+
const entries = await readdir(absDest, { withFileTypes: true });
|
|
171
|
+
await Promise.all(entries
|
|
172
|
+
.filter((entry) => entry.isDirectory() && VERSION_PATTERN.test(entry.name) && (entry.name !== subdir))
|
|
173
|
+
.map((entry) => rm(join(absDest, entry.name), { force: true, recursive: true })));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 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
|
|
177
|
+
* place between the shared `FEATURE OPTIONS:BEGIN` / `END` markers. This centralizes the read/splice/atomic-write orchestration so a plugin's `build-docs` script
|
|
178
|
+
* only needs a single line invoking this subcommand instead of a bespoke `*-gendocs.ts` shim.
|
|
179
|
+
*
|
|
180
|
+
* 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
|
|
181
|
+
* 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
|
|
182
|
+
* 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
|
|
183
|
+
* 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
|
|
184
|
+
* diagnostic naming the offending module and export rather than a downstream type error inside the renderer.
|
|
185
|
+
*
|
|
186
|
+
* The same catalog module may OPTIONALLY export scope hooks - `describeCategoryScope` and `describeOptionScope` - that the renderer threads through to contribute
|
|
187
|
+
* 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
|
|
188
|
+
* (Protect/Access); a zero-config plugin (ratgdo) exports neither and documents every option unconditionally. Each hook is validated INDEPENDENTLY: if a module exports
|
|
189
|
+
* 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
|
|
190
|
+
* the renderer already treats an absent hook as "omit cleanly". Because the hooks arrive through the dynamic-imported catalog namespace (never a static relative
|
|
191
|
+
* import), they preserve the bin's symlink-safe load-time edge discipline exactly as the catalog arrays do.
|
|
192
|
+
*
|
|
193
|
+
* 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
|
|
194
|
+
* 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.
|
|
195
|
+
*
|
|
196
|
+
* @param args
|
|
197
|
+
* @param args.catalogModulePath - Absolute path to the plugin's compiled catalog module exporting `featureOptionCategories` (an array) and `featureOptions` (an
|
|
198
|
+
* object). Resolved to a `file:` URL before the dynamic import.
|
|
199
|
+
* @param args.docPath - Absolute path to the doc whose marked region is replaced (typically the plugin's `docs/FeatureOptions.md`).
|
|
200
|
+
* @param args.render - The injected {@link renderFeatureOptionsReference} from `featureOptions-docs.ts`.
|
|
201
|
+
* @param args.splice - The injected {@link spliceMarkedRegion} from `featureOptions-docs.ts`.
|
|
202
|
+
*
|
|
203
|
+
* @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
|
|
204
|
+
* present-but-non-function `describeCategoryScope` or `describeOptionScope`, and propagates the splice's own framed errors when the doc's marker pair is absent
|
|
205
|
+
* or ambiguous.
|
|
206
|
+
*/
|
|
207
|
+
export async function prepareDocs({ catalogModulePath, docPath, render, splice }) {
|
|
208
|
+
// Load the catalog by dynamic import. A bare absolute filesystem path is not a portable ESM specifier (Windows drive letters in particular are mis-parsed), so we
|
|
209
|
+
// convert it to a `file:` URL first - the same indirection the `hblog` bin uses to reach its sibling library from a single-file launcher. The namespace is typed with
|
|
210
|
+
// the required catalog exports plus the OPTIONAL scope hooks, each `unknown` until validated below - the same honest-until-checked shape the catalog arrays
|
|
211
|
+
// carry, so nothing reaches the renderer as a mis-typed value.
|
|
212
|
+
const catalog = await import(__rewriteRelativeImportExtension(pathToFileURL(catalogModulePath).href));
|
|
213
|
+
// Validate the catalog's required exports before rendering so a mis-built or wrong-module path fails with a diagnostic that names what is wrong and where,
|
|
214
|
+
// rather than surfacing as an opaque type error deep inside the renderer's traversal.
|
|
215
|
+
if (!Array.isArray(catalog.featureOptionCategories)) {
|
|
216
|
+
throw new Error("Catalog module " + catalogModulePath + " does not export a `featureOptionCategories` array.");
|
|
217
|
+
}
|
|
218
|
+
if ((typeof catalog.featureOptions !== "object") || (catalog.featureOptions === null)) {
|
|
219
|
+
throw new Error("Catalog module " + catalogModulePath + " does not export a `featureOptions` object.");
|
|
220
|
+
}
|
|
221
|
+
// Validate every OPTIONAL scope hook independently. Each is present-but-must-be-a-function or absent: a present non-function is a mis-shaped catalog and fails
|
|
222
|
+
// with a diagnostic naming the module and the offending export (the same framing the required exports use), while an absent hook stays `undefined` and is inert -
|
|
223
|
+
// the renderer treats a missing hook as "omit cleanly", so the zero-hook path (ratgdo) is unchanged. We check before casting so the cast to the renderer's hook
|
|
224
|
+
// parameter type only happens once the value is known to be callable, mirroring how the catalog arrays are cast only after their shape guard.
|
|
225
|
+
if ((catalog.describeCategoryScope !== undefined) && (typeof catalog.describeCategoryScope !== "function")) {
|
|
226
|
+
throw new Error("Catalog module " + catalogModulePath + " exports a non-function `describeCategoryScope`.");
|
|
227
|
+
}
|
|
228
|
+
if ((catalog.describeOptionScope !== undefined) && (typeof catalog.describeOptionScope !== "function")) {
|
|
229
|
+
throw new Error("Catalog module " + catalogModulePath + " exports a non-function `describeOptionScope`.");
|
|
230
|
+
}
|
|
231
|
+
// Render the live catalog into the markdown fragment. The optional scope hooks are forwarded through alongside the catalog; an `undefined` hook is inert, so a
|
|
232
|
+
// zero-hook catalog renders cleanly. The renderer is the single source of truth for the index, the per-category tables, and the scope prose the optional hooks
|
|
233
|
+
// contribute, while this function only wires the catalog and its hooks to it. Each hook is cast to the renderer's parameter type only after its function
|
|
234
|
+
// guard above, the same discipline the catalog arrays follow.
|
|
235
|
+
const reference = render({ categories: catalog.featureOptionCategories,
|
|
236
|
+
describeCategoryScope: catalog.describeCategoryScope,
|
|
237
|
+
describeOptionScope: catalog.describeOptionScope,
|
|
238
|
+
options: catalog.featureOptions });
|
|
239
|
+
// Read the existing doc and splice the rendered fragment in place between the markers, leaving the hand-written header and intro untouched. The splice throws its
|
|
240
|
+
// own framed errors on a missing or ambiguous marker pair; we let those propagate so the dispatch site frames them uniformly.
|
|
241
|
+
const source = await readFile(docPath, "utf8");
|
|
242
|
+
const updated = splice(source, reference);
|
|
243
|
+
// Atomic write: stage in a sibling temp file, then rename over the doc.
|
|
244
|
+
await writeFile(docPath + ".tmp", updated, "utf8");
|
|
245
|
+
await rename(docPath + ".tmp", docPath);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 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
|
|
249
|
+
* `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
|
|
250
|
+
* tmpdir working directory without ever touching `process.exit` or the real filesystem outside the tmpdir scope.
|
|
251
|
+
*
|
|
252
|
+
* @param args
|
|
253
|
+
* @param args.argv - Positional and flag arguments (typically `process.argv.slice(2)`).
|
|
254
|
+
* @param args.cwd - The working directory that relative subcommand path arguments resolve against. Production passes `process.cwd()`; tests pass a tmpdir.
|
|
255
|
+
* @param args.sourceRoot - Path to HBPU's package root (resolved from the CLI's real path by the entry block; tests pass a tmpdir).
|
|
256
|
+
* @param args.stderr - Stream-like sink for usage and error output. Production passes `process.stderr`; tests pass a captured-output collector.
|
|
257
|
+
*
|
|
258
|
+
* @returns The exit code: `0` on success or on an explicit no-arg invocation showing the usage banner, `1` on misuse or subcommand failure.
|
|
259
|
+
*/
|
|
260
|
+
export async function runCli({ argv, cwd, sourceRoot, stderr }) {
|
|
261
|
+
// parseArgs runs in strict mode, so an unrecognized flag (e.g. a typo'd `--docs`) throws `ERR_PARSE_ARGS_UNKNOWN_OPTION` here rather than falling through to the
|
|
262
|
+
// usage banner. That throw is intentionally uncaught - the per-case try/catch blocks below wrap only the subcommand work - so it surfaces as a rejected `runCli`
|
|
263
|
+
// promise at the entry point, distinct from the default case's banner handling for an unknown positional command.
|
|
264
|
+
const { positionals, values } = parseArgs({ allowPositionals: true, args: [...argv], options: { doc: { type: "string" } }, strict: true });
|
|
265
|
+
const [command, ...rest] = positionals;
|
|
266
|
+
switch (command) {
|
|
267
|
+
case "prepare-ui": {
|
|
268
|
+
const [destination] = rest;
|
|
269
|
+
if (!destination) {
|
|
270
|
+
stderr.write("homebridge-plugin-utils prepare-ui: missing required destination argument.\n");
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
await prepareUi({ dest: destination, sourceRoot });
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
stderr.write("homebridge-plugin-utils prepare-ui: " + (error instanceof Error ? error.message : String(error)) + "\n");
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
case "prepare-docs": {
|
|
283
|
+
const [catalogArg] = rest;
|
|
284
|
+
if (!catalogArg) {
|
|
285
|
+
stderr.write("homebridge-plugin-utils prepare-docs: missing required catalog-module argument.\n");
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
// Resolve both paths against the injected working directory. The catalog argument is the plugin's compiled options module; the doc defaults to the family's
|
|
289
|
+
// canonical `docs/FeatureOptions.md` and is overridable through `--doc` for a plugin that ships its reference elsewhere.
|
|
290
|
+
const catalogModulePath = resolve(cwd, catalogArg);
|
|
291
|
+
const docPath = resolve(cwd, values.doc ?? "docs/FeatureOptions.md");
|
|
292
|
+
// Reach HBPU's own renderer through a computed dynamic import of its compiled module - never a static relative import - so the single-file bin stays
|
|
293
|
+
// symlink-safe (it imports only `node:` builtins at load time). A failed import here means HBPU itself has not been built, which is a distinct, actionable
|
|
294
|
+
// condition from a downstream render/splice failure, so we frame it separately and point at the corrective action.
|
|
295
|
+
const rendererPath = join(sourceRoot, "dist", "featureOptions-docs.js");
|
|
296
|
+
let renderer;
|
|
297
|
+
try {
|
|
298
|
+
renderer = await import(__rewriteRelativeImportExtension(pathToFileURL(rendererPath).href));
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
stderr.write("homebridge-plugin-utils prepare-docs: HBPU has not been built: " + rendererPath + " is missing. Run `npm run build` in HBPU first.\n");
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await prepareDocs({ catalogModulePath, docPath, render: renderer.renderFeatureOptionsReference, splice: renderer.spliceMarkedRegion });
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
stderr.write("homebridge-plugin-utils prepare-docs: " + (error instanceof Error ? error.message : String(error)) + "\n");
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
default: {
|
|
314
|
+
stderr.write(USAGE);
|
|
315
|
+
// A bare `homebridge-plugin-utils` invocation (no command at all) is treated as a help request and exits 0; an unknown command is a misuse and exits 1. Both
|
|
316
|
+
// write the usage banner so the user sees the same prompt either way - only the exit code differentiates.
|
|
317
|
+
return command ? 1 : 0;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Decide whether this module is the program entry point (run as the bin) versus imported as a library (by the test suite). Canonicalizes the real path of both the
|
|
323
|
+
* launch path (`process.argv[1]`) and this module's own URL before comparing them.
|
|
324
|
+
*
|
|
325
|
+
* The realpath normalization is the load-bearing detail. npm exposes a bin as a symlink in `node_modules/.bin`, so under default Node the launch path is the
|
|
326
|
+
* symlink while `import.meta.url` is the resolved target - a raw string comparison never matches and the CLI silently does nothing. Canonicalizing both sides
|
|
327
|
+
* collapses that indirection (and any `file:`-dependency, copied-package, or `--preserve-symlinks` layout) to a single real path, so the check holds however the
|
|
328
|
+
* launcher reached this file. We keep this explicit realpath comparison rather than deferring to `import.meta.main`: the symlink and copied-package indirection is the
|
|
329
|
+
* load-bearing concern the entry-point check exists to handle, and resolving it explicitly keeps that handling visible at the call site.
|
|
330
|
+
*
|
|
331
|
+
* @returns `true` when invoked as the program entry, `false` when imported or when the launch path cannot be resolved.
|
|
332
|
+
*/
|
|
333
|
+
function isEntryPoint() {
|
|
334
|
+
const entryPath = process.argv[1];
|
|
335
|
+
if (!entryPath) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
return realpathSync(entryPath) === realpathSync(fileURLToPath(import.meta.url));
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// A realpath throws only when a path does not resolve on disk - not a state a genuine entry-point invocation reaches, so treat it as "not the entry."
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Execute the CLI when this module is the program entry point. When imported by the test suite instead, `isEntryPoint()` is false and the module exposes
|
|
347
|
+
// `prepareUi` / `prepareDocs` / `runCli` / `USAGE` as a side-effect-free library surface.
|
|
348
|
+
if (isEntryPoint()) {
|
|
349
|
+
// Resolve HBPU's package root from this file's real location. The compiled CLI sits at `dist/cli/index.js`; walking two segments up from its real directory
|
|
350
|
+
// reaches the package root regardless of how the package is installed (a plugin's `node_modules`, a workspace symlink, a global install) or which Node
|
|
351
|
+
// symlink-resolution mode is in effect - the realpath has already collapsed any indirection.
|
|
352
|
+
const sourceRoot = resolve(dirname(realpathSync(fileURLToPath(import.meta.url))), "..", "..");
|
|
353
|
+
process.exit(await runCli({ argv: process.argv.slice(2), cwd: process.cwd(), sourceRoot, stderr: process.stderr }));
|
|
354
|
+
}
|
|
355
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reusable, controllable {@link Clock} test double.
|
|
3
|
+
*
|
|
4
|
+
* The {@link Clock} seam in `clock.ts` exists so a consuming plugin's time-dependent code can be driven without real wall-clock waits. This module ships the fake that
|
|
5
|
+
* cashes that in: a {@link TestClock} over a virtual timeline a test advances explicitly. `now()` returns the virtual time; `delay()` registers a pending wait that
|
|
6
|
+
* resolves only when {@link TestClock.advance} crosses its deadline, or rejects when its signal aborts - matching `node:timers/promises` `setTimeout`'s `AbortError`
|
|
7
|
+
* shape. No real timers and no wall-clock are used, so a consumer's pacing/timeout/duration path runs deterministically and instantly under test.
|
|
8
|
+
*
|
|
9
|
+
* The double builds on the library's own primitives rather than hand-rolling them: {@link onAbort} wires the abort listener and yields the `Disposable` that detaches
|
|
10
|
+
* it, and `Promise.withResolvers` captures each pending wait's deferred. The abort listener is detached on EITHER resolution path (deadline-crossed or aborted), so no
|
|
11
|
+
* listener leaks onto a long-lived signal across many short waits.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { Clock } from "./clock.ts";
|
|
16
|
+
/**
|
|
17
|
+
* A controllable {@link Clock} double over virtual time. `now()` returns the current virtual time; `delay()` registers a pending wait that resolves only when
|
|
18
|
+
* {@link TestClock.advance} crosses its deadline (in ascending-deadline order), or rejects with an `AbortError` (matching `node:timers/promises` `setTimeout` - `name`
|
|
19
|
+
* `"AbortError"`, `code` `"ABORT_ERR"`, NOT the signal's reason) when its signal aborts. No real timers or wall-clock are used.
|
|
20
|
+
*
|
|
21
|
+
* The virtual time is a RELATIVE timeline seeded at `start` (default `0`), NOT real epoch milliseconds. A consumer that compares `now()` against an absolute real-epoch
|
|
22
|
+
* constant would diverge; consumers must only compare `now()` values to each other (deriving elapsed intervals from differences), which is the only use a
|
|
23
|
+
* `Date.now()`-style read serves in the consuming pacing path - all its time reads come from the one injected clock.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { TestClock } from "homebridge-plugin-utils";
|
|
29
|
+
*
|
|
30
|
+
* const clock = new TestClock();
|
|
31
|
+
*
|
|
32
|
+
* const waited = clock.delay(100);
|
|
33
|
+
*
|
|
34
|
+
* // Nothing resolves until virtual time crosses the deadline.
|
|
35
|
+
* clock.advance(100);
|
|
36
|
+
*
|
|
37
|
+
* await waited;
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @see Clock
|
|
41
|
+
*
|
|
42
|
+
* @category Testing
|
|
43
|
+
*/
|
|
44
|
+
export declare class TestClock implements Clock {
|
|
45
|
+
#private;
|
|
46
|
+
/**
|
|
47
|
+
* Construct a clock seeded at `start` (default `0`). The seed is the initial value `now()` returns; `advance` moves it forward (or back, for a negative delta).
|
|
48
|
+
*
|
|
49
|
+
* @param start - The initial virtual time, in the consumer's relative timeline. Defaults to `0`.
|
|
50
|
+
*/
|
|
51
|
+
constructor(start?: number);
|
|
52
|
+
/**
|
|
53
|
+
* Advance virtual time by `ms` and resolve every delay whose deadline the new time has reached. The delta is applied regardless of sign, so a negative `ms` moves time
|
|
54
|
+
* backward; `advance(0)` moves time nowhere but STILL flushes any already-due entry (a `delay(0)` or a `delay` with a non-positive `ms`), so a zero or negative delay
|
|
55
|
+
* is never a lost wakeup.
|
|
56
|
+
*
|
|
57
|
+
* Due entries resolve in ASCENDING deadline order; entries that share a deadline keep their FIFO registration order, because the snapshot is taken before any removal
|
|
58
|
+
* and the numeric sort is stable - matching how `setTimeout` fires equal-deadline timers in scheduling order. Each due entry is removed by identity and has its abort
|
|
59
|
+
* listener detached before it resolves, so the resolve path leaks no listener and the iteration is immune to the index shifts a forward in-place splice would cause.
|
|
60
|
+
*
|
|
61
|
+
* @param ms - The amount of virtual time to advance, in milliseconds. May be zero or negative.
|
|
62
|
+
*/
|
|
63
|
+
advance(ms: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* Register a delay that resolves when virtual time reaches `this.now() + ms`, or rejects with an `AbortError` (matching `node:timers/promises`) if `init.signal` aborts
|
|
66
|
+
* first. A non-positive `ms` yields a deadline at or before the current time, which the very next {@link TestClock.advance} (including `advance(0)`) flushes.
|
|
67
|
+
*
|
|
68
|
+
* A pre-aborted signal rejects on the executor's microtask exactly as `systemClock` does (NOT a synchronous throw): {@link onAbort} fires the handler inline, which
|
|
69
|
+
* removes the just-registered entry and rejects, so the entry never lingers in `pending`.
|
|
70
|
+
*
|
|
71
|
+
* @param ms - The delay, in milliseconds. May be zero or negative (flushed by the next `advance`).
|
|
72
|
+
* @param init - Optional init options. A supplied `signal` rejects the wait with an `AbortError` when it aborts.
|
|
73
|
+
*
|
|
74
|
+
* @returns A promise that resolves when the deadline is crossed, or rejects with an `AbortError` if the signal aborts first.
|
|
75
|
+
*/
|
|
76
|
+
delay(ms: number, init?: {
|
|
77
|
+
signal?: AbortSignal;
|
|
78
|
+
}): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Return the current virtual time. Compare these values to each other to derive elapsed intervals - they are a relative timeline, not real epoch milliseconds.
|
|
81
|
+
*
|
|
82
|
+
* @returns The current virtual time.
|
|
83
|
+
*/
|
|
84
|
+
now(): number;
|
|
85
|
+
/**
|
|
86
|
+
* The number of registered delays that have neither resolved nor rejected. A test reads this to assert a consumer registered its waits and later cleared them (no
|
|
87
|
+
* leak).
|
|
88
|
+
*
|
|
89
|
+
* @returns The count of unsettled delays.
|
|
90
|
+
*/
|
|
91
|
+
get pending(): number;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=clock-double.d.ts.map
|