@vibe-agent-toolkit/agent-skills 0.1.42 → 0.2.0-rc.10
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 +34 -1
- package/dist/files-config.d.ts.map +1 -1
- package/dist/files-config.js +277 -39
- package/dist/files-config.js.map +1 -1
- package/dist/fs-attribution.d.ts +45 -0
- package/dist/fs-attribution.d.ts.map +1 -0
- package/dist/fs-attribution.js +57 -0
- package/dist/fs-attribution.js.map +1 -0
- 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 +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -7
- package/dist/index.js.map +1 -1
- package/dist/inventory/detectors/declared-but-missing.d.ts +1 -1
- package/dist/inventory/detectors/declared-but-missing.d.ts.map +1 -1
- package/dist/inventory/detectors/marketplace-source-missing.d.ts +1 -1
- package/dist/inventory/detectors/marketplace-source-missing.d.ts.map +1 -1
- package/dist/inventory/detectors/present-but-undeclared.d.ts +1 -1
- package/dist/inventory/detectors/present-but-undeclared.d.ts.map +1 -1
- package/dist/inventory/detectors/reference-target-missing.d.ts +1 -1
- package/dist/inventory/detectors/reference-target-missing.d.ts.map +1 -1
- package/dist/inventory/index.d.ts +1 -1
- package/dist/inventory/index.d.ts.map +1 -1
- package/dist/inventory/index.js +1 -1
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/serialize.d.ts +4 -4
- package/dist/inventory/serialize.d.ts.map +1 -1
- package/dist/inventory/serialize.js +6 -6
- package/dist/inventory/serialize.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/link-graph-members.d.ts +18 -0
- package/dist/link-graph-members.d.ts.map +1 -0
- package/dist/link-graph-members.js +18 -0
- package/dist/link-graph-members.js.map +1 -0
- 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 +70 -6
- package/dist/plugin-distribution-layout.d.ts.map +1 -1
- package/dist/plugin-distribution-layout.js +84 -11
- package/dist/plugin-distribution-layout.js.map +1 -1
- package/dist/post-build-checks.d.ts +38 -1
- package/dist/post-build-checks.d.ts.map +1 -1
- package/dist/post-build-checks.js +68 -17
- package/dist/post-build-checks.js.map +1 -1
- package/dist/projection/skill-extent.d.ts +247 -0
- package/dist/projection/skill-extent.d.ts.map +1 -0
- package/dist/projection/skill-extent.js +560 -0
- package/dist/projection/skill-extent.js.map +1 -0
- package/dist/schemas/agent-skill-frontmatter.js +1 -1
- package/dist/schemas/agent-skill-frontmatter.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 +18 -3
- package/dist/skill-identity.d.ts.map +1 -1
- package/dist/skill-identity.js +25 -6
- package/dist/skill-identity.js.map +1 -1
- package/dist/skill-packager.d.ts +144 -20
- package/dist/skill-packager.d.ts.map +1 -1
- package/dist/skill-packager.js +447 -105
- 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 +1 -1
- package/dist/skill-source/git-clone.d.ts.map +1 -1
- package/dist/skill-source/git-clone.js +10 -10
- 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 +2 -2
- 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-delta.d.ts +1575 -0
- package/dist/skill-test/baseline-delta.d.ts.map +1 -0
- package/dist/skill-test/baseline-delta.js +272 -0
- package/dist/skill-test/baseline-delta.js.map +1 -0
- package/dist/skill-test/baseline-integrity.d.ts +944 -0
- package/dist/skill-test/baseline-integrity.d.ts.map +1 -0
- package/dist/skill-test/baseline-integrity.js +3090 -0
- package/dist/skill-test/baseline-integrity.js.map +1 -0
- 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 -5
- package/dist/skill-test/build-hook.js.map +1 -1
- package/dist/skill-test/configure-writer.js +1 -1
- package/dist/skill-test/configure-writer.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 +8 -9
- package/dist/skill-test/declared-env.js.map +1 -1
- package/dist/skill-test/eval-executor.d.ts +21 -5
- package/dist/skill-test/eval-executor.d.ts.map +1 -1
- package/dist/skill-test/eval-executor.js +6 -6
- package/dist/skill-test/eval-executor.js.map +1 -1
- package/dist/skill-test/eval-fragment.d.ts +106 -28
- package/dist/skill-test/eval-fragment.d.ts.map +1 -1
- package/dist/skill-test/eval-fragment.js +78 -16
- package/dist/skill-test/eval-fragment.js.map +1 -1
- package/dist/skill-test/eval-grader.d.ts +65 -7
- package/dist/skill-test/eval-grader.d.ts.map +1 -1
- package/dist/skill-test/eval-grader.js +263 -64
- package/dist/skill-test/eval-grader.js.map +1 -1
- package/dist/skill-test/eval-inputs.d.ts +110 -8
- package/dist/skill-test/eval-inputs.d.ts.map +1 -1
- package/dist/skill-test/eval-inputs.js +177 -22
- package/dist/skill-test/eval-inputs.js.map +1 -1
- package/dist/skill-test/eval-lint.d.ts +17 -4
- package/dist/skill-test/eval-lint.d.ts.map +1 -1
- package/dist/skill-test/eval-lint.js +22 -9
- 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 +15 -7
- package/dist/skill-test/executor-prompt.d.ts.map +1 -1
- package/dist/skill-test/executor-prompt.js +10 -7
- package/dist/skill-test/executor-prompt.js.map +1 -1
- 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 +3 -20
- package/dist/skill-test/fragment-merge.d.ts.map +1 -1
- package/dist/skill-test/fragment-merge.js +67 -8
- package/dist/skill-test/fragment-merge.js.map +1 -1
- package/dist/skill-test/grader-prompt.d.ts +29 -9
- package/dist/skill-test/grader-prompt.d.ts.map +1 -1
- package/dist/skill-test/grader-prompt.js +264 -68
- package/dist/skill-test/grader-prompt.js.map +1 -1
- package/dist/skill-test/grader-text.d.ts +145 -0
- package/dist/skill-test/grader-text.d.ts.map +1 -0
- package/dist/skill-test/grader-text.js +410 -0
- package/dist/skill-test/grader-text.js.map +1 -0
- package/dist/skill-test/grading-adapter.d.ts +38 -2
- package/dist/skill-test/grading-adapter.d.ts.map +1 -1
- package/dist/skill-test/grading-adapter.js +57 -9
- package/dist/skill-test/grading-adapter.js.map +1 -1
- package/dist/skill-test/grading-schema.d.ts +160 -5
- package/dist/skill-test/grading-schema.d.ts.map +1 -1
- package/dist/skill-test/grading-schema.js +66 -7
- package/dist/skill-test/grading-schema.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 +2 -2
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +2 -2
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/lock.d.ts +58 -1
- package/dist/skill-test/lock.d.ts.map +1 -1
- package/dist/skill-test/lock.js +78 -6
- package/dist/skill-test/lock.js.map +1 -1
- package/dist/skill-test/pipeline.d.ts +19 -1
- package/dist/skill-test/pipeline.d.ts.map +1 -1
- package/dist/skill-test/pipeline.js +21 -10
- package/dist/skill-test/pipeline.js.map +1 -1
- package/dist/skill-test/preflight.d.ts +1 -1
- package/dist/skill-test/preflight.d.ts.map +1 -1
- package/dist/skill-test/preflight.js +59 -16
- 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 +665 -85
- package/dist/skill-test/run-harness.d.ts.map +1 -1
- package/dist/skill-test/run-harness.js +1522 -237
- package/dist/skill-test/run-harness.js.map +1 -1
- package/dist/skill-test/staging.d.ts +20 -8
- package/dist/skill-test/staging.d.ts.map +1 -1
- package/dist/skill-test/staging.js +25 -18
- 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.d.ts +6 -6
- 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 +59 -8
- package/dist/test-input.d.ts.map +1 -1
- package/dist/test-input.js +64 -19
- package/dist/test-input.js.map +1 -1
- package/dist/validators/agent-instruction-presence.d.ts +1 -1
- package/dist/validators/agent-instruction-presence.d.ts.map +1 -1
- package/dist/validators/agent-instruction-presence.js +45 -2
- package/dist/validators/agent-instruction-presence.js.map +1 -1
- package/dist/validators/bundled-resource-link-detection.d.ts +6 -4
- package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
- package/dist/validators/bundled-resource-link-detection.js +39 -39
- package/dist/validators/bundled-resource-link-detection.js.map +1 -1
- package/dist/validators/compat-detectors.d.ts +18 -1
- package/dist/validators/compat-detectors.d.ts.map +1 -1
- package/dist/validators/compat-detectors.js +47 -20
- package/dist/validators/compat-detectors.js.map +1 -1
- package/dist/validators/cross-skill-dependency-detection.d.ts +1 -1
- package/dist/validators/cross-skill-dependency-detection.d.ts.map +1 -1
- package/dist/validators/cross-skill-dependency-detection.js +1 -1
- package/dist/validators/cross-skill-dependency-detection.js.map +1 -1
- package/dist/validators/description-style-detection.d.ts +1 -1
- package/dist/validators/description-style-detection.d.ts.map +1 -1
- package/dist/validators/description-style-detection.js +1 -1
- package/dist/validators/description-style-detection.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.d.ts +1 -1
- package/dist/validators/frontmatter-validation.d.ts.map +1 -1
- package/dist/validators/frontmatter-validation.js +2 -2
- package/dist/validators/frontmatter-validation.js.map +1 -1
- package/dist/validators/imperative-body-detection.d.ts +1 -1
- package/dist/validators/imperative-body-detection.d.ts.map +1 -1
- package/dist/validators/imperative-body-detection.js +1 -1
- package/dist/validators/imperative-body-detection.js.map +1 -1
- package/dist/validators/index.d.ts +1 -1
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/kebab-case-detection.d.ts +1 -1
- package/dist/validators/kebab-case-detection.d.ts.map +1 -1
- package/dist/validators/kebab-case-detection.js +1 -1
- package/dist/validators/kebab-case-detection.js.map +1 -1
- package/dist/validators/marketplace-validator.d.ts.map +1 -1
- package/dist/validators/marketplace-validator.js +7 -2
- package/dist/validators/marketplace-validator.js.map +1 -1
- package/dist/validators/mcp-tool-qualification.d.ts +158 -0
- package/dist/validators/mcp-tool-qualification.d.ts.map +1 -0
- package/dist/validators/mcp-tool-qualification.js +467 -0
- package/dist/validators/mcp-tool-qualification.js.map +1 -0
- package/dist/validators/packaged-size-limit.d.ts +296 -0
- package/dist/validators/packaged-size-limit.d.ts.map +1 -0
- package/dist/validators/packaged-size-limit.js +492 -0
- package/dist/validators/packaged-size-limit.js.map +1 -0
- package/dist/validators/packaging-validator.d.ts +149 -10
- package/dist/validators/packaging-validator.d.ts.map +1 -1
- package/dist/validators/packaging-validator.js +371 -55
- package/dist/validators/packaging-validator.js.map +1 -1
- package/dist/validators/plugin-hosted-shape.d.ts +4 -2
- package/dist/validators/plugin-hosted-shape.d.ts.map +1 -1
- package/dist/validators/plugin-hosted-shape.js +6 -7
- package/dist/validators/plugin-hosted-shape.js.map +1 -1
- package/dist/validators/plugin-recommended-fields.d.ts +1 -1
- package/dist/validators/plugin-recommended-fields.d.ts.map +1 -1
- package/dist/validators/plugin-recommended-fields.js +1 -1
- package/dist/validators/plugin-recommended-fields.js.map +1 -1
- package/dist/validators/referenced-path-missing.d.ts +211 -0
- package/dist/validators/referenced-path-missing.d.ts.map +1 -0
- package/dist/validators/referenced-path-missing.js +378 -0
- package/dist/validators/referenced-path-missing.js.map +1 -0
- package/dist/validators/registry-validator.d.ts.map +1 -1
- package/dist/validators/registry-validator.js +1 -2
- 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 +20 -3
- package/dist/validators/rule-engine/rule-context.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-context.js +3 -1
- package/dist/validators/rule-engine/rule-context.js.map +1 -1
- package/dist/validators/rule-engine/rule-engine.d.ts +2 -2
- package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-engine.js +52 -13
- 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 +91 -54
- 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 +4 -5
- package/dist/validators/unified-validator.js.map +1 -1
- package/dist/validators/validation-rules.d.ts +4 -4
- 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 +9 -3
- package/dist/validators/walker-to-issues.d.ts.map +1 -1
- package/dist/validators/walker-to-issues.js +64 -19
- package/dist/validators/walker-to-issues.js.map +1 -1
- package/dist/walk-link-graph.d.ts +64 -5
- package/dist/walk-link-graph.d.ts.map +1 -1
- package/dist/walk-link-graph.js +376 -92
- package/dist/walk-link-graph.js.map +1 -1
- package/package.json +14 -22
- package/dist/skill-test/exit-codes.d.ts +0 -97
- package/dist/skill-test/exit-codes.d.ts.map +0 -1
- package/dist/skill-test/exit-codes.js +0 -141
- package/dist/skill-test/exit-codes.js.map +0 -1
package/dist/skill-packager.js
CHANGED
|
@@ -17,19 +17,25 @@
|
|
|
17
17
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
18
18
|
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
19
19
|
import { basename, dirname } from 'node:path';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { findProjectRoot, isGlob, issueLocation, resolveAssetReference, toForwardSlash,
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
20
|
+
import { DeferredArtifacts, DuplicateResourceIdError, ResourceRegistry, loadConfig, openFrontmatter, resolveLocalHref, rewriteFrontmatterUriReferencesFromSchema, rewriteHtmlLinks, transformContent, parseFileCached, } from '@vibe-agent-toolkit/resources';
|
|
21
|
+
import { allowUnusedIssues, createAllowUsageLedger, runValidationFramework, } from '@vibe-agent-toolkit/schema';
|
|
22
|
+
import { direntKindFollowingSync, findProjectRoot, isGlob, isSingleFsSegment, issueLocation, resolveAssetReference, safePath, toForwardSlash, toForwardSlashAnyPlatform, VatError, } from '@vibe-agent-toolkit/utils';
|
|
23
|
+
import { readTextContent } from '@vibe-agent-toolkit/utils/fs';
|
|
24
|
+
import { getResourceSubdirForFile } from './content-type-routing.js';
|
|
25
|
+
import { applyFilesConfig, buildArtifactHint, droppedGlobMatchesToIssues, explicitFilesConfigDests, globEntryDest, skippedGlobMatchesToIssues, } from './files-config.js';
|
|
26
|
+
import { READ_REMEDY, withFsAttribution } from './fs-attribution.js';
|
|
27
|
+
import { LINK_GRAPH_MEMBER_GLOBS } from './link-graph-members.js';
|
|
28
|
+
import { checkBrokenPackagedLinks, checkMissingReferencedPaths, checkUnreferencedFiles } from './post-build-checks.js';
|
|
26
29
|
import { checkPackagedTestInput, partitionTestInputFileEntries, resolveTestInputDirs, testInputExcludeRules, testInputFileEntryIssues, testInputLinkIssues, } from './test-input.js';
|
|
27
30
|
import { detectPackagedAgentInstructionFiles } from './validators/agent-instruction-presence.js';
|
|
31
|
+
import { checkPackagedSizeLimit } from './validators/packaged-size-limit.js';
|
|
28
32
|
import { validateSkillForPackaging } from './validators/packaging-validator.js';
|
|
29
33
|
import { materializeIssue } from './validators/rule-engine/index.js';
|
|
30
|
-
import { deferredAssetsToIssues, walkerExclusionsToIssues } from './validators/walker-to-issues.js';
|
|
34
|
+
import { deferredAssetsToIssues, outsideSkillDirLinksToIssues, walkerExclusionsToIssues } from './validators/walker-to-issues.js';
|
|
31
35
|
import { walkLinkGraph } from './walk-link-graph.js';
|
|
32
36
|
const PACKAGE_JSON_FILENAME = 'package.json';
|
|
37
|
+
/** `withFsAttribution` action for a write in `copyAndRewriteFile` — never a copy, since the bytes are rewritten first. */
|
|
38
|
+
const WRITE_ACTION = 'written into the bundle';
|
|
33
39
|
/**
|
|
34
40
|
* Default template for excluded links when no explicit template is configured —
|
|
35
41
|
* renders just the link text.
|
|
@@ -37,18 +43,20 @@ const PACKAGE_JSON_FILENAME = 'package.json';
|
|
|
37
43
|
* `{{link.rawText}}`, not `{{link.text}}`, for the same two reasons the rewrite
|
|
38
44
|
* branch uses it (see `bundledLinkTemplate`):
|
|
39
45
|
*
|
|
40
|
-
* - **
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* one phrase for an unrelated one in shipped prose; the rewrite branch never
|
|
45
|
-
* exposed it because it re-emits `rawText`.
|
|
46
|
+
* - **Per-occurrence identity.** `link.text` is a property of the parsed link,
|
|
47
|
+
* and two occurrences sharing an href are two links whose text may differ.
|
|
48
|
+
* `rawText` is always the text of the occurrence actually being replaced, so
|
|
49
|
+
* it cannot swap one phrase for an unrelated one in shipped prose.
|
|
46
50
|
* - **Formatting.** `rawText` keeps the inline markup the author wrote, so
|
|
47
51
|
* ``[`foo.yaml`](…)`` strips to ``` `foo.yaml` ``` rather than bare `foo.yaml`.
|
|
48
52
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
53
|
+
* > This used to explain the first reason as a defence against `transformContent`
|
|
54
|
+
* > keying parsed links by href and letting the FIRST occurrence win. That
|
|
55
|
+
* > correlation is gone: `transformContent` now splices each parsed link at its
|
|
56
|
+
* > own `[startOffset, endOffset)` span, so there is no shared-href map to collide
|
|
57
|
+
* > in for any link the parser located. `rawText` remains correct and remains the
|
|
58
|
+
* > right choice — the reasoning above no longer depends on a collision that the
|
|
59
|
+
* > rewriter can still have.
|
|
52
60
|
*/
|
|
53
61
|
const DEFAULT_STRIP_TEMPLATE = '{{link.rawText}}';
|
|
54
62
|
/**
|
|
@@ -105,8 +113,16 @@ const DEFAULT_PACKAGING_TARGET = 'claude-code';
|
|
|
105
113
|
* repo has been bitten by). It is required rather than defaulted because the test-input
|
|
106
114
|
* rule is project-wide: omitting it silently packages another skill's eval answer key.
|
|
107
115
|
* A lane with genuinely no project to enumerate passes `[]` explicitly.
|
|
116
|
+
*
|
|
117
|
+
* `hasConventionalSuite` is the RUN's conventional-suite probe, and travels with
|
|
118
|
+
* `projectSkills` for the same reason and on the same schedule: this conversion runs
|
|
119
|
+
* once per skill and asks the filesystem about every root in `projectSkills`, so a
|
|
120
|
+
* per-call probe makes a build of S skills cost S² probes over S paths. Required, not
|
|
121
|
+
* defaulted — a `probe?` would let every existing caller keep the quadratic silently,
|
|
122
|
+
* which is precisely how the `projectSkills` rule above once shipped as a no-op. A
|
|
123
|
+
* lane packaging a single skill calls `conventionalSuiteProbe()` at its own call site.
|
|
108
124
|
*/
|
|
109
|
-
export function packagingConfigToPackageOptions(config, anchors, projectSkills) {
|
|
125
|
+
export function packagingConfigToPackageOptions(config, anchors, projectSkills, hasConventionalSuite) {
|
|
110
126
|
return {
|
|
111
127
|
outputPath: anchors.outputPath,
|
|
112
128
|
formats: ['directory'],
|
|
@@ -135,7 +151,7 @@ export function packagingConfigToPackageOptions(config, anchors, projectSkills)
|
|
|
135
151
|
// excluded as well. Keyed to this skill alone, that sibling's answer key was
|
|
136
152
|
// ordinary content and shipped.
|
|
137
153
|
...(() => {
|
|
138
|
-
const testInputDirs = resolveTestInputDirs(config, dirname(anchors.skillPath), projectSkills);
|
|
154
|
+
const testInputDirs = resolveTestInputDirs(config, dirname(anchors.skillPath), projectSkills, hasConventionalSuite);
|
|
139
155
|
return testInputDirs.length > 0 ? { testInputDirs } : {};
|
|
140
156
|
})(),
|
|
141
157
|
};
|
|
@@ -176,6 +192,11 @@ export function packagingConfigToPackageOptions(config, anchors, projectSkills)
|
|
|
176
192
|
* (`vat build` also validates the SOURCE tree, whose matches count too).
|
|
177
193
|
* Containment does not change that: a skill that threw may still have matched
|
|
178
194
|
* allow entries before it threw, and those matches count for the run.
|
|
195
|
+
* @param runOptions - Registry-build options for the RUN. Named apart from the
|
|
196
|
+
* per-skill `options` destructured in the loop below, which it is not: it
|
|
197
|
+
* configures the ONE shared registry {@link createProjectRegistry} builds here.
|
|
198
|
+
* It does NOT reach the per-skill post-build validation, which builds a private
|
|
199
|
+
* registry over the BUILT tree — see {@link runPostBuildValidation}
|
|
179
200
|
* @returns One outcome per input spec, in input order
|
|
180
201
|
*
|
|
181
202
|
* @example
|
|
@@ -189,17 +210,26 @@ export function packagingConfigToPackageOptions(config, anchors, projectSkills)
|
|
|
189
210
|
* const runIssues = allowUnusedIssues(ledger);
|
|
190
211
|
* ```
|
|
191
212
|
*/
|
|
192
|
-
export async function packageSkills(skills, projectRoot, allowLedger) {
|
|
213
|
+
export async function packageSkills(skills, projectRoot, allowLedger, runOptions = {}) {
|
|
193
214
|
// 1. Create one registry for the entire project. Through the shared builder:
|
|
194
215
|
// this used to call `fromCrawl` directly and omit the config, so skills built
|
|
195
216
|
// here belonged to no collection while a skill built through the single-skill
|
|
196
217
|
// fallback did.
|
|
197
|
-
|
|
218
|
+
//
|
|
219
|
+
// `runOptions` is forwarded rather than absorbed: this is the ONLY registry the
|
|
220
|
+
// run builds, so a caller that wants the run on the projection lane has no
|
|
221
|
+
// other seam to reach. Dropping it here would leave `vat skills build`
|
|
222
|
+
// reaching a store it opened and never used.
|
|
223
|
+
const registry = await createProjectRegistry(projectRoot, runOptions);
|
|
198
224
|
// 2. Package each skill against the shared registry
|
|
199
225
|
const outcomes = [];
|
|
200
226
|
for (const { skillPath, options } of skills) {
|
|
201
227
|
try {
|
|
202
|
-
const result = await packageSkill(skillPath, {
|
|
228
|
+
const result = await packageSkill(skillPath, {
|
|
229
|
+
...options,
|
|
230
|
+
registry,
|
|
231
|
+
allowLedger,
|
|
232
|
+
});
|
|
203
233
|
outcomes.push({ status: 'built', skillPath, result });
|
|
204
234
|
}
|
|
205
235
|
catch (error) {
|
|
@@ -235,7 +265,7 @@ export async function packageSkills(skills, projectRoot, allowLedger) {
|
|
|
235
265
|
export async function packageSkill(skillPath, options = {}) {
|
|
236
266
|
const { formats = ['directory'], rewriteLinks = true, resourceNaming = 'basename', stripPrefix, target = DEFAULT_PACKAGING_TARGET, } = options;
|
|
237
267
|
// 1. Parse SKILL.md frontmatter and links
|
|
238
|
-
const parseResult = await
|
|
268
|
+
const parseResult = await parseFileCached(skillPath, 'markdown');
|
|
239
269
|
const skillMetadata = extractSkillMetadata(parseResult, skillPath);
|
|
240
270
|
// 2. Find project boundary (config root -> git root -> skill dir).
|
|
241
271
|
// Library callers fall back to the skill directory when canonical
|
|
@@ -266,7 +296,7 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
266
296
|
const registry = options.registry ?? await createProjectRegistry(projectRoot);
|
|
267
297
|
// 3b. Load per-collection frontmatter schemas (Gap 3: packager rewrites frontmatter URI-refs
|
|
268
298
|
// against the same schemas the validator uses, with body parity).
|
|
269
|
-
const collectionSchemas = await loadCollectionSchemas(registry.config, projectRoot);
|
|
299
|
+
const { schemas: collectionSchemas, issues: collectionSchemaIssues } = await loadCollectionSchemas(registry.config, projectRoot);
|
|
270
300
|
// 4. Walk the link graph using registry data
|
|
271
301
|
const linkFollowDepth = options.linkFollowDepth ?? 2;
|
|
272
302
|
const excludeConfig = options.excludeReferencesFromBundle;
|
|
@@ -296,13 +326,13 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
296
326
|
if (options.gitTracker !== undefined) {
|
|
297
327
|
packagerWalkOptions.gitTracker = options.gitTracker;
|
|
298
328
|
}
|
|
299
|
-
const { bundledResources, bundledAssets, excludedReferences, deferredAssets } = walkLinkGraph(skillResourceId, registry, packagerWalkOptions);
|
|
329
|
+
const { bundledResources, bundledAssets, excludedReferences, outsideSkillDirLinks, deferredAssets } = walkLinkGraph(skillResourceId, registry, packagerWalkOptions);
|
|
300
330
|
// Register non-markdown bundled assets in the source registry so link rewriting
|
|
301
331
|
// can resolve them (resolvedId must be set on links pointing to YAML, JSON, etc.).
|
|
302
|
-
// For any asset whose
|
|
303
|
-
//
|
|
304
|
-
// links pointing to it so link rewriting still works.
|
|
305
|
-
const collidedAssets = await registerBundledAssets(registry, bundledAssets);
|
|
332
|
+
// For any asset whose id is already taken (a path-slug clash — `a-b/c.html`
|
|
333
|
+
// and `a/b-c.html` both flatten to `a-b-c-html`), we set a synthetic
|
|
334
|
+
// resolvedId on links pointing to it so link rewriting still works.
|
|
335
|
+
const { collidedAssets, collisions: assetCollisions } = await registerBundledAssets(registry, bundledAssets, projectRoot);
|
|
306
336
|
resolveCollidedAssetLinks(collectResourcesWithLinks(bundledResources, skillResource), collidedAssets);
|
|
307
337
|
// Combine bundled file paths: markdown resources + non-markdown assets
|
|
308
338
|
const bundledFiles = [
|
|
@@ -318,7 +348,6 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
318
348
|
// 7. Clean stale output (skip when source SKILL.md lives inside the output, e.g. builder flow)
|
|
319
349
|
const resolvedOutput = safePath.resolve(outputPath);
|
|
320
350
|
const sourceInOutput = safePath.resolve(skillPath).startsWith(resolvedOutput + '/');
|
|
321
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- outputPath is validated
|
|
322
351
|
if (!sourceInOutput && existsSync(resolvedOutput)) {
|
|
323
352
|
await rm(resolvedOutput, { recursive: true });
|
|
324
353
|
}
|
|
@@ -378,12 +407,21 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
378
407
|
// 11. Build unified rewrite rules (bundled + excluded, all via transformContent)
|
|
379
408
|
const rewriteRules = buildRewriteRules(excludedIds, excludeConfig?.rules ?? [], excludeConfig?.defaultTemplate);
|
|
380
409
|
// 12. Copy and rewrite files
|
|
381
|
-
//
|
|
382
|
-
|
|
410
|
+
// The build's own output directory. An unwritable `dist/` or a full disk failed
|
|
411
|
+
// here with a bare errno naming an output path the author never typed — the
|
|
412
|
+
// same shape as the copiers, at the step before any of them run.
|
|
413
|
+
await withFsAttribution(`skill '${skillMetadata.name}' output directory ${issueLocation(outputPath, projectRoot) || '.'}`, () => mkdir(outputPath, { recursive: true }), 'created');
|
|
383
414
|
await copyAndRewriteFiles(skillPath, bundledFiles, {
|
|
384
415
|
pathMap,
|
|
385
416
|
rewriteLinks,
|
|
386
417
|
fromRegistry: registry,
|
|
418
|
+
// Both sources, because they record disjoint populations: the registry's
|
|
419
|
+
// own log holds crawl-time drops made by `addResources`, while
|
|
420
|
+
// `registerBundledAssets` holds the per-asset drops `addResource` only ever
|
|
421
|
+
// signalled by throwing. An HTML file that collides with a same-named
|
|
422
|
+
// markdown file appears ONLY in the second.
|
|
423
|
+
duplicateIdDrops: new Map([...registry.getDuplicateIdCollisions(), ...assetCollisions]
|
|
424
|
+
.map(c => [toForwardSlash(c.conflictingPath), c.existingPath])),
|
|
387
425
|
toRegistry: outputRegistry,
|
|
388
426
|
rewriteRules,
|
|
389
427
|
templateContext: { skill: { name: skillMetadata.name } },
|
|
@@ -425,6 +463,30 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
425
463
|
...collisionIssues,
|
|
426
464
|
...await checkUnreferencedFiles(outputPath, filesConfigDests),
|
|
427
465
|
...await checkBrokenPackagedLinks(outputPath, droppedGlobDests),
|
|
466
|
+
// The inverse of checkUnreferencedFiles: paths the docs NAME that the bundle
|
|
467
|
+
// does not contain. Built phase only — a `files:` dest exists here and not in
|
|
468
|
+
// the source tree, so the same check at source phase flags every injected
|
|
469
|
+
// script.
|
|
470
|
+
//
|
|
471
|
+
// SKILL-LOCAL, and this is the ONLY caller. The packager knows its own output
|
|
472
|
+
// directory, not the plugin the skill will be installed into, so there is no
|
|
473
|
+
// wider root to give it — the sibling-search parameter that measured 1.9%
|
|
474
|
+
// instead of 3.8% was deleted for having no caller. The shipped rate is 3.8%.
|
|
475
|
+
...await checkMissingReferencedPaths(outputPath, target),
|
|
476
|
+
// The only byte measurement in the pipeline. Built phase for the same reason
|
|
477
|
+
// as its neighbour above: a `files:` entry materializes files here that the
|
|
478
|
+
// source tree does not have, so the bytes that ship are only knowable now.
|
|
479
|
+
//
|
|
480
|
+
// ⚠️ Runs before step 14, so it does not weigh what `generatePackageArtifacts`
|
|
481
|
+
// adds. Measured, so the residue is not a guess: the `zip` and `npm` formats
|
|
482
|
+
// write to `<outputPath>.zip` / `.tgz`, OUTSIDE the bundle and outside the
|
|
483
|
+
// upload; only the `npm` format's synthetic package.json and the `marketplace`
|
|
484
|
+
// format's manifest land inside, and both are a few hundred bytes. The order
|
|
485
|
+
// is not free to change — checkUnreferencedFiles two lines up would flag that
|
|
486
|
+
// same synthetic package.json — so the under-count is stated rather than
|
|
487
|
+
// fixed. If an artifact step ever writes something LARGE into outputPath, this
|
|
488
|
+
// call has to move after it and the framework run with it.
|
|
489
|
+
...checkPackagedSizeLimit(outputPath),
|
|
428
490
|
// A receipt for every file a glob matched and the never-package list refused.
|
|
429
491
|
// Reported as an issue, not written to stderr: a file vanishing from a bundle
|
|
430
492
|
// has to be visible in `issueCounts`, or CI reads a clean report for a build
|
|
@@ -433,6 +495,10 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
433
495
|
// dropped file is a source file that never reached the output, so the only
|
|
434
496
|
// path a reader can open is its source path.
|
|
435
497
|
...droppedGlobMatchesToIssues(appliedFiles.dropped, projectRoot),
|
|
498
|
+
// The same receipt, for matches that are not copyable files at all. Anchored
|
|
499
|
+
// identically and for the identical reason. Without a channel of its own this
|
|
500
|
+
// population is invisible: the build used to die on the first one it met.
|
|
501
|
+
...skippedGlobMatchesToIssues(appliedFiles.skipped, projectRoot),
|
|
436
502
|
// Presence-side backstop for agent-instruction files. The walker excludes
|
|
437
503
|
// them from link-following, but a `files:` glob can still copy one in, and
|
|
438
504
|
// a file that arrives without a link is invisible to the link lane.
|
|
@@ -446,6 +512,9 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
446
512
|
// A receipt for each `files:` entry that was dropped for pointing into declared
|
|
447
513
|
// test input — the build already produced the right artifact; this just says so.
|
|
448
514
|
...testInputFileEntryIssues(droppedTestInputFiles),
|
|
515
|
+
// A collection schema that could not be loaded, so its frontmatter was not
|
|
516
|
+
// rewritten (see loadCollectionSchemas).
|
|
517
|
+
...collectionSchemaIssues,
|
|
449
518
|
// Backstop: if declared test input reached the output despite both exclusions,
|
|
450
519
|
// say so rather than shipping an answer key silently.
|
|
451
520
|
...checkPackagedTestInput({ pathMap, outputPath, testInputDirs }),
|
|
@@ -457,6 +526,7 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
457
526
|
// (a pattern match is author-declared intent; this exclusion is VAT's).
|
|
458
527
|
...testInputLinkIssues(excludedReferences, testInputDirs, projectRoot),
|
|
459
528
|
...deferredAssetsToIssues(deferredAssets, projectRoot),
|
|
529
|
+
...outsideSkillDirLinksToIssues(outsideSkillDirLinks, projectRoot),
|
|
460
530
|
];
|
|
461
531
|
// ONE ledger for BOTH lanes below. `options.validation.allow` governs the
|
|
462
532
|
// build-receipt lane AND the built-SKILL.md lane, but the two see disjoint
|
|
@@ -516,10 +586,70 @@ function withRunAllowUnused(framework, ledger) {
|
|
|
516
586
|
*
|
|
517
587
|
* `allowLedger` is required, not optional: this lane is one half of a build, so
|
|
518
588
|
* it must never conclude on its own that an allow entry matched nothing.
|
|
589
|
+
*
|
|
590
|
+
* ⛔ This lane stays on the walk and is handed no `populationSource`. It validates
|
|
591
|
+
* the BUILT tree under `outputPath` — a different tree from the one the run
|
|
592
|
+
* enumerated, not tracked by git, and not covered by the packaging registry. A
|
|
593
|
+
* source produced by `withResourcePopulationSource` is bound to the PROJECT root
|
|
594
|
+
* and its git tree hash, so offering it a build-output directory asks it a
|
|
595
|
+
* question about a tree it does not describe. Forwarding it here was tried and
|
|
596
|
+
* reverted — the giveaway was a source offered `<root>/out/<skill>` instead of
|
|
597
|
+
* the root.
|
|
598
|
+
*
|
|
599
|
+
* The store-poisoning half of that hazard is now closed at the seam rather than by
|
|
600
|
+
* this comment: `ResourceRegistry.populationFrom` compares the source's own bound
|
|
601
|
+
* root against the crawl's base and declines a mismatch back onto the walk. So a
|
|
602
|
+
* future forward here would be a no-op that warns, not a corrupted extent key.
|
|
603
|
+
* What it would still NOT be is a speed-up, which is why nothing is forwarded:
|
|
604
|
+
* the built tree is a different tree, so there is no stored answer for it to hit.
|
|
605
|
+
*
|
|
606
|
+
* ## 🚨 This lane's link graph is EMPTY — and that is the NORMAL case
|
|
607
|
+
*
|
|
608
|
+
* Measured on a probe fixture (one skill, one `reference.md` reachable by
|
|
609
|
+
* a single relative link from `SKILL.md`), run three ways with ONE variable —
|
|
610
|
+
* where the packaged output lands:
|
|
611
|
+
*
|
|
612
|
+
* | lane | output location | `fileCount` | `directFileCount` | `maxLinkDepth` | `totalLines` |
|
|
613
|
+
* |---|---|---:|---:|---:|---:|
|
|
614
|
+
* | source | n/a | 2 | 1 | 1 | 13 |
|
|
615
|
+
* | built | INSIDE the project (`<root>/dist/skills/<name>/`) | **1** | **0** | **0** | **10** |
|
|
616
|
+
* | built | outside any project root (control) | 2 | 1 | 1 | 14 |
|
|
617
|
+
*
|
|
618
|
+
* The middle row is the defect; the third row PROVES the cause — the only
|
|
619
|
+
* difference between them is whether the output falls under the crawl's
|
|
620
|
+
* `**\/dist\/**` exclusion (`BUILD_OUTPUT_GLOBS`). `packageSkill`'s default output
|
|
621
|
+
* IS the middle row, so this is the normal case, not an edge case. The result's
|
|
622
|
+
* own `files.dependencies` still reads `["reference.md"]`: the packager copied it,
|
|
623
|
+
* and the validator invoked here — immediately afterwards — cannot see it.
|
|
624
|
+
*
|
|
625
|
+
* ⚠️ It is not only a metrics hole. `collectNonPortableAssetReferenceIssues` and
|
|
626
|
+
* `collectNonPortableCommandIssues` iterate `bundledFiles`, so in the built lane
|
|
627
|
+
* they never open a single bundled reference file. A non-portable
|
|
628
|
+
* `${CLAUDE_PLUGIN_ROOT}` invocation inside a bundled reference is caught at
|
|
629
|
+
* source and CANNOT be caught in the built artifact.
|
|
630
|
+
*
|
|
631
|
+
* 🔑 Measured vs inferred. The three rows are MEASURED (that probe).
|
|
632
|
+
* That the finding is still LIVE is inferred from this function's own body: it
|
|
633
|
+
* passes only the built `SKILL.md` path, with no registry and no population
|
|
634
|
+
* source rooted at the output tree, so `validateSkillForPackaging` resolves
|
|
635
|
+
* `findProjectRoot` back to the SOURCE project and crawls it under the default
|
|
636
|
+
* exclude — the built tree is never enumerated, `getResource` misses, and the
|
|
637
|
+
* walk starts from nothing. The probe is no longer only a probe: it is pinned as
|
|
638
|
+
* a committed regression test at
|
|
639
|
+
* `packages/cli/test/integration/built-lane-link-graph.integration.test.ts`
|
|
640
|
+
* (fixture `BUNDLING_SKILL_FILES`), asserting the middle row as today's
|
|
641
|
+
* behaviour. When this is fixed, those assertions flip in the fixing change.
|
|
642
|
+
*
|
|
643
|
+
* ⛔ Do NOT fix it by widening the crawl. That same `**\/dist\/**` exclusion is
|
|
644
|
+
* the stated correctness proof for `crawlAndResolveRegistry`'s process-lifetime
|
|
645
|
+
* memo, which the audit lane, the validate lane AND this lane all share — see the
|
|
646
|
+
* comment on `walkRegistryCache` in `validators/packaging-validator.ts`. Widening
|
|
647
|
+
* it retracts that argument for every lane sharing the memo; this is not a
|
|
648
|
+
* one-lane fix.
|
|
519
649
|
*/
|
|
520
650
|
async function runPostBuildValidation(outputPath, validation, allowLedger) {
|
|
521
651
|
const builtSkillPath = safePath.join(outputPath, 'SKILL.md');
|
|
522
|
-
return validateSkillForPackaging(builtSkillPath, validation ? { validation } : undefined, 'built', { allowLedger });
|
|
652
|
+
return validateSkillForPackaging(builtSkillPath, validation ? { validation } : undefined, 'built', { allowLedger, unreadable: 'refuse' });
|
|
523
653
|
}
|
|
524
654
|
/**
|
|
525
655
|
* Assemble the final PackageSkillResult from intermediate data.
|
|
@@ -546,9 +676,6 @@ function assemblePackageResult(input) {
|
|
|
546
676
|
}
|
|
547
677
|
return result;
|
|
548
678
|
}
|
|
549
|
-
// ============================================================================
|
|
550
|
-
// Registry Creation
|
|
551
|
-
// ============================================================================
|
|
552
679
|
/**
|
|
553
680
|
* Build THE project registry: every markdown file under `projectRoot`, parsed,
|
|
554
681
|
* with links resolved and the project config attached.
|
|
@@ -566,12 +693,41 @@ function assemblePackageResult(input) {
|
|
|
566
693
|
* so a lane that built its own config-less registry rewrote frontmatter
|
|
567
694
|
* differently from the lane that used this one — which is why there is now only
|
|
568
695
|
* one builder rather than two that happened to differ in one argument.
|
|
696
|
+
*
|
|
697
|
+
* ## Why the include set is markdown-only, unlike `crawlAndResolveRegistry`'s
|
|
698
|
+
*
|
|
699
|
+
* The difference is deliberate and is NOT a hole that leaves HTML unrewritten.
|
|
700
|
+
* A bundled HTML file joins this registry later, on demand, via
|
|
701
|
+
* {@link registerBundledAssets} — which parses it and resolves its links before
|
|
702
|
+
* the copy step, so `rewriteHtmlLinks` reaches it. Crawling every `.html` under
|
|
703
|
+
* the project up front would parse generated output the build never bundles,
|
|
704
|
+
* and would move `page.md` vs `page.html` id collisions from the narrow
|
|
705
|
+
* bundled-asset path onto the whole-project crawl.
|
|
706
|
+
*
|
|
707
|
+
* Widening this glob would NOT widen what the walker follows: routing is
|
|
708
|
+
* markdown-only regardless — see `isRoutable` in `walk-link-graph.ts`.
|
|
709
|
+
*
|
|
710
|
+
* ## The optional population source, and why it cannot widen that glob
|
|
711
|
+
*
|
|
712
|
+
* `populationSource` replaces the ENUMERATION only. `ResourceRegistry.crawl`
|
|
713
|
+
* re-applies this function's `include` — and the crawl's default `exclude` —
|
|
714
|
+
* to whatever the source offers, through the same compiled matcher the walk
|
|
715
|
+
* itself uses, so a source that enumerates a whole tree still yields exactly
|
|
716
|
+
* the project's markdown here. That is what makes handing this builder a
|
|
717
|
+
* projection a cost change rather than a scope change.
|
|
718
|
+
*
|
|
719
|
+
* Selecting the lane stays the CLI's job: this signature takes a source, never
|
|
720
|
+
* an environment. A library caller that passes nothing keeps the walk.
|
|
569
721
|
*/
|
|
570
|
-
export async function createProjectRegistry(projectRoot) {
|
|
722
|
+
export async function createProjectRegistry(projectRoot, options = {}) {
|
|
571
723
|
const config = await loadConfig(projectRoot);
|
|
572
724
|
const registry = await ResourceRegistry.fromCrawl({
|
|
573
725
|
baseDir: projectRoot,
|
|
574
|
-
include: [
|
|
726
|
+
include: [...LINK_GRAPH_MEMBER_GLOBS],
|
|
727
|
+
// A build must not ship a shorter bundle: a directory this crawl cannot
|
|
728
|
+
// list refuses the run by name — see `RegistryUnreadablePolicy`.
|
|
729
|
+
unreadable: 'refuse',
|
|
730
|
+
...(options.populationSource !== undefined && { populationSource: options.populationSource }),
|
|
575
731
|
}, config === undefined ? undefined : { config });
|
|
576
732
|
registry.resolveLinks();
|
|
577
733
|
return registry;
|
|
@@ -582,30 +738,40 @@ export async function createProjectRegistry(projectRoot) {
|
|
|
582
738
|
* Mirrors ResourceRegistry.validateAgainstCollectionSchema's loading flow so
|
|
583
739
|
* the packager rewrites frontmatter URI-refs against the same schemas the
|
|
584
740
|
* validator uses. Collections without a frontmatterSchema configured are
|
|
585
|
-
* absent from the map.
|
|
586
|
-
*
|
|
587
|
-
*
|
|
741
|
+
* absent from the map.
|
|
742
|
+
*
|
|
743
|
+
* A schema that cannot be resolved, read or parsed is absent from the map too,
|
|
744
|
+
* and that is REPORTED as a `FRONTMATTER_SCHEMA_ERROR` issue in the build's own
|
|
745
|
+
* receipt — the same code `vat validate` gives the same config. It used to be
|
|
746
|
+
* swallowed on the theory that "the validator will surface it elsewhere", which
|
|
747
|
+
* left `vat build` shipping a bundle whose frontmatter URI-refs still pointed
|
|
748
|
+
* at source-tree paths, with a clean report and nothing to say why.
|
|
588
749
|
*/
|
|
589
750
|
async function loadCollectionSchemas(config, baseDir) {
|
|
590
751
|
const schemas = new Map();
|
|
752
|
+
const issues = [];
|
|
591
753
|
const collections = config?.resources?.collections;
|
|
592
754
|
if (!collections)
|
|
593
|
-
return schemas;
|
|
755
|
+
return { schemas, issues };
|
|
594
756
|
for (const [collectionId, collectionConfig] of Object.entries(collections)) {
|
|
595
757
|
const schemaPath = collectionConfig.validation?.frontmatterSchema;
|
|
596
758
|
if (schemaPath === undefined)
|
|
597
759
|
continue;
|
|
598
760
|
try {
|
|
599
761
|
const resolvedPath = resolveAssetReference(schemaPath, baseDir);
|
|
600
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- schema path from validated config
|
|
601
762
|
const content = await readFile(resolvedPath, 'utf-8');
|
|
602
763
|
schemas.set(collectionId, JSON.parse(content));
|
|
603
764
|
}
|
|
604
|
-
catch {
|
|
605
|
-
|
|
765
|
+
catch (error) {
|
|
766
|
+
issues.push(materializeIssue('FRONTMATTER_SCHEMA_ERROR', {
|
|
767
|
+
location: schemaPath,
|
|
768
|
+
message: `Collection "${collectionId}" declares frontmatterSchema "${schemaPath}", which could not ` +
|
|
769
|
+
`be loaded: ${error instanceof Error ? error.message : String(error)}. Frontmatter ` +
|
|
770
|
+
`URI-references in this collection were NOT rewritten for the packaged output.`,
|
|
771
|
+
}));
|
|
606
772
|
}
|
|
607
773
|
}
|
|
608
|
-
return schemas;
|
|
774
|
+
return { schemas, issues };
|
|
609
775
|
}
|
|
610
776
|
/**
|
|
611
777
|
* Generate a synthetic resource ID for a non-markdown asset that collides with an
|
|
@@ -636,26 +802,51 @@ function collectResourcesWithLinks(bundledResources, skillResource) {
|
|
|
636
802
|
* and compute the output `relativePath`. Without this, non-markdown links get stripped
|
|
637
803
|
* to empty `()` parentheses.
|
|
638
804
|
*
|
|
639
|
-
* Collision handling: if an asset's generated
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
805
|
+
* Collision handling: if an asset's generated id is already taken, `addResource`
|
|
806
|
+
* throws. We catch this, skip source-registry indexing for the asset, and return
|
|
807
|
+
* it so the caller can synthesize a unique ID for link rewriting.
|
|
808
|
+
*
|
|
809
|
+
* The clash is a **path-slug** one, not an extension one. This used to claim the
|
|
810
|
+
* example was "paired `config.yaml` + `config.md`, both producing id
|
|
811
|
+
* `resources-config`" — {@link generateIdFromPath} appends the extension, so
|
|
812
|
+
* those are `config-yaml` and `config-md` and cannot collide. What does collide
|
|
813
|
+
* is two paths that flatten to the same slug: `a-b/c.html` and `a/b-c.html` are
|
|
814
|
+
* both `a-b-c-html`.
|
|
643
815
|
*
|
|
644
|
-
* @returns
|
|
645
|
-
*
|
|
816
|
+
* @returns The colliding asset paths, and the collisions themselves. The caller
|
|
817
|
+
* must wire the paths up manually; the collisions are what let the
|
|
818
|
+
* verbatim-copy diagnostic name the file that actually won the id.
|
|
646
819
|
*/
|
|
647
|
-
async function registerBundledAssets(registry, bundledAssets) {
|
|
820
|
+
async function registerBundledAssets(registry, bundledAssets, projectRoot) {
|
|
648
821
|
const collidedAssets = [];
|
|
822
|
+
const collisions = [];
|
|
649
823
|
if (bundledAssets.length === 0) {
|
|
650
|
-
return collidedAssets;
|
|
824
|
+
return { collidedAssets, collisions };
|
|
651
825
|
}
|
|
652
826
|
for (const assetPath of bundledAssets) {
|
|
653
827
|
try {
|
|
654
|
-
|
|
828
|
+
// `addResource` parses the file, so it READS it — which makes this the first
|
|
829
|
+
// place a build touches a linked asset, and the place an unreadable one
|
|
830
|
+
// actually fails. It reported a bare `EACCES … open '/abs/path'` with no
|
|
831
|
+
// skill named and no remedy: the same shape as the copiers, one step
|
|
832
|
+
// earlier, and the step that fires first.
|
|
833
|
+
await withFsAttribution(`linked file ${issueLocation(assetPath, projectRoot) || '.'}`, () => registry.addResource(assetPath),
|
|
834
|
+
// The file is read to discover its links before anything is copied, so
|
|
835
|
+
// naming the copy would point past the step that actually failed.
|
|
836
|
+
'read while collecting the files this skill links to', READ_REMEDY);
|
|
655
837
|
}
|
|
656
838
|
catch (error) {
|
|
657
|
-
|
|
839
|
+
// `addResource` (singular) THROWS on a collision and — unlike
|
|
840
|
+
// `addResources` — records nothing in the registry's collision log. So
|
|
841
|
+
// this catch is the ONLY place a bundled-asset collision is ever
|
|
842
|
+
// observable; drop the structured error here and the fact is gone.
|
|
843
|
+
if (error instanceof DuplicateResourceIdError) {
|
|
658
844
|
collidedAssets.push(assetPath);
|
|
845
|
+
collisions.push({
|
|
846
|
+
id: error.id,
|
|
847
|
+
existingPath: error.existingPath,
|
|
848
|
+
conflictingPath: error.conflictingPath,
|
|
849
|
+
});
|
|
659
850
|
}
|
|
660
851
|
else {
|
|
661
852
|
throw error;
|
|
@@ -663,7 +854,7 @@ async function registerBundledAssets(registry, bundledAssets) {
|
|
|
663
854
|
}
|
|
664
855
|
}
|
|
665
856
|
registry.resolveLinks();
|
|
666
|
-
return collidedAssets;
|
|
857
|
+
return { collidedAssets, collisions };
|
|
667
858
|
}
|
|
668
859
|
/**
|
|
669
860
|
* Manually set `resolvedId` on links pointing to collided assets.
|
|
@@ -798,9 +989,23 @@ function applyFilesEntriesToPathMap(filesConfig, projectRoot, outputPath, pathMa
|
|
|
798
989
|
// joinUnderRoot guards against a dest escaping the output dir (zip-slip class),
|
|
799
990
|
// defense-in-depth beyond the schema refine on SkillFileEntry.dest.
|
|
800
991
|
const absoluteDest = safePath.joinUnderRoot(outputPath, fileEntry.dest);
|
|
801
|
-
// Validate source exists at build time
|
|
802
|
-
//
|
|
803
|
-
|
|
992
|
+
// Validate source exists at build time.
|
|
993
|
+
//
|
|
994
|
+
// `statSync` in a guard, not `existsSync`: `existsSync` swallows EACCES and
|
|
995
|
+
// answers FALSE, so a source the process cannot REACH — one under a directory
|
|
996
|
+
// it may not traverse — was reported as "does not exist", and for a `dist/`
|
|
997
|
+
// path `buildArtifactHint` then told the author to run a build that would not
|
|
998
|
+
// have helped. A wrong diagnosis with a confident wrong remedy attached.
|
|
999
|
+
// Only ENOENT is absence; a refusal is reported as itself, naming the entry.
|
|
1000
|
+
try {
|
|
1001
|
+
statSync(absoluteSource);
|
|
1002
|
+
}
|
|
1003
|
+
catch (error) {
|
|
1004
|
+
if (error.code !== 'ENOENT') {
|
|
1005
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1006
|
+
throw new Error(`files entry for skill '${skillName}': source '${fileEntry.source}' could not be read: ${reason}. ` +
|
|
1007
|
+
`Check the file's permissions and ownership, and that every directory above it is traversable.`);
|
|
1008
|
+
}
|
|
804
1009
|
throw new Error(`files entry for skill '${skillName}': source '${fileEntry.source}' does not exist.${buildArtifactHint(fileEntry.source)}`);
|
|
805
1010
|
}
|
|
806
1011
|
// If this source was auto-discovered, override its destination; otherwise add it
|
|
@@ -933,21 +1138,6 @@ function stampDeferredDestResolvedId(resources, absDestTarget, id) {
|
|
|
933
1138
|
}
|
|
934
1139
|
return linked;
|
|
935
1140
|
}
|
|
936
|
-
// ============================================================================
|
|
937
|
-
// Path Map Building
|
|
938
|
-
// ============================================================================
|
|
939
|
-
/**
|
|
940
|
-
* Determine the resource subdirectory for a file.
|
|
941
|
-
*
|
|
942
|
-
* For claude-web target: uses the existing references directory.
|
|
943
|
-
* For claude-code target: uses content-type routing based on file extension.
|
|
944
|
-
*/
|
|
945
|
-
export function getResourceSubdirForFile(filePath, target) {
|
|
946
|
-
if (target === 'claude-web') {
|
|
947
|
-
return CLAUDE_WEB_REFERENCES_SUBDIR;
|
|
948
|
-
}
|
|
949
|
-
return getTargetSubdir(filePath);
|
|
950
|
-
}
|
|
951
1141
|
/**
|
|
952
1142
|
* The FILENAME_COLLISION finding for one pair of sources that package to one dest.
|
|
953
1143
|
*
|
|
@@ -1132,32 +1322,175 @@ async function copyAndRewriteFiles(skillPath, bundledFiles, ctx) {
|
|
|
1132
1322
|
*
|
|
1133
1323
|
* For non-markdown files, performs a plain binary copy.
|
|
1134
1324
|
*/
|
|
1325
|
+
/**
|
|
1326
|
+
* Re-base a resource's link spans onto the frontmatter-stripped body.
|
|
1327
|
+
*
|
|
1328
|
+
* ## 🚨 Why this exists: the span rewrite was INERT here, and could mis-splice
|
|
1329
|
+
*
|
|
1330
|
+
* `transformContent` identifies each link by its parsed `[startOffset,
|
|
1331
|
+
* endOffset)` span rather than by replaying a regex and correlating on href —
|
|
1332
|
+
* which is the whole point, because the two grammars disagree about what the
|
|
1333
|
+
* href IS. Its docstring states the precondition outright: *links must come from
|
|
1334
|
+
* the same bytes as content*.
|
|
1335
|
+
*
|
|
1336
|
+
* This call site violated it. `resource.links` are parsed from the WHOLE FILE,
|
|
1337
|
+
* and `editor.body` has the frontmatter block removed — so every offset was off
|
|
1338
|
+
* by the frontmatter's length. Measured on a four-line skill: span `[45, 77)`
|
|
1339
|
+
* against a body shifted by 40, where `body.slice(45, 81)` is `"ails.\n"`. Every
|
|
1340
|
+
* splice declined and every link fell back to the pre-fix regex path, so the
|
|
1341
|
+
* headline defect (`[](url)` silently not rewritten) was still
|
|
1342
|
+
* shipping. Worse, a stale span can land on a *different*, structurally valid
|
|
1343
|
+
* link and rewrite ITS href to the first link's target.
|
|
1344
|
+
*
|
|
1345
|
+
* ## The re-base is VERIFIED, not assumed
|
|
1346
|
+
*
|
|
1347
|
+
* Subtracting `content.length - body.length` is correct if and only if `body` is
|
|
1348
|
+
* a literal SUFFIX of `content` — that is exactly the statement "every body index
|
|
1349
|
+
* `i` is content index `i + offset`". `String.endsWith` decides it outright, so
|
|
1350
|
+
* the arithmetic is checked rather than argued, and a body that is not a suffix
|
|
1351
|
+
* comes back with its spans REMOVED instead of with coordinates nothing verified.
|
|
1352
|
+
* `openFrontmatter` does yield a suffix today, across CRLF, a commented block, an
|
|
1353
|
+
* absent block and empty input; the check is what keeps that a fact rather than a
|
|
1354
|
+
* comment that outlives the code it describes.
|
|
1355
|
+
*
|
|
1356
|
+
* ⛔ **This used to claim `splicableFrom` as its backstop, and that claim was
|
|
1357
|
+
* false.** The words were: "`splicableFrom` refuses any span whose destination is
|
|
1358
|
+
* not exactly the parser's href, so a misalignment that survives this degrades to
|
|
1359
|
+
* *no rewrite* rather than to a wrong one." It does not. That comparison is keyed
|
|
1360
|
+
* on the HREF, which two links in one document routinely share, so a stale span
|
|
1361
|
+
* landing on a same-href neighbour passes it and the neighbour is rewritten
|
|
1362
|
+
* through the first link's metadata. Constructed and pinned in
|
|
1363
|
+
* `packages/resources/test/content-transform.test.ts` › *two links sharing an
|
|
1364
|
+
* href defeat the destination comparison*, where a tuned frontmatter length makes
|
|
1365
|
+
* two labels swap places. A mitigation keyed on a value the two candidates have
|
|
1366
|
+
* in common cannot be a guarantee, and a guard resting on one is resting on
|
|
1367
|
+
* nothing.
|
|
1368
|
+
*
|
|
1369
|
+
* 🔑 What holds instead, in full, because the precondition has two halves:
|
|
1370
|
+
*
|
|
1371
|
+
* 1. *The spans address `content`* — discharged by the reader, not by this
|
|
1372
|
+
* function. `resource.links` and `content` must come from the same decode of
|
|
1373
|
+
* the same file, which is why the caller's read is pinned to
|
|
1374
|
+
* `readTextContent` with a 🚨 of its own. A second reader is the one way this
|
|
1375
|
+
* half fails, and it fails invisibly.
|
|
1376
|
+
* 2. *The subtraction maps `content` onto `body`* — discharged here, by
|
|
1377
|
+
* `endsWith`.
|
|
1378
|
+
*
|
|
1379
|
+
* `splicableFrom`'s destination check remains a useful mitigation for the
|
|
1380
|
+
* residue. It is not counted on, and nothing here degrades gracefully because of
|
|
1381
|
+
* it.
|
|
1382
|
+
*
|
|
1383
|
+
* @param content - The file as read, which the links were parsed from
|
|
1384
|
+
* @param body - The frontmatter-stripped body the rewrite will run over
|
|
1385
|
+
* @param links - The resource's links, carrying whole-file offsets
|
|
1386
|
+
* @param onUnverifiable - Called when `body` is not a suffix of `content`, so the
|
|
1387
|
+
* caller can say which file lost its span-driven rewrite. Never called on the
|
|
1388
|
+
* normal path.
|
|
1389
|
+
* @returns The same links with offsets stated against `body`, or with no offsets
|
|
1390
|
+
* at all when the re-base could not be verified
|
|
1391
|
+
*/
|
|
1392
|
+
export function bodyRelativeLinks(content, body, links, onUnverifiable) {
|
|
1393
|
+
if (!content.endsWith(body)) {
|
|
1394
|
+
onUnverifiable();
|
|
1395
|
+
return links.map(withoutSpan);
|
|
1396
|
+
}
|
|
1397
|
+
const offset = content.length - body.length;
|
|
1398
|
+
if (offset === 0)
|
|
1399
|
+
return [...links];
|
|
1400
|
+
return links.map((link) => ({
|
|
1401
|
+
...link,
|
|
1402
|
+
...(link.startOffset === undefined ? {} : { startOffset: link.startOffset - offset }),
|
|
1403
|
+
...(link.endOffset === undefined ? {} : { endOffset: link.endOffset - offset }),
|
|
1404
|
+
}));
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* The link with its coordinates dropped and everything else kept.
|
|
1408
|
+
*
|
|
1409
|
+
* ⚠️ Dropping them is the point, and returning the link unchanged would be the
|
|
1410
|
+
* bug: an unrebased whole-file span still ADDRESSES the body's bytes, just the
|
|
1411
|
+
* wrong ones, so `splicableFrom` would happily splice at it. Without a span the
|
|
1412
|
+
* link is `UNRECOGNISED`, which routes it to the pre-span regex replay — the
|
|
1413
|
+
* behaviour this call site had before spans existed. A documented degrade, not a
|
|
1414
|
+
* silent one; the caller warns.
|
|
1415
|
+
*
|
|
1416
|
+
* @param link - A link whose offsets could not be re-based
|
|
1417
|
+
* @returns The same link with no `startOffset` and no `endOffset` key
|
|
1418
|
+
*/
|
|
1419
|
+
function withoutSpan(link) {
|
|
1420
|
+
const stripped = { ...link };
|
|
1421
|
+
delete stripped.startOffset;
|
|
1422
|
+
delete stripped.endOffset;
|
|
1423
|
+
return stripped;
|
|
1424
|
+
}
|
|
1135
1425
|
async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
1426
|
+
// Through the SAME attribution point as the `files:` lanes. This is the default
|
|
1427
|
+
// path for every ordinary markdown-linked file in a build — the most-travelled
|
|
1428
|
+
// copier in the packager — and it was the last one still letting a raw errno be
|
|
1429
|
+
// the build's whole explanation: `EACCES: permission denied, open '/abs/path'`,
|
|
1430
|
+
// with no skill named and no remedy. Easy to believe it was covered because the
|
|
1431
|
+
// `files:` fix landed in this same file; it is a different function.
|
|
1432
|
+
const subject = `linked file ${issueLocation(sourcePath, ctx.projectRoot) || '.'}`;
|
|
1136
1433
|
// Ensure target directory exists
|
|
1137
|
-
|
|
1138
|
-
|
|
1434
|
+
await withFsAttribution(subject, async () => {
|
|
1435
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
1436
|
+
});
|
|
1139
1437
|
const lower = sourcePath.toLowerCase();
|
|
1140
1438
|
const isMarkdown = lower.endsWith('.md');
|
|
1141
1439
|
const isHtml = lower.endsWith('.html') || lower.endsWith('.htm');
|
|
1142
1440
|
// Non-rewritable files or rewriting disabled: plain binary copy
|
|
1143
1441
|
if ((!isMarkdown && !isHtml) || !ctx.rewriteLinks) {
|
|
1144
|
-
await copyFile(sourcePath, targetPath);
|
|
1442
|
+
await withFsAttribution(subject, () => copyFile(sourcePath, targetPath));
|
|
1145
1443
|
return;
|
|
1146
1444
|
}
|
|
1147
|
-
// Read source file
|
|
1148
|
-
//
|
|
1149
|
-
|
|
1445
|
+
// Read source file.
|
|
1446
|
+
//
|
|
1447
|
+
// 🚨 `readTextContent`, never `readFile(path, 'utf-8')` — and here the reason
|
|
1448
|
+
// is sharper than the general one `link-parser.ts:149` gives. THE LINKS WERE
|
|
1449
|
+
// PARSED FROM THIS FILE BY THAT READER, and `bodyRelativeLinks` below
|
|
1450
|
+
// re-bases their offsets by differencing two string lengths. Two different
|
|
1451
|
+
// readers make that subtraction meaningless: `readTextContent` strips a BOM
|
|
1452
|
+
// and decodes UTF-16/32, so on a BOM-bearing file `readFile` returned a
|
|
1453
|
+
// string one character longer, `OPENING_FENCE` (`/^---\r?\n/`) then failed to
|
|
1454
|
+
// match, `openFrontmatter` reported no frontmatter, the offset came out 0,
|
|
1455
|
+
// and every span stayed whole-file — silently reverting the fix this call
|
|
1456
|
+
// site exists to deliver.
|
|
1457
|
+
//
|
|
1458
|
+
// ⛔ This comment used to add "it failed CLOSED rather than corrupting, only
|
|
1459
|
+
// because `splicableFrom` compares the destination to the href." That is not
|
|
1460
|
+
// a guarantee and must not be read as one: the destination check is keyed on
|
|
1461
|
+
// the href, which two links in one document routinely share, so a stale span
|
|
1462
|
+
// landing on a same-href neighbour passes it (constructed in
|
|
1463
|
+
// `content-transform.test.ts` › *two links sharing an href defeat the
|
|
1464
|
+
// destination comparison*). The BOM shift also survives
|
|
1465
|
+
// `bodyRelativeLinks`'s own `endsWith` check, because a body equal to the
|
|
1466
|
+
// content IS a suffix of it — nothing downstream can see that the links came
|
|
1467
|
+
// from a different decode. **One reader is the only thing standing between
|
|
1468
|
+
// this lane and a wrong rewrite.** Do not swap it.
|
|
1469
|
+
//
|
|
1470
|
+
// It also stops a UTF-16 source being written back as mojibake, which the
|
|
1471
|
+
// `utf-8` read did on the copy as well as the rewrite.
|
|
1472
|
+
const { text: content } = await withFsAttribution(subject, () => readTextContent(sourcePath),
|
|
1473
|
+
// Not "copied": the read is the step that failed, and this lane reads before
|
|
1474
|
+
// it rewrites, so naming the copy would point past the actual failure.
|
|
1475
|
+
'read for link rewriting', READ_REMEDY);
|
|
1150
1476
|
// Look up the resource in the "from" registry
|
|
1151
1477
|
const resource = ctx.fromRegistry.getResource(safePath.resolve(sourcePath));
|
|
1152
1478
|
if (!resource) {
|
|
1153
|
-
// Resource not in registry — write content as-is
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
//
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
//
|
|
1160
|
-
|
|
1479
|
+
// Resource not in registry — write content as-is, links unrewritten.
|
|
1480
|
+
//
|
|
1481
|
+
// The cause used to be guessed from the basename ("typically an ID
|
|
1482
|
+
// collision with a same-named markdown file"), which is a plausible
|
|
1483
|
+
// hypothesis this function had no way to check, and which named no file the
|
|
1484
|
+
// author could go look at. The collision is now looked up, so the message
|
|
1485
|
+
// either names the winning file as an observed fact or says nothing about
|
|
1486
|
+
// cause at all.
|
|
1487
|
+
const winner = ctx.duplicateIdDrops.get(toForwardSlash(sourcePath));
|
|
1488
|
+
ctx.warn(winner === undefined
|
|
1489
|
+
? `Copied '${sourcePath}' verbatim without link rewriting: it is not in the resource registry. ` +
|
|
1490
|
+
`Source-relative links inside it are not rewritten.`
|
|
1491
|
+
: `Copied '${sourcePath}' verbatim without link rewriting: it lost a resource-id collision to ` +
|
|
1492
|
+
`'${winner}', which was registered first and holds the id. Source-relative links inside it are not rewritten.`);
|
|
1493
|
+
await withFsAttribution(subject, () => writeFile(targetPath, content, 'utf-8'), WRITE_ACTION);
|
|
1161
1494
|
return;
|
|
1162
1495
|
}
|
|
1163
1496
|
// HTML: offset-splice link rewrite (no frontmatter, no template body rewrite).
|
|
@@ -1167,8 +1500,7 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1167
1500
|
ctx.warn(`Could not rewrite <${info.tagName} ${info.attr}="${info.from}"> in '${sourcePath}' (${info.reason}); ` +
|
|
1168
1501
|
`the original value was kept.`);
|
|
1169
1502
|
});
|
|
1170
|
-
|
|
1171
|
-
await writeFile(targetPath, rewritten, 'utf-8');
|
|
1503
|
+
await withFsAttribution(subject, () => writeFile(targetPath, rewritten, 'utf-8'), WRITE_ACTION);
|
|
1172
1504
|
return;
|
|
1173
1505
|
}
|
|
1174
1506
|
// Parse once via FrontmatterEditor so comments survive any frontmatter
|
|
@@ -1176,7 +1508,13 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1176
1508
|
// for the existing rule/template body-link rewrite contract (unchanged).
|
|
1177
1509
|
const editor = openFrontmatter(content);
|
|
1178
1510
|
// Body rewrite (existing behavior, unchanged contract).
|
|
1179
|
-
|
|
1511
|
+
const rebased = bodyRelativeLinks(content, editor.body, resource.links, () => {
|
|
1512
|
+
ctx.warn(`Rewrote links in '${sourcePath}' without parsed spans: the frontmatter-stripped body is not a ` +
|
|
1513
|
+
`suffix of the file as read, so the offsets the parser reported cannot be moved onto it. ` +
|
|
1514
|
+
`Links are matched by href instead, which is the pre-span behaviour — a link the two grammars ` +
|
|
1515
|
+
`disagree about (an image inside a link, most often) may go unrewritten.`);
|
|
1516
|
+
});
|
|
1517
|
+
editor.body = transformContent(editor.body, rebased, {
|
|
1180
1518
|
linkRewriteRules: ctx.rewriteRules,
|
|
1181
1519
|
resourceRegistry: ctx.toRegistry,
|
|
1182
1520
|
sourceFilePath: targetPath, // Output path so relativePath is computed from output location
|
|
@@ -1196,8 +1534,7 @@ async function copyAndRewriteFile(sourcePath, targetPath, ctx) {
|
|
|
1196
1534
|
}
|
|
1197
1535
|
}
|
|
1198
1536
|
}
|
|
1199
|
-
|
|
1200
|
-
await writeFile(targetPath, editor.toString(), 'utf-8');
|
|
1537
|
+
await withFsAttribution(subject, () => writeFile(targetPath, editor.toString(), 'utf-8'), WRITE_ACTION);
|
|
1201
1538
|
}
|
|
1202
1539
|
/**
|
|
1203
1540
|
* Build the per-href rewrite callback used for frontmatter URI-refs and HTML attributes.
|
|
@@ -1251,10 +1588,10 @@ function buildHrefRewriter(fromRegistry, toRegistry, sourcePath, targetPath, pro
|
|
|
1251
1588
|
* but this check acts as a safety net in case files are introduced through other means.
|
|
1252
1589
|
*/
|
|
1253
1590
|
async function validateNoNestedSkillMd(outputPath, skillName) {
|
|
1254
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- outputPath is validated
|
|
1255
1591
|
const entries = readdirSync(outputPath, { recursive: true, withFileTypes: true });
|
|
1256
1592
|
const nestedSkillMds = entries
|
|
1257
|
-
|
|
1593
|
+
// Followed: a symlinked nested SKILL.md is a nested skill marker all the same.
|
|
1594
|
+
.filter(entry => entry.name === 'SKILL.md' && direntKindFollowingSync(entry.parentPath, entry) === 'file')
|
|
1258
1595
|
.map(entry => safePath.relative(outputPath, safePath.join(entry.parentPath, entry.name)))
|
|
1259
1596
|
.filter(relativePath => relativePath !== 'SKILL.md'); // Exclude the root SKILL.md
|
|
1260
1597
|
if (nestedSkillMds.length > 0) {
|
|
@@ -1380,9 +1717,10 @@ export function generateTargetPath(filePath, basePath, strategy = 'basename', st
|
|
|
1380
1717
|
// Strip prefix from relative path (if specified)
|
|
1381
1718
|
// Works for both resource-id and preserve-path strategies
|
|
1382
1719
|
if (stripPrefix) {
|
|
1383
|
-
//
|
|
1384
|
-
|
|
1385
|
-
const
|
|
1720
|
+
// `relPath` is already forward-slashed by `safePath.relative`; `stripPrefix`
|
|
1721
|
+
// is author-written config, so its backslashes read as separators anywhere.
|
|
1722
|
+
const normalizedRelPath = relPath;
|
|
1723
|
+
const normalizedPrefix = toForwardSlashAnyPlatform(stripPrefix).replace(/\/$/, ''); // Remove trailing slash
|
|
1386
1724
|
if (normalizedRelPath.startsWith(normalizedPrefix + '/')) {
|
|
1387
1725
|
// Strip the prefix and leading slash
|
|
1388
1726
|
relPath = normalizedRelPath.substring(normalizedPrefix.length + 1);
|
|
@@ -1422,14 +1760,13 @@ const ZIP_SIZE_ERROR_BYTES = 8 * 1024 * 1024;
|
|
|
1422
1760
|
* Thrown when a claude-web ZIP exceeds the 8MB Claude.ai upload limit.
|
|
1423
1761
|
* The CLI catches this and exits with code 1.
|
|
1424
1762
|
*/
|
|
1425
|
-
export class ZipSizeLimitError extends
|
|
1763
|
+
export class ZipSizeLimitError extends VatError {
|
|
1426
1764
|
sizeBytes;
|
|
1427
1765
|
limitBytes;
|
|
1428
1766
|
constructor(sizeBytes, limitBytes) {
|
|
1429
1767
|
const mb = (sizeBytes / 1024 / 1024).toFixed(1);
|
|
1430
|
-
super(`ZIP size ${mb}MB exceeds 8MB limit for Claude.ai upload. ` +
|
|
1768
|
+
super('ZIP_SIZE_LIMIT', `ZIP size ${mb}MB exceeds 8MB limit for Claude.ai upload. ` +
|
|
1431
1769
|
`Reduce the number of linked resources or use --target claude-code.`);
|
|
1432
|
-
this.name = 'ZipSizeLimitError';
|
|
1433
1770
|
this.sizeBytes = sizeBytes;
|
|
1434
1771
|
this.limitBytes = limitBytes;
|
|
1435
1772
|
}
|
|
@@ -1441,7 +1778,6 @@ export class ZipSizeLimitError extends Error {
|
|
|
1441
1778
|
* @param zipPath - Path to the ZIP file
|
|
1442
1779
|
*/
|
|
1443
1780
|
function validateZipSize(zipPath) {
|
|
1444
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- zipPath is constructed from validated outputPath
|
|
1445
1781
|
const stats = statSync(zipPath);
|
|
1446
1782
|
const bytes = stats.size;
|
|
1447
1783
|
if (bytes >= ZIP_SIZE_ERROR_BYTES) {
|
|
@@ -1522,7 +1858,6 @@ async function createNpmPackage(outputPath, metadata) {
|
|
|
1522
1858
|
files: ['**/*.md'],
|
|
1523
1859
|
};
|
|
1524
1860
|
const packageJsonPath = safePath.join(outputPath, PACKAGE_JSON_FILENAME);
|
|
1525
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is constructed from validated outputPath
|
|
1526
1861
|
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf-8');
|
|
1527
1862
|
// For now, just return a placeholder path
|
|
1528
1863
|
// Full npm pack implementation would require running `npm pack`
|
|
@@ -1547,7 +1882,6 @@ async function createMarketplaceManifest(outputPath, metadata) {
|
|
|
1547
1882
|
created: new Date().toISOString(),
|
|
1548
1883
|
};
|
|
1549
1884
|
const manifestPath = safePath.join(dirname(outputPath), `${metadata.name}.marketplace.json`);
|
|
1550
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is constructed from validated outputPath
|
|
1551
1885
|
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
1552
1886
|
return manifestPath;
|
|
1553
1887
|
}
|
|
@@ -1561,6 +1895,15 @@ async function createMarketplaceManifest(outputPath, metadata) {
|
|
|
1561
1895
|
* @returns Default output path
|
|
1562
1896
|
*/
|
|
1563
1897
|
function getDefaultSkillOutputPath(skillPath, skillName) {
|
|
1898
|
+
// The name may have come from the H1 title when frontmatter carries none,
|
|
1899
|
+
// and the directory this returns is `rm -rf`'d before the package is
|
|
1900
|
+
// written: a SKILL.md headed `# ../../../canary` used to delete `<root>/canary`
|
|
1901
|
+
// through the public `packageSkill` export, issues empty, `hasErrors` false.
|
|
1902
|
+
if (!isSingleFsSegment(skillName)) {
|
|
1903
|
+
throw new VatError('SKILL_NAME_NOT_A_SEGMENT', `Cannot derive an output directory from skill name "${skillName}": a name must be a single ` +
|
|
1904
|
+
`path segment (no separators, not "." or ".."). Declare \`name:\` in the SKILL.md frontmatter ` +
|
|
1905
|
+
`or pass an explicit outputPath.`);
|
|
1906
|
+
}
|
|
1564
1907
|
const skillPackageRoot = findPackageRoot(skillPath);
|
|
1565
1908
|
return safePath.join(skillPackageRoot, 'dist', 'skills', skillName);
|
|
1566
1909
|
}
|
|
@@ -1579,7 +1922,6 @@ function findPackageRoot(skillPath, fallbackToSkillDir = false) {
|
|
|
1579
1922
|
// Walk up until we find a package.json or hit the filesystem root
|
|
1580
1923
|
while (currentDir !== dirname(currentDir)) {
|
|
1581
1924
|
const packageJsonPath = safePath.join(currentDir, PACKAGE_JSON_FILENAME);
|
|
1582
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Searching for package.json
|
|
1583
1925
|
if (existsSync(packageJsonPath)) {
|
|
1584
1926
|
return currentDir;
|
|
1585
1927
|
}
|