@vibe-agent-toolkit/utils 0.2.0-rc.3 → 0.2.0-rc.4
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 +21 -13
- package/dist/crawl-timing.d.ts +43 -66
- package/dist/crawl-timing.d.ts.map +1 -1
- package/dist/crawl-timing.js +35 -79
- package/dist/crawl-timing.js.map +1 -1
- package/dist/crawl.d.ts +2 -4
- package/dist/crawl.d.ts.map +1 -1
- package/dist/crawl.js +2 -4
- package/dist/crawl.js.map +1 -1
- package/dist/git.d.ts +11 -4
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +11 -4
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +19 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -41
- package/dist/index.js.map +1 -1
- package/dist/project.d.ts +7 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +7 -5
- package/dist/project.js.map +1 -1
- package/dist/testing.d.ts +73 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +93 -3
- package/dist/testing.js.map +1 -1
- package/dist/timing-dump.d.ts +22 -5
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +89 -23
- package/dist/timing-dump.js.map +1 -1
- package/eslint/index.cjs +9 -0
- package/eslint/index.d.cts +14 -7
- package/eslint/rules/no-process-exit-in-phase.cjs +117 -0
- package/package.json +5 -7
- package/dist/link-auth/build-headers.d.ts +0 -34
- package/dist/link-auth/build-headers.d.ts.map +0 -1
- package/dist/link-auth/build-headers.js +0 -58
- package/dist/link-auth/build-headers.js.map +0 -1
- package/dist/link-auth/expand-macro.d.ts +0 -38
- package/dist/link-auth/expand-macro.d.ts.map +0 -1
- package/dist/link-auth/expand-macro.js +0 -139
- package/dist/link-auth/expand-macro.js.map +0 -1
- package/dist/link-auth/macros.yaml +0 -50
- package/dist/link-auth/resolve-token.d.ts +0 -83
- package/dist/link-auth/resolve-token.d.ts.map +0 -1
- package/dist/link-auth/resolve-token.js +0 -115
- package/dist/link-auth/resolve-token.js.map +0 -1
- package/dist/link-auth/resolve.d.ts +0 -102
- package/dist/link-auth/resolve.d.ts.map +0 -1
- package/dist/link-auth/resolve.js +0 -66
- package/dist/link-auth/resolve.js.map +0 -1
- package/dist/link-auth/rewrite.d.ts +0 -52
- package/dist/link-auth/rewrite.d.ts.map +0 -1
- package/dist/link-auth/rewrite.js +0 -102
- package/dist/link-auth/rewrite.js.map +0 -1
- package/dist/link-auth/select-provider.d.ts +0 -30
- package/dist/link-auth/select-provider.d.ts.map +0 -1
- package/dist/link-auth/select-provider.js +0 -55
- package/dist/link-auth/select-provider.js.map +0 -1
- package/dist/link-auth/template.d.ts +0 -40
- package/dist/link-auth/template.d.ts.map +0 -1
- package/dist/link-auth/template.js +0 -89
- package/dist/link-auth/template.js.map +0 -1
- package/dist/link-auth/transforms.d.ts +0 -46
- package/dist/link-auth/transforms.d.ts.map +0 -1
- package/dist/link-auth/transforms.js +0 -52
- package/dist/link-auth/transforms.js.map +0 -1
- package/dist/template-entry.d.ts +0 -10
- package/dist/template-entry.d.ts.map +0 -1
- package/dist/template-entry.js +0 -10
- package/dist/template-entry.js.map +0 -1
- package/dist/template.d.ts +0 -7
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -18
- package/dist/template.js.map +0 -1
package/dist/timing-dump.js
CHANGED
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
* 2. A dump failure is written to **stderr and never thrown**. These run from
|
|
19
19
|
* an `exit` listener, where a throw changes the process's exit behaviour, and
|
|
20
20
|
* they must never touch stdout, which carries vat's report.
|
|
21
|
-
* 3. A pid can file more than one dump
|
|
22
|
-
* once per phase and pids are
|
|
23
|
-
*
|
|
21
|
+
* 3. A pid can file more than one dump, and the name is CLAIMED rather than
|
|
22
|
+
* checked. `vat validate` spawns the vat binary once per phase and pids are
|
|
23
|
+
* reused, so `<stem>-<pid>.json` genuinely collides; worse, worker threads
|
|
24
|
+
* SHARE their parent's pid, so a thread pool's whole cohort competes for one
|
|
25
|
+
* `<stem>-<pid>` sequence at the same instant. The name gains a counter
|
|
26
|
+
* rather than overwriting, and the counter is settled by
|
|
27
|
+
* {@link EXCLUSIVE_CREATE} so the OS — not a check-then-write gap — decides
|
|
28
|
+
* who won it.
|
|
24
29
|
* 4. The process's own wall and CPU time is read ONCE, at dump time. It is a
|
|
25
30
|
* lifetime figure for the process and never a duration of the measured work;
|
|
26
31
|
* its value is the RATIO, which tells a reader whether the wall-timed
|
|
@@ -42,7 +47,7 @@
|
|
|
42
47
|
* thing a reader has to be able to trust identically. What each seam keeps for
|
|
43
48
|
* itself is its accumulator shape, its dump body and the noun it is called by.
|
|
44
49
|
*/
|
|
45
|
-
import {
|
|
50
|
+
import { writeFileSync } from 'node:fs';
|
|
46
51
|
import { safePath } from './path-core.js';
|
|
47
52
|
import { mkdirSyncReal } from './path-utils.js';
|
|
48
53
|
/** `process.cpuUsage()` reports microseconds; a dump reports milliseconds. */
|
|
@@ -51,10 +56,27 @@ const MICROSECONDS_PER_MS = 1000;
|
|
|
51
56
|
const MS_PER_SECOND = 1000;
|
|
52
57
|
/**
|
|
53
58
|
* Ceiling on the pid-collision search. A directory holding this many dumps for
|
|
54
|
-
* one pid is a runaway, not a collision
|
|
55
|
-
* outcome than spinning.
|
|
59
|
+
* one pid is a runaway, not a collision, and giving up on a reported line is a
|
|
60
|
+
* better outcome than spinning.
|
|
61
|
+
*
|
|
62
|
+
* Exhausting it does NOT fall back to overwriting the last slot. That was the
|
|
63
|
+
* old behaviour, and it destroyed a dump for the same reason the check-then-
|
|
64
|
+
* write gap did — silently, and precisely when the directory was busiest.
|
|
56
65
|
*/
|
|
57
|
-
const MAX_DUMP_COLLISIONS = 1000;
|
|
66
|
+
export const MAX_DUMP_COLLISIONS = 1000;
|
|
67
|
+
/**
|
|
68
|
+
* The `writeFileSync` flag that makes claiming a name atomic.
|
|
69
|
+
*
|
|
70
|
+
* `wx` is create-exclusive: the OS fails the call with `EEXIST` when the path
|
|
71
|
+
* already exists, so exactly one caller wins each name no matter how many are
|
|
72
|
+
* racing for it. Asking `existsSync` first and writing second is two operations
|
|
73
|
+
* with a gap in between, and threads sharing a pid land in that gap routinely —
|
|
74
|
+
* a measured 8-worker parse pool lost 6 of 9 dumps to it, reporting 44 of 172
|
|
75
|
+
* documents as if that were the whole run.
|
|
76
|
+
*/
|
|
77
|
+
const EXCLUSIVE_CREATE = 'wx';
|
|
78
|
+
/** How the failure line names the ceiling case, so it reads unlike an OS error. */
|
|
79
|
+
const CEILING_DETAIL = `every name up to the ${String(MAX_DUMP_COLLISIONS)}-collision ceiling was already claimed`;
|
|
58
80
|
/**
|
|
59
81
|
* Reduce a raw env value to a directory or `null`.
|
|
60
82
|
*
|
|
@@ -119,46 +141,90 @@ export function readTimingProcess() {
|
|
|
119
141
|
};
|
|
120
142
|
}
|
|
121
143
|
/**
|
|
122
|
-
*
|
|
144
|
+
* The nth candidate name in one pid's dump sequence.
|
|
145
|
+
*
|
|
146
|
+
* @param directory - Directory dumps are written to
|
|
147
|
+
* @param stem - Basename and pid, already joined
|
|
148
|
+
* @param collision - 0 for the unsuffixed name, then the collision counter
|
|
149
|
+
* @returns The candidate path
|
|
150
|
+
*/
|
|
151
|
+
function timingDumpCandidate(directory, stem, collision) {
|
|
152
|
+
const suffix = collision === 0 ? '' : `-${String(collision)}`;
|
|
153
|
+
return safePath.join(directory, `${stem}${suffix}.json`);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether a caught write failure means somebody else already holds the name.
|
|
157
|
+
*
|
|
158
|
+
* @param error - Whatever `writeFileSync` threw
|
|
159
|
+
* @returns `true` only for `EEXIST`, which is a lost race and not a fault
|
|
160
|
+
*/
|
|
161
|
+
function isNameAlreadyTaken(error) {
|
|
162
|
+
return error instanceof Error && 'code' in error && error.code === 'EEXIST';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Claim a name by creating it, and fill it in the same operation.
|
|
166
|
+
*
|
|
167
|
+
* Choosing the name and writing it CANNOT be two steps: whatever separates them
|
|
168
|
+
* is a window in which another writer takes the name that was just declared
|
|
169
|
+
* free, and the loser's dump disappears with no error raised anywhere. So each
|
|
170
|
+
* candidate is written create-exclusively ({@link EXCLUSIVE_CREATE}) and an
|
|
171
|
+
* `EEXIST` — the OS saying somebody else got there — advances to the next
|
|
172
|
+
* counter rather than overwriting.
|
|
123
173
|
*
|
|
124
174
|
* @param directory - Directory dumps are written to
|
|
125
175
|
* @param basename - Basename stem; the pid and any collision counter follow
|
|
126
|
-
* @
|
|
176
|
+
* @param contents - The already-serialized dump body
|
|
177
|
+
* @returns Which name was claimed, or why none could be
|
|
127
178
|
*/
|
|
128
|
-
function
|
|
179
|
+
function claimTimingDump(directory, basename, contents) {
|
|
129
180
|
const stem = `${basename}-${String(process.pid)}`;
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
181
|
+
for (let collision = 0; collision <= MAX_DUMP_COLLISIONS; collision += 1) {
|
|
182
|
+
const candidate = timingDumpCandidate(directory, stem, collision);
|
|
183
|
+
try {
|
|
184
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- operator-supplied diagnostic directory from a VAT_*_TIMING variable
|
|
185
|
+
writeFileSync(candidate, contents, { encoding: 'utf-8', flag: EXCLUSIVE_CREATE });
|
|
186
|
+
return { outcome: 'written', path: candidate };
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (!isNameAlreadyTaken(error))
|
|
190
|
+
return { outcome: 'failed', path: candidate, error };
|
|
191
|
+
}
|
|
135
192
|
}
|
|
136
|
-
return
|
|
193
|
+
return {
|
|
194
|
+
outcome: 'exhausted',
|
|
195
|
+
path: timingDumpCandidate(directory, stem, MAX_DUMP_COLLISIONS),
|
|
196
|
+
};
|
|
137
197
|
}
|
|
138
198
|
/**
|
|
139
199
|
* Write one seam's dump, if the seam is on.
|
|
140
200
|
*
|
|
141
201
|
* The body is built lazily, inside this call, so a disabled seam never pays to
|
|
142
|
-
* snapshot accumulators nobody will read
|
|
202
|
+
* snapshot accumulators nobody will read — and a `build` that throws is reported
|
|
203
|
+
* like any other failure, because this runs from an `exit` listener where a
|
|
204
|
+
* throw would change the process's exit behaviour.
|
|
143
205
|
*
|
|
144
206
|
* @param noun - What the seam is called, for any failure line
|
|
145
207
|
* @param directory - Where to write, or `null` when the seam is off
|
|
146
208
|
* @param basename - Basename stem for the file
|
|
147
209
|
* @param build - Produces the dump body
|
|
148
|
-
* @returns The path written, or `null` when the seam is off or
|
|
210
|
+
* @returns The path written, or `null` when the seam is off or nothing was written
|
|
149
211
|
*/
|
|
150
212
|
export function writeTimingDump(noun, directory, basename, build) {
|
|
151
213
|
if (directory === null)
|
|
152
214
|
return null;
|
|
153
|
-
|
|
215
|
+
let claim;
|
|
154
216
|
try {
|
|
155
|
-
|
|
156
|
-
writeFileSync(target, `${JSON.stringify(build(), null, 2)}\n`, 'utf-8');
|
|
217
|
+
claim = claimTimingDump(directory, basename, `${JSON.stringify(build(), null, 2)}\n`);
|
|
157
218
|
}
|
|
158
219
|
catch (error) {
|
|
159
|
-
|
|
220
|
+
// Only `build()` and its serialization can reach here; every filesystem
|
|
221
|
+
// failure is already an outcome rather than a throw.
|
|
222
|
+
reportTimingDumpFailure(noun, directory, error);
|
|
160
223
|
return null;
|
|
161
224
|
}
|
|
162
|
-
|
|
225
|
+
if (claim.outcome === 'written')
|
|
226
|
+
return claim.path;
|
|
227
|
+
reportTimingDumpFailure(noun, claim.path, claim.outcome === 'exhausted' ? CEILING_DETAIL : claim.error);
|
|
228
|
+
return null;
|
|
163
229
|
}
|
|
164
230
|
//# sourceMappingURL=timing-dump.js.map
|
package/dist/timing-dump.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing-dump.js","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"timing-dump.js","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"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"}
|
package/eslint/index.cjs
CHANGED
|
@@ -54,6 +54,7 @@ const rules = {
|
|
|
54
54
|
'no-self-package-import': require('./rules/no-self-package-import.cjs'),
|
|
55
55
|
'require-justified-skip': require('./rules/require-justified-skip.cjs'),
|
|
56
56
|
'no-bare-symlink-in-tests': require('./rules/no-bare-symlink-in-tests.cjs'),
|
|
57
|
+
'no-process-exit-in-phase': require('./rules/no-process-exit-in-phase.cjs'),
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -136,6 +137,14 @@ const RECOMMENDED_EXCLUDE = new Set([
|
|
|
136
137
|
// ride in `recommended`; that is a public-API change and has not been made.
|
|
137
138
|
// VAT enables the whole rule explicitly, scoped to its own conventions.
|
|
138
139
|
'no-bare-symlink-in-tests',
|
|
140
|
+
// Excluded because it keys on a NAMING CONVENTION that is VAT's, not a
|
|
141
|
+
// portable fact — the same reason `no-unsafe-root-join` is excluded above. The
|
|
142
|
+
// hazard it guards is real and general (an in-process orchestrator whose step
|
|
143
|
+
// calls `process.exit()` silently skips every later step), but the marker is
|
|
144
|
+
// the `…Phase` suffix, and an adopter with an unrelated `computeRenderPhase()`
|
|
145
|
+
// that legitimately exits would get a finding they cannot act on. It ships in
|
|
146
|
+
// `rules` and VAT enables it explicitly, scoped to its own orchestrators.
|
|
147
|
+
'no-process-exit-in-phase',
|
|
139
148
|
]);
|
|
140
149
|
|
|
141
150
|
/**
|
package/eslint/index.d.cts
CHANGED
|
@@ -47,13 +47,20 @@ declare namespace plugin {
|
|
|
47
47
|
rules: Record<string, RuleModule>;
|
|
48
48
|
configs: {
|
|
49
49
|
/**
|
|
50
|
-
* The cross-platform safety core: 18 of the
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
50
|
+
* The cross-platform safety core: 18 of the 25 rules, 15 `error` / 3 `warn`.
|
|
51
|
+
*
|
|
52
|
+
* Seven are excluded, for four reasons. `no-test-scoped-functions`,
|
|
53
|
+
* `require-justified-skip` and `no-bare-symlink-in-tests` are positions on
|
|
54
|
+
* TEST STYLE rather than portability facts. `no-unsafe-root-join` and
|
|
55
|
+
* `no-process-exit-in-phase` key on NAMING rather than on the property they
|
|
56
|
+
* care about (taint, and an orchestrated call site). `no-raw-text-decode`
|
|
57
|
+
* names a decoding SEAM that only exists in the consuming repo. And
|
|
58
|
+
* `no-self-package-import` REQUIRES an option this config cannot supply.
|
|
59
|
+
*
|
|
60
|
+
* All seven still ship in `rules` and are enabled by naming them — which is
|
|
61
|
+
* what this repo's own `eslint.config.js` does. The count above is asserted
|
|
62
|
+
* by `packages/utils/test/eslint/rules.test.ts`, so it cannot drift
|
|
63
|
+
* unnoticed the way it did when this comment said "four".
|
|
57
64
|
*/
|
|
58
65
|
recommended: FlatConfig;
|
|
59
66
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: no-process-exit-in-phase
|
|
3
|
+
*
|
|
4
|
+
* Forbid `process.exit()` inside a phase entry point — a function whose name
|
|
5
|
+
* ends in `Phase`.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this rule exists
|
|
8
|
+
*
|
|
9
|
+
* `vat validate`, `vat verify` and `vat build` used to run each phase as a
|
|
10
|
+
* SEPARATE CHILD PROCESS. A `process.exit()` inside a phase was then perfectly
|
|
11
|
+
* safe: it ended that child, the parent read the exit code, and the run carried
|
|
12
|
+
* on to the next phase. Nothing about the code said so — the safety came
|
|
13
|
+
* entirely from the process boundary.
|
|
14
|
+
*
|
|
15
|
+
* That boundary is gone. Phases now run in the orchestrator's own process, so a
|
|
16
|
+
* surviving `process.exit()` ends THE WHOLE RUN: every later phase is silently
|
|
17
|
+
* skipped, the parent's aggregation never happens, and the process exits 0-or-1
|
|
18
|
+
* having done half the work with nothing in the document to say so. It is the
|
|
19
|
+
* worst shape of failure this codebase has — a confident, well-formed, wrong
|
|
20
|
+
* answer — and it cannot be caught by a type, because `process.exit()`
|
|
21
|
+
* typechecks anywhere.
|
|
22
|
+
*
|
|
23
|
+
* A phase must RETURN `{ document, exitCode }` and let its caller decide whether
|
|
24
|
+
* to print and exit (a command-line run) or to fold the result into the run (an
|
|
25
|
+
* orchestrated one).
|
|
26
|
+
*
|
|
27
|
+
* ## Why the name is the marker
|
|
28
|
+
*
|
|
29
|
+
* The `…Phase` suffix is the convention every phase entry point already follows
|
|
30
|
+
* (`runResourcesValidatePhase`, `runSkillsBuildPhase`, …). Keying the rule to it
|
|
31
|
+
* means a new phase is protected the moment it is named like one, with no list
|
|
32
|
+
* to keep in sync — a list is the thing that goes stale silently, which is the
|
|
33
|
+
* defect class this rule exists to prevent in the first place.
|
|
34
|
+
*
|
|
35
|
+
* The thin Commander wrappers that CALL these functions keep their
|
|
36
|
+
* `process.exit()`: deciding how the process ends is exactly their job, and they
|
|
37
|
+
* are not named `…Phase`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Does this call expression read as `process.exit(...)`? */
|
|
41
|
+
function isProcessExitCall(node) {
|
|
42
|
+
const { callee } = node;
|
|
43
|
+
return (
|
|
44
|
+
callee.type === 'MemberExpression' &&
|
|
45
|
+
!callee.computed &&
|
|
46
|
+
callee.object.type === 'Identifier' &&
|
|
47
|
+
callee.object.name === 'process' &&
|
|
48
|
+
callee.property.type === 'Identifier' &&
|
|
49
|
+
callee.property.name === 'exit'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The declared name of a function-ish node, however it was declared.
|
|
55
|
+
*
|
|
56
|
+
* Covers the three spellings a phase entry point can legitimately take: a
|
|
57
|
+
* function declaration, a `const x = function () {}`, and a `const x = () => {}`.
|
|
58
|
+
* A rule that only understood declarations would be silently inert against an
|
|
59
|
+
* arrow const — the same blind spot `no-test-scoped-functions` documents.
|
|
60
|
+
*/
|
|
61
|
+
function functionName(node) {
|
|
62
|
+
if (node.id && node.id.type === 'Identifier') return node.id.name;
|
|
63
|
+
|
|
64
|
+
const { parent } = node;
|
|
65
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
|
66
|
+
return parent.id.name;
|
|
67
|
+
}
|
|
68
|
+
if (parent && parent.type === 'Property' && parent.key.type === 'Identifier') {
|
|
69
|
+
return parent.key.name;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FUNCTION_TYPES = new Set([
|
|
75
|
+
'FunctionDeclaration',
|
|
76
|
+
'FunctionExpression',
|
|
77
|
+
'ArrowFunctionExpression',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/** The nearest enclosing function named like a phase entry point, if any. */
|
|
81
|
+
function enclosingPhaseName(node) {
|
|
82
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
83
|
+
if (!FUNCTION_TYPES.has(current.type)) continue;
|
|
84
|
+
const name = functionName(current);
|
|
85
|
+
if (name !== undefined && name.endsWith('Phase')) return name;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
meta: {
|
|
92
|
+
type: 'problem',
|
|
93
|
+
docs: {
|
|
94
|
+
description:
|
|
95
|
+
'Forbid process.exit() inside a phase entry point, where it would end the whole orchestrated run',
|
|
96
|
+
category: 'Agentic Code Safety',
|
|
97
|
+
recommended: true,
|
|
98
|
+
},
|
|
99
|
+
fixable: null,
|
|
100
|
+
schema: [],
|
|
101
|
+
messages: {
|
|
102
|
+
exitInPhase:
|
|
103
|
+
"'{{name}}' is a phase entry point, so process.exit() here ends the ENTIRE run — every later phase is skipped and the orchestrator never aggregates. Return { document, exitCode } instead and let the caller decide how the process ends.",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
create(context) {
|
|
108
|
+
return {
|
|
109
|
+
CallExpression(node) {
|
|
110
|
+
if (!isProcessExitCall(node)) return;
|
|
111
|
+
const name = enclosingPhaseName(node);
|
|
112
|
+
if (name === undefined) return;
|
|
113
|
+
context.report({ node, messageId: 'exitInPhase', data: { name } });
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-agent-toolkit/utils",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core utility functions shared across the vibe-agent-toolkit packages",
|
|
6
6
|
"sideEffects": [
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"types": "./dist/zod.d.ts",
|
|
55
55
|
"import": "./dist/zod.js"
|
|
56
56
|
},
|
|
57
|
-
"./
|
|
58
|
-
"types": "./dist/
|
|
59
|
-
"import": "./dist/
|
|
57
|
+
"./skill-test": {
|
|
58
|
+
"types": "./dist/skill-test/index.d.ts",
|
|
59
|
+
"import": "./dist/skill-test/index.js"
|
|
60
60
|
},
|
|
61
61
|
"./yaml": {
|
|
62
62
|
"types": "./dist/yaml.d.ts",
|
|
@@ -90,17 +90,15 @@
|
|
|
90
90
|
"README.md"
|
|
91
91
|
],
|
|
92
92
|
"scripts": {
|
|
93
|
-
"build": "rimraf --glob dist \"*.tsbuildinfo\" && tsc
|
|
93
|
+
"build": "rimraf --glob dist \"*.tsbuildinfo\" && tsc",
|
|
94
94
|
"test:unit": "vitest run",
|
|
95
95
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
96
|
-
"test:system": "vitest run --config vitest.system.config.ts",
|
|
97
96
|
"test:watch": "vitest",
|
|
98
97
|
"typecheck": "tsc --noEmit",
|
|
99
98
|
"clean": "rimraf dist *.tsbuildinfo"
|
|
100
99
|
},
|
|
101
100
|
"dependencies": {
|
|
102
101
|
"@vibe-validate/git": "0.20.1",
|
|
103
|
-
"handlebars": "^4.7.8",
|
|
104
102
|
"ignore": "^7.0.5",
|
|
105
103
|
"picomatch": "^4.0.3",
|
|
106
104
|
"which": "^5.0.0",
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render auth-header templates with rendered values, and a structural
|
|
3
|
-
* redaction helper for serialization.
|
|
4
|
-
*
|
|
5
|
-
* `buildHeaders` renders each header value template against a context that
|
|
6
|
-
* carries `${token}` plus any named captures / vars from the rewrite step.
|
|
7
|
-
* `redactHeaders` masks `Authorization` values for any caller that needs to
|
|
8
|
-
* serialize headers into logs, errors, or cache entries — the design's §8
|
|
9
|
-
* "tokens never leak" claim depends on every such site routing through this.
|
|
10
|
-
*
|
|
11
|
-
* Per design issue #113 §4 (auth.headers vocabulary) and §8 (redaction is
|
|
12
|
-
* structural; Authorization values never appear in serialized output).
|
|
13
|
-
*/
|
|
14
|
-
export declare const REDACTED_VALUE = "<redacted>";
|
|
15
|
-
/**
|
|
16
|
-
* Render a map of header templates into a map of concrete header values.
|
|
17
|
-
*
|
|
18
|
-
* @throws {TemplateMissingVarError} if a header template references an
|
|
19
|
-
* unknown context key
|
|
20
|
-
* @throws {TemplateSyntaxError} from a malformed template expression
|
|
21
|
-
* @throws {UnknownTransformError} from a template calling an unknown transform
|
|
22
|
-
*/
|
|
23
|
-
export declare function buildHeaders(templates: Record<string, string>, context: Record<string, string>): Record<string, string>;
|
|
24
|
-
/**
|
|
25
|
-
* Return a copy of `headers` with sensitive values replaced by `REDACTED_VALUE`.
|
|
26
|
-
* Header-name matching is case-insensitive but exact (no prefix matching) — a
|
|
27
|
-
* header like `X-Authorization-Foo` is NOT considered sensitive.
|
|
28
|
-
*
|
|
29
|
-
* **Input must be a plain key-value object.** A `Headers` instance (Web Fetch
|
|
30
|
-
* API) or a `Map` yields `[]` from `Object.entries` and would silently no-op
|
|
31
|
-
* redaction — converting to a plain object is the caller's responsibility.
|
|
32
|
-
*/
|
|
33
|
-
export declare function redactHeaders(headers: Record<string, string>): Record<string, string>;
|
|
34
|
-
//# sourceMappingURL=build-headers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"build-headers.d.ts","sourceRoot":"","sources":["../../src/link-auth/build-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,cAAc,eAAe,CAAC;AAc3C;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMrF"}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render auth-header templates with rendered values, and a structural
|
|
3
|
-
* redaction helper for serialization.
|
|
4
|
-
*
|
|
5
|
-
* `buildHeaders` renders each header value template against a context that
|
|
6
|
-
* carries `${token}` plus any named captures / vars from the rewrite step.
|
|
7
|
-
* `redactHeaders` masks `Authorization` values for any caller that needs to
|
|
8
|
-
* serialize headers into logs, errors, or cache entries — the design's §8
|
|
9
|
-
* "tokens never leak" claim depends on every such site routing through this.
|
|
10
|
-
*
|
|
11
|
-
* Per design issue #113 §4 (auth.headers vocabulary) and §8 (redaction is
|
|
12
|
-
* structural; Authorization values never appear in serialized output).
|
|
13
|
-
*/
|
|
14
|
-
import { renderTemplate } from './template.js';
|
|
15
|
-
export const REDACTED_VALUE = '<redacted>';
|
|
16
|
-
/**
|
|
17
|
-
* Header names whose values must be masked when serialized.
|
|
18
|
-
*
|
|
19
|
-
* v1 ships only `authorization` because that is the only secret-bearing
|
|
20
|
-
* header the current macros emit. **Omission is the security risk** — any
|
|
21
|
-
* future macro that emits a header carrying a secret (`Cookie`,
|
|
22
|
-
* `Proxy-Authorization`, `X-API-Key`, custom bearer-style headers) must add
|
|
23
|
-
* that name here, or the token will silently leak through serialization.
|
|
24
|
-
* Extending the set is not the dangerous edit; forgetting to extend it is.
|
|
25
|
-
*/
|
|
26
|
-
const SENSITIVE_HEADER_NAMES = new Set(['authorization']);
|
|
27
|
-
/**
|
|
28
|
-
* Render a map of header templates into a map of concrete header values.
|
|
29
|
-
*
|
|
30
|
-
* @throws {TemplateMissingVarError} if a header template references an
|
|
31
|
-
* unknown context key
|
|
32
|
-
* @throws {TemplateSyntaxError} from a malformed template expression
|
|
33
|
-
* @throws {UnknownTransformError} from a template calling an unknown transform
|
|
34
|
-
*/
|
|
35
|
-
export function buildHeaders(templates, context) {
|
|
36
|
-
const headers = Object.create(null);
|
|
37
|
-
for (const [name, template] of Object.entries(templates)) {
|
|
38
|
-
headers[name] = renderTemplate(template, context);
|
|
39
|
-
}
|
|
40
|
-
return headers;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Return a copy of `headers` with sensitive values replaced by `REDACTED_VALUE`.
|
|
44
|
-
* Header-name matching is case-insensitive but exact (no prefix matching) — a
|
|
45
|
-
* header like `X-Authorization-Foo` is NOT considered sensitive.
|
|
46
|
-
*
|
|
47
|
-
* **Input must be a plain key-value object.** A `Headers` instance (Web Fetch
|
|
48
|
-
* API) or a `Map` yields `[]` from `Object.entries` and would silently no-op
|
|
49
|
-
* redaction — converting to a plain object is the caller's responsibility.
|
|
50
|
-
*/
|
|
51
|
-
export function redactHeaders(headers) {
|
|
52
|
-
const redacted = Object.create(null);
|
|
53
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
54
|
-
redacted[name] = SENSITIVE_HEADER_NAMES.has(name.toLowerCase()) ? REDACTED_VALUE : value;
|
|
55
|
-
}
|
|
56
|
-
return redacted;
|
|
57
|
-
}
|
|
58
|
-
//# sourceMappingURL=build-headers.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"build-headers.js","sourceRoot":"","sources":["../../src/link-auth/build-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,sBAAsB,GAAwB,IAAI,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,SAAiC,EACjC,OAA+B;IAE/B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IAC9D,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,OAA+B;IAC3D,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IAC/D,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC;IAC3F,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Macro loader + expander.
|
|
3
|
-
*
|
|
4
|
-
* Loads the bundled `macros.yaml` once at module init and exposes
|
|
5
|
-
* `expandMacro(name, overrides?)` which deep-merges the named macro with any
|
|
6
|
-
* adopter overrides. The merge is "adopter wins": objects merge recursively,
|
|
7
|
-
* arrays and primitives are replaced wholesale (no element-wise array merge).
|
|
8
|
-
*
|
|
9
|
-
* The shipped macro file is at `link-auth/macros.yaml`, copied into the dist
|
|
10
|
-
* tree by `packages/dev-tools/src/copy-yaml-assets.ts` during build so the
|
|
11
|
-
* runtime `fs.readFileSync(new URL('./macros.yaml', import.meta.url))`
|
|
12
|
-
* resolves in both source-mode (vitest) and built-mode (dist).
|
|
13
|
-
*
|
|
14
|
-
* Per design issue #113 §5 (macros are config, not a privileged code path).
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Thrown when a `use: <name>` references a macro not in the shipped set.
|
|
18
|
-
* Message lists the available macros so a typo surfaces clearly.
|
|
19
|
-
*/
|
|
20
|
-
export declare class UnknownMacroError extends Error {
|
|
21
|
-
constructor(name: string, available: readonly string[]);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Look up a macro by name and deep-merge optional adopter overrides on top.
|
|
25
|
-
*
|
|
26
|
-
* Merge semantics:
|
|
27
|
-
* - Plain objects merge recursively (sibling keys preserved).
|
|
28
|
-
* - Arrays are replaced wholesale (override's array wins; no concat).
|
|
29
|
-
* - Primitives are replaced.
|
|
30
|
-
* - `undefined` in an override is treated as "not provided" (base wins).
|
|
31
|
-
*
|
|
32
|
-
* The returned object and all nested plain objects use null prototypes so
|
|
33
|
-
* `__proto__` / `constructor` keys can never poison consumers.
|
|
34
|
-
*
|
|
35
|
-
* @throws {UnknownMacroError} if `name` is not in the shipped macro set
|
|
36
|
-
*/
|
|
37
|
-
export declare function expandMacro(name: string, overrides?: Record<string, unknown>): Record<string, unknown>;
|
|
38
|
-
//# sourceMappingURL=expand-macro.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"expand-macro.d.ts","sourceRoot":"","sources":["../../src/link-auth/expand-macro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA+CH;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE;CAIvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB"}
|