@vibe-agent-toolkit/agent-skills 0.1.41-rc.7 → 0.1.41
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/dist/content-type-routing.d.ts +18 -1
- package/dist/content-type-routing.d.ts.map +1 -1
- package/dist/content-type-routing.js +17 -0
- package/dist/content-type-routing.js.map +1 -1
- package/dist/evidence/build-evidence.d.ts +38 -0
- package/dist/evidence/build-evidence.d.ts.map +1 -0
- package/dist/evidence/build-evidence.js +60 -0
- package/dist/evidence/build-evidence.js.map +1 -0
- package/dist/evidence/index.d.ts +1 -0
- package/dist/evidence/index.d.ts.map +1 -1
- package/dist/evidence/index.js +1 -0
- package/dist/evidence/index.js.map +1 -1
- package/dist/files-config.d.ts +225 -31
- package/dist/files-config.d.ts.map +1 -1
- package/dist/files-config.js +445 -123
- package/dist/files-config.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/post-build-checks.d.ts +10 -1
- package/dist/post-build-checks.d.ts.map +1 -1
- package/dist/post-build-checks.js +55 -6
- package/dist/post-build-checks.js.map +1 -1
- package/dist/schemas/index.d.ts +3 -2
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/installed-plugins-registry.d.ts +137 -63
- package/dist/schemas/installed-plugins-registry.d.ts.map +1 -1
- package/dist/schemas/installed-plugins-registry.js +94 -6
- package/dist/schemas/installed-plugins-registry.js.map +1 -1
- package/dist/schemas/known-marketplaces-registry.d.ts +165 -100
- package/dist/schemas/known-marketplaces-registry.d.ts.map +1 -1
- package/dist/schemas/known-marketplaces-registry.js +108 -21
- package/dist/schemas/known-marketplaces-registry.js.map +1 -1
- package/dist/schemas/registry-drift.d.ts +53 -0
- package/dist/schemas/registry-drift.d.ts.map +1 -0
- package/dist/schemas/registry-drift.js +50 -0
- package/dist/schemas/registry-drift.js.map +1 -0
- package/dist/skill-packager.d.ts +75 -5
- package/dist/skill-packager.d.ts.map +1 -1
- package/dist/skill-packager.js +304 -50
- package/dist/skill-packager.js.map +1 -1
- package/dist/skill-source/git-clone.d.ts.map +1 -1
- package/dist/skill-source/git-clone.js +32 -11
- package/dist/skill-source/git-clone.js.map +1 -1
- package/dist/skill-test/eval-inputs.js +2 -2
- package/dist/skill-test/eval-inputs.js.map +1 -1
- package/dist/skill-test/eval-lint.js +2 -2
- package/dist/skill-test/eval-lint.js.map +1 -1
- package/dist/skill-test/grader-prompt.js +2 -2
- package/dist/skill-test/grader-prompt.js.map +1 -1
- package/dist/test-input.d.ts +59 -24
- package/dist/test-input.d.ts.map +1 -1
- package/dist/test-input.js +77 -16
- package/dist/test-input.js.map +1 -1
- package/dist/validators/agent-instruction-presence.d.ts +41 -0
- package/dist/validators/agent-instruction-presence.d.ts.map +1 -0
- package/dist/validators/agent-instruction-presence.js +79 -0
- package/dist/validators/agent-instruction-presence.js.map +1 -0
- package/dist/validators/bundled-resource-link-detection.d.ts +20 -3
- package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
- package/dist/validators/bundled-resource-link-detection.js +78 -34
- package/dist/validators/bundled-resource-link-detection.js.map +1 -1
- package/dist/validators/compat-detectors.d.ts +9 -4
- package/dist/validators/compat-detectors.d.ts.map +1 -1
- package/dist/validators/compat-detectors.js +18 -30
- package/dist/validators/compat-detectors.js.map +1 -1
- package/dist/validators/index.d.ts +1 -0
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/index.js +1 -0
- package/dist/validators/index.js.map +1 -1
- package/dist/validators/packaging-validator.d.ts +68 -10
- package/dist/validators/packaging-validator.d.ts.map +1 -1
- package/dist/validators/packaging-validator.js +51 -10
- package/dist/validators/packaging-validator.js.map +1 -1
- package/dist/validators/registry-validator.d.ts.map +1 -1
- package/dist/validators/registry-validator.js +28 -7
- package/dist/validators/registry-validator.js.map +1 -1
- package/dist/validators/rule-engine/rule-context.d.ts +4 -2
- package/dist/validators/rule-engine/rule-context.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-context.js.map +1 -1
- package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-engine.js +15 -3
- package/dist/validators/rule-engine/rule-engine.js.map +1 -1
- package/dist/validators/skill-validator.d.ts.map +1 -1
- package/dist/validators/skill-validator.js +16 -10
- package/dist/validators/skill-validator.js.map +1 -1
- package/dist/validators/source-only-codes.d.ts.map +1 -1
- package/dist/validators/source-only-codes.js +1 -0
- package/dist/validators/source-only-codes.js.map +1 -1
- package/dist/validators/validation-rules.d.ts +135 -17
- package/dist/validators/validation-rules.d.ts.map +1 -1
- package/dist/validators/validation-rules.js +192 -38
- package/dist/validators/validation-rules.js.map +1 -1
- package/dist/validators/walker-to-issues.d.ts.map +1 -1
- package/dist/validators/walker-to-issues.js +20 -7
- package/dist/validators/walker-to-issues.js.map +1 -1
- package/dist/walk-link-graph.d.ts +10 -1
- package/dist/walk-link-graph.d.ts.map +1 -1
- package/dist/walk-link-graph.js +124 -39
- package/dist/walk-link-graph.js.map +1 -1
- package/package.json +5 -5
package/dist/files-config.js
CHANGED
|
@@ -10,8 +10,80 @@
|
|
|
10
10
|
import { existsSync, statSync } from 'node:fs';
|
|
11
11
|
import { copyFile, mkdir } from 'node:fs/promises';
|
|
12
12
|
import { dirname } from 'node:path';
|
|
13
|
-
import { fileContentHash, globMagicRemainder, hasParentTraversalSegment, isGlob, safePath, staticGlobBase, toForwardSlash, } from '@vibe-agent-toolkit/utils';
|
|
13
|
+
import { fileContentHash, globMagicRemainder, hasParentTraversalSegment, isGlob, issueLocation, safePath, staticGlobBase, toForwardSlash, } from '@vibe-agent-toolkit/utils';
|
|
14
14
|
import { glob } from 'glob';
|
|
15
|
+
import picomatch from 'picomatch';
|
|
16
|
+
import { materializeIssue } from './validators/rule-engine/index.js';
|
|
17
|
+
import { isNeverPackagedBasename } from './validators/validation-rules.js';
|
|
18
|
+
/**
|
|
19
|
+
* Partition a glob's matches into the files that may be packaged and the
|
|
20
|
+
* never-packaged ones it happened to catch.
|
|
21
|
+
*
|
|
22
|
+
* Only globs come through here. Naming `source: extras/README.md` explicitly is an
|
|
23
|
+
* unambiguous instruction to ship that file, and it is honored; a glob is a net,
|
|
24
|
+
* not a declaration, so it does not get to launder the exemption that an explicit
|
|
25
|
+
* declaration earns. Without this split, link-following (which already refuses
|
|
26
|
+
* these files) and `files:` expansion disagree about what belongs in a bundle —
|
|
27
|
+
* and the glob's copy wins silently.
|
|
28
|
+
*/
|
|
29
|
+
function partitionNeverPackaged(matches) {
|
|
30
|
+
const kept = [];
|
|
31
|
+
const dropped = [];
|
|
32
|
+
for (const rel of matches) {
|
|
33
|
+
const basename = rel.slice(rel.lastIndexOf('/') + 1);
|
|
34
|
+
// THE shared matcher, not a local `Set.has` — a raw set is case-sensitive, and
|
|
35
|
+
// `Claude.md` satisfies Claude Code's project-local lookup exactly as
|
|
36
|
+
// `CLAUDE.md` does on APFS/NTFS, so a case-sensitive check leaves the harm
|
|
37
|
+
// reachable by renaming one letter.
|
|
38
|
+
(isNeverPackagedBasename(basename) ? dropped : kept).push(rel);
|
|
39
|
+
}
|
|
40
|
+
return { kept, dropped };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Turn never-package drops into reportable findings.
|
|
44
|
+
*
|
|
45
|
+
* Lives here rather than in the packager because this module owns the concept:
|
|
46
|
+
* the drop and the issue describing it must not be able to disagree.
|
|
47
|
+
*
|
|
48
|
+
* Anchored at the SOURCE file, in ONE coordinate system for both the location
|
|
49
|
+
* and the message. The finding used to name the would-be `dest` in both, which
|
|
50
|
+
* meant the only path it gave you was an output path nothing was ever written to
|
|
51
|
+
* — so the issue list mixed output coordinates (this code) with source
|
|
52
|
+
* coordinates (every other pre-build code) and an adopter could not open the
|
|
53
|
+
* thing that went missing. The glob pattern stays in the message because it
|
|
54
|
+
* answers the second question ("which entry caught this?"), but it is no longer
|
|
55
|
+
* the only identifiable thing in the finding.
|
|
56
|
+
*
|
|
57
|
+
* @param locationRoot The ONE base this run anchors locations at. Required, not
|
|
58
|
+
* defaulted to the project root: a defaulted anchor is how a multi-project run
|
|
59
|
+
* silently emits paths relative to the wrong tree.
|
|
60
|
+
*/
|
|
61
|
+
export function droppedGlobMatchesToIssues(dropped, locationRoot) {
|
|
62
|
+
return dropped.map((drop) => {
|
|
63
|
+
const file = issueLocation(drop.absFile, locationRoot);
|
|
64
|
+
return materializeIssue('FILES_GLOB_DROPPED_NEVER_PACKAGED', {
|
|
65
|
+
location: file,
|
|
66
|
+
detail: `${file} matched by files: source '${drop.source}'`,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* An absolute path spelled relative to the run's anchor, for a message or a
|
|
72
|
+
* location a consumer reads.
|
|
73
|
+
*
|
|
74
|
+
* `issueLocation` returns `''` when the path IS the anchor root (a
|
|
75
|
+
* project-root-wide glob such as `**\/*`); `.` is that same directory spelled
|
|
76
|
+
* as a path a reader can act on.
|
|
77
|
+
*
|
|
78
|
+
* Load-bearing for every THROWN message in this module, not only for issue
|
|
79
|
+
* locations: `applyFilesConfig`'s throws reach `vat skills build`'s stdout
|
|
80
|
+
* verbatim as `failedSkills[].message`, so an absolute path there publishes the
|
|
81
|
+
* developer's home directory into whatever issue or CI log the report is pasted
|
|
82
|
+
* into. Build-report messages are project-relative, never absolute.
|
|
83
|
+
*/
|
|
84
|
+
function anchoredPath(absolute, root) {
|
|
85
|
+
return issueLocation(absolute, root) || '.';
|
|
86
|
+
}
|
|
15
87
|
/**
|
|
16
88
|
* Returns a hint clause (with a leading space) when `source` looks like a build
|
|
17
89
|
* artifact that hasn't been produced yet, otherwise returns `''`.
|
|
@@ -50,6 +122,40 @@ export function normalizeRelPath(p) {
|
|
|
50
122
|
}
|
|
51
123
|
return normalized;
|
|
52
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The skill-output-relative dest a GLOB `files:` entry gives one absolute source
|
|
127
|
+
* path, or `undefined` when the entry's expansion would not include it.
|
|
128
|
+
*
|
|
129
|
+
* Answers, without running the glob, the question the packager must settle BEFORE
|
|
130
|
+
* anything is copied: "does the config already declare where this file goes?" A
|
|
131
|
+
* glob's expansion is late-bound to copy time ({@link copyGlobEntry}), so the path
|
|
132
|
+
* map used to have no entry for its matches — and a match that link traversal ALSO
|
|
133
|
+
* found was therefore dropped at its type-derived location while the glob copied
|
|
134
|
+
* it to the declared dest. Identical bytes shipped twice, the rewritten link
|
|
135
|
+
* pointed at traversal's copy, and the declared `dest:` was dead. One file, one
|
|
136
|
+
* dest: the declaration wins, and this is how the path map learns it.
|
|
137
|
+
*
|
|
138
|
+
* The three gates below are exactly {@link copyGlobEntry}'s own, restated as a
|
|
139
|
+
* predicate so the dest computed here is the dest the copy actually writes:
|
|
140
|
+
* under the static base, matching the magic remainder (`dot: true`, as the copy
|
|
141
|
+
* expands), and not refused by {@link partitionNeverPackaged}. A never-packaged
|
|
142
|
+
* match has NO dest — re-pointing the path map at one would rewrite a link to a
|
|
143
|
+
* file that never arrives.
|
|
144
|
+
*/
|
|
145
|
+
export function globEntryDest(entry, projectRoot, absSource) {
|
|
146
|
+
if (!isGlob(entry.source))
|
|
147
|
+
return undefined;
|
|
148
|
+
const absoluteBase = toForwardSlash(safePath.resolve(safePath.join(projectRoot, staticGlobBase(entry.source))));
|
|
149
|
+
const abs = toForwardSlash(safePath.resolve(absSource));
|
|
150
|
+
if (!abs.startsWith(absoluteBase + '/'))
|
|
151
|
+
return undefined;
|
|
152
|
+
const rel = abs.slice(absoluteBase.length + 1);
|
|
153
|
+
if (!picomatch.isMatch(rel, globMagicRemainder(entry.source), { dot: true }))
|
|
154
|
+
return undefined;
|
|
155
|
+
if (isNeverPackagedBasename(rel.slice(rel.lastIndexOf('/') + 1)))
|
|
156
|
+
return undefined;
|
|
157
|
+
return normalizeRelPath(safePath.join(entry.dest, rel));
|
|
158
|
+
}
|
|
53
159
|
/**
|
|
54
160
|
* Merge defaults and per-skill files entries.
|
|
55
161
|
*
|
|
@@ -98,52 +204,23 @@ export function mergeFilesConfig(defaults, perSkill) {
|
|
|
98
204
|
return merged;
|
|
99
205
|
}
|
|
100
206
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* Returns the matching entry and whether it matched on source or dest.
|
|
104
|
-
* Source matches take priority over dest matches.
|
|
105
|
-
*
|
|
106
|
-
* For GLOB entries (`isGlob(entry.source)`), matching is by directory prefix:
|
|
107
|
-
* - source match: link is equal to or under the glob's static base
|
|
108
|
-
* - dest match: link is equal to or under entry.dest
|
|
207
|
+
* The skill-output-relative dests named by EXPLICIT (non-glob) `files:` entries.
|
|
109
208
|
*
|
|
110
|
-
*
|
|
209
|
+
* THE definition of "the config declared this output path", shared by every lane
|
|
210
|
+
* that has to decide whether a file in a bundle is there on purpose. Naming a
|
|
211
|
+
* `source`/`dest` pair is an unambiguous instruction to ship that file; a GLOB
|
|
212
|
+
* never named the file it caught, so its expansion is deliberately absent from
|
|
213
|
+
* this list — a net is not a declaration. (`walk-link-graph.ts`'s
|
|
214
|
+
* `refusesAgentInstructionFile` draws the same line on the link side, and
|
|
215
|
+
* {@link partitionNeverPackaged} on the copy side; all three must agree or the
|
|
216
|
+
* mechanisms disagree about one bundle.)
|
|
111
217
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
218
|
+
* Spelled via {@link normalizeRelPath}, which is the spelling a packaged file's
|
|
219
|
+
* output-relative path is computed with — so membership can be tested by exact
|
|
220
|
+
* equality, never by a prefix test.
|
|
115
221
|
*/
|
|
116
|
-
export function
|
|
117
|
-
|
|
118
|
-
/** True when `candidate` equals `normalized` or is a path-prefix of it. */
|
|
119
|
-
function isPrefixMatch(candidate) {
|
|
120
|
-
return normalized === candidate || normalized.startsWith(candidate + '/');
|
|
121
|
-
}
|
|
122
|
-
// Source match has priority (checked before dest across all entries)
|
|
123
|
-
for (const entry of files) {
|
|
124
|
-
if (isGlob(entry.source)) {
|
|
125
|
-
const base = normalizeRelPath(staticGlobBase(entry.source));
|
|
126
|
-
if (isPrefixMatch(base)) {
|
|
127
|
-
return { match: 'source', entry };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
else if (normalizeRelPath(entry.source) === normalized) {
|
|
131
|
-
return { match: 'source', entry };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
// Then check dest match
|
|
135
|
-
for (const entry of files) {
|
|
136
|
-
if (isGlob(entry.source)) {
|
|
137
|
-
const destBase = normalizeRelPath(entry.dest);
|
|
138
|
-
if (isPrefixMatch(destBase)) {
|
|
139
|
-
return { match: 'dest', entry };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
else if (normalizeRelPath(entry.dest) === normalized) {
|
|
143
|
-
return { match: 'dest', entry };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return null;
|
|
222
|
+
export function explicitFilesConfigDests(files) {
|
|
223
|
+
return files.filter((entry) => !isGlob(entry.source)).map((entry) => normalizeRelPath(entry.dest));
|
|
147
224
|
}
|
|
148
225
|
/**
|
|
149
226
|
* Verify that each (absSource, absDest) pair has byte-identical content.
|
|
@@ -178,16 +255,20 @@ export function verifyFilesIntegrity(pairs) {
|
|
|
178
255
|
* diffing against the expected rel set is what catches a misrouted rebase, a
|
|
179
256
|
* stale leftover, or a dropped file.
|
|
180
257
|
*
|
|
181
|
-
* SAFETY ASSUMPTION (why this can't false-positive): a glob entry
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
258
|
+
* SAFETY ASSUMPTION (why this can't false-positive): a glob entry does NOT own
|
|
259
|
+
* its dest directory outright — another `files:` entry may legitimately declare a
|
|
260
|
+
* dest inside the same subtree, and that is exactly how the documented escape
|
|
261
|
+
* hatch works (an explicit `source: extras/README.md` re-ships a file the glob
|
|
262
|
+
* refused). So `expectedRel` is the union of what THIS entry copied and every
|
|
263
|
+
* other entry's declared dest under the same subtree, and the caller runs this
|
|
264
|
+
* only after ALL entries have copied — see {@link applyFilesConfig}. What is left
|
|
265
|
+
* over is a file no entry accounts for: a genuine bug, not a co-tenant. (The build
|
|
266
|
+
* wipes the skill output dir before copying — skill-packager.ts removes
|
|
267
|
+
* `resolvedOutput` recursively — so there are no stale files either.)
|
|
187
268
|
*
|
|
188
269
|
* @param destDir Absolute path to the glob entry's dest subtree.
|
|
189
|
-
* @param expectedRel Forward-slash rel paths (relative to `destDir`) the
|
|
190
|
-
*
|
|
270
|
+
* @param expectedRel Forward-slash rel paths (relative to `destDir`) that the
|
|
271
|
+
* whole `files:` config accounts for inside this subtree.
|
|
191
272
|
* @param source The entry's `source` (for error messages only).
|
|
192
273
|
* @throws if the actual subtree omits an expected file or contains an extra one.
|
|
193
274
|
*/
|
|
@@ -217,10 +298,10 @@ export async function verifyDestSet(destDir, expectedRel, source) {
|
|
|
217
298
|
* Returns `[destPath]` (the single entry.dest string) on success.
|
|
218
299
|
* Throws on missing source or if source is a directory.
|
|
219
300
|
*/
|
|
220
|
-
async function copyNonGlobEntry(entry, absoluteSource, skillOutputDir) {
|
|
301
|
+
async function copyNonGlobEntry(entry, absoluteSource, projectRoot, skillOutputDir) {
|
|
221
302
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- source path from validated config
|
|
222
303
|
if (!existsSync(absoluteSource)) {
|
|
223
|
-
throw new Error(`files: source '${entry.source}' does not exist (resolved to ${absoluteSource}).${buildArtifactHint(entry.source)}`);
|
|
304
|
+
throw new Error(`files: source '${entry.source}' does not exist (resolved to ${anchoredPath(absoluteSource, projectRoot)}).${buildArtifactHint(entry.source)}`);
|
|
224
305
|
}
|
|
225
306
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- source path from validated config
|
|
226
307
|
if (statSync(absoluteSource).isDirectory()) {
|
|
@@ -235,26 +316,88 @@ async function copyNonGlobEntry(entry, absoluteSource, skillOutputDir) {
|
|
|
235
316
|
return { relDest: normalizeRelPath(entry.dest), absSource: absoluteSource, absDest: absoluteDest };
|
|
236
317
|
}
|
|
237
318
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
319
|
+
* THE expansion of one GLOB `files:` entry — the single answer to "which files does
|
|
320
|
+
* this pattern name, and which of them never ship?".
|
|
321
|
+
*
|
|
322
|
+
* Extracted so the copy ({@link copyGlobEntry}) and the pre-build gates
|
|
323
|
+
* ({@link collectDroppedGlobMatches}) cannot expand the same glob two ways. A
|
|
324
|
+
* second expansion that disagrees with the copy's is exactly how a lane comes to
|
|
325
|
+
* report a bundle that was never produced.
|
|
326
|
+
*
|
|
327
|
+
* Deliberately reports NO verdict: zero matches is a fact here, and only the caller
|
|
328
|
+
* knows whether it is a failure (the copy, after a build) or the expected state (a
|
|
329
|
+
* validate run before one).
|
|
330
|
+
*
|
|
331
|
+
* @throws if the glob's magic remainder contains a `..` segment — `glob` honors it
|
|
332
|
+
* and climbs above `absoluteBase`, so the pattern cannot be expanded safely at
|
|
333
|
+
* any phase. (The static base may legitimately contain a leading `..`: that is
|
|
334
|
+
* the deliberate sibling-base monorepo feature.)
|
|
240
335
|
*/
|
|
241
|
-
async function
|
|
242
|
-
const base = staticGlobBase(entry.source);
|
|
336
|
+
async function expandGlobEntry(entry, projectRoot) {
|
|
243
337
|
const remainder = globMagicRemainder(entry.source);
|
|
244
|
-
// H2: the static base may legitimately contain leading '..' (the deliberate
|
|
245
|
-
// sibling-base monorepo feature), but the MAGIC REMAINDER must never contain a
|
|
246
|
-
// '..' segment — `glob` honors it and climbs above absoluteBase. Reject it.
|
|
247
338
|
if (hasParentTraversalSegment(remainder)) {
|
|
248
339
|
throw new Error(`files: source '${entry.source}' (glob) has a '..' segment in its glob portion ('${remainder}'); ` +
|
|
249
340
|
`parent-directory traversal is not allowed after the static base.`);
|
|
250
341
|
}
|
|
251
|
-
const absoluteBase = safePath.resolve(safePath.join(projectRoot,
|
|
342
|
+
const absoluteBase = safePath.resolve(safePath.join(projectRoot, staticGlobBase(entry.source)));
|
|
252
343
|
// dot: true so hidden files under the source subtree are included — keeps the
|
|
253
344
|
// package symmetric with verifyDestSet (M10: silent dot-file drop).
|
|
254
345
|
const rawMatches = await glob(remainder, { cwd: absoluteBase, nodir: true, dot: true });
|
|
255
|
-
const
|
|
346
|
+
const allMatches = rawMatches.map((m) => toForwardSlash(m)).sort((a, b) => a.localeCompare(b));
|
|
347
|
+
const { kept, dropped } = partitionNeverPackaged(allMatches);
|
|
348
|
+
return {
|
|
349
|
+
absoluteBase,
|
|
350
|
+
allMatches,
|
|
351
|
+
kept,
|
|
352
|
+
droppedRel: dropped,
|
|
353
|
+
dropped: dropped.map((rel) => ({
|
|
354
|
+
source: entry.source,
|
|
355
|
+
// The refused file itself. Computed HERE, where the base a match is
|
|
356
|
+
// relative to is known for certain, rather than re-derived by each
|
|
357
|
+
// consumer from `dest` — a dest cannot be inverted back to a source once
|
|
358
|
+
// the entry has rebased it.
|
|
359
|
+
absFile: toForwardSlash(safePath.joinUnderRoot(absoluteBase, rel)),
|
|
360
|
+
dest: normalizeRelPath(safePath.join(entry.dest, rel)),
|
|
361
|
+
})),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Expand a glob entry, copy all matched files, and return copied rel-dest paths
|
|
366
|
+
* plus source/dest pairs for optional integrity verification.
|
|
367
|
+
*
|
|
368
|
+
* There is deliberately NO bundled-file skip here, unlike {@link applyNonGlobFileEntry}.
|
|
369
|
+
* Both spellings obey one invariant — *after this function runs, every file the
|
|
370
|
+
* entry matches exists at its declared dest and is reported* — but only the
|
|
371
|
+
* non-glob path can satisfy it by skipping: `applyNonGlobEntriesToPathMap`
|
|
372
|
+
* (skill-packager.ts) re-points the path map so link traversal's copy lands AT
|
|
373
|
+
* `entry.dest`, making the skip a pure de-duplication. Glob entries are
|
|
374
|
+
* deliberately absent from that path map (late binding owns their expansion), so
|
|
375
|
+
* traversal drops a link-bundled match at its own resource-named location instead.
|
|
376
|
+
* Skipping the copy here would leave the declared dest subtree short a declared
|
|
377
|
+
* file — silently, and with `integrity: true` silently defeated too, because the
|
|
378
|
+
* skipped file lands in neither `rels` nor the on-disk subtree `verifyDestSet`
|
|
379
|
+
* diffs against. Copying it unconditionally keeps `copied`, `pairs`, and `rels`
|
|
380
|
+
* complete and makes integrity mean what it says.
|
|
381
|
+
*/
|
|
382
|
+
async function copyGlobEntry(entry, projectRoot, skillOutputDir) {
|
|
383
|
+
const { absoluteBase, allMatches, kept: matches, droppedRel, dropped } = await expandGlobEntry(entry, projectRoot);
|
|
384
|
+
// Project-relative in both throws below: these messages are published verbatim
|
|
385
|
+
// as `failedSkills[].message` (see {@link anchoredPath}).
|
|
386
|
+
const reportedBase = anchoredPath(absoluteBase, projectRoot);
|
|
387
|
+
if (allMatches.length === 0) {
|
|
388
|
+
throw new Error(`files: source '${entry.source}' (glob) matched no files under ${reportedBase} — has your build run?`);
|
|
389
|
+
}
|
|
390
|
+
// Distinct from the zero-match case above: the build DID run and the glob DID
|
|
391
|
+
// match — every match is simply a file that never ships. Saying "has your build
|
|
392
|
+
// run?" here would send the author hunting a build failure that isn't there.
|
|
256
393
|
if (matches.length === 0) {
|
|
257
|
-
throw new Error(`files: source '${entry.source}' (glob) matched
|
|
394
|
+
throw new Error(`files: source '${entry.source}' (glob) matched ${allMatches.length} file(s) under ${reportedBase}, ` +
|
|
395
|
+
`but all of them are never packaged into a skill bundle: ${droppedRel.join(', ')}. ` +
|
|
396
|
+
// NOT "widen the glob": the filter is on basename and applies at any width, so a
|
|
397
|
+
// wider glob clears this error while still shipping none of these files — advice
|
|
398
|
+
// that looks like it worked and silently does not.
|
|
399
|
+
`Declare an explicit source: entry for a file you intend to ship, ` +
|
|
400
|
+
`or point the glob at a directory that holds files which can be packaged.`);
|
|
258
401
|
}
|
|
259
402
|
const copied = [];
|
|
260
403
|
const pairs = [];
|
|
@@ -267,8 +410,6 @@ async function copyGlobEntry(entry, projectRoot, skillOutputDir, bundledFileSet)
|
|
|
267
410
|
// that the rebased dest stays under the skill output dir (write) — H1/H2
|
|
268
411
|
// defense-in-depth against a traversal that slipped past earlier guards.
|
|
269
412
|
const absSource = safePath.joinUnderRoot(absoluteBase, rel);
|
|
270
|
-
if (bundledFileSet.has(toForwardSlash(absSource)))
|
|
271
|
-
continue;
|
|
272
413
|
const relDest = normalizeRelPath(safePath.join(entry.dest, rel));
|
|
273
414
|
const absDest = safePath.joinUnderRoot(skillOutputDir, entry.dest, rel);
|
|
274
415
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- dest path from validated config
|
|
@@ -278,56 +419,182 @@ async function copyGlobEntry(entry, projectRoot, skillOutputDir, bundledFileSet)
|
|
|
278
419
|
pairs.push({ absSource, absDest });
|
|
279
420
|
rels.push(toForwardSlash(rel));
|
|
280
421
|
}
|
|
281
|
-
return { copied, pairs, rels };
|
|
422
|
+
return { copied, pairs, rels, dropped };
|
|
282
423
|
}
|
|
283
424
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
425
|
+
* What the project's GLOB `files:` entries would do, without copying anything.
|
|
426
|
+
*
|
|
427
|
+
* The pre-build half of {@link applyFilesConfig}: `vat skills validate` and
|
|
428
|
+
* `vat audit` can answer "what will this config fail to ship?" before a build
|
|
429
|
+
* exists, and they answer it by running THE expansion the copy runs
|
|
430
|
+
* ({@link expandGlobEntry}) — a second, independently written expansion would be
|
|
431
|
+
* free to disagree with the copy about what ships, which is the whole defect class
|
|
432
|
+
* this module exists to close.
|
|
433
|
+
*
|
|
434
|
+
* Non-throwing by construction, unlike {@link copyGlobEntry}: a pre-build gate runs
|
|
435
|
+
* before the artifact exists, so a glob over an unbuilt `dist/` matching nothing is
|
|
436
|
+
* the expected state, not a failure. `copyGlobEntry` still raises there, where the
|
|
437
|
+
* build HAS run and zero matches is real.
|
|
438
|
+
*
|
|
439
|
+
* That reasoning settles the SEVERITY of a failing expansion, not whether to
|
|
440
|
+
* mention it — and this used to return only `dropped`, so the gate reported the
|
|
441
|
+
* drop that is harmless by design (a glob is a net) and said nothing about either
|
|
442
|
+
* expansion outcome that is certain to fail the very next command. `unmatched`
|
|
443
|
+
* and `allRefused` end that silence, graded `info` and `warning` respectively (see
|
|
444
|
+
* FILES_GLOB_MATCHED_NOTHING / FILES_GLOB_MATCHED_ONLY_NEVER_PACKAGED).
|
|
445
|
+
*
|
|
446
|
+
* The three buckets partition the entries as two of `copyGlobEntry`'s three throws
|
|
447
|
+
* plus its success path do — same predicates, same order, one expansion. Its THIRD
|
|
448
|
+
* throw (`expandGlobEntry` refusing a `..` segment in the magic remainder) has no
|
|
449
|
+
* bucket, and the skip below explains why — but the consequence belongs here: this
|
|
450
|
+
* function's answer to "what will this config fail to ship?" is silent for that one
|
|
451
|
+
* malformed shape, which is the same class of silence `unmatched`/`allRefused` were
|
|
452
|
+
* added to end. Closing it needs a code of its own (a wrong-shaped PATTERN, not an
|
|
453
|
+
* unbuilt or unshippable artifact), or a `source` refine on `SkillFileEntrySchema`
|
|
454
|
+
* that rejects the pattern at config load and makes the skip unreachable.
|
|
455
|
+
*
|
|
456
|
+
* Drops re-shipped by an explicit entry are filtered out for the same reason
|
|
457
|
+
* {@link applyFilesConfig} filters them: reporting "it did not ship" about a file
|
|
458
|
+
* the documented escape hatch puts in the bundle tells an author their remediation
|
|
459
|
+
* failed when it worked. That filter is deliberately NOT applied to `allRefused`:
|
|
460
|
+
* `copyGlobEntry` throws on an entry whose own kept-set is empty no matter what
|
|
461
|
+
* any other entry ships, so filtering it would predict a green build that fails.
|
|
315
462
|
*/
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
463
|
+
export async function collectPreBuildGlobFindings(filesConfig, projectRoot) {
|
|
464
|
+
const shippedDests = new Set(explicitFilesConfigDests(filesConfig));
|
|
465
|
+
const dropped = [];
|
|
466
|
+
const allRefused = [];
|
|
467
|
+
const unmatched = [];
|
|
468
|
+
for (const entry of filesConfig) {
|
|
469
|
+
if (!isGlob(entry.source))
|
|
470
|
+
continue;
|
|
471
|
+
// A '..' segment in the magic remainder is a malformed pattern that
|
|
472
|
+
// `expandGlobEntry` refuses to expand. It is the BUILD's error to raise (and
|
|
473
|
+
// `copyGlobEntry` does); a pre-build gate asking "what would this do?" must not
|
|
474
|
+
// abort the whole run over it — and it must not put the entry in either failure
|
|
475
|
+
// bucket, which would name a cause (an unbuilt or unshippable artifact) that is
|
|
476
|
+
// not the one.
|
|
477
|
+
//
|
|
478
|
+
// KNOWN GAP, stated rather than implied by a `continue`: this skip is total, so
|
|
479
|
+
// the entry produces NO finding in any pre-build lane and the author first hears
|
|
480
|
+
// about it from a failed build. Not "misreported", but not reported either. The
|
|
481
|
+
// honest fix is a distinct code for a wrong-shaped pattern, or rejecting the
|
|
482
|
+
// pattern in `SkillFileEntrySchema.source` the way `dest` already is — at which
|
|
483
|
+
// point this line becomes defense-in-depth against a config that cannot load.
|
|
484
|
+
if (hasParentTraversalSegment(globMagicRemainder(entry.source)))
|
|
485
|
+
continue;
|
|
486
|
+
const expansion = await expandGlobEntry(entry, projectRoot);
|
|
487
|
+
const absBase = toForwardSlash(expansion.absoluteBase);
|
|
488
|
+
// The two predicates below are `copyGlobEntry`'s two throws, in its order and
|
|
489
|
+
// keyed on its fields: zero total matches first, then zero SURVIVING matches.
|
|
490
|
+
// Keeping them distinct is what stops a wholly-refused glob from being
|
|
491
|
+
// reported as an unbuilt one — the build says "has your build run?" only in
|
|
492
|
+
// the first case, and sending an author to re-run a build that already ran is
|
|
493
|
+
// exactly the misdirection the second error was written to avoid.
|
|
494
|
+
if (expansion.allMatches.length === 0) {
|
|
495
|
+
unmatched.push({ source: entry.source, absBase });
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (expansion.kept.length === 0) {
|
|
499
|
+
// Supersedes this entry's per-file drops rather than adding to them: the
|
|
500
|
+
// build raises ONE error listing every refused file, and reporting the same
|
|
501
|
+
// cause again at file granularity would double-count a single defect. The
|
|
502
|
+
// file names are preserved on `absRefused`.
|
|
503
|
+
allRefused.push({
|
|
504
|
+
source: entry.source,
|
|
505
|
+
absBase,
|
|
506
|
+
absRefused: expansion.dropped.map((d) => d.absFile),
|
|
507
|
+
});
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
dropped.push(...expansion.dropped.filter((d) => !shippedDests.has(normalizeRelPath(d.dest))));
|
|
325
511
|
}
|
|
326
|
-
return
|
|
512
|
+
return { dropped, allRefused, unmatched };
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Turn {@link collectPreBuildGlobFindings} into reportable pre-build issues.
|
|
516
|
+
*
|
|
517
|
+
* THE emitter for the pre-build gates, so a lane cannot pick up one half of the
|
|
518
|
+
* findings and quietly drop the other — which is how the zero-match went
|
|
519
|
+
* unreported for as long as it did.
|
|
520
|
+
*
|
|
521
|
+
* @param locationRoot The ONE base this run anchors locations at (see
|
|
522
|
+
* {@link droppedGlobMatchesToIssues}).
|
|
523
|
+
*/
|
|
524
|
+
export function preBuildGlobFindingsToIssues(findings, locationRoot) {
|
|
525
|
+
// Both entry-level findings anchor at the glob's static base: they are about an
|
|
526
|
+
// ENTRY, not a file, and the base is the only path they have.
|
|
527
|
+
const anchorBase = (absBase) => anchoredPath(absBase, locationRoot);
|
|
528
|
+
return [
|
|
529
|
+
...droppedGlobMatchesToIssues(findings.dropped, locationRoot),
|
|
530
|
+
...findings.allRefused.map((entry) => {
|
|
531
|
+
const base = anchorBase(entry.absBase);
|
|
532
|
+
// Every refused file, in the SAME source coordinates the drop findings use —
|
|
533
|
+
// this issue is the only surviving report of those names.
|
|
534
|
+
const refused = entry.absRefused.map((f) => issueLocation(f, locationRoot)).join(', ');
|
|
535
|
+
return materializeIssue('FILES_GLOB_MATCHED_ONLY_NEVER_PACKAGED', {
|
|
536
|
+
location: base,
|
|
537
|
+
detail: `files: source '${entry.source}' matched ${entry.absRefused.length} file(s) under ${base}, ` +
|
|
538
|
+
`all never packaged: ${refused}`,
|
|
539
|
+
});
|
|
540
|
+
}),
|
|
541
|
+
...findings.unmatched.map((entry) => {
|
|
542
|
+
const base = anchorBase(entry.absBase);
|
|
543
|
+
return materializeIssue('FILES_GLOB_MATCHED_NOTHING', {
|
|
544
|
+
location: base,
|
|
545
|
+
detail: `files: source '${entry.source}' expands under ${base}`,
|
|
546
|
+
});
|
|
547
|
+
}),
|
|
548
|
+
];
|
|
327
549
|
}
|
|
328
|
-
/** Copy
|
|
550
|
+
/** Copy one GLOB `files:` entry. */
|
|
551
|
+
async function applyGlobFileEntry(fileEntry, opts) {
|
|
552
|
+
const { copied, pairs, rels, dropped } = await copyGlobEntry(fileEntry, opts.projectRoot, opts.skillOutputDir);
|
|
553
|
+
return {
|
|
554
|
+
dests: copied,
|
|
555
|
+
dropped,
|
|
556
|
+
pending: fileEntry.integrity === true ? { entry: fileEntry, pairs, rels } : undefined,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Reject a NON-GLOB `dest` that names a directory rather than a file.
|
|
561
|
+
*
|
|
562
|
+
* A non-glob entry is a typed single-file slot: one source, one output path. A
|
|
563
|
+
* `dest` ending in a separator (or spelling the dest root itself) names a
|
|
564
|
+
* directory, and nothing downstream noticed — `copyFile` wrote the source's bytes
|
|
565
|
+
* to a file NAMED `guides`, and the config then accounted for `guides/` while the
|
|
566
|
+
* bundle held `guides`, so the two never compared equal.
|
|
567
|
+
*
|
|
568
|
+
* Worth its own error rather than a silent normalization because of WHAT can ride
|
|
569
|
+
* in: an explicit entry is the one thing that sanctions shipping an
|
|
570
|
+
* agent-instruction file, and at a directory-shaped dest that file arrives under a
|
|
571
|
+
* basename no never-package rule and no presence detector recognizes. Renaming
|
|
572
|
+
* `CLAUDE.md` to `guides` on the author's behalf is not a service.
|
|
573
|
+
*
|
|
574
|
+
* GLOB entries are deliberately exempt: their `dest` IS a subtree root, and every
|
|
575
|
+
* match is joined onto it (`safePath.join('packs/', 'a/x.json')`), which drops the
|
|
576
|
+
* trailing separator on its own.
|
|
577
|
+
*/
|
|
578
|
+
function assertFileShapedDest(entry) {
|
|
579
|
+
const normalized = normalizeRelPath(entry.dest);
|
|
580
|
+
if (normalized !== '' && normalized !== '.' && !normalized.endsWith('/'))
|
|
581
|
+
return;
|
|
582
|
+
let base = normalized;
|
|
583
|
+
while (base.endsWith('/'))
|
|
584
|
+
base = base.slice(0, -1);
|
|
585
|
+
const suggested = `${base}/${entry.source.slice(entry.source.lastIndexOf('/') + 1)}`;
|
|
586
|
+
throw new Error(`files: dest '${entry.dest}' names a directory, but a non-glob entry copies ONE file to ONE path. ` +
|
|
587
|
+
`Name the output file itself (e.g. dest: '${normalizeRelPath(suggested)}'), ` +
|
|
588
|
+
`or use a glob source to copy a whole subtree.`);
|
|
589
|
+
}
|
|
590
|
+
/** Copy one NON-GLOB `files:` entry. */
|
|
329
591
|
async function applyNonGlobFileEntry(fileEntry, opts, bundledFileSet) {
|
|
592
|
+
// Before the bundled-skip branch below, never inside it: which branch an entry
|
|
593
|
+
// takes depends on whether link traversal happened to reach the source first,
|
|
594
|
+
// and a malformed dest must not be validated by an ordering accident.
|
|
595
|
+
assertFileShapedDest(fileEntry);
|
|
330
596
|
const absoluteSource = safePath.resolve(safePath.join(opts.projectRoot, fileEntry.source));
|
|
597
|
+
const optedIn = fileEntry.integrity === true;
|
|
331
598
|
if (bundledFileSet.has(toForwardSlash(absoluteSource))) {
|
|
332
599
|
// Already materialized by link traversal (copied to entry.dest via the path
|
|
333
600
|
// map) — don't copy it a second time. But a requested `integrity` check must
|
|
@@ -335,31 +602,86 @@ async function applyNonGlobFileEntry(fileEntry, opts, bundledFileSet) {
|
|
|
335
602
|
// dest is byte-identical to the source, exactly as the copy path below would.
|
|
336
603
|
// (The bundled copy lands at entry.dest — see applyNonGlobEntriesToPathMap in
|
|
337
604
|
// skill-packager.ts.)
|
|
338
|
-
|
|
339
|
-
const absDest = safePath.joinUnderRoot(opts.skillOutputDir, fileEntry.dest);
|
|
340
|
-
verifyFilesIntegrity([{ absSource: absoluteSource, absDest }]);
|
|
341
|
-
}
|
|
605
|
+
const absDest = safePath.joinUnderRoot(opts.skillOutputDir, fileEntry.dest);
|
|
342
606
|
// The dest is still reported: this function answers "which output paths does
|
|
343
607
|
// `files:` account for," not "which bytes did I move." Whether link traversal
|
|
344
608
|
// or this copy put the file there is an ordering accident, and callers that
|
|
345
609
|
// ask "is this packaged file declared?" must not get a different answer for it.
|
|
346
|
-
return
|
|
610
|
+
return {
|
|
611
|
+
dests: [normalizeRelPath(fileEntry.dest)],
|
|
612
|
+
dropped: [],
|
|
613
|
+
pending: optedIn
|
|
614
|
+
? { entry: fileEntry, pairs: [{ absSource: absoluteSource, absDest }], rels: undefined }
|
|
615
|
+
: undefined,
|
|
616
|
+
};
|
|
347
617
|
}
|
|
348
|
-
const { relDest, absSource, absDest } = await copyNonGlobEntry(fileEntry, absoluteSource, opts.skillOutputDir);
|
|
349
|
-
|
|
350
|
-
|
|
618
|
+
const { relDest, absSource, absDest } = await copyNonGlobEntry(fileEntry, absoluteSource, opts.projectRoot, opts.skillOutputDir);
|
|
619
|
+
return {
|
|
620
|
+
dests: [relDest],
|
|
621
|
+
dropped: [],
|
|
622
|
+
pending: optedIn ? { entry: fileEntry, pairs: [{ absSource, absDest }], rels: undefined } : undefined,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* `dest` expressed relative to the `destRoot` subtree, or `null` when it does not
|
|
627
|
+
* live inside it. Both arguments are skill-output-relative and normalized.
|
|
628
|
+
*
|
|
629
|
+
* Equality returns `null` on purpose: a dest EQUAL to the subtree root names the
|
|
630
|
+
* directory itself, not a file inside it, so it contributes nothing to a file set.
|
|
631
|
+
*/
|
|
632
|
+
function relUnderDest(destRoot, dest) {
|
|
633
|
+
if (destRoot === '' || destRoot === '.')
|
|
634
|
+
return dest;
|
|
635
|
+
return dest.startsWith(destRoot + '/') ? dest.slice(destRoot.length + 1) : null;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Run every opted-in integrity check once the whole config has copied.
|
|
639
|
+
*
|
|
640
|
+
* `allDests` is the config's complete accounting, which is what makes a glob's
|
|
641
|
+
* dest-set check order-independent: a sibling entry's dest inside the same subtree
|
|
642
|
+
* is EXPECTED (that is the escape hatch working), while a file no entry declares
|
|
643
|
+
* is still unexpected — so this stays a real assertion about the shipped bundle
|
|
644
|
+
* rather than being weakened into a permissive one.
|
|
645
|
+
*/
|
|
646
|
+
async function runDeferredIntegrity(pending, allDests, skillOutputDir) {
|
|
647
|
+
for (const { entry, pairs, rels } of pending) {
|
|
648
|
+
verifyFilesIntegrity(pairs);
|
|
649
|
+
if (rels === undefined)
|
|
650
|
+
continue;
|
|
651
|
+
const destRoot = normalizeRelPath(entry.dest);
|
|
652
|
+
const expected = new Set(rels);
|
|
653
|
+
for (const dest of allDests) {
|
|
654
|
+
const rel = relUnderDest(destRoot, dest);
|
|
655
|
+
if (rel !== null && rel !== '')
|
|
656
|
+
expected.add(rel);
|
|
657
|
+
}
|
|
658
|
+
await verifyDestSet(safePath.joinUnderRoot(skillOutputDir, entry.dest), [...expected], entry.source);
|
|
351
659
|
}
|
|
352
|
-
return [relDest];
|
|
353
660
|
}
|
|
354
661
|
export async function applyFilesConfig(opts) {
|
|
355
662
|
const bundledFileSet = new Set((opts.bundledFiles ?? []).map((f) => toForwardSlash(f)));
|
|
356
|
-
const
|
|
663
|
+
const dests = [];
|
|
664
|
+
const dropped = [];
|
|
665
|
+
const pending = [];
|
|
357
666
|
for (const fileEntry of opts.filesConfig) {
|
|
358
|
-
const
|
|
359
|
-
? await applyGlobFileEntry(fileEntry, opts
|
|
667
|
+
const outcome = isGlob(fileEntry.source)
|
|
668
|
+
? await applyGlobFileEntry(fileEntry, opts)
|
|
360
669
|
: await applyNonGlobFileEntry(fileEntry, opts, bundledFileSet);
|
|
361
|
-
|
|
670
|
+
dests.push(...outcome.dests);
|
|
671
|
+
dropped.push(...outcome.dropped);
|
|
672
|
+
if (outcome.pending)
|
|
673
|
+
pending.push(outcome.pending);
|
|
362
674
|
}
|
|
363
|
-
|
|
675
|
+
await runDeferredIntegrity(pending, dests, opts.skillOutputDir);
|
|
676
|
+
// A drop is only worth reporting if the file genuinely did not ship. In the
|
|
677
|
+
// escape-hatch config the guide prescribes — a glob over a subtree plus an
|
|
678
|
+
// explicit entry naming one never-packaged file inside it — the glob drops
|
|
679
|
+
// that dest and the explicit entry writes it. Reporting the drop there would
|
|
680
|
+
// assert "it did not ship" about a file sitting in the bundle, telling an
|
|
681
|
+
// author their documented remediation had failed. Filtering on the FINAL dest
|
|
682
|
+
// set (not on entry order) keeps the finding true whichever order they wrote.
|
|
683
|
+
const shippedDests = new Set(dests.map((d) => normalizeRelPath(d)));
|
|
684
|
+
const reportableDropped = dropped.filter((d) => !shippedDests.has(normalizeRelPath(d.dest)));
|
|
685
|
+
return { dests, dropped: reportableDropped };
|
|
364
686
|
}
|
|
365
687
|
//# sourceMappingURL=files-config.js.map
|