Releases: systeminit/swamp
swamp 20260318.000231.0-sha.42789cff
What's Changed
- feat: wire custom datastores into repo context (#749)
Summary
PR 3 of 4 in the custom datastore extension series. PRs 1-2 (#735, #745) established the datastore type registry, loader, and extension push/pull. This PR wires registered custom datastore types into the runtime — every switch point in the repo context that previously hardcoded if (type === "filesystem") ... else if (type === "s3") ... now has a third path that delegates to the DatastoreProvider obtained from the registry.
- Add
CustomDatastoreConfigto theDatastoreConfigdiscriminated union with eagerly-resolveddatastorePath/cachePath - Handle custom types in
resolve_datastore.tsfor env var, CLI arg, and.swamp.yamlpaths - Add custom branches to all 6
repo_context.tsswitch points (read-only, locked, unlocked, model lock, model lock acquisition, global lock) - Wire custom verifier/sync into
datastore status,datastore sync, anddatastore lockcommands - Generalize
DatastoreSyncCoordinatorto accept anySyncableService, not justS3CacheSyncService - 11 new tests covering custom type resolution, config validation, type guard, and error paths
Design decisions and trade-offs
Built-in paths untouched
The filesystem and S3 code paths are not modified — they continue to use their richer interfaces (S3CacheSyncService.pushAll(), pullChangedForModel(), etc.) that the generic DatastoreProvider interface cannot expose. Custom types get the generic pullChanged()/pushChanged() interface. This means custom datastores don't support model-scoped sync (they pull everything), but it avoids breaking the well-tested built-in paths.
Custom check first for type narrowing
Because CustomDatastoreConfig.type is string (it can be any registered type name), TypeScript can't narrow the discriminated union when checking type === "filesystem" first. All switch points check isCustomDatastoreConfig() first, then the built-in types narrow correctly in else if / else branches. This is a compile-time concern only — no runtime cost.
Single provider instance in acquireModelLocks
The custom DatastoreProvider is resolved once at the top of acquireModelLocks and reused for global lock inspection, per-model lock creation, sync service creation, and the flush push lock. This avoids repeated registry lookups and prevents correctness issues with stateful providers that track locks internally.
Sync coordinator label parameter
The coordinator's log messages now use a label parameter ("S3", the custom type name, etc.) instead of hardcoded "S3". Existing S3 users see identical log output — the label defaults to "datastore" but S3 registration passes "S3" explicitly.
No health check on read-only path
The read-only path (requireInitializedRepoReadOnly) does not run a health check for custom datastores — it only ensures the cache directory exists, matching what S3 does. Health checks run in datastore status where they belong. This avoids adding potentially slow network round-trips to every read-only command (search, list, get, validate).
datastore sync for custom types
Custom sync uses the generic pullChanged()/pushChanged() interface (no pushAll/pullAll/sync full-sync methods). The datastore sync command for custom types does pull + push sequentially. This parallels how requireInitializedRepo also registers a sync service with the coordinator, so there is some redundant pull/push — but this matches the existing S3 pattern where the coordinator does incremental sync and the manual command does full sync.
User impact
- Filesystem users: Zero change. No new code executes.
- S3 users: Zero behavioral change. Log messages unchanged. Same lock/sync lifecycle.
- Custom datastore users: Can now configure a custom datastore in
.swamp.yamlor viaSWAMP_DATASTOREenv var and have it work with locking, sync, health checks, and all CLI commands.
Verification
-
deno check— clean (0 errors) -
deno lint— clean -
deno fmt— clean -
deno run test— 3186 passed, 0 failed -
deno run compile— binary compiles
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260318.000231.0-sha.42789cff/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260318.000231.0-sha.42789cff/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260318.000231.0-sha.42789cff/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260318.000231.0-sha.42789cff/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.234411.0-sha.5705468c
What's Changed
- fix: skip quality-check false positives when deno 2.7.x exits non-zero with no output (#748)
Summary
extension pushwas blocked by spurious quality-check failures on Deno 2.7.5 where bothdeno fmt --check --no-configanddeno lint --no-configexit with a non-zero code but write nothing to stdout or stderr- The checker was treating any non-zero exit code as a real quality issue, producing
"Formatting"and"Lint"errors with empty""descriptions - Fix: only add a quality issue to the result when the tool produces non-empty output — an empty-output non-zero exit is a deno version-specific no-op, not a real problem
Root cause
Deno 2.7.x introduced a behavioural change where deno fmt --check --no-config and deno lint --no-config can exit 1 with no diagnostic output when run as a subprocess via Deno.Command with piped stdio (confirmed on Deno 2.7.5, macOS aarch64). On Deno 2.6.x the same invocation exits 0 on clean files.
Every known version of deno fmt --check and deno lint that detects a real formatting or lint problem always writes a non-empty diagnostic to stderr. An empty-output non-zero exit is therefore a version-specific no-op rather than a genuine quality failure, so skipping it is safe.
The deno changelog and issue tracker do not document this as an intentional change; it is most likely a regression introduced in the 2.7.x series related to how these tools detect TTY/pipe mode and gate diagnostic output accordingly.
Test plan
- All 25
extension_quality_checker_test.tstests continue to pass -
deno fmt --check— no formatting issues -
deno lint— no lint errors -
deno run test— 3183 passed, 0 failed
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.234411.0-sha.5705468c/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.234411.0-sha.5705468c/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.234411.0-sha.5705468c/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.234411.0-sha.5705468c/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.225941.0-sha.96bd3573
What's Changed
- fix: guard against path traversal in extension archive extraction and add driver/datastore tests (#746)
Summary
Addresses two issues raised in PR review:
-
Path traversal during archive extraction (
src/cli/commands/extension_pull.ts): Aftertarextracts an extension archive to a temp directory, each extracted file path is now resolved and validated to confirm it starts within the temp directory. If any entry contains path traversal sequences (e.g.../../.bashrc) that would escape the temp dir, aUserErroris thrown before any further processing occurs. -
Missing test coverage for driver/datastore content extraction (
src/domain/extensions/extension_content_extractor_test.ts): Added 8 unit tests forextractDriverFromSourceandextractDatastoreFromSourcefollowing the same patterns as the existing vault tests:- Extracts type, name, and description
- Extracts
configSchemafields (inlinez.object) - Skips files without the relevant export (
driver/datastore) - Skips exports that are missing the required
typefield
Test Plan
- All 3183 existing tests continue to pass (
deno run test) - 8 new unit tests added and passing for driver/datastore extraction
-
deno checkpasses (no type errors) -
deno lintpasses (no lint errors) -
deno fmt --checkpasses (no formatting issues)
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.225941.0-sha.96bd3573/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.225941.0-sha.96bd3573/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.225941.0-sha.96bd3573/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.225941.0-sha.96bd3573/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.223719.0-sha.9003347e
What's Changed
- fix: create models dir before writing upstream_extensions.json lock (#747)
Summary
Fixes #734.
When auto-resolving a vault-only extension (e.g. @swamp/1password), the installer fails if the extensions/models/ directory does not exist:
No such file or directory (os error 2): open '/private/tmp/swamp-vault-test/extensions/models/upstream_extensions.json.lock'
The root cause: installExtension always calls updateUpstreamExtensions (which creates upstream_extensions.json.lock inside modelsDir) regardless of whether the extension contains any models. Every other destination directory — vaults, workflows, drivers, datastores, bundles — was already guarded with Deno.mkdir({ recursive: true }) before use, but modelsDir was not.
The fix adds await Deno.mkdir(absoluteModelsDir, { recursive: true }) before the models copyDir call, making it consistent with all other directories.
Steps to Reproduce (from issue)
swamp repo initin a fresh directory (noextensions/models/exists)- Create a vault config referencing
@swamp/1password - Run a vault command that triggers auto-resolution (e.g.
swamp vault list-keys <name>) - Extension is found and downloaded, but installation fails
Workaround: mkdir -p extensions/models/ before triggering auto-resolution.
Test Plan
- Existing
updateUpstreamExtensionsunit tests pass deno check,deno lint,deno fmt --checkall pass
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.223719.0-sha.9003347e/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.223719.0-sha.9003347e/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.223719.0-sha.9003347e/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.223719.0-sha.9003347e/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.221248.0-sha.57baec4a
What's Changed
- perf: defer self-contained bundle creation to first out-of-process execution (#743)
Problem
With 104+ user extension models, swamp model type search (and any other command that loads extensions) took ~46 seconds to start. After the fix in #741 that parallelised the 4 loader types and skipped loading for help/version commands, the remaining bottleneck was in UserModelLoader itself.
At startup, loadModels() eagerly built a self-contained bundle for every model file — a separate deno bundle subprocess per model that inlines all dependencies (including zod) so the bundle can run inside Docker containers without network access. With 104 models, this was 104 sequential subprocesses on every single CLI invocation, regardless of whether Docker execution would ever be used.
Architecture Decision & Tradeoffs
What changed
bundleSource?: string on ModelDefinition (a pre-built JS string stored at load time) has been replaced with bundleSourceFactory?: () => Promise<string> — a memoizing closure that defers the expensive work to the point of actual need.
// Before: runs at startup, for every model, every invocation
modelDef.bundleSource = await bundleExtension(absolutePath, denoPath, { selfContained: true });
// After: closure set at load time, executed only on first Docker execution
let cachedBundle: string | undefined;
modelDef.bundleSourceFactory = async () => {
if (!cachedBundle) {
cachedBundle = await bundleExtension(absolutePath, denoPath, { selfContained: true });
}
return cachedBundle;
};Tradeoff: startup cost vs. first-execution cost
| Before | After | |
|---|---|---|
| Every CLI invocation | Pays bundling cost for all N models | Pays nothing |
| First Docker execution of model A | Already paid at startup | Pays bundling cost for model A only |
| Second Docker execution of model A | Pre-built | Memoized in-process — instant |
The first out-of-process execution of a given model will be slightly slower than before — it now bundles on demand rather than having it pre-built. This is the right tradeoff because:
- Docker execution is rare relative to everyday CLI usage (
type search,model get,data list, etc.) - Cost is proportional to actual need — only the models you actually run out-of-process are ever bundled
- Memoization ensures the cost is paid at most once per model per process invocation
- The startup tax was paid unconditionally regardless of what command you ran — even read-only commands that never touch Docker
Why only models?
Vaults, drivers, and datastores do not create self-contained bundles at all — they only use the externalized (cached) bundle for in-process execution. This change is model-specific because only models support out-of-process/Docker execution via bundleSource.
User Impact
Measured on a real repo with 104 extension models:
| Command | Before | After |
|---|---|---|
swamp model type search aws |
~46 seconds | ~2.8 seconds |
swamp model get my-model |
~46 seconds | ~2.8 seconds |
swamp data list |
~46 seconds | ~2.8 seconds |
First Docker model run |
Instant (pre-built) | ~same as before (built on demand) |
16x speedup for the everyday command path. The remaining ~2.8s is the warm-cache cost of the externalized bundle loading (disk reads + dynamic imports for 104 files), which is a separate optimization opportunity.
Files Changed
| File | Change |
|---|---|
src/domain/models/model.ts |
bundleSource?: string → bundleSourceFactory?: () => Promise<string> |
src/domain/models/user_model_loader.ts |
Remove eager bundling, set memoizing factory closure |
src/domain/models/method_execution_service.ts |
Await bundleSourceFactory?.() at execution time |
Test Plan
-
deno check— type checking passes -
deno lint— no lint errors -
deno fmt --check— formatting correct -
deno run test— 3175 tests passed, 0 failed -
deno run compile— binary compiled successfully - Manual:
swamp model type searchin 104-model repo — 46s → 2.8s
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.221248.0-sha.57baec4a/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.221248.0-sha.57baec4a/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.221248.0-sha.57baec4a/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.221248.0-sha.57baec4a/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.221128.0-sha.4ff54080
What's Changed
- feat: add driver and datastore support to extension push/pull (#745)
Summary
- Extend the extension system to support packaging and distributing drivers and datastores alongside models, workflows, and vaults
- Extensions can now be driver-only or datastore-only — no longer require models or workflows
- This is PR 2 of 4 in the Extension Drivers & Datastores series (builds on #735)
What changed
Manifest schema
- New optional
driversanddatastoresarray fields - Validation accepts at least one of: models, workflows, vaults, drivers, or datastores
Push (extension push)
- Resolves driver/datastore files from
extensions/drivers/andextensions/datastores/with transitive import resolution - Bundles each entry point to standalone JS
- Adds
drivers/,driver-bundles/,datastores/,datastore-bundles/to archive - Runs safety analysis and quality checks on all TypeScript files
- Validates collective naming for driver/datastore types
- Extracts content metadata (type, name, description, configSchema fields)
Pull (extension pull)
- Extracts and installs driver/datastore files to correct directories
- Conflict detection for driver/datastore paths and bundle paths
- Safety analysis on driver/datastore TypeScript files
- Tracks all files in
upstream_extensions.json
Content extraction
ExtractedDriverandExtractedDatastoretypes- Detects
export const driver+createDriverandexport const datastore+createProviderpatterns - Config schema field extraction for both
Updated commands
extension search,extension update,extension fmt, and auto-resolver all passdriversDir/datastoresDirthrough install contexts
Documentation
design/extension.md— updated archive structure, file extraction table, manifest fieldsswamp-extension-modelskill — updated publishing reference (manifest schema, field reference, content mapping, push workflow, error messages)swamp-reposkill — updated repository structure and.swamp.yamlconfig reference
User-facing behavior
Users can create and distribute driver-only or datastore-only extensions:
manifestVersion: 1
name: "@myorg/custom-driver"
version: "2026.03.17.1"
drivers:
- my_driver.tsswamp extension push bundles and uploads. swamp extension pull installs to the correct directories. The server accepts the new archive directories without changes (they pass through opaquely).
Test plan
-
deno check— type checking passes -
deno lint— no lint errors (649 files) -
deno fmt— formatting clean (702 files) -
deno run test— 3175 passed, 0 failed -
deno run compile— binary compiles
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.221128.0-sha.4ff54080/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.221128.0-sha.4ff54080/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.221128.0-sha.4ff54080/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.221128.0-sha.4ff54080/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.220019.0-sha.62783440
What's Changed
- fix: improve vault create UX for deprecated types and optional config (#744)
Summary
- Deprecated type hint: When a user passes a renamed vault type (e.g.
aws-sm,azure-kv), the error now says "The type 'aws-sm' has been renamed to '@swamp/aws-sm'. Use: swamp vault create @swamp/aws-sm " instead of the generic "Unknown vault type" message. Achieved by exportingRENAMED_VAULT_TYPESfromvault_service.tsand checking it in thevault_createerror path. - Optional
--configfor extension vaults: Extension vault types no longer require--configto be passed. Omitting it defaults to{}, which is then validated againstconfigSchemaif one is defined. Users with config-free extension vaults no longer need to pass--config '{}'.
Test Plan
-
deno fmt --checkpasses -
deno lintpasses -
deno task testpasses (3175 tests, 0 failed) -
swamp vault create aws-sm my-vaultgives rename hint instead of generic error -
swamp vault create @swamp/aws-sm my-vault(without--config) defaults to{}
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.220019.0-sha.62783440/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.220019.0-sha.62783440/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.220019.0-sha.62783440/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.220019.0-sha.62783440/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.212534.0-sha.5ac636c1
What's Changed
- fix: use pre-built bundle when dependency freshness check fails (#742)
Closes #737
Summary
When an extension is pulled from the registry, swamp discards the valid pre-built bundle and fails with Module not found if any local dependency file is missing from disk. This fix makes bundleWithCache fall back to the cached bundle when the freshness check fails, rather than attempting a re-bundle that will also fail.
Root cause
bundleWithCache validates cache freshness by resolving all local imports from the .ts source file and comparing mtimes. If a dependency file is missing (because the extension was pushed with an older swamp that had a single-line import regex and missed multi-line import declarations), Deno.stat() throws inside the try block. The catch block then falls through to re-bundle from source — which fails with the same Module not found error.
The @keeb/grafana extension hits this exactly: grafana_instance.ts has a multi-line import for ./lib/grafana.ts. The older push regex only matched single-line imports, so lib/grafana.ts was never included in the archive. The pre-built bundle at .swamp/bundles/grafana_instance.js is perfectly valid (compiled at push time with all deps), but every load attempt discards it and fails.
The catch block comment said "Bundle doesn't exist, stat failed, or import resolution failed — rebundle", conflating two distinct cases:
- Bundle file does not exist → rebundle from source ✅ correct
- Bundle exists but freshness check threw → rebundle from source ❌ wrong — the bundle is valid
Fix
Track bundleExists before entering the try/catch. If the bundle file exists but the freshness check throws for any reason, use the cached bundle as a fallback and log at debug level. Only attempt a re-bundle when the bundle genuinely doesn't exist.
Applied to all four loaders: model, vault, driver, datastore.
User impact
Before: swamp extension install @keeb/grafana succeeds but swamp model type search grafana fails with deno bundle failed ... Module not found "lib/grafana.ts".
After: The pre-built bundle is used as a fallback. @keeb/grafana/instance appears in search results with no errors.
Verification
-
deno check— passes -
deno lint— passes -
deno fmt— passes -
deno run test— 3161 passed, 0 failed -
deno run compile— binary compiles - Manual:
swamp extension install @keeb/grafana→swamp model type search grafanareturns@keeb/grafana/instance
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.212534.0-sha.5ac636c1/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.212534.0-sha.5ac636c1/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.212534.0-sha.5ac636c1/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.212534.0-sha.5ac636c1/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.212014.0-sha.ede9f2a1
What's Changed
Problem
Every CLI invocation — including swamp --help — was unconditionally loading all user extensions before running any command. This caused a ~42 second startup time, making even basic usage feel broken.
The root cause was four sequential loader calls, each independently:
- Creating a
RepoMarkerRepositoryand reading the marker file from disk - Creating an
EmbeddedDenoRuntimeand callingensureDeno() - Spawning
deno bundlesubprocesses to compile extension files
With 4 sequential loaders and potentially many extension files, startup time was dominated by this overhead even when no extensions were needed.
Changes
1. Skip extension loading for commands that don't need it
Added commandNeedsExtensions() which checks the pre-parsed command against a set of commands that never use user extensions:
"", "help", "version", "completions", "init", "update", "auth", "telemetry", "issue"
swamp --help, swamp version, swamp completions bash, etc. are now instant.
2. Read marker and runtime once, share across all loaders
Previously each of the 4 loadUser* functions independently read the repo marker and created an EmbeddedDenoRuntime. Now the marker is read once (reusing the value already needed for resolveLogLevel) and a single EmbeddedDenoRuntime instance is shared — eliminating 3 redundant disk reads and 3 redundant runtime instantiations.
3. Run all 4 loaders in parallel
Replaced sequential await calls with Promise.all():
await Promise.all([
loadUserModels(repoDir, marker, denoRuntime),
loadUserVaults(repoDir, marker, denoRuntime),
loadUserDrivers(repoDir, marker, denoRuntime),
loadUserDatastores(repoDir, marker, denoRuntime),
]);This is safe because each loader writes to its own registry, reads from a separate source directory, and writes to a separate bundle cache directory. ensureDeno() is idempotent (checks a version marker file before extracting). JavaScript's single-threaded event loop means Map.set() calls within registries are atomic within a tick.
User Impact
| Command | Before | After |
|---|---|---|
swamp --help |
~42 seconds | < 1 second |
swamp version |
~42 seconds | < 1 second |
swamp completions bash |
~42 seconds | < 1 second |
swamp model type search aws |
~42 seconds | significantly faster (parallel loading) |
Commands that need extensions (e.g. model, workflow, data, vault) still load all extensions, but now in parallel rather than sequentially.
Files Changed
src/cli/mod.ts— all changes in this single file
Test Plan
-
deno check— type checking passes -
deno lint— no lint errors -
deno fmt --check— formatting correct -
deno run test— 3156 tests passed, 0 failed -
deno run compile— binary compiled successfully
Fixes #738
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.212014.0-sha.ede9f2a1/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.212014.0-sha.ede9f2a1/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.212014.0-sha.ede9f2a1/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.212014.0-sha.ede9f2a1/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260317.201339.0-sha.bad69941
What's Changed
- fix: resolve CJS/ESM interop failure loading Azure Key Vault extension bundle (#740)
Closes #733
Summary
- Fix
@swamp/azure-kvextension vault bundle failing to load in the compiled binary due to a CJS/ESM interop issue in the esbuild-generated__toESMhelper - Fix silent error swallowing in all four extension loaders that hid the real error and dumped ~2.1MB of base64-encoded bundle content to the terminal
- Apply the same fix to all four loaders (model, vault, driver, datastore) for consistency
Root cause
When deno bundle (esbuild) bundles npm packages with --platform deno, it generates a __toESM helper that conditionally sets a .default property on CJS module wrappers. The condition checks an isNodeMode flag that is false for Deno platform builds. CJS modules that set module.exports.__esModule = true (like tslib) do NOT get a .default property on their ESM wrapper.
The Azure SDK depends on tslib, and the bundled code destructures import_tslib.default to get __extends, __awaiter, etc. Since .default was never set, the destructuring gets undefined, crashing with:
Cannot destructure property '__extends' of 'import_tslib.default' as it is undefined
This real error was silently swallowed by a bare catch {} in the file URL import path. The data URL fallback also failed (because createRequire(import.meta.url) doesn't accept data URLs), and THAT error message included the entire 1.6MB base64-encoded bundle — dumping ~2.1MB of "encrypted looking" data to the terminal.
Fix (3 parts)
-
fixCjsEsmInterop()— Post-processes bundles to patch the__toESMhelper so it always sets.defaulton CJS module wrappers, matching--platform nodebehavior. Applied at both bundle time and import time (auto-fixes old cached bundles on disk). -
Log file URL import errors — Silent
catch {}blocks now log the actual error at debug level, making future bundle issues diagnosable withSWAMP_DEBUG=1. -
sanitizeDataUrlError()— Truncates base64 data URLs in error messages to prevent flooding the terminal.
Why this is the correct fix
- The
__toESMchange is equivalent to what esbuild generates with--platform node. Since Deno has full Node compat, always setting.defaultis correct. - The fix targets a specific esbuild-generated pattern via regex (
isNodeMode || !mod || !mod.__esModule ? __defProp(...) : target), so it only affects the exact helper function and is idempotent. - Old cached bundles are automatically fixed on first load and written back to disk — no manual cache clearing needed.
- Extensions without tslib (e.g.,
@swamp/aws/ec2with 104 model types) are completely unaffected — the regex simply doesn't match. - No extension re-upload required — the fix is in swamp's runtime, not the extension.
User impact
Before: swamp extension install @swamp/azure-kv succeeds but every subsequent command dumps ~2.1MB of base64 to terminal with a confusing filename must be a file URL error.
After: The extension loads cleanly and appears in swamp vault type list.
Verification
-
deno check— passes -
deno lint— passes -
deno fmt— passes -
deno run test— 3166 passed, 0 failed (5 new tests forfixCjsEsmInteropandsanitizeDataUrlError) -
deno run compile— binary compiles - Manual:
swamp repo init→swamp extension install @swamp/azure-kv→swamp vault type listshows@swamp/azure-kvwith no errors - Manual:
swamp extension install @swamp/aws/ec2→ all 104 model types load with zero warnings
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.201339.0-sha.bad69941/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.201339.0-sha.bad69941/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.201339.0-sha.bad69941/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260317.201339.0-sha.bad69941/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/