@vibe-agent-toolkit/agent-skills 0.2.0-rc.6 → 0.2.0-rc.8
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 +5 -5
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +7 -17
- package/dist/builder.js.map +1 -1
- package/dist/content-type-routing.d.ts +32 -0
- package/dist/content-type-routing.d.ts.map +1 -1
- package/dist/content-type-routing.js +23 -0
- package/dist/content-type-routing.js.map +1 -1
- package/dist/files-config.d.ts +2 -2
- package/dist/files-config.d.ts.map +1 -1
- package/dist/files-config.js +1 -7
- package/dist/files-config.js.map +1 -1
- package/dist/import.d.ts.map +1 -1
- package/dist/import.js +0 -4
- package/dist/import.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/inventory/types.d.ts +8 -0
- package/dist/inventory/types.d.ts.map +1 -1
- package/dist/inventory/types.js.map +1 -1
- package/dist/parsers/frontmatter-parser.d.ts.map +1 -1
- package/dist/parsers/frontmatter-parser.js +30 -6
- package/dist/parsers/frontmatter-parser.js.map +1 -1
- package/dist/plugin-distribution-layout.d.ts.map +1 -1
- package/dist/plugin-distribution-layout.js +13 -2
- package/dist/plugin-distribution-layout.js.map +1 -1
- package/dist/post-build-checks.d.ts +9 -1
- package/dist/post-build-checks.d.ts.map +1 -1
- package/dist/post-build-checks.js +21 -13
- package/dist/post-build-checks.js.map +1 -1
- package/dist/schemas/allowed-tools.d.ts +31 -0
- package/dist/schemas/allowed-tools.d.ts.map +1 -0
- package/dist/schemas/allowed-tools.js +88 -0
- package/dist/schemas/allowed-tools.js.map +1 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/installed-plugins-registry.d.ts.map +1 -1
- package/dist/schemas/installed-plugins-registry.js +7 -1
- package/dist/schemas/installed-plugins-registry.js.map +1 -1
- package/dist/schemas/marketplace-manifest.d.ts.map +1 -1
- package/dist/schemas/marketplace-manifest.js +17 -6
- package/dist/schemas/marketplace-manifest.js.map +1 -1
- package/dist/skill-identity.d.ts +8 -3
- package/dist/skill-identity.d.ts.map +1 -1
- package/dist/skill-identity.js +13 -6
- package/dist/skill-identity.js.map +1 -1
- package/dist/skill-packager.d.ts +87 -14
- package/dist/skill-packager.d.ts.map +1 -1
- package/dist/skill-packager.js +200 -72
- package/dist/skill-packager.js.map +1 -1
- package/dist/skill-source/content-hash.d.ts.map +1 -1
- package/dist/skill-source/content-hash.js +21 -10
- package/dist/skill-source/content-hash.js.map +1 -1
- package/dist/skill-source/fetch-cache.d.ts.map +1 -1
- package/dist/skill-source/fetch-cache.js +0 -5
- package/dist/skill-source/fetch-cache.js.map +1 -1
- package/dist/skill-source/git-clone.d.ts.map +1 -1
- package/dist/skill-source/git-clone.js +0 -2
- package/dist/skill-source/git-clone.js.map +1 -1
- package/dist/skill-source/sources/url-source.d.ts.map +1 -1
- package/dist/skill-source/sources/url-source.js +0 -1
- package/dist/skill-source/sources/url-source.js.map +1 -1
- package/dist/skill-source/stage.d.ts.map +1 -1
- package/dist/skill-source/stage.js +0 -6
- package/dist/skill-source/stage.js.map +1 -1
- package/dist/skill-test/baseline-integrity.d.ts.map +1 -1
- package/dist/skill-test/baseline-integrity.js +8 -2
- package/dist/skill-test/baseline-integrity.js.map +1 -1
- package/dist/skill-test/build-hook.d.ts +3 -2
- package/dist/skill-test/build-hook.d.ts.map +1 -1
- package/dist/skill-test/build-hook.js +4 -4
- package/dist/skill-test/build-hook.js.map +1 -1
- package/dist/skill-test/declared-env.d.ts +5 -4
- package/dist/skill-test/declared-env.d.ts.map +1 -1
- package/dist/skill-test/declared-env.js +7 -9
- package/dist/skill-test/declared-env.js.map +1 -1
- package/dist/skill-test/eval-executor.d.ts +1 -1
- package/dist/skill-test/eval-executor.js +2 -2
- package/dist/skill-test/eval-executor.js.map +1 -1
- package/dist/skill-test/eval-fragment.d.ts +12 -8
- package/dist/skill-test/eval-fragment.d.ts.map +1 -1
- package/dist/skill-test/eval-fragment.js +18 -10
- package/dist/skill-test/eval-fragment.js.map +1 -1
- package/dist/skill-test/eval-grader.d.ts +64 -6
- package/dist/skill-test/eval-grader.d.ts.map +1 -1
- package/dist/skill-test/eval-grader.js +139 -77
- package/dist/skill-test/eval-grader.js.map +1 -1
- package/dist/skill-test/eval-inputs.d.ts +8 -7
- package/dist/skill-test/eval-inputs.d.ts.map +1 -1
- package/dist/skill-test/eval-inputs.js +9 -11
- package/dist/skill-test/eval-inputs.js.map +1 -1
- package/dist/skill-test/eval-lint.d.ts +4 -4
- package/dist/skill-test/eval-lint.js +5 -5
- package/dist/skill-test/eval-lint.js.map +1 -1
- package/dist/skill-test/eval-suite-isolation.d.ts.map +1 -1
- package/dist/skill-test/eval-suite-isolation.js +8 -5
- package/dist/skill-test/eval-suite-isolation.js.map +1 -1
- package/dist/skill-test/evals-template.d.ts +1 -1
- package/dist/skill-test/evals-template.d.ts.map +1 -1
- package/dist/skill-test/evals-template.js +1 -3
- package/dist/skill-test/evals-template.js.map +1 -1
- package/dist/skill-test/executor-prompt.d.ts +2 -2
- package/dist/skill-test/executor-prompt.js +2 -2
- package/dist/skill-test/failure-reason.d.ts +113 -0
- package/dist/skill-test/failure-reason.d.ts.map +1 -0
- package/dist/skill-test/failure-reason.js +124 -0
- package/dist/skill-test/failure-reason.js.map +1 -0
- package/dist/skill-test/fragment-merge.d.ts +2 -2
- package/dist/skill-test/fragment-merge.js +4 -4
- package/dist/skill-test/grader-prompt.d.ts +2 -2
- package/dist/skill-test/grader-prompt.d.ts.map +1 -1
- package/dist/skill-test/grader-prompt.js +12 -8
- package/dist/skill-test/grader-prompt.js.map +1 -1
- package/dist/skill-test/grading-adapter.d.ts +7 -3
- package/dist/skill-test/grading-adapter.d.ts.map +1 -1
- package/dist/skill-test/grading-adapter.js +10 -9
- package/dist/skill-test/grading-adapter.js.map +1 -1
- package/dist/skill-test/harness-location.d.ts +3 -2
- package/dist/skill-test/harness-location.d.ts.map +1 -1
- package/dist/skill-test/harness-location.js +5 -16
- package/dist/skill-test/harness-location.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/lock.d.ts +11 -10
- package/dist/skill-test/lock.d.ts.map +1 -1
- package/dist/skill-test/lock.js +13 -14
- package/dist/skill-test/lock.js.map +1 -1
- package/dist/skill-test/pipeline.d.ts +3 -1
- package/dist/skill-test/pipeline.d.ts.map +1 -1
- package/dist/skill-test/pipeline.js +5 -1
- package/dist/skill-test/pipeline.js.map +1 -1
- package/dist/skill-test/preflight.d.ts.map +1 -1
- package/dist/skill-test/preflight.js +0 -1
- package/dist/skill-test/preflight.js.map +1 -1
- package/dist/skill-test/prompt-invariants.d.ts +4 -2
- package/dist/skill-test/prompt-invariants.d.ts.map +1 -1
- package/dist/skill-test/prompt-invariants.js +5 -4
- package/dist/skill-test/prompt-invariants.js.map +1 -1
- package/dist/skill-test/run-harness.d.ts +160 -123
- package/dist/skill-test/run-harness.d.ts.map +1 -1
- package/dist/skill-test/run-harness.js +309 -173
- package/dist/skill-test/run-harness.js.map +1 -1
- package/dist/skill-test/staging.d.ts +12 -6
- package/dist/skill-test/staging.d.ts.map +1 -1
- package/dist/skill-test/staging.js +15 -12
- package/dist/skill-test/staging.js.map +1 -1
- package/dist/skill-test/tier-plan.d.ts +1 -1
- package/dist/skill-test/tier-plan.js +1 -1
- package/dist/skill-test/tool-eval-schema.js +2 -2
- package/dist/skill-test/vendor-manifest.d.ts +2 -1
- package/dist/skill-test/vendor-manifest.d.ts.map +1 -1
- package/dist/skill-test/vendor-manifest.js +10 -10
- package/dist/skill-test/vendor-manifest.js.map +1 -1
- package/dist/test-input.d.ts +2 -2
- package/dist/test-input.d.ts.map +1 -1
- package/dist/test-input.js +4 -5
- package/dist/test-input.js.map +1 -1
- package/dist/validators/agent-instruction-presence.d.ts.map +1 -1
- package/dist/validators/agent-instruction-presence.js +44 -2
- package/dist/validators/agent-instruction-presence.js.map +1 -1
- package/dist/validators/bundled-resource-link-detection.d.ts +5 -3
- package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
- package/dist/validators/bundled-resource-link-detection.js +38 -38
- package/dist/validators/bundled-resource-link-detection.js.map +1 -1
- package/dist/validators/compat-detectors.d.ts +17 -0
- package/dist/validators/compat-detectors.d.ts.map +1 -1
- package/dist/validators/compat-detectors.js +46 -19
- package/dist/validators/compat-detectors.js.map +1 -1
- package/dist/validators/format-detection.d.ts +3 -0
- package/dist/validators/format-detection.d.ts.map +1 -1
- package/dist/validators/format-detection.js +41 -26
- package/dist/validators/format-detection.js.map +1 -1
- package/dist/validators/frontmatter-validation.js +1 -1
- package/dist/validators/frontmatter-validation.js.map +1 -1
- package/dist/validators/marketplace-validator.d.ts.map +1 -1
- package/dist/validators/marketplace-validator.js +6 -1
- package/dist/validators/marketplace-validator.js.map +1 -1
- package/dist/validators/mcp-tool-qualification.d.ts +1 -1
- package/dist/validators/mcp-tool-qualification.js +1 -1
- package/dist/validators/packaged-size-limit.d.ts.map +1 -1
- package/dist/validators/packaged-size-limit.js +0 -2
- package/dist/validators/packaged-size-limit.js.map +1 -1
- package/dist/validators/packaging-validator.d.ts +33 -4
- package/dist/validators/packaging-validator.d.ts.map +1 -1
- package/dist/validators/packaging-validator.js +95 -22
- package/dist/validators/packaging-validator.js.map +1 -1
- package/dist/validators/plugin-hosted-shape.d.ts +3 -1
- package/dist/validators/plugin-hosted-shape.d.ts.map +1 -1
- package/dist/validators/plugin-hosted-shape.js +5 -6
- package/dist/validators/plugin-hosted-shape.js.map +1 -1
- package/dist/validators/referenced-path-missing.d.ts +15 -6
- package/dist/validators/referenced-path-missing.d.ts.map +1 -1
- package/dist/validators/referenced-path-missing.js +42 -22
- package/dist/validators/referenced-path-missing.js.map +1 -1
- package/dist/validators/registry-validator.d.ts.map +1 -1
- package/dist/validators/registry-validator.js +0 -1
- package/dist/validators/registry-validator.js.map +1 -1
- package/dist/validators/rule-engine/index.d.ts +1 -1
- package/dist/validators/rule-engine/rule-context.d.ts +3 -3
- package/dist/validators/rule-engine/rule-context.js +1 -1
- package/dist/validators/rule-engine/rule-engine.d.ts +1 -1
- package/dist/validators/rule-engine/rule-engine.js +5 -5
- 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 +41 -51
- package/dist/validators/skill-validator.js.map +1 -1
- package/dist/validators/source-only-codes.js +1 -1
- package/dist/validators/source-only-codes.js.map +1 -1
- package/dist/validators/types.d.ts +21 -1
- package/dist/validators/types.d.ts.map +1 -1
- package/dist/validators/unified-validator.d.ts.map +1 -1
- package/dist/validators/unified-validator.js +3 -4
- package/dist/validators/unified-validator.js.map +1 -1
- package/dist/validators/validation-rules.d.ts +3 -3
- package/dist/validators/validation-rules.d.ts.map +1 -1
- package/dist/validators/validation-rules.js +9 -38
- package/dist/validators/validation-rules.js.map +1 -1
- package/dist/validators/walker-to-issues.d.ts +2 -0
- package/dist/validators/walker-to-issues.d.ts.map +1 -1
- package/dist/validators/walker-to-issues.js +17 -12
- package/dist/validators/walker-to-issues.js.map +1 -1
- package/dist/walk-link-graph.d.ts +2 -0
- package/dist/walk-link-graph.d.ts.map +1 -1
- package/dist/walk-link-graph.js +46 -15
- package/dist/walk-link-graph.js.map +1 -1
- package/package.json +12 -20
- package/dist/skill-test/exit-codes.d.ts +0 -134
- package/dist/skill-test/exit-codes.d.ts.map +0 -1
- package/dist/skill-test/exit-codes.js +0 -224
- package/dist/skill-test/exit-codes.js.map +0 -1
package/dist/skill-packager.js
CHANGED
|
@@ -19,8 +19,9 @@ import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
|
19
19
|
import { basename, dirname } from 'node:path';
|
|
20
20
|
import { DeferredArtifacts, DuplicateResourceIdError, ResourceRegistry, loadConfig, openFrontmatter, resolveLocalHref, rewriteFrontmatterUriReferencesFromSchema, rewriteHtmlLinks, transformContent, parseFileCached, } from '@vibe-agent-toolkit/resources';
|
|
21
21
|
import { allowUnusedIssues, createAllowUsageLedger, runValidationFramework, } from '@vibe-agent-toolkit/schema';
|
|
22
|
-
import { findProjectRoot, isGlob, issueLocation, resolveAssetReference, toForwardSlash,
|
|
23
|
-
import {
|
|
22
|
+
import { direntKindFollowingSync, findProjectRoot, isGlob, isSingleFsSegment, issueLocation, resolveAssetReference, safePath, toForwardSlash, VatError, } from '@vibe-agent-toolkit/utils';
|
|
23
|
+
import { readTextContent } from '@vibe-agent-toolkit/utils/fs';
|
|
24
|
+
import { getResourceSubdirForFile } from './content-type-routing.js';
|
|
24
25
|
import { applyFilesConfig, buildArtifactHint, droppedGlobMatchesToIssues, explicitFilesConfigDests, globEntryDest, skippedGlobMatchesToIssues, } from './files-config.js';
|
|
25
26
|
import { READ_REMEDY, withFsAttribution } from './fs-attribution.js';
|
|
26
27
|
import { checkBrokenPackagedLinks, checkMissingReferencedPaths, checkUnreferencedFiles } from './post-build-checks.js';
|
|
@@ -29,7 +30,7 @@ import { detectPackagedAgentInstructionFiles } from './validators/agent-instruct
|
|
|
29
30
|
import { checkPackagedSizeLimit } from './validators/packaged-size-limit.js';
|
|
30
31
|
import { validateSkillForPackaging } from './validators/packaging-validator.js';
|
|
31
32
|
import { materializeIssue } from './validators/rule-engine/index.js';
|
|
32
|
-
import { deferredAssetsToIssues, walkerExclusionsToIssues } from './validators/walker-to-issues.js';
|
|
33
|
+
import { deferredAssetsToIssues, outsideSkillDirLinksToIssues, walkerExclusionsToIssues } from './validators/walker-to-issues.js';
|
|
33
34
|
import { walkLinkGraph } from './walk-link-graph.js';
|
|
34
35
|
const PACKAGE_JSON_FILENAME = 'package.json';
|
|
35
36
|
/** `withFsAttribution` action for a write in `copyAndRewriteFile` — never a copy, since the bytes are rewritten first. */
|
|
@@ -41,18 +42,20 @@ const WRITE_ACTION = 'written into the bundle';
|
|
|
41
42
|
* `{{link.rawText}}`, not `{{link.text}}`, for the same two reasons the rewrite
|
|
42
43
|
* branch uses it (see `bundledLinkTemplate`):
|
|
43
44
|
*
|
|
44
|
-
* - **
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* one phrase for an unrelated one in shipped prose; the rewrite branch never
|
|
49
|
-
* exposed it because it re-emits `rawText`.
|
|
45
|
+
* - **Per-occurrence identity.** `link.text` is a property of the parsed link,
|
|
46
|
+
* and two occurrences sharing an href are two links whose text may differ.
|
|
47
|
+
* `rawText` is always the text of the occurrence actually being replaced, so
|
|
48
|
+
* it cannot swap one phrase for an unrelated one in shipped prose.
|
|
50
49
|
* - **Formatting.** `rawText` keeps the inline markup the author wrote, so
|
|
51
50
|
* ``[`foo.yaml`](…)`` strips to ``` `foo.yaml` ``` rather than bare `foo.yaml`.
|
|
52
51
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* > This used to explain the first reason as a defence against `transformContent`
|
|
53
|
+
* > keying parsed links by href and letting the FIRST occurrence win. That
|
|
54
|
+
* > correlation is gone: `transformContent` now splices each parsed link at its
|
|
55
|
+
* > own `[startOffset, endOffset)` span, so there is no shared-href map to collide
|
|
56
|
+
* > in for any link the parser located. `rawText` remains correct and remains the
|
|
57
|
+
* > right choice — the reasoning above no longer depends on a collision that the
|
|
58
|
+
* > rewriter can still have.
|
|
56
59
|
*/
|
|
57
60
|
const DEFAULT_STRIP_TEMPLATE = '{{link.rawText}}';
|
|
58
61
|
/**
|
|
@@ -292,7 +295,7 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
292
295
|
const registry = options.registry ?? await createProjectRegistry(projectRoot);
|
|
293
296
|
// 3b. Load per-collection frontmatter schemas (Gap 3: packager rewrites frontmatter URI-refs
|
|
294
297
|
// against the same schemas the validator uses, with body parity).
|
|
295
|
-
const collectionSchemas = await loadCollectionSchemas(registry.config, projectRoot);
|
|
298
|
+
const { schemas: collectionSchemas, issues: collectionSchemaIssues } = await loadCollectionSchemas(registry.config, projectRoot);
|
|
296
299
|
// 4. Walk the link graph using registry data
|
|
297
300
|
const linkFollowDepth = options.linkFollowDepth ?? 2;
|
|
298
301
|
const excludeConfig = options.excludeReferencesFromBundle;
|
|
@@ -322,7 +325,7 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
322
325
|
if (options.gitTracker !== undefined) {
|
|
323
326
|
packagerWalkOptions.gitTracker = options.gitTracker;
|
|
324
327
|
}
|
|
325
|
-
const { bundledResources, bundledAssets, excludedReferences, deferredAssets } = walkLinkGraph(skillResourceId, registry, packagerWalkOptions);
|
|
328
|
+
const { bundledResources, bundledAssets, excludedReferences, outsideSkillDirLinks, deferredAssets } = walkLinkGraph(skillResourceId, registry, packagerWalkOptions);
|
|
326
329
|
// Register non-markdown bundled assets in the source registry so link rewriting
|
|
327
330
|
// can resolve them (resolvedId must be set on links pointing to YAML, JSON, etc.).
|
|
328
331
|
// For any asset whose id is already taken (a path-slug clash — `a-b/c.html`
|
|
@@ -344,7 +347,6 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
344
347
|
// 7. Clean stale output (skip when source SKILL.md lives inside the output, e.g. builder flow)
|
|
345
348
|
const resolvedOutput = safePath.resolve(outputPath);
|
|
346
349
|
const sourceInOutput = safePath.resolve(skillPath).startsWith(resolvedOutput + '/');
|
|
347
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- outputPath is validated
|
|
348
350
|
if (!sourceInOutput && existsSync(resolvedOutput)) {
|
|
349
351
|
await rm(resolvedOutput, { recursive: true });
|
|
350
352
|
}
|
|
@@ -407,9 +409,7 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
407
409
|
// The build's own output directory. An unwritable `dist/` or a full disk failed
|
|
408
410
|
// here with a bare errno naming an output path the author never typed — the
|
|
409
411
|
// same shape as the copiers, at the step before any of them run.
|
|
410
|
-
await withFsAttribution(`skill '${skillMetadata.name}' output directory ${issueLocation(outputPath, projectRoot) || '.'}`,
|
|
411
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- outputPath is validated
|
|
412
|
-
() => mkdir(outputPath, { recursive: true }), 'created');
|
|
412
|
+
await withFsAttribution(`skill '${skillMetadata.name}' output directory ${issueLocation(outputPath, projectRoot) || '.'}`, () => mkdir(outputPath, { recursive: true }), 'created');
|
|
413
413
|
await copyAndRewriteFiles(skillPath, bundledFiles, {
|
|
414
414
|
pathMap,
|
|
415
415
|
rewriteLinks,
|
|
@@ -471,7 +471,7 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
471
471
|
// directory, not the plugin the skill will be installed into, so there is no
|
|
472
472
|
// wider root to give it — the sibling-search parameter that measured 1.9%
|
|
473
473
|
// instead of 3.8% was deleted for having no caller. The shipped rate is 3.8%.
|
|
474
|
-
...await checkMissingReferencedPaths(outputPath),
|
|
474
|
+
...await checkMissingReferencedPaths(outputPath, target),
|
|
475
475
|
// The only byte measurement in the pipeline. Built phase for the same reason
|
|
476
476
|
// as its neighbour above: a `files:` entry materializes files here that the
|
|
477
477
|
// source tree does not have, so the bytes that ship are only knowable now.
|
|
@@ -495,8 +495,8 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
495
495
|
// path a reader can open is its source path.
|
|
496
496
|
...droppedGlobMatchesToIssues(appliedFiles.dropped, projectRoot),
|
|
497
497
|
// The same receipt, for matches that are not copyable files at all. Anchored
|
|
498
|
-
// identically and for the identical reason.
|
|
499
|
-
//
|
|
498
|
+
// identically and for the identical reason. Without a channel of its own this
|
|
499
|
+
// population is invisible: the build used to die on the first one it met.
|
|
500
500
|
...skippedGlobMatchesToIssues(appliedFiles.skipped, projectRoot),
|
|
501
501
|
// Presence-side backstop for agent-instruction files. The walker excludes
|
|
502
502
|
// them from link-following, but a `files:` glob can still copy one in, and
|
|
@@ -511,6 +511,9 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
511
511
|
// A receipt for each `files:` entry that was dropped for pointing into declared
|
|
512
512
|
// test input — the build already produced the right artifact; this just says so.
|
|
513
513
|
...testInputFileEntryIssues(droppedTestInputFiles),
|
|
514
|
+
// A collection schema that could not be loaded, so its frontmatter was not
|
|
515
|
+
// rewritten (see loadCollectionSchemas).
|
|
516
|
+
...collectionSchemaIssues,
|
|
514
517
|
// Backstop: if declared test input reached the output despite both exclusions,
|
|
515
518
|
// say so rather than shipping an answer key silently.
|
|
516
519
|
...checkPackagedTestInput({ pathMap, outputPath, testInputDirs }),
|
|
@@ -522,6 +525,7 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
522
525
|
// (a pattern match is author-declared intent; this exclusion is VAT's).
|
|
523
526
|
...testInputLinkIssues(excludedReferences, testInputDirs, projectRoot),
|
|
524
527
|
...deferredAssetsToIssues(deferredAssets, projectRoot),
|
|
528
|
+
...outsideSkillDirLinksToIssues(outsideSkillDirLinks, projectRoot),
|
|
525
529
|
];
|
|
526
530
|
// ONE ledger for BOTH lanes below. `options.validation.allow` governs the
|
|
527
531
|
// build-receipt lane AND the built-SKILL.md lane, but the two see disjoint
|
|
@@ -600,7 +604,7 @@ function withRunAllowUnused(framework, ledger) {
|
|
|
600
604
|
*
|
|
601
605
|
* ## 🚨 This lane's link graph is EMPTY — and that is the NORMAL case
|
|
602
606
|
*
|
|
603
|
-
* Measured
|
|
607
|
+
* Measured on a probe fixture (one skill, one `reference.md` reachable by
|
|
604
608
|
* a single relative link from `SKILL.md`), run three ways with ONE variable —
|
|
605
609
|
* where the packaged output lands:
|
|
606
610
|
*
|
|
@@ -623,7 +627,7 @@ function withRunAllowUnused(framework, ledger) {
|
|
|
623
627
|
* `${CLAUDE_PLUGIN_ROOT}` invocation inside a bundled reference is caught at
|
|
624
628
|
* source and CANNOT be caught in the built artifact.
|
|
625
629
|
*
|
|
626
|
-
* 🔑 Measured vs inferred. The three rows are MEASURED (
|
|
630
|
+
* 🔑 Measured vs inferred. The three rows are MEASURED (that probe).
|
|
627
631
|
* That the finding is still LIVE is inferred from this function's own body: it
|
|
628
632
|
* passes only the built `SKILL.md` path, with no registry and no population
|
|
629
633
|
* source rooted at the output tree, so `validateSkillForPackaging` resolves
|
|
@@ -644,7 +648,7 @@ function withRunAllowUnused(framework, ledger) {
|
|
|
644
648
|
*/
|
|
645
649
|
async function runPostBuildValidation(outputPath, validation, allowLedger) {
|
|
646
650
|
const builtSkillPath = safePath.join(outputPath, 'SKILL.md');
|
|
647
|
-
return validateSkillForPackaging(builtSkillPath, validation ? { validation } : undefined, 'built', { allowLedger });
|
|
651
|
+
return validateSkillForPackaging(builtSkillPath, validation ? { validation } : undefined, 'built', { allowLedger, unreadable: 'refuse' });
|
|
648
652
|
}
|
|
649
653
|
/**
|
|
650
654
|
* Assemble the final PackageSkillResult from intermediate data.
|
|
@@ -719,6 +723,9 @@ export async function createProjectRegistry(projectRoot, options = {}) {
|
|
|
719
723
|
const registry = await ResourceRegistry.fromCrawl({
|
|
720
724
|
baseDir: projectRoot,
|
|
721
725
|
include: ['**/*.md'],
|
|
726
|
+
// A build must not ship a shorter bundle: a directory this crawl cannot
|
|
727
|
+
// list refuses the run by name — see `RegistryUnreadablePolicy`.
|
|
728
|
+
unreadable: 'refuse',
|
|
722
729
|
...(options.populationSource !== undefined && { populationSource: options.populationSource }),
|
|
723
730
|
}, config === undefined ? undefined : { config });
|
|
724
731
|
registry.resolveLinks();
|
|
@@ -730,30 +737,40 @@ export async function createProjectRegistry(projectRoot, options = {}) {
|
|
|
730
737
|
* Mirrors ResourceRegistry.validateAgainstCollectionSchema's loading flow so
|
|
731
738
|
* the packager rewrites frontmatter URI-refs against the same schemas the
|
|
732
739
|
* validator uses. Collections without a frontmatterSchema configured are
|
|
733
|
-
* absent from the map.
|
|
734
|
-
*
|
|
735
|
-
*
|
|
740
|
+
* absent from the map.
|
|
741
|
+
*
|
|
742
|
+
* A schema that cannot be resolved, read or parsed is absent from the map too,
|
|
743
|
+
* and that is REPORTED as a `FRONTMATTER_SCHEMA_ERROR` issue in the build's own
|
|
744
|
+
* receipt — the same code `vat validate` gives the same config. It used to be
|
|
745
|
+
* swallowed on the theory that "the validator will surface it elsewhere", which
|
|
746
|
+
* left `vat build` shipping a bundle whose frontmatter URI-refs still pointed
|
|
747
|
+
* at source-tree paths, with a clean report and nothing to say why.
|
|
736
748
|
*/
|
|
737
749
|
async function loadCollectionSchemas(config, baseDir) {
|
|
738
750
|
const schemas = new Map();
|
|
751
|
+
const issues = [];
|
|
739
752
|
const collections = config?.resources?.collections;
|
|
740
753
|
if (!collections)
|
|
741
|
-
return schemas;
|
|
754
|
+
return { schemas, issues };
|
|
742
755
|
for (const [collectionId, collectionConfig] of Object.entries(collections)) {
|
|
743
756
|
const schemaPath = collectionConfig.validation?.frontmatterSchema;
|
|
744
757
|
if (schemaPath === undefined)
|
|
745
758
|
continue;
|
|
746
759
|
try {
|
|
747
760
|
const resolvedPath = resolveAssetReference(schemaPath, baseDir);
|
|
748
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- schema path from validated config
|
|
749
761
|
const content = await readFile(resolvedPath, 'utf-8');
|
|
750
762
|
schemas.set(collectionId, JSON.parse(content));
|
|
751
763
|
}
|
|
752
|
-
catch {
|
|
753
|
-
|
|
764
|
+
catch (error) {
|
|
765
|
+
issues.push(materializeIssue('FRONTMATTER_SCHEMA_ERROR', {
|
|
766
|
+
location: schemaPath,
|
|
767
|
+
message: `Collection "${collectionId}" declares frontmatterSchema "${schemaPath}", which could not ` +
|
|
768
|
+
`be loaded: ${error instanceof Error ? error.message : String(error)}. Frontmatter ` +
|
|
769
|
+
`URI-references in this collection were NOT rewritten for the packaged output.`,
|
|
770
|
+
}));
|
|
754
771
|
}
|
|
755
772
|
}
|
|
756
|
-
return schemas;
|
|
773
|
+
return { schemas, issues };
|
|
757
774
|
}
|
|
758
775
|
/**
|
|
759
776
|
* Generate a synthetic resource ID for a non-markdown asset that collides with an
|
|
@@ -980,7 +997,6 @@ function applyFilesEntriesToPathMap(filesConfig, projectRoot, outputPath, pathMa
|
|
|
980
997
|
// have helped. A wrong diagnosis with a confident wrong remedy attached.
|
|
981
998
|
// Only ENOENT is absence; a refusal is reported as itself, naming the entry.
|
|
982
999
|
try {
|
|
983
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- source path from validated config
|
|
984
1000
|
statSync(absoluteSource);
|
|
985
1001
|
}
|
|
986
1002
|
catch (error) {
|
|
@@ -1121,21 +1137,6 @@ function stampDeferredDestResolvedId(resources, absDestTarget, id) {
|
|
|
1121
1137
|
}
|
|
1122
1138
|
return linked;
|
|
1123
1139
|
}
|
|
1124
|
-
// ============================================================================
|
|
1125
|
-
// Path Map Building
|
|
1126
|
-
// ============================================================================
|
|
1127
|
-
/**
|
|
1128
|
-
* Determine the resource subdirectory for a file.
|
|
1129
|
-
*
|
|
1130
|
-
* For claude-web target: uses the existing references directory.
|
|
1131
|
-
* For claude-code target: uses content-type routing based on file extension.
|
|
1132
|
-
*/
|
|
1133
|
-
export function getResourceSubdirForFile(filePath, target) {
|
|
1134
|
-
if (target === 'claude-web') {
|
|
1135
|
-
return CLAUDE_WEB_REFERENCES_SUBDIR;
|
|
1136
|
-
}
|
|
1137
|
-
return getTargetSubdir(filePath);
|
|
1138
|
-
}
|
|
1139
1140
|
/**
|
|
1140
1141
|
* The FILENAME_COLLISION finding for one pair of sources that package to one dest.
|
|
1141
1142
|
*
|
|
@@ -1320,6 +1321,106 @@ async function copyAndRewriteFiles(skillPath, bundledFiles, ctx) {
|
|
|
1320
1321
|
*
|
|
1321
1322
|
* For non-markdown files, performs a plain binary copy.
|
|
1322
1323
|
*/
|
|
1324
|
+
/**
|
|
1325
|
+
* Re-base a resource's link spans onto the frontmatter-stripped body.
|
|
1326
|
+
*
|
|
1327
|
+
* ## 🚨 Why this exists: the span rewrite was INERT here, and could mis-splice
|
|
1328
|
+
*
|
|
1329
|
+
* `transformContent` identifies each link by its parsed `[startOffset,
|
|
1330
|
+
* endOffset)` span rather than by replaying a regex and correlating on href —
|
|
1331
|
+
* which is the whole point, because the two grammars disagree about what the
|
|
1332
|
+
* href IS. Its docstring states the precondition outright: *links must come from
|
|
1333
|
+
* the same bytes as content*.
|
|
1334
|
+
*
|
|
1335
|
+
* This call site violated it. `resource.links` are parsed from the WHOLE FILE,
|
|
1336
|
+
* and `editor.body` has the frontmatter block removed — so every offset was off
|
|
1337
|
+
* by the frontmatter's length. Measured on a four-line skill: span `[45, 77)`
|
|
1338
|
+
* against a body shifted by 40, where `body.slice(45, 81)` is `"ails.\n"`. Every
|
|
1339
|
+
* splice declined and every link fell back to the pre-fix regex path, so the
|
|
1340
|
+
* headline defect (`[](url)` silently not rewritten) was still
|
|
1341
|
+
* shipping. Worse, a stale span can land on a *different*, structurally valid
|
|
1342
|
+
* link and rewrite ITS href to the first link's target.
|
|
1343
|
+
*
|
|
1344
|
+
* ## The re-base is VERIFIED, not assumed
|
|
1345
|
+
*
|
|
1346
|
+
* Subtracting `content.length - body.length` is correct if and only if `body` is
|
|
1347
|
+
* a literal SUFFIX of `content` — that is exactly the statement "every body index
|
|
1348
|
+
* `i` is content index `i + offset`". `String.endsWith` decides it outright, so
|
|
1349
|
+
* the arithmetic is checked rather than argued, and a body that is not a suffix
|
|
1350
|
+
* comes back with its spans REMOVED instead of with coordinates nothing verified.
|
|
1351
|
+
* `openFrontmatter` does yield a suffix today, across CRLF, a commented block, an
|
|
1352
|
+
* absent block and empty input; the check is what keeps that a fact rather than a
|
|
1353
|
+
* comment that outlives the code it describes.
|
|
1354
|
+
*
|
|
1355
|
+
* ⛔ **This used to claim `splicableFrom` as its backstop, and that claim was
|
|
1356
|
+
* false.** The words were: "`splicableFrom` refuses any span whose destination is
|
|
1357
|
+
* not exactly the parser's href, so a misalignment that survives this degrades to
|
|
1358
|
+
* *no rewrite* rather than to a wrong one." It does not. That comparison is keyed
|
|
1359
|
+
* on the HREF, which two links in one document routinely share, so a stale span
|
|
1360
|
+
* landing on a same-href neighbour passes it and the neighbour is rewritten
|
|
1361
|
+
* through the first link's metadata. Constructed and pinned in
|
|
1362
|
+
* `packages/resources/test/content-transform.test.ts` › *two links sharing an
|
|
1363
|
+
* href defeat the destination comparison*, where a tuned frontmatter length makes
|
|
1364
|
+
* two labels swap places. A mitigation keyed on a value the two candidates have
|
|
1365
|
+
* in common cannot be a guarantee, and a guard resting on one is resting on
|
|
1366
|
+
* nothing.
|
|
1367
|
+
*
|
|
1368
|
+
* 🔑 What holds instead, in full, because the precondition has two halves:
|
|
1369
|
+
*
|
|
1370
|
+
* 1. *The spans address `content`* — discharged by the reader, not by this
|
|
1371
|
+
* function. `resource.links` and `content` must come from the same decode of
|
|
1372
|
+
* the same file, which is why the caller's read is pinned to
|
|
1373
|
+
* `readTextContent` with a 🚨 of its own. A second reader is the one way this
|
|
1374
|
+
* half fails, and it fails invisibly.
|
|
1375
|
+
* 2. *The subtraction maps `content` onto `body`* — discharged here, by
|
|
1376
|
+
* `endsWith`.
|
|
1377
|
+
*
|
|
1378
|
+
* `splicableFrom`'s destination check remains a useful mitigation for the
|
|
1379
|
+
* residue. It is not counted on, and nothing here degrades gracefully because of
|
|
1380
|
+
* it.
|
|
1381
|
+
*
|
|
1382
|
+
* @param content - The file as read, which the links were parsed from
|
|
1383
|
+
* @param body - The frontmatter-stripped body the rewrite will run over
|
|
1384
|
+
* @param links - The resource's links, carrying whole-file offsets
|
|
1385
|
+
* @param onUnverifiable - Called when `body` is not a suffix of `content`, so the
|
|
1386
|
+
* caller can say which file lost its span-driven rewrite. Never called on the
|
|
1387
|
+
* normal path.
|
|
1388
|
+
* @returns The same links with offsets stated against `body`, or with no offsets
|
|
1389
|
+
* at all when the re-base could not be verified
|
|
1390
|
+
*/
|
|
1391
|
+
export function bodyRelativeLinks(content, body, links, onUnverifiable) {
|
|
1392
|
+
if (!content.endsWith(body)) {
|
|
1393
|
+
onUnverifiable();
|
|
1394
|
+
return links.map(withoutSpan);
|
|
1395
|
+
}
|
|
1396
|
+
const offset = content.length - body.length;
|
|
1397
|
+
if (offset === 0)
|
|
1398
|
+
return [...links];
|
|
1399
|
+
return links.map((link) => ({
|
|
1400
|
+
...link,
|
|
1401
|
+
...(link.startOffset === undefined ? {} : { startOffset: link.startOffset - offset }),
|
|
1402
|
+
...(link.endOffset === undefined ? {} : { endOffset: link.endOffset - offset }),
|
|
1403
|
+
}));
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* The link with its coordinates dropped and everything else kept.
|
|
1407
|
+
*
|
|
1408
|
+
* ⚠️ Dropping them is the point, and returning the link unchanged would be the
|
|
1409
|
+
* bug: an unrebased whole-file span still ADDRESSES the body's bytes, just the
|
|
1410
|
+
* wrong ones, so `splicableFrom` would happily splice at it. Without a span the
|
|
1411
|
+
* link is `UNRECOGNISED`, which routes it to the pre-span regex replay — the
|
|
1412
|
+
* behaviour this call site had before spans existed. A documented degrade, not a
|
|
1413
|
+
* silent one; the caller warns.
|
|
1414
|
+
*
|
|
1415
|
+
* @param link - A link whose offsets could not be re-based
|
|
1416
|
+
* @returns The same link with no `startOffset` and no `endOffset` key
|
|
1417
|
+
*/
|
|
1418
|
+
function withoutSpan(link) {
|
|
1419
|
+
const stripped = { ...link };
|
|
1420
|
+
delete stripped.startOffset;
|
|
1421
|
+
delete stripped.endOffset;
|
|
1422
|
+
return stripped;
|
|
1423
|
+
}
|
|
1323
1424
|
async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
1324
1425
|
// Through the SAME attribution point as the `files:` lanes. This is the default
|
|
1325
1426
|
// path for every ordinary markdown-linked file in a build — the most-travelled
|
|
@@ -1330,7 +1431,6 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1330
1431
|
const subject = `linked file ${issueLocation(sourcePath, ctx.projectRoot) || '.'}`;
|
|
1331
1432
|
// Ensure target directory exists
|
|
1332
1433
|
await withFsAttribution(subject, async () => {
|
|
1333
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- targetPath is constructed from validated paths
|
|
1334
1434
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
1335
1435
|
});
|
|
1336
1436
|
const lower = sourcePath.toLowerCase();
|
|
@@ -1341,10 +1441,34 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1341
1441
|
await withFsAttribution(subject, () => copyFile(sourcePath, targetPath));
|
|
1342
1442
|
return;
|
|
1343
1443
|
}
|
|
1344
|
-
// Read source file
|
|
1345
|
-
|
|
1346
|
-
//
|
|
1347
|
-
|
|
1444
|
+
// Read source file.
|
|
1445
|
+
//
|
|
1446
|
+
// 🚨 `readTextContent`, never `readFile(path, 'utf-8')` — and here the reason
|
|
1447
|
+
// is sharper than the general one `link-parser.ts:149` gives. THE LINKS WERE
|
|
1448
|
+
// PARSED FROM THIS FILE BY THAT READER, and `bodyRelativeLinks` below
|
|
1449
|
+
// re-bases their offsets by differencing two string lengths. Two different
|
|
1450
|
+
// readers make that subtraction meaningless: `readTextContent` strips a BOM
|
|
1451
|
+
// and decodes UTF-16/32, so on a BOM-bearing file `readFile` returned a
|
|
1452
|
+
// string one character longer, `OPENING_FENCE` (`/^---\r?\n/`) then failed to
|
|
1453
|
+
// match, `openFrontmatter` reported no frontmatter, the offset came out 0,
|
|
1454
|
+
// and every span stayed whole-file — silently reverting the fix this call
|
|
1455
|
+
// site exists to deliver.
|
|
1456
|
+
//
|
|
1457
|
+
// ⛔ This comment used to add "it failed CLOSED rather than corrupting, only
|
|
1458
|
+
// because `splicableFrom` compares the destination to the href." That is not
|
|
1459
|
+
// a guarantee and must not be read as one: the destination check is keyed on
|
|
1460
|
+
// the href, which two links in one document routinely share, so a stale span
|
|
1461
|
+
// landing on a same-href neighbour passes it (constructed in
|
|
1462
|
+
// `content-transform.test.ts` › *two links sharing an href defeat the
|
|
1463
|
+
// destination comparison*). The BOM shift also survives
|
|
1464
|
+
// `bodyRelativeLinks`'s own `endsWith` check, because a body equal to the
|
|
1465
|
+
// content IS a suffix of it — nothing downstream can see that the links came
|
|
1466
|
+
// from a different decode. **One reader is the only thing standing between
|
|
1467
|
+
// this lane and a wrong rewrite.** Do not swap it.
|
|
1468
|
+
//
|
|
1469
|
+
// It also stops a UTF-16 source being written back as mojibake, which the
|
|
1470
|
+
// `utf-8` read did on the copy as well as the rewrite.
|
|
1471
|
+
const { text: content } = await withFsAttribution(subject, () => readTextContent(sourcePath),
|
|
1348
1472
|
// Not "copied": the read is the step that failed, and this lane reads before
|
|
1349
1473
|
// it rewrites, so naming the copy would point past the actual failure.
|
|
1350
1474
|
'read for link rewriting', READ_REMEDY);
|
|
@@ -1365,9 +1489,7 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1365
1489
|
`Source-relative links inside it are not rewritten.`
|
|
1366
1490
|
: `Copied '${sourcePath}' verbatim without link rewriting: it lost a resource-id collision to ` +
|
|
1367
1491
|
`'${winner}', which was registered first and holds the id. Source-relative links inside it are not rewritten.`);
|
|
1368
|
-
await withFsAttribution(subject,
|
|
1369
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- targetPath is constructed from validated paths
|
|
1370
|
-
() => writeFile(targetPath, content, 'utf-8'), WRITE_ACTION);
|
|
1492
|
+
await withFsAttribution(subject, () => writeFile(targetPath, content, 'utf-8'), WRITE_ACTION);
|
|
1371
1493
|
return;
|
|
1372
1494
|
}
|
|
1373
1495
|
// HTML: offset-splice link rewrite (no frontmatter, no template body rewrite).
|
|
@@ -1377,9 +1499,7 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1377
1499
|
ctx.warn(`Could not rewrite <${info.tagName} ${info.attr}="${info.from}"> in '${sourcePath}' (${info.reason}); ` +
|
|
1378
1500
|
`the original value was kept.`);
|
|
1379
1501
|
});
|
|
1380
|
-
await withFsAttribution(subject,
|
|
1381
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- targetPath is constructed from validated paths
|
|
1382
|
-
() => writeFile(targetPath, rewritten, 'utf-8'), WRITE_ACTION);
|
|
1502
|
+
await withFsAttribution(subject, () => writeFile(targetPath, rewritten, 'utf-8'), WRITE_ACTION);
|
|
1383
1503
|
return;
|
|
1384
1504
|
}
|
|
1385
1505
|
// Parse once via FrontmatterEditor so comments survive any frontmatter
|
|
@@ -1387,7 +1507,13 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1387
1507
|
// for the existing rule/template body-link rewrite contract (unchanged).
|
|
1388
1508
|
const editor = openFrontmatter(content);
|
|
1389
1509
|
// Body rewrite (existing behavior, unchanged contract).
|
|
1390
|
-
|
|
1510
|
+
const rebased = bodyRelativeLinks(content, editor.body, resource.links, () => {
|
|
1511
|
+
ctx.warn(`Rewrote links in '${sourcePath}' without parsed spans: the frontmatter-stripped body is not a ` +
|
|
1512
|
+
`suffix of the file as read, so the offsets the parser reported cannot be moved onto it. ` +
|
|
1513
|
+
`Links are matched by href instead, which is the pre-span behaviour — a link the two grammars ` +
|
|
1514
|
+
`disagree about (an image inside a link, most often) may go unrewritten.`);
|
|
1515
|
+
});
|
|
1516
|
+
editor.body = transformContent(editor.body, rebased, {
|
|
1391
1517
|
linkRewriteRules: ctx.rewriteRules,
|
|
1392
1518
|
resourceRegistry: ctx.toRegistry,
|
|
1393
1519
|
sourceFilePath: targetPath, // Output path so relativePath is computed from output location
|
|
@@ -1407,9 +1533,7 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1407
1533
|
}
|
|
1408
1534
|
}
|
|
1409
1535
|
}
|
|
1410
|
-
await withFsAttribution(subject,
|
|
1411
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- targetPath is constructed from validated paths
|
|
1412
|
-
() => writeFile(targetPath, editor.toString(), 'utf-8'), WRITE_ACTION);
|
|
1536
|
+
await withFsAttribution(subject, () => writeFile(targetPath, editor.toString(), 'utf-8'), WRITE_ACTION);
|
|
1413
1537
|
}
|
|
1414
1538
|
/**
|
|
1415
1539
|
* Build the per-href rewrite callback used for frontmatter URI-refs and HTML attributes.
|
|
@@ -1463,10 +1587,10 @@ function buildHrefRewriter(fromRegistry, toRegistry, sourcePath, targetPath, pro
|
|
|
1463
1587
|
* but this check acts as a safety net in case files are introduced through other means.
|
|
1464
1588
|
*/
|
|
1465
1589
|
async function validateNoNestedSkillMd(outputPath, skillName) {
|
|
1466
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- outputPath is validated
|
|
1467
1590
|
const entries = readdirSync(outputPath, { recursive: true, withFileTypes: true });
|
|
1468
1591
|
const nestedSkillMds = entries
|
|
1469
|
-
|
|
1592
|
+
// Followed: a symlinked nested SKILL.md is a nested skill marker all the same.
|
|
1593
|
+
.filter(entry => entry.name === 'SKILL.md' && direntKindFollowingSync(entry.parentPath, entry) === 'file')
|
|
1470
1594
|
.map(entry => safePath.relative(outputPath, safePath.join(entry.parentPath, entry.name)))
|
|
1471
1595
|
.filter(relativePath => relativePath !== 'SKILL.md'); // Exclude the root SKILL.md
|
|
1472
1596
|
if (nestedSkillMds.length > 0) {
|
|
@@ -1634,14 +1758,13 @@ const ZIP_SIZE_ERROR_BYTES = 8 * 1024 * 1024;
|
|
|
1634
1758
|
* Thrown when a claude-web ZIP exceeds the 8MB Claude.ai upload limit.
|
|
1635
1759
|
* The CLI catches this and exits with code 1.
|
|
1636
1760
|
*/
|
|
1637
|
-
export class ZipSizeLimitError extends
|
|
1761
|
+
export class ZipSizeLimitError extends VatError {
|
|
1638
1762
|
sizeBytes;
|
|
1639
1763
|
limitBytes;
|
|
1640
1764
|
constructor(sizeBytes, limitBytes) {
|
|
1641
1765
|
const mb = (sizeBytes / 1024 / 1024).toFixed(1);
|
|
1642
|
-
super(`ZIP size ${mb}MB exceeds 8MB limit for Claude.ai upload. ` +
|
|
1766
|
+
super('ZIP_SIZE_LIMIT', `ZIP size ${mb}MB exceeds 8MB limit for Claude.ai upload. ` +
|
|
1643
1767
|
`Reduce the number of linked resources or use --target claude-code.`);
|
|
1644
|
-
this.name = 'ZipSizeLimitError';
|
|
1645
1768
|
this.sizeBytes = sizeBytes;
|
|
1646
1769
|
this.limitBytes = limitBytes;
|
|
1647
1770
|
}
|
|
@@ -1653,7 +1776,6 @@ export class ZipSizeLimitError extends Error {
|
|
|
1653
1776
|
* @param zipPath - Path to the ZIP file
|
|
1654
1777
|
*/
|
|
1655
1778
|
function validateZipSize(zipPath) {
|
|
1656
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- zipPath is constructed from validated outputPath
|
|
1657
1779
|
const stats = statSync(zipPath);
|
|
1658
1780
|
const bytes = stats.size;
|
|
1659
1781
|
if (bytes >= ZIP_SIZE_ERROR_BYTES) {
|
|
@@ -1734,7 +1856,6 @@ async function createNpmPackage(outputPath, metadata) {
|
|
|
1734
1856
|
files: ['**/*.md'],
|
|
1735
1857
|
};
|
|
1736
1858
|
const packageJsonPath = safePath.join(outputPath, PACKAGE_JSON_FILENAME);
|
|
1737
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is constructed from validated outputPath
|
|
1738
1859
|
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf-8');
|
|
1739
1860
|
// For now, just return a placeholder path
|
|
1740
1861
|
// Full npm pack implementation would require running `npm pack`
|
|
@@ -1759,7 +1880,6 @@ async function createMarketplaceManifest(outputPath, metadata) {
|
|
|
1759
1880
|
created: new Date().toISOString(),
|
|
1760
1881
|
};
|
|
1761
1882
|
const manifestPath = safePath.join(dirname(outputPath), `${metadata.name}.marketplace.json`);
|
|
1762
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is constructed from validated outputPath
|
|
1763
1883
|
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
1764
1884
|
return manifestPath;
|
|
1765
1885
|
}
|
|
@@ -1773,6 +1893,15 @@ async function createMarketplaceManifest(outputPath, metadata) {
|
|
|
1773
1893
|
* @returns Default output path
|
|
1774
1894
|
*/
|
|
1775
1895
|
function getDefaultSkillOutputPath(skillPath, skillName) {
|
|
1896
|
+
// The name may have come from the H1 title when frontmatter carries none,
|
|
1897
|
+
// and the directory this returns is `rm -rf`'d before the package is
|
|
1898
|
+
// written: a SKILL.md headed `# ../../../canary` used to delete `<root>/canary`
|
|
1899
|
+
// through the public `packageSkill` export, issues empty, `hasErrors` false.
|
|
1900
|
+
if (!isSingleFsSegment(skillName)) {
|
|
1901
|
+
throw new VatError('SKILL_NAME_NOT_A_SEGMENT', `Cannot derive an output directory from skill name "${skillName}": a name must be a single ` +
|
|
1902
|
+
`path segment (no separators, not "." or ".."). Declare \`name:\` in the SKILL.md frontmatter ` +
|
|
1903
|
+
`or pass an explicit outputPath.`);
|
|
1904
|
+
}
|
|
1776
1905
|
const skillPackageRoot = findPackageRoot(skillPath);
|
|
1777
1906
|
return safePath.join(skillPackageRoot, 'dist', 'skills', skillName);
|
|
1778
1907
|
}
|
|
@@ -1791,7 +1920,6 @@ function findPackageRoot(skillPath, fallbackToSkillDir = false) {
|
|
|
1791
1920
|
// Walk up until we find a package.json or hit the filesystem root
|
|
1792
1921
|
while (currentDir !== dirname(currentDir)) {
|
|
1793
1922
|
const packageJsonPath = safePath.join(currentDir, PACKAGE_JSON_FILENAME);
|
|
1794
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Searching for package.json
|
|
1795
1923
|
if (existsSync(packageJsonPath)) {
|
|
1796
1924
|
return currentDir;
|
|
1797
1925
|
}
|