@vibe-agent-toolkit/utils 0.2.0-rc.2 → 0.2.0-rc.3
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 -3
- package/dist/compare-code-units.d.ts +28 -0
- package/dist/compare-code-units.d.ts.map +1 -0
- package/dist/compare-code-units.js +33 -0
- package/dist/compare-code-units.js.map +1 -0
- package/dist/crawl-timing.d.ts +680 -0
- package/dist/crawl-timing.d.ts.map +1 -0
- package/dist/crawl-timing.js +809 -0
- package/dist/crawl-timing.js.map +1 -0
- package/dist/file-crawler.d.ts +22 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +31 -7
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +17 -0
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +69 -0
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +3 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +5 -1
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +167 -0
- package/dist/git-run.d.ts.map +1 -0
- package/dist/git-run.js +154 -0
- package/dist/git-run.js.map +1 -0
- package/dist/git-snapshot.d.ts +181 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +232 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +94 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +135 -3
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +38 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +103 -80
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +2 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +2 -1
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +6 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -1
- package/dist/index.js.map +1 -1
- package/dist/link-auth/expand-macro.d.ts.map +1 -1
- package/dist/link-auth/expand-macro.js +7 -1
- package/dist/link-auth/expand-macro.js.map +1 -1
- package/dist/link-auth/resolve-token.d.ts.map +1 -1
- package/dist/link-auth/resolve-token.js +15 -1
- package/dist/link-auth/resolve-token.js.map +1 -1
- package/dist/path-core.d.ts +1 -1
- package/dist/path-core.js +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +6 -3
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +38 -10
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +88 -19
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-test/index.d.ts +1 -1
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +1 -1
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +28 -0
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +51 -2
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts +41 -2
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +47 -3
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/test-helpers.d.ts +185 -13
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +274 -27
- package/dist/test-helpers.js.map +1 -1
- package/dist/text-content.d.ts +197 -0
- package/dist/text-content.d.ts.map +1 -0
- package/dist/text-content.js +348 -0
- package/dist/text-content.js.map +1 -0
- package/dist/text-file.d.ts +45 -0
- package/dist/text-file.d.ts.map +1 -0
- package/dist/text-file.js +53 -0
- package/dist/text-file.js.map +1 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +16 -0
- package/dist/text.js.map +1 -0
- package/dist/timing-dump.d.ts +119 -0
- package/dist/timing-dump.d.ts.map +1 -0
- package/dist/timing-dump.js +164 -0
- package/dist/timing-dump.js.map +1 -0
- package/eslint/README.md +105 -3
- package/eslint/index.cjs +41 -0
- package/eslint/index.d.cts +7 -4
- package/eslint/rules/no-bare-symlink-in-tests.cjs +200 -0
- package/eslint/rules/no-raw-text-decode.cjs +296 -0
- package/eslint/rules/no-self-package-import.cjs +157 -0
- package/package.json +9 -2
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vibe-agent-toolkit/utils/text
|
|
3
|
+
*
|
|
4
|
+
* **The one way bytes become text.** Encoding detection from a byte-order mark,
|
|
5
|
+
* BOM stripping, and a stated default when there is no BOM — see
|
|
6
|
+
* `text-content.ts` for what is a fact, what is an assumption, and the two
|
|
7
|
+
* limitations that are deliberately not guessed around.
|
|
8
|
+
*
|
|
9
|
+
* Pure: this entry reaches no `node:*` builtin and no third-party package, so
|
|
10
|
+
* bytes from a git blob, an HTTP response or a zip entry decode through exactly
|
|
11
|
+
* the same function as bytes from disk. For the read-a-file case, import
|
|
12
|
+
* `readTextContent` from `@vibe-agent-toolkit/utils/fs`, which is this plus a
|
|
13
|
+
* `readFile`.
|
|
14
|
+
*/
|
|
15
|
+
export { decodeTextContent, type DecodedText, type EncodingSource, type TextEncoding, type TextProvenance, } from './text-content.js';
|
|
16
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,cAAc,GACpB,MAAM,mBAAmB,CAAC"}
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vibe-agent-toolkit/utils/text
|
|
3
|
+
*
|
|
4
|
+
* **The one way bytes become text.** Encoding detection from a byte-order mark,
|
|
5
|
+
* BOM stripping, and a stated default when there is no BOM — see
|
|
6
|
+
* `text-content.ts` for what is a fact, what is an assumption, and the two
|
|
7
|
+
* limitations that are deliberately not guessed around.
|
|
8
|
+
*
|
|
9
|
+
* Pure: this entry reaches no `node:*` builtin and no third-party package, so
|
|
10
|
+
* bytes from a git blob, an HTTP response or a zip entry decode through exactly
|
|
11
|
+
* the same function as bytes from disk. For the read-a-file case, import
|
|
12
|
+
* `readTextContent` from `@vibe-agent-toolkit/utils/fs`, which is this plus a
|
|
13
|
+
* `readFile`.
|
|
14
|
+
*/
|
|
15
|
+
export { decodeTextContent, } from './text-content.js';
|
|
16
|
+
//# sourceMappingURL=text.js.map
|
package/dist/text.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,iBAAiB,GAKlB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The on-disk plumbing every VAT timing seam shares.
|
|
3
|
+
*
|
|
4
|
+
* Two seams write per-process JSON dumps to an operator-named directory:
|
|
5
|
+
* `@vibe-agent-toolkit/resources`' `parse-timing.ts` (which pass inside a parser
|
|
6
|
+
* owns the time) and this package's `crawl-timing.ts` (which contributor or
|
|
7
|
+
* crawler owns the time). They sit in different packages because `crawl-timing`
|
|
8
|
+
* has to bracket `GitTracker`, which is here — so this module lives at the lower
|
|
9
|
+
* of the two and is exported for the higher one. What they
|
|
10
|
+
* MEASURE could not be less alike — one axis is a closed enum of parser passes,
|
|
11
|
+
* the other is an open set of contributor ids — but every property that makes
|
|
12
|
+
* the *file* trustworthy is identical between them, and the lab's
|
|
13
|
+
* `harness/dumps.ts` already says so from the reading end:
|
|
14
|
+
*
|
|
15
|
+
* 1. A directory that cannot be created is reported the moment the seam turns
|
|
16
|
+
* on, while there is still a run to abandon — never at exit, where a failure
|
|
17
|
+
* costs the whole measurement.
|
|
18
|
+
* 2. A dump failure is written to **stderr and never thrown**. These run from
|
|
19
|
+
* an `exit` listener, where a throw changes the process's exit behaviour, and
|
|
20
|
+
* they must never touch stdout, which carries vat's report.
|
|
21
|
+
* 3. A pid can file more than one dump. `vat validate` spawns the vat binary
|
|
22
|
+
* once per phase and pids are reused, so `<stem>-<pid>.json` genuinely
|
|
23
|
+
* collides; the name gains a counter rather than overwriting.
|
|
24
|
+
* 4. The process's own wall and CPU time is read ONCE, at dump time. It is a
|
|
25
|
+
* lifetime figure for the process and never a duration of the measured work;
|
|
26
|
+
* its value is the RATIO, which tells a reader whether the wall-timed
|
|
27
|
+
* brackets above it were competing with a loaded machine.
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ **A reader must never SUM these across dumps.** Point 3 means one
|
|
30
|
+
* command routinely files several, and a vat command's phase processes
|
|
31
|
+
* overlap in time — so summing their lifetimes counts the same wall clock
|
|
32
|
+
* more than once and produces a "total" longer than the command took. The
|
|
33
|
+
* figure is per process, and the only honest aggregate over several is the
|
|
34
|
+
* per-process ratio read one dump at a time. The `crawl` facet keeps one
|
|
35
|
+
* record per dump and publishes no total for exactly this reason; `parse`
|
|
36
|
+
* still sums, which is review finding F2 (2026-08-14) and is annotated at
|
|
37
|
+
* `facets/parse/dump.ts`. This list is where both seams learn what makes a
|
|
38
|
+
* dump trustworthy, so the hazard belongs here rather than only beside the
|
|
39
|
+
* consumer that already fixed it.
|
|
40
|
+
*
|
|
41
|
+
* Writing that twice would give two seams two chances to diverge on the one
|
|
42
|
+
* thing a reader has to be able to trust identically. What each seam keeps for
|
|
43
|
+
* itself is its accumulator shape, its dump body and the noun it is called by.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Process-level wall and CPU time, read ONCE when a dump is written.
|
|
47
|
+
*
|
|
48
|
+
* All three are lifetime figures for the whole process, not for the measured
|
|
49
|
+
* work: the point of carrying them is the *ratio*. CPU well below wall means the
|
|
50
|
+
* process was waiting rather than computing, and every wall-timed bracket in the
|
|
51
|
+
* dump carries that waiting inside it. CPU above wall is normal and not an
|
|
52
|
+
* error — `process.cpuUsage()` sums every thread, including libuv's pool.
|
|
53
|
+
*/
|
|
54
|
+
export interface TimingProcess {
|
|
55
|
+
/** Wall clock since this process started. */
|
|
56
|
+
wallMs: number;
|
|
57
|
+
/** User CPU consumed by the process, across all its threads. */
|
|
58
|
+
cpuUserMs: number;
|
|
59
|
+
/** System CPU consumed by the process, across all its threads. */
|
|
60
|
+
cpuSystemMs: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reduce a raw env value to a directory or `null`.
|
|
64
|
+
*
|
|
65
|
+
* An empty-string value counts as absent: `VAT_PARSE_TIMING=` in a shell profile
|
|
66
|
+
* is a variable somebody meant to unset, not a request to dump into the process's
|
|
67
|
+
* working directory.
|
|
68
|
+
*
|
|
69
|
+
* @param raw - The env var's value, if set
|
|
70
|
+
* @returns The dump directory, or `null` when the seam is off
|
|
71
|
+
*/
|
|
72
|
+
export declare function normalizeTimingDirectory(raw: string | undefined): string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Report a dump problem on stderr.
|
|
75
|
+
*
|
|
76
|
+
* Never throws and never touches stdout: vat's stdout carries a YAML report, and
|
|
77
|
+
* an exit handler that threw would change the process's exit behaviour.
|
|
78
|
+
*
|
|
79
|
+
* @param noun - What the seam is called, so a reader knows which instrument failed
|
|
80
|
+
* @param target - Path the failure concerns
|
|
81
|
+
* @param error - Whatever was caught
|
|
82
|
+
*/
|
|
83
|
+
export declare function reportTimingDumpFailure(noun: string, target: string, error: unknown): void;
|
|
84
|
+
/**
|
|
85
|
+
* Create the dump directory, swallowing failure.
|
|
86
|
+
*
|
|
87
|
+
* Done once when the seam turns on rather than at exit, so a bad path is
|
|
88
|
+
* reported while there is still a run to abandon — and so the exit handler does
|
|
89
|
+
* the minimum possible work.
|
|
90
|
+
*
|
|
91
|
+
* @param noun - What the seam is called, for the failure line
|
|
92
|
+
* @param directory - Directory dumps will be written to
|
|
93
|
+
*/
|
|
94
|
+
export declare function ensureTimingDirectory(noun: string, directory: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Read this process's lifetime wall and CPU time.
|
|
97
|
+
*
|
|
98
|
+
* Called ONCE per dump — two syscalls for a whole run, which is why the process
|
|
99
|
+
* level can afford a CPU reading that a per-bracket level cannot. Deliberately
|
|
100
|
+
* not an accumulator and deliberately not reset: it describes the process, not
|
|
101
|
+
* the measurement window.
|
|
102
|
+
*
|
|
103
|
+
* @returns Wall clock and CPU since process start, in milliseconds
|
|
104
|
+
*/
|
|
105
|
+
export declare function readTimingProcess(): TimingProcess;
|
|
106
|
+
/**
|
|
107
|
+
* Write one seam's dump, if the seam is on.
|
|
108
|
+
*
|
|
109
|
+
* The body is built lazily, inside this call, so a disabled seam never pays to
|
|
110
|
+
* snapshot accumulators nobody will read.
|
|
111
|
+
*
|
|
112
|
+
* @param noun - What the seam is called, for any failure line
|
|
113
|
+
* @param directory - Where to write, or `null` when the seam is off
|
|
114
|
+
* @param basename - Basename stem for the file
|
|
115
|
+
* @param build - Produces the dump body
|
|
116
|
+
* @returns The path written, or `null` when the seam is off or the write failed
|
|
117
|
+
*/
|
|
118
|
+
export declare function writeTimingDump(noun: string, directory: string | null, basename: string, build: () => unknown): string | null;
|
|
119
|
+
//# sourceMappingURL=timing-dump.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing-dump.d.ts","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAOH;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;CACrB;AAeD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAE/E;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAG1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAM3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,IAAI,aAAa,CAOjD;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,OAAO,GACnB,MAAM,GAAG,IAAI,CAYf"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The on-disk plumbing every VAT timing seam shares.
|
|
3
|
+
*
|
|
4
|
+
* Two seams write per-process JSON dumps to an operator-named directory:
|
|
5
|
+
* `@vibe-agent-toolkit/resources`' `parse-timing.ts` (which pass inside a parser
|
|
6
|
+
* owns the time) and this package's `crawl-timing.ts` (which contributor or
|
|
7
|
+
* crawler owns the time). They sit in different packages because `crawl-timing`
|
|
8
|
+
* has to bracket `GitTracker`, which is here — so this module lives at the lower
|
|
9
|
+
* of the two and is exported for the higher one. What they
|
|
10
|
+
* MEASURE could not be less alike — one axis is a closed enum of parser passes,
|
|
11
|
+
* the other is an open set of contributor ids — but every property that makes
|
|
12
|
+
* the *file* trustworthy is identical between them, and the lab's
|
|
13
|
+
* `harness/dumps.ts` already says so from the reading end:
|
|
14
|
+
*
|
|
15
|
+
* 1. A directory that cannot be created is reported the moment the seam turns
|
|
16
|
+
* on, while there is still a run to abandon — never at exit, where a failure
|
|
17
|
+
* costs the whole measurement.
|
|
18
|
+
* 2. A dump failure is written to **stderr and never thrown**. These run from
|
|
19
|
+
* an `exit` listener, where a throw changes the process's exit behaviour, and
|
|
20
|
+
* they must never touch stdout, which carries vat's report.
|
|
21
|
+
* 3. A pid can file more than one dump. `vat validate` spawns the vat binary
|
|
22
|
+
* once per phase and pids are reused, so `<stem>-<pid>.json` genuinely
|
|
23
|
+
* collides; the name gains a counter rather than overwriting.
|
|
24
|
+
* 4. The process's own wall and CPU time is read ONCE, at dump time. It is a
|
|
25
|
+
* lifetime figure for the process and never a duration of the measured work;
|
|
26
|
+
* its value is the RATIO, which tells a reader whether the wall-timed
|
|
27
|
+
* brackets above it were competing with a loaded machine.
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ **A reader must never SUM these across dumps.** Point 3 means one
|
|
30
|
+
* command routinely files several, and a vat command's phase processes
|
|
31
|
+
* overlap in time — so summing their lifetimes counts the same wall clock
|
|
32
|
+
* more than once and produces a "total" longer than the command took. The
|
|
33
|
+
* figure is per process, and the only honest aggregate over several is the
|
|
34
|
+
* per-process ratio read one dump at a time. The `crawl` facet keeps one
|
|
35
|
+
* record per dump and publishes no total for exactly this reason; `parse`
|
|
36
|
+
* still sums, which is review finding F2 (2026-08-14) and is annotated at
|
|
37
|
+
* `facets/parse/dump.ts`. This list is where both seams learn what makes a
|
|
38
|
+
* dump trustworthy, so the hazard belongs here rather than only beside the
|
|
39
|
+
* consumer that already fixed it.
|
|
40
|
+
*
|
|
41
|
+
* Writing that twice would give two seams two chances to diverge on the one
|
|
42
|
+
* thing a reader has to be able to trust identically. What each seam keeps for
|
|
43
|
+
* itself is its accumulator shape, its dump body and the noun it is called by.
|
|
44
|
+
*/
|
|
45
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
46
|
+
import { safePath } from './path-core.js';
|
|
47
|
+
import { mkdirSyncReal } from './path-utils.js';
|
|
48
|
+
/** `process.cpuUsage()` reports microseconds; a dump reports milliseconds. */
|
|
49
|
+
const MICROSECONDS_PER_MS = 1000;
|
|
50
|
+
/** `process.uptime()` reports seconds; a dump reports milliseconds. */
|
|
51
|
+
const MS_PER_SECOND = 1000;
|
|
52
|
+
/**
|
|
53
|
+
* Ceiling on the pid-collision search. A directory holding this many dumps for
|
|
54
|
+
* one pid is a runaway, not a collision; overwriting the last slot is a better
|
|
55
|
+
* outcome than spinning.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_DUMP_COLLISIONS = 1000;
|
|
58
|
+
/**
|
|
59
|
+
* Reduce a raw env value to a directory or `null`.
|
|
60
|
+
*
|
|
61
|
+
* An empty-string value counts as absent: `VAT_PARSE_TIMING=` in a shell profile
|
|
62
|
+
* is a variable somebody meant to unset, not a request to dump into the process's
|
|
63
|
+
* working directory.
|
|
64
|
+
*
|
|
65
|
+
* @param raw - The env var's value, if set
|
|
66
|
+
* @returns The dump directory, or `null` when the seam is off
|
|
67
|
+
*/
|
|
68
|
+
export function normalizeTimingDirectory(raw) {
|
|
69
|
+
return raw === undefined || raw === '' ? null : raw;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Report a dump problem on stderr.
|
|
73
|
+
*
|
|
74
|
+
* Never throws and never touches stdout: vat's stdout carries a YAML report, and
|
|
75
|
+
* an exit handler that threw would change the process's exit behaviour.
|
|
76
|
+
*
|
|
77
|
+
* @param noun - What the seam is called, so a reader knows which instrument failed
|
|
78
|
+
* @param target - Path the failure concerns
|
|
79
|
+
* @param error - Whatever was caught
|
|
80
|
+
*/
|
|
81
|
+
export function reportTimingDumpFailure(noun, target, error) {
|
|
82
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
83
|
+
process.stderr.write(`vat: ${noun} dump failed for ${target}: ${detail}\n`);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create the dump directory, swallowing failure.
|
|
87
|
+
*
|
|
88
|
+
* Done once when the seam turns on rather than at exit, so a bad path is
|
|
89
|
+
* reported while there is still a run to abandon — and so the exit handler does
|
|
90
|
+
* the minimum possible work.
|
|
91
|
+
*
|
|
92
|
+
* @param noun - What the seam is called, for the failure line
|
|
93
|
+
* @param directory - Directory dumps will be written to
|
|
94
|
+
*/
|
|
95
|
+
export function ensureTimingDirectory(noun, directory) {
|
|
96
|
+
try {
|
|
97
|
+
mkdirSyncReal(directory, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
reportTimingDumpFailure(noun, directory, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Read this process's lifetime wall and CPU time.
|
|
105
|
+
*
|
|
106
|
+
* Called ONCE per dump — two syscalls for a whole run, which is why the process
|
|
107
|
+
* level can afford a CPU reading that a per-bracket level cannot. Deliberately
|
|
108
|
+
* not an accumulator and deliberately not reset: it describes the process, not
|
|
109
|
+
* the measurement window.
|
|
110
|
+
*
|
|
111
|
+
* @returns Wall clock and CPU since process start, in milliseconds
|
|
112
|
+
*/
|
|
113
|
+
export function readTimingProcess() {
|
|
114
|
+
const cpu = process.cpuUsage();
|
|
115
|
+
return {
|
|
116
|
+
wallMs: process.uptime() * MS_PER_SECOND,
|
|
117
|
+
cpuUserMs: cpu.user / MICROSECONDS_PER_MS,
|
|
118
|
+
cpuSystemMs: cpu.system / MICROSECONDS_PER_MS,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Pick a dump path that does not already exist.
|
|
123
|
+
*
|
|
124
|
+
* @param directory - Directory dumps are written to
|
|
125
|
+
* @param basename - Basename stem; the pid and any collision counter follow
|
|
126
|
+
* @returns An unused path, or the last candidate tried
|
|
127
|
+
*/
|
|
128
|
+
function nextTimingDumpPath(directory, basename) {
|
|
129
|
+
const stem = `${basename}-${String(process.pid)}`;
|
|
130
|
+
let candidate = safePath.join(directory, `${stem}.json`);
|
|
131
|
+
for (let collision = 1;
|
|
132
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- operator-supplied diagnostic directory from a VAT_*_TIMING variable
|
|
133
|
+
collision <= MAX_DUMP_COLLISIONS && existsSync(candidate); collision += 1) {
|
|
134
|
+
candidate = safePath.join(directory, `${stem}-${String(collision)}.json`);
|
|
135
|
+
}
|
|
136
|
+
return candidate;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Write one seam's dump, if the seam is on.
|
|
140
|
+
*
|
|
141
|
+
* The body is built lazily, inside this call, so a disabled seam never pays to
|
|
142
|
+
* snapshot accumulators nobody will read.
|
|
143
|
+
*
|
|
144
|
+
* @param noun - What the seam is called, for any failure line
|
|
145
|
+
* @param directory - Where to write, or `null` when the seam is off
|
|
146
|
+
* @param basename - Basename stem for the file
|
|
147
|
+
* @param build - Produces the dump body
|
|
148
|
+
* @returns The path written, or `null` when the seam is off or the write failed
|
|
149
|
+
*/
|
|
150
|
+
export function writeTimingDump(noun, directory, basename, build) {
|
|
151
|
+
if (directory === null)
|
|
152
|
+
return null;
|
|
153
|
+
const target = nextTimingDumpPath(directory, basename);
|
|
154
|
+
try {
|
|
155
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- operator-supplied diagnostic directory from a VAT_*_TIMING variable
|
|
156
|
+
writeFileSync(target, `${JSON.stringify(build(), null, 2)}\n`, 'utf-8');
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
reportTimingDumpFailure(noun, target, error);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return target;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=timing-dump.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing-dump.js","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"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"}
|
package/eslint/README.md
CHANGED
|
@@ -25,7 +25,7 @@ export default [
|
|
|
25
25
|
];
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
`configs.recommended` registers the plugin under the `@vibe-agent-toolkit` namespace and enables the **cross-platform safety core** — 18 of the
|
|
28
|
+
`configs.recommended` registers the plugin under the `@vibe-agent-toolkit` namespace and enables the **cross-platform safety core** — 18 of the 22 rules, most at `error` and three at `warn` (see [Severities](#severities)). The other four are opt-in; the [rule tables](#rules) mark each rule's `recommended` severity, and `—` means not in `recommended`.
|
|
29
29
|
|
|
30
30
|
To pick rules yourself, register the plugin and name them:
|
|
31
31
|
|
|
@@ -111,6 +111,21 @@ Two ways your target can be wrong, which surface differently: `ERR_MODULE_NOT_FO
|
|
|
111
111
|
| `no-fs-promises-cp` | `cp()` from `node:fs/promises` (drops nested files on Node 22) | `cpSync()` from `node:fs` | — | ✓ | `error` |
|
|
112
112
|
| `no-child-process-execSync` | `child_process.execSync()` | `safeExecSync()` | `/process` | ✓ | `error` |
|
|
113
113
|
| `no-unix-shell-commands` | `tar`, `grep`, `rm`, `echo`, … spawned directly | Node APIs, or a portable script fixture | — | | `error` |
|
|
114
|
+
| `no-bare-symlink-in-tests` | unguarded `fs.symlinkSync()` / `fs.promises.symlink()` | in tests: `createSymlink(cap, …)` / `createSymlinkAsync(cap, …)`; in shipped code: a win32 junction, or a `catch` naming the privilege | `/testing` | | — |
|
|
115
|
+
|
|
116
|
+
**`no-bare-symlink-in-tests` reports two different remedies, and the name is narrower than the rule.**
|
|
117
|
+
Creating a symlink on Windows requires `SeCreateSymbolicLinkPrivilege` — Developer Mode or an
|
|
118
|
+
elevated shell — which most user machines and CI agents lack. In a **test file** the fix is to probe
|
|
119
|
+
with `symlinkCapability()` and pass the resulting token to `createSymlink()`, so a host without the
|
|
120
|
+
privilege produces a visible `skip()` rather than a failure or a silently-swallowed one. In **shipped
|
|
121
|
+
code** there is deliberately no wrapper to route through: `createSymlink()` lives on the `/testing`
|
|
122
|
+
subpath, and pointing production code at a test helper would be worse advice than the bare call.
|
|
123
|
+
There the guidance is a junction for a directory link on win32 (no elevation required), or catching
|
|
124
|
+
the failure and naming the missing privilege. Declaring a platform out of scope is legitimate —
|
|
125
|
+
say so in an `eslint-disable` justification, which is what `vat agent install --dev` does.
|
|
126
|
+
|
|
127
|
+
`exemptFiles` matters here: the implementation file holding the sanctioned `symlinkSync` is not a
|
|
128
|
+
test file, so it needs an explicit exemption once the rule covers shipped code.
|
|
114
129
|
|
|
115
130
|
The member-call rules here check the **receiver**, not just the method name, so `env.tmpdir()` on some unrelated object is not a finding — and the namespace they check for can be bound by a static `import * as os`, by `const os = require('node:os')`, or by `const os = await import('node:os')`. The fix replaces the whole callee (`os.tmpdir()` → `normalizedTmpdir()`), which is correct however the binding was made. Matching the method name alone was the earlier behaviour and it produced `os.normalizedTmpdir()` — a method that does not exist, compiles, and throws.
|
|
116
131
|
|
|
@@ -122,6 +137,63 @@ The member-call rules here check the **receiver**, not just the method name, so
|
|
|
122
137
|
| `no-bare-dynamic-import-path` | `await import(absolutePath)` | `dynamicImportPath()` / `pathToFileURL(p).href` | `/fs` | | `error` |
|
|
123
138
|
| `no-file-url-string-concat` | `` `file://${p}` `` | `pathToFileURL(p).href` | — | | `error` |
|
|
124
139
|
|
|
140
|
+
### Content decoding
|
|
141
|
+
|
|
142
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
143
|
+
|---|---|---|---|---|---|
|
|
144
|
+
| `no-raw-text-decode` | `buf.toString('utf-8')`, `new TextDecoder(…)`, `readFile(p, 'utf-8')` | one project-owned decoding seam | — | | — |
|
|
145
|
+
|
|
146
|
+
`buf.toString('utf-8')` ignores every byte-order mark and cannot express UTF-16BE at all — Node's `Buffer` has no such encoding. A UTF-16 document therefore decodes to NUL-interleaved mojibake, and whatever sniffs for binary content downstream believes it. PowerShell 5.1's `Out-File` and `>` write UTF-16LE by default, so this is a Windows-authored file, not an exotic one.
|
|
147
|
+
|
|
148
|
+
This rule has no wrapper to point at, because the seam is yours: write one decoder, name it with `safeModule`, and exempt its own file with `exemptFiles`. Put the decoder at the **bottom** of your dependency arrow — a seam in a leaf package cannot be imported by the primitive packages the rule also lints, and those files would then have no legal way to comply.
|
|
149
|
+
|
|
150
|
+
**Not every `'utf-8'` read is a content read**, and this is the distinction that decides whether the rule survives. Three categories:
|
|
151
|
+
|
|
152
|
+
1. **A document you did not write** — an adopter's markdown, config, schema, `.gitignore`, `package.json`. The encoding must be **discovered**. This is the rule's target.
|
|
153
|
+
2. **An artifact your project wrote** — its own cache entry, its own published asset. The encoding was **chosen at the write**; reading it back the same way is a closed loop.
|
|
154
|
+
3. **Bytes that were never a file** — subprocess stdout, an HTTP body, a Buffer you built. The **producer's contract** decides.
|
|
155
|
+
|
|
156
|
+
Static analysis cannot tell them apart, so the rule reports all three and you settle 2 and 3 at the call site with a one-line `eslint-disable-next-line` that **names the writer or the producer**:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
// eslint-disable-next-line @vibe-agent-toolkit/no-raw-text-decode -- subprocess stdout; producer is the credential helper spawned above
|
|
160
|
+
const out = result.stdout.toString('utf8');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
That gives a reviewer a falsifiable test: a justification that cannot name who wrote the bytes is a category-1 call wearing a disable comment. Do not settle these by adding paths to `exemptFiles` — that list is for the seam's own implementation file.
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
{
|
|
167
|
+
files: ['src/corpus/**/*.ts'],
|
|
168
|
+
rules: {
|
|
169
|
+
'@vibe-agent-toolkit/no-raw-text-decode': ['error', {
|
|
170
|
+
safeModule: '@my-org/resources',
|
|
171
|
+
exemptFiles: ['src/corpus/text-content.ts'],
|
|
172
|
+
}],
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Only a string **literal** encoding triggers it. `buf.toString(enc)` is deliberately not reported: without type information it is indistinguishable from `n.toString(radix)`, and `readFile(p, cb)` from `readFile(p, encoding)`.
|
|
178
|
+
|
|
179
|
+
### Build correctness
|
|
180
|
+
|
|
181
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
182
|
+
|---|---|---|---|---|---|
|
|
183
|
+
| `no-self-package-import` | importing the enclosing package by its own name | a relative path to the defining module | — | | — (needs `packageName`) |
|
|
184
|
+
|
|
185
|
+
A file inside `packages/foo` that writes `import … from '@scope/foo'` resolves out through `node_modules` to its own `package.json`, whose `types` point at `./dist/index.d.ts` — a file the compiler is in the middle of producing. It works only by a TypeScript courtesy: while `dist` **is** the running project's output path, that declaration is recognised as the project's own output and the import is redirected back to `src`, so it resolves with no `dist/` on disk.
|
|
186
|
+
|
|
187
|
+
Change `outDir` — to a staging directory that makes emit atomic, say — and the redirect is gone, tsc looks for a literal `dist/index.d.ts`, and a tree that has never been built has none:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
error TS2307: Cannot find module '@scope/foo' or its corresponding type declarations.
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The knock-on `TS2339`s land wherever a local type extended one of the now-unresolved imports, which is what makes it read as a type bug in code nobody touched.
|
|
194
|
+
|
|
195
|
+
It is latent by construction, and worse, **it is invisible to any tree that has built before**: a stale `dist/` satisfies the literal lookup, so the build passes by typechecking against the *previous* build's declarations. In a monorepo whose worktrees live inside the main checkout, resolution walks up past the worktree and satisfies it from the *parent checkout's* `dist/`. Both are green locally and red in CI, which is the only genuinely pristine tree. Lint is the only stage that sees it on the author's machine.
|
|
196
|
+
|
|
125
197
|
### Code and test hygiene
|
|
126
198
|
|
|
127
199
|
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
@@ -132,7 +204,7 @@ The member-call rules here check the **receiver**, not just the method name, so
|
|
|
132
204
|
|
|
133
205
|
### What `recommended` deliberately leaves out
|
|
134
206
|
|
|
135
|
-
|
|
207
|
+
Six rules ship without riding in `recommended`, for five different reasons.
|
|
136
208
|
|
|
137
209
|
**Test-style opinions** — `no-test-scoped-functions` (where a helper may be declared) and `require-justified-skip` (the annotation grammar for a disabled test). Neither is a portability or correctness fact, and installing this package for `safePath.join()` should not also import someone else's test conventions. Both are worth turning on deliberately.
|
|
138
210
|
|
|
@@ -147,7 +219,27 @@ safePath.join(base, userInput) // SILENT — the shape it exists to
|
|
|
147
219
|
|
|
148
220
|
A rule that misses its own target does not belong in a config named `recommended` at any severity: a safety core that cries wolf teaches people to ignore it, and that costs you the true positives too. It still ships, and it still earns `error` when scoped to directories where a path escape is a security boundary — which is how this repo uses it, on its skill-test staging code. It will return to `recommended` when it keys on taint rather than on naming.
|
|
149
221
|
|
|
150
|
-
|
|
222
|
+
**No wrapper to point at** — `no-raw-text-decode`. Every other rule in this pack names a replacement this package publishes; this one names a decoding seam that only exists once *you* write it. Shipped in `recommended`, its every message would read "use `decodeTextContent()` from your content-decoding module", which is advice nobody can follow. Turn it on with `safeModule` and `exemptFiles` set, as shown above.
|
|
223
|
+
|
|
224
|
+
**Needs an option, and only in the directories you compile** — `no-self-package-import`. The import it bans is a genuine build-breaker with no style opinion in it, but the rule cannot discover on its own which package a file is in: reading `package.json` would mean `require('node:fs')`, and every module on this subpath is plain data that requires *nothing* — not `eslint`, not a third-party package, not even a Node builtin. That is what keeps `eslint` an optional peer dependency and lets these rules ship as a subpath of a runtime package rather than as one of their own. So the caller names the package. The caller is a config file, which already runs in full Node and can read every manifest it likes:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
228
|
+
|
|
229
|
+
export default readdirSync('packages').flatMap((dir) => {
|
|
230
|
+
const { name } = JSON.parse(readFileSync(`packages/${dir}/package.json`, 'utf8'));
|
|
231
|
+
return [{
|
|
232
|
+
files: [`packages/${dir}/src/**/*.ts`],
|
|
233
|
+
rules: { '@vibe-agent-toolkit/no-self-package-import': ['error', { packageName: name }] },
|
|
234
|
+
}];
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Scope it to the sources you **compile**. Test and example trees — normally excluded from the build — import their own package by name **on purpose**, to exercise the public entry point exactly as a consumer does. This repo has ~10 such imports, every one of them correct.
|
|
239
|
+
|
|
240
|
+
**Half its advice is unreachable without a helper you may not have** — `no-bare-symlink-in-tests`. In a test file it points at `createSymlink()` / `createSymlinkAsync()`, which live on *this* package's `./testing` subpath and route through a probed capability token; an adopter on a different test runner, or with no symlink-heavy tests, should not silently inherit that opinion — nor the vitest-specific `skip()` idiom the message names. Its **shipped-code** half (`unguardedSymlink`) carries neither dependency and is portable advice on its own, so this is the one exclusion that is half arbitrary; it stays out because the two halves cannot be enabled separately. This repo turns it on explicitly, scoped to its own test-file convention.
|
|
241
|
+
|
|
242
|
+
Enable any of the six by naming it:
|
|
151
243
|
|
|
152
244
|
```js
|
|
153
245
|
import vat from '@vibe-agent-toolkit/utils/eslint';
|
|
@@ -160,6 +252,16 @@ export default [
|
|
|
160
252
|
'@vibe-agent-toolkit/require-justified-skip': 'error',
|
|
161
253
|
},
|
|
162
254
|
},
|
|
255
|
+
{
|
|
256
|
+
// Scope it to the code that reads files whose encoding you do not choose.
|
|
257
|
+
files: ['src/corpus/**/*.ts'],
|
|
258
|
+
rules: {
|
|
259
|
+
'@vibe-agent-toolkit/no-raw-text-decode': ['error', {
|
|
260
|
+
safeModule: '@my-org/resources',
|
|
261
|
+
exemptFiles: ['src/corpus/text-content.ts'],
|
|
262
|
+
}],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
163
265
|
{
|
|
164
266
|
// Scope it to where an escape is a security boundary, not repo-wide.
|
|
165
267
|
files: ['src/staging/**/*.ts'],
|
package/eslint/index.cjs
CHANGED
|
@@ -50,7 +50,10 @@ const rules = {
|
|
|
50
50
|
'no-file-url-string-concat': require('./rules/no-file-url-string-concat.cjs'),
|
|
51
51
|
'prefer-startswith-over-regex': require('./rules/prefer-startswith-over-regex.cjs'),
|
|
52
52
|
'no-unsafe-root-join': require('./rules/no-unsafe-root-join.cjs'),
|
|
53
|
+
'no-raw-text-decode': require('./rules/no-raw-text-decode.cjs'),
|
|
54
|
+
'no-self-package-import': require('./rules/no-self-package-import.cjs'),
|
|
53
55
|
'require-justified-skip': require('./rules/require-justified-skip.cjs'),
|
|
56
|
+
'no-bare-symlink-in-tests': require('./rules/no-bare-symlink-in-tests.cjs'),
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
/**
|
|
@@ -95,6 +98,44 @@ const RECOMMENDED_EXCLUDE = new Set([
|
|
|
95
98
|
// a security boundary (this repo scopes it to the skill-test staging code).
|
|
96
99
|
// Re-include it when it keys on taint rather than on naming.
|
|
97
100
|
'no-unsafe-root-join',
|
|
101
|
+
// Excluded because it names a seam that does not exist until a consumer builds
|
|
102
|
+
// one. `decodeTextContent()` is VAT's module, in VAT's repo; an adopter who
|
|
103
|
+
// installed this package for `safePath.join()` has no content-decoding seam to
|
|
104
|
+
// be pointed at, so `recommended` would hand them a rule whose every message
|
|
105
|
+
// advises importing from `your content-decoding module`. It ships in `rules`
|
|
106
|
+
// and is enabled — with `safeModule` naming the real seam and `exemptFiles`
|
|
107
|
+
// naming the file that implements it — by whoever has one. VAT itself does
|
|
108
|
+
// exactly that, scoped to the directories that read corpus documents.
|
|
109
|
+
'no-raw-text-decode',
|
|
110
|
+
// Excluded for two reasons, either of which would be enough. It REQUIRES a
|
|
111
|
+
// `packageName` option — it will not read a `package.json` to find out, because
|
|
112
|
+
// that would mean `require('node:fs')` and break the empty-external-set property
|
|
113
|
+
// this whole subpath rests on — and a rule with a required option cannot ride in
|
|
114
|
+
// a config that supplies none. And the directories it must not fire in are a
|
|
115
|
+
// property of the adopter's `tsconfig`, not of this package: a self-import only
|
|
116
|
+
// breaks the build in files the package actually COMPILES, while test and
|
|
117
|
+
// example trees import their own package by name on purpose, to exercise the
|
|
118
|
+
// public entry point the way a consumer does. This repo has ~10 such imports
|
|
119
|
+
// across `utils`, `agent-skills`, `claude-marketplace` and
|
|
120
|
+
// `vat-example-cat-agents`, every one of them correct. It ships in `rules`, and
|
|
121
|
+
// `eslint.config.js` generates one scoped block per workspace package.
|
|
122
|
+
'no-self-package-import',
|
|
123
|
+
// Excluded for its TEST half only, which is the honest way to say it. That
|
|
124
|
+
// half names a vitest-specific idiom (`skip()` from the per-test context) and
|
|
125
|
+
// a replacement — `createSymlink()` / `createSymlinkAsync()` /
|
|
126
|
+
// `symlinkCapability()` — living on THIS package's own `./testing` subpath
|
|
127
|
+
// rather than on a seam every consumer already has. An adopter using a
|
|
128
|
+
// different test runner, or no symlink-heavy tests at all, should not
|
|
129
|
+
// silently inherit an opinion about vitest control flow.
|
|
130
|
+
//
|
|
131
|
+
// ⚠️ The rule now covers SHIPPED code too, and that half (`unguardedSymlink`)
|
|
132
|
+
// has neither dependency: it names the Windows privilege and a junction, both
|
|
133
|
+
// portable facts. So this exclusion is weaker than it reads — the two halves
|
|
134
|
+
// share one rule id and cannot be enabled separately, and the test half is
|
|
135
|
+
// what keeps the pair out. Splitting the rule would let the production half
|
|
136
|
+
// ride in `recommended`; that is a public-API change and has not been made.
|
|
137
|
+
// VAT enables the whole rule explicitly, scoped to its own conventions.
|
|
138
|
+
'no-bare-symlink-in-tests',
|
|
98
139
|
]);
|
|
99
140
|
|
|
100
141
|
/**
|
package/eslint/index.d.cts
CHANGED
|
@@ -47,10 +47,13 @@ declare namespace plugin {
|
|
|
47
47
|
rules: Record<string, RuleModule>;
|
|
48
48
|
configs: {
|
|
49
49
|
/**
|
|
50
|
-
* The cross-platform safety core: 18 of the
|
|
51
|
-
* `no-test-scoped-functions` and
|
|
52
|
-
* are positions on test style
|
|
53
|
-
* `
|
|
50
|
+
* The cross-platform safety core: 18 of the 22 rules, 15 `error` / 3 `warn`.
|
|
51
|
+
* Four are excluded, for three reasons: `no-test-scoped-functions` and
|
|
52
|
+
* `require-justified-skip` are positions on test style rather than
|
|
53
|
+
* portability facts; `no-unsafe-root-join` keys on naming rather than taint;
|
|
54
|
+
* and `no-raw-text-decode` names a decoding seam that only exists in the
|
|
55
|
+
* consuming repo. All four still ship in `rules` and are enabled by naming
|
|
56
|
+
* them.
|
|
54
57
|
*/
|
|
55
58
|
recommended: FlatConfig;
|
|
56
59
|
};
|