@homeflare/config 0.11.0 → 0.11.1
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 +6 -1
- package/dist/hooks/gates.d.ts.map +1 -1
- package/dist/hooks/oxfmt-config.d.ts +25 -0
- package/dist/hooks/oxfmt-config.d.ts.map +1 -0
- package/dist/hooks/report.d.ts +10 -0
- package/dist/hooks/report.d.ts.map +1 -1
- package/dist/hooks.js +70 -11
- package/dist/hooks.js.map +6 -5
- package/dist/repo-shape/yaml.d.ts +20 -3
- package/dist/repo-shape/yaml.d.ts.map +1 -1
- package/dist/repo-shape.js +8 -2
- package/dist/repo-shape.js.map +3 -3
- package/dist/versions.js +8 -2
- package/dist/versions.js.map +3 -3
- package/docs/hooks.md +19 -0
- package/package.json +1 -1
- package/src/hooks/gates.ts +70 -12
- package/src/hooks/oxfmt-config.ts +67 -0
- package/src/hooks/report.ts +15 -0
- package/src/repo-shape/yaml.ts +26 -4
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `.oxfmtrc.*` file governs this repo — so the hook can name it explicitly.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ BUG, MEASURED 2026-09-23 (homeflare-desktop docs agent): the shared pre-commit ran
|
|
5
|
+
* bare `oxfmt`, and bare `oxfmt` auto-discovers only `.oxfmtrc.json` and `.oxfmtrc.jsonc`.
|
|
6
|
+
* Checked against the pinned oxfmt 0.68.0 directly: a repo with only `.oxfmtrc.mjs` and
|
|
7
|
+
* no `-c` prints "No config found, using defaults" and formats with oxfmt's built-in
|
|
8
|
+
* style, silently — even though `oxfmt --help` lists `.ts/.mts/.cts/.js/.mjs/.cjs` as
|
|
9
|
+
* valid `--config` targets. Desktop's fix was renaming to `.oxfmtrc.json`; this fixes the
|
|
10
|
+
* hook so a repo does not have to.
|
|
11
|
+
* ★ WHY A FILE SCAN, NOT A PACKAGE.JSON READ. The repo's own `format`/`check` script names
|
|
12
|
+
* the config it wants (or relies on auto-discovery finding `.oxfmtrc.json`), but its exact
|
|
13
|
+
* invocation shape varies per repo. Finding the config file on disk and handing it to
|
|
14
|
+
* oxfmt directly gets the same result without parsing an arbitrary shell command.
|
|
15
|
+
* ⛔ EVERY `.oxfmtrc.*` LIVES AT THE REPO ROOT TODAY (checked: every repo in the estate has
|
|
16
|
+
* exactly one, at root — no nested per-package overrides). Explicit `-c` disables oxfmt's
|
|
17
|
+
* own nested-config search (measured: a nested `.oxfmtrc.json` was ignored once `-c` named
|
|
18
|
+
* the root one), so this only scans `root` itself — it would misfire on a repo that added a
|
|
19
|
+
* package-level override, which none currently do.
|
|
20
|
+
*/
|
|
21
|
+
import { readdir } from 'node:fs/promises';
|
|
22
|
+
|
|
23
|
+
/** Auto-discovered by bare `oxfmt` — nothing for the hook to pass. */
|
|
24
|
+
const AUTO_DISCOVERED = ['.oxfmtrc.json', '.oxfmtrc.jsonc'];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Accepted by `oxfmt -c/--config` (per `oxfmt --help`, oxfmt 0.68.0) but never found on its
|
|
28
|
+
* own — checked empirically for each extension. Listed in the order `--help` documents them.
|
|
29
|
+
*/
|
|
30
|
+
const NEEDS_EXPLICIT_CONFIG = [
|
|
31
|
+
'.oxfmtrc.ts',
|
|
32
|
+
'.oxfmtrc.mts',
|
|
33
|
+
'.oxfmtrc.cts',
|
|
34
|
+
'.oxfmtrc.js',
|
|
35
|
+
'.oxfmtrc.mjs',
|
|
36
|
+
'.oxfmtrc.cjs',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export type OxfmtConfigResolution =
|
|
40
|
+
/** `.oxfmtrc.json` or `.oxfmtrc.jsonc` exists — bare oxfmt already finds it. */
|
|
41
|
+
| { readonly kind: 'auto' }
|
|
42
|
+
/** No `.oxfmtrc.*` at all — let oxfmt use its built-in defaults, as today. */
|
|
43
|
+
| { readonly kind: 'none' }
|
|
44
|
+
/** Exactly one config oxfmt would not find unaided — pass it with `-c`. */
|
|
45
|
+
| { readonly kind: 'explicit'; readonly path: string }
|
|
46
|
+
/**
|
|
47
|
+
* Two or more configs, none of them auto-discovered — bare oxfmt (and a repo's own
|
|
48
|
+
* bare `bun run format`) would silently fall back to defaults too, honouring neither.
|
|
49
|
+
* Guessing which one the repo meant would just move the silent-defaults bug here.
|
|
50
|
+
*/
|
|
51
|
+
| { readonly kind: 'ambiguous'; readonly files: readonly string[] };
|
|
52
|
+
|
|
53
|
+
/** Every `.oxfmtrc.*` file actually present at `root`, in a fixed, deterministic order. */
|
|
54
|
+
async function present(root: string, names: readonly string[]): Promise<readonly string[]> {
|
|
55
|
+
const entries = new Set(await readdir(root).catch(() => []));
|
|
56
|
+
return names.filter((name) => entries.has(name));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function resolveOxfmtConfig(root: string): Promise<OxfmtConfigResolution> {
|
|
60
|
+
if ((await present(root, AUTO_DISCOVERED)).length > 0) return { kind: 'auto' };
|
|
61
|
+
|
|
62
|
+
const explicit = await present(root, NEEDS_EXPLICIT_CONFIG);
|
|
63
|
+
const [only, ...rest] = explicit;
|
|
64
|
+
if (only === undefined) return { kind: 'none' };
|
|
65
|
+
if (rest.length === 0) return { kind: 'explicit', path: `${root}/${only}` };
|
|
66
|
+
return { kind: 'ambiguous', files: explicit };
|
|
67
|
+
}
|
package/src/hooks/report.ts
CHANGED
|
@@ -52,6 +52,21 @@ export async function capture(cmd: readonly string[]): Promise<string> {
|
|
|
52
52
|
return (await probe(cmd)).stdout;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Run a command, relaying its stdout live-enough (after it exits) while also handing the
|
|
57
|
+
* caller the text — gates.ts reads oxfmt's own "on N files" summary from it. `stderr` stays
|
|
58
|
+
* `inherit`: diagnostics (a parse error, "no files matched") must show immediately, and nothing
|
|
59
|
+
* here needs to inspect them.
|
|
60
|
+
*/
|
|
61
|
+
export async function runCaptured(
|
|
62
|
+
cmd: readonly string[],
|
|
63
|
+
): Promise<{ readonly code: number; readonly stdout: string }> {
|
|
64
|
+
const proc = Bun.spawn([...cmd], { stdout: 'pipe', stderr: 'inherit' });
|
|
65
|
+
const stdout = await new Response(proc.stdout).text();
|
|
66
|
+
process.stdout.write(stdout);
|
|
67
|
+
return { code: await proc.exited, stdout };
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
/** Capture a command's stdout AND its exit code — git plumbing that answers by status. */
|
|
56
71
|
export async function probe(cmd: readonly string[]): Promise<{ code: number; stdout: string }> {
|
|
57
72
|
const proc = Bun.spawn([...cmd], { stdout: 'pipe', stderr: 'ignore' });
|
package/src/repo-shape/yaml.ts
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
* this house are the product. The rendered workflows are written as text with holes;
|
|
7
7
|
* only the step lists, whose shape varies per repository, go through here.
|
|
8
8
|
*
|
|
9
|
-
* ⚠️ Bun
|
|
10
|
-
* against Bun 1.4.0 on 2026-09-
|
|
11
|
-
*
|
|
9
|
+
* ⚠️ Bun.YAML.stringify EXISTS (`Object.keys(Bun.YAML)` is `["parse", "stringify"]`,
|
|
10
|
+
* measured against Bun 1.4.0 on 2026-09-23) but its output does not fit these rules:
|
|
11
|
+
* (a) a multi-line `run:` comes out as a double-quoted string with `\n` escapes, e.g.
|
|
12
|
+
* `run: "echo a\necho b\n"`, not a `|` block scalar; (b) `09:00` comes out UNQUOTED,
|
|
13
|
+
* e.g. `cron: 09:00`, the YAML 1.1 sexagesimal trap the `scalar()` comment below guards
|
|
14
|
+
* against; (c) a mapping key is followed by a trailing space, e.g. `"steps: \n - ..."`;
|
|
15
|
+
* and (d) a plain JS object has nowhere to attach a comment, so it cannot carry one. The
|
|
16
|
+
* tests parse what this writes with `Bun.YAML.parse` and compare structures, so a
|
|
17
|
+
* malformed emission fails rather than shipping.
|
|
12
18
|
*/
|
|
13
19
|
import type { JobStep } from './shape.ts';
|
|
14
20
|
|
|
@@ -53,13 +59,29 @@ function renderMapping(
|
|
|
53
59
|
return Object.entries(entries).map(([key, value]) => `${indent(depth)}${key}: ${scalar(value)}`);
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Trim trailing `\n` characters the way `command.replace(/\n+$/, '')` used to, but
|
|
64
|
+
* linear instead of backtracking — the same pattern as `normalizeBaseUrl` in
|
|
65
|
+
* `packages/distilled-netbox/src/credentials.ts`. Exported so a test can compare it
|
|
66
|
+
* against the old regex directly.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ Linear on purpose: a `/\n+$/` regex backtracks polynomially on a long run of
|
|
69
|
+
* "\n" that is not at the end (CodeQL js/polynomial-redos), and `command` here is
|
|
70
|
+
* repository-configured step text.
|
|
71
|
+
*/
|
|
72
|
+
export function trimTrailingNewlines(value: string): string {
|
|
73
|
+
let end = value.length;
|
|
74
|
+
while (end > 0 && value.charCodeAt(end - 1) === 10) end--;
|
|
75
|
+
return value.slice(0, end);
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
/**
|
|
57
79
|
* ★ BLOCK SCALAR FOR EVERY MULTI-LINE `run:`. A folded or quoted form would join the
|
|
58
80
|
* lines, and a shell script whose `if` and `then` end up on one line is a syntax error
|
|
59
81
|
* at job time rather than at lint time. `|` keeps them exactly as written.
|
|
60
82
|
*/
|
|
61
83
|
function renderRun(command: string, depth: number): string[] {
|
|
62
|
-
const lines = command
|
|
84
|
+
const lines = trimTrailingNewlines(command).split('\n');
|
|
63
85
|
if (lines.length === 1) return [`${indent(depth)}run: ${scalar(lines[0] ?? '')}`];
|
|
64
86
|
return [`${indent(depth)}run: |`, ...lines.map((line) => `${indent(depth + 1)}${line}`)];
|
|
65
87
|
}
|