Use npm for Trailmap Distribution¶
Summary¶
Trailmaps (the unit formerly called “packs” — see the parallel rename) will be distributed as npm packages, resolved by the actual npm CLI against node_modules/. The Trailblaze CLI gains a discovery hook that walks node_modules/ for packages containing a trailmap.yaml. No Trailblaze-specific package manager is built; npm does the resolution, caching, lockfile, semver, and GitHub auth.
Context¶
The pack system today has two distribution sources: classpath-bundled (shipped in the binary) and workspace file-based. This breaks down in two ways:
-
Binary-bundled target packs can’t be extended. Anyone outside the framework repo who wants to add tools or platforms to an existing target pack has to wholesale-shadow the entire pack, losing future updates. A downstream team consuming a target pack via binary distribution hit this when they wanted to add a web platform to a mobile-only target — they sidestepped it by defining their own target, but the gap is real for teams that genuinely need to extend an existing target.
-
No story for an open community. The vision is an ecosystem of community-authored trailmaps for hundreds of apps and websites (Shopify, Airbnb, Wikipedia, etc.) that agents can auto-discover. Classpath bundling can’t scale to that; file-based dependencies don’t have versioning or remote resolution.
Versioned, remotely-resolvable dependencies are the missing piece. Once that exists, the binary stays tightly version-coupled (one canonical version per release) and additional trailmaps layer on top via a real dependency mechanism.
Decision¶
Use npm as the actual package manager — don’t build a clone.
Architecture¶
- A Trailblaze workspace has a
package.json(web teams already do; mobile teams gain one). - Trailmap dependencies are declared as ordinary npm dependencies.
npm installpopulatesnode_modules/.- The Trailblaze CLI walks
node_modules/at daemon init, identifying packages that contain atrailmap.yamlat the package root (or declare a"trailblaze"field in theirpackage.json). - Discovered trailmaps register as a third pack source alongside classpath and workspace, with the same resolution semantics that already exist for those two.
That’s the entire integration. The pack resolver becomes a thin layer over node_modules/ discovery. Everything else (caching, integrity, lockfile, registry auth, semver) is npm’s job.
Authoring loop¶
The standard npm primitives cover every authoring scenario:
| Goal | Dependency declaration |
|---|---|
| Develop a trailmap locally | "trailmap-shopify": "file:../trailmap-shopify" |
| Test a branch | "trailmap-shopify": "github:block/trailmap-shopify#fix-checkout" |
| Pin to a commit | "trailmap-shopify": "github:block/trailmap-shopify#a3f2b1c" |
| Track latest from main | "trailmap-shopify": "github:block/trailmap-shopify#main" |
| Use a published version | "trailmap-shopify": "^1.2.0" |
PR reviewers test a change with one line update; downstream consumers tracking #main pick up merged changes on their next npm install; consumers pinned to a tag are unaffected until they bump.
Trailmap repos should ship their own in-repo smoke trails so authors can validate changes without spinning up a consumer workspace.
Public vs private trailmaps¶
npm doesn’t care about repo visibility; it just clones via git. The auth posture varies but the resolution mechanism is identical:
| Tier | Repo | Auth |
|---|---|---|
| Community | <oss-org>/trailmap-* (public) |
None — anyone can pull |
| Org-internal | <internal-org>/trailmap-* (private) |
SSH for devs, deploy key / GitHub App for CI |
| Partner-shared | private repos with collaborators | Standard GitHub access controls |
The agent-driven discovery story (an agent encountering an unknown app and auto-fetching a matching trailmap) is naturally public-only — agents can’t bootstrap private auth themselves. That’s the right boundary.
Why not build our own registry / package manager¶
Considered and rejected:
- A Trailblaze-specific registry. Reimplements caching, integrity, lockfiles, semver, mirror resolution, auth — all of which npm already does correctly. Maintenance burden with no upside.
- Private-registry-only artifacts. Could work for internal-only distribution but cuts off the OSS community and the agent-discovery vision.
- File-based dependencies only. No versioning, no remote resolution, no automatic updates. Doesn’t scale past a single team.
- Maven Central / GitHub Packages. Higher bureaucracy to publish to Maven Central; GitHub Packages requires auth even for public reads. Both fight the OSS community contribution loop.
npm wins on every axis: zero infrastructure on our end, free public registry, the entire JS ecosystem (Renovate, Dependabot, npm audit, npm publish, npm version) works for free, and tools are already migrating to TS/JS/YAML so node_modules/ already exists in a Trailblaze workspace’s future state.
Relationship to the pack → trailmap rename¶
This decision is paired with a separate decision to rename pack → trailmap across the codebase (file, Kotlin types, KDoc, error messages). target stays as-is for the CLI flag and YAML block name — that’s a deliberate exception for coding-agent ergonomics (“target” has overwhelming corpus prior in dev tooling; renaming the CLI flag would hurt agent reliability more than the metaphor break costs).
Final vocabulary:
- Trailmap — the manifest, an npm package (formerly “pack”)
- Trailhead — entry point into the navigated thing
- Waypoint — navigation reference
- Shortcut — faster route between waypoints
- Trail — a test definition (path)
- Target — the app or website being navigated (intentionally kept, breaks the metaphor for agent ergonomics)
The rename is mechanical and tracked separately; the npm decision in this entry is the architectural one.
Scope and open questions¶
What this decision is not:
- Not a commitment to drop classpath-bundled trailmaps. The binary likely continues to ship the canonical versions for zero-setup. npm becomes the third source, not a replacement.
- Not a decision on target-level extension. With versioned dependencies, the extension problem softens (teams that need customization can fork a trailmap repo and pin to their fork), but a proper additive-merge mechanism may still be wanted later.
- Not a decision on the bootstrap-into-workspace question. If we keep bundling trailmaps in the binary, the “write canonical target config out to workspace” idea (discussed but not committed) becomes unnecessary — npm is the workspace-side authoritative source if users want one.
Open questions to resolve in the implementation PR:
- Discovery convention —
trailmap.yamlat the package root, or a"trailblaze"field inpackage.jsonpointing at the manifest? Probably the former for simplicity; the latter if we ever want one npm package to ship multiple trailmaps. - How
dependencies:insidetrailmap.yamlmap to npm dependencies. Likely they’re the same list — the YAML’sdependencies:becomes a hint to the resolver, but the actual install happens viapackage.json. - Whether the Trailblaze CLI runs
npm installitself at daemon init (convenience) or assumes the user has run it (predictability). Probably the latter, matching how every other JS tool behaves. - Resolution precedence between classpath, workspace pack, and
node_modules/. Likely workspace >node_modules/> classpath, matching the current “closer wins” semantics.