additionalFiles flatten to basenames on push and lack a runtime access API, creating a source-vs-pulled layout mismatch
Opened by martinwhite1617 · 4/22/2026· Shipped 4/23/2026
Context
I'm building an extension (@hivemq/adversarial-review) that ships LLM prompts as markdown alongside its TypeScript models. The models need to read those prompts at runtime. The additionalFiles manifest field looks like the right mechanism, but the round-trip has some sharp edges that make it hard to use in practice.
Current behavior
Push flattens directory structure to basenames. In
src/libswamp/extensions/push.ts:1115-1118:for (const af of input.additionalFilePaths) { const destPath = join(extDir, "files", basename(af)); await Deno.copyFile(af, destPath); }
An entry like
extensions/models/adversarial-review/prompts/review.mdgets packed asfiles/review.md. Any directory structure under the source path is lost.Basename collisions silently overwrite. Because the
files/dir is flat, twoadditionalFilesentries with the same basename (e.g.prompts/review.mdandtemplates/review.md) produce a collision.Deno.copyFileoverwrites; nothing validates uniqueness before packing.Source-mode and pulled-mode layouts diverge. Pulled consumers find the file at
.swamp/pulled-extensions/<name>/files/<basename>(src/libswamp/extensions/pull.ts:848, 985-990). But during local extension development (swamp extension source add <path>), the same model runs againstextensions/models/<name>/prompts/<basename>in the source repo. A model that doesDeno.readTextFilemust handle both paths, or its smoke tests pass while pulled consumers break.No runtime context API for locating
additionalFiles.MethodContextexposesrepoDir(the consumer's repo), but there's nocontext.extensionFilesDirorcontext.readExtensionFile(name). Models have to hardcode the pulled-extensions path layout, which means they're coupled to the storage convention rather than a documented API.
Impact
The combination makes "ship an asset alongside the code that needs it" harder than expected:
- Authors can't organize assets into subdirectories (e.g.
prompts/reviewer/,prompts/judge/) — they all flatten together. - Authors have to implement runtime path detection for source vs pulled mode, or they'll have a smoke-test blind spot.
- Authors who want to avoid all of that fall back to inlining assets as TypeScript string constants at build time, which loses the benefit of
additionalFilesentirely.
Proposed direction
Not prescriptive — open to whatever fits swamp's model. Some combination of:
- Preserve directory structure on push, using the relative path from the manifest rather than
basename(af). The archive'sfiles/dir would mirror the source layout;pull.ts's existing recursivecopyDiralready preserves that on the consumer side. - Reject basename collisions at push time with a clear validation error, regardless of whether (1) lands.
- Add a
MethodContexthelper likecontext.extensionFile(relativePath: string): stringthat returns the correct absolute path whether the extension is source-loaded or pulled. This would let models doawait Deno.readTextFile(context.extensionFile("prompts/review.md"))portably.
Workaround in use
Generating a prompts.ts from .md files at build time and importing strings from it. Works, but means the .md files never actually ship with the extension — the additionalFiles mechanism stays unused.
Links to code
- Push flattening:
src/libswamp/extensions/push.ts:1115-1118 - Pull unpack location:
src/libswamp/extensions/pull.ts:848, 985-990
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.