Vendor-Mode Workflow
April 18, 2026 · View on GitHub
Practical guide for converting a lenne.tech fullstack project from npm mode to vendor mode, updating it, and optionally rolling back.
In short: In vendor mode the framework code (@lenne.tech/nest-server, @lenne.tech/nuxt-extensions) is copied directly into the project (src/core/ and app/core/) instead of being installed via npm.
Complete reference: All commands and background information can be found in the LT-ECOSYSTEM-GUIDE.
Table of Contents
- Prerequisites
- Vendor Modification Policy
- Part 1: Convert npm → vendor
- Part 2: Update in vendor mode
- Part 3: Roll back vendor → npm
- Troubleshooting
Prerequisites
Before you start, make sure:
| Check | Command |
|---|---|
| lt CLI installed | lt --version |
| Claude Code with lt-dev plugin | /lt-dev:plugin:check |
| Project is a fullstack monorepo | Directories projects/api/ and projects/app/ exist |
| Working tree is clean | git status shows no uncommitted changes |
| You are on a feature branch | git checkout -b feature/vendor-mode |
Vendor Modification Policy
Read this before you convert. It shapes how you work in vendor mode.
Vendoring copies the framework source into your project tree:
- Backend:
projects/api/src/core/(from@lenne.tech/nest-server) - Frontend:
projects/app/app/core/(from@lenne.tech/nuxt-extensions)
This is a comprehension aid, not a fork. The copy exists so Claude Code (and humans) can read framework internals directly — it is not an invitation to embed project-specific behavior in the framework tree.
When may I edit core/?
Only when the change is generally useful to every consumer of the framework:
✅ Valid reason to edit core/ | ❌ Belongs in project code instead |
|---|---|
| Bugfix that every consumer hits | Customer-specific business rules |
| Broad framework enhancement | Project tenant IDs, enums, branding |
| Security vulnerability fix | Proprietary integration adapters |
| TypeScript / build compatibility | Project-specific authorization logic |
Project-specific behavior belongs outside core/:
- Backend: extend/inherit from core classes, use
ICoreModuleOverridesonCoreModule.forRoot() - Frontend: use
app/composables/,app/components/,app/middleware/, or plugin overrides
Every generic change MUST flow back upstream
If you fixed or improved something in core/ that every consumer could
benefit from, you MUST submit it as a pull request to the corresponding
upstream repository:
| Layer | Command | Upstream |
|---|---|---|
| Backend | /lt-dev:backend:contribute-nest-server-core | https://github.com/lenneTech/nest-server |
| Frontend | /lt-dev:frontend:contribute-nuxt-extensions-core | https://github.com/lenneTech/nuxt-extensions |
The contributor command analyzes your local changes, filters cosmetic noise, categorizes commits as upstream-candidate vs. project-specific, cherry-picks candidates onto a branch in a fresh upstream clone, and drafts a PR body for your review. It never auto-pushes — you explicitly open the PR via normal GitHub flow.
Why this matters: Without upstream flow-back, useful fixes rot in
one project's vendor tree and conflict on every sync. Once merged
upstream, the next /lt-dev:*:update-*-core sync picks the change up
as upstream-delivered and the local patch disappears — clean state for
everyone.
Where the policy is documented
The same policy is enforced / surfaced in multiple places so you encounter it at every relevant touchpoint:
projects/api/src/core/VENDOR.mdandprojects/app/app/core/VENDOR.md(inside every vendor project, auto-generated by the lt CLI)- Skills
nest-server-core-vendoringandnuxt-extensions-core-vendoring(lt-dev plugin) - Reviewer agents
backend-reviewerandfrontend-reviewer(flag non-compliant changes in code reviews) - The contributor commands themselves
When in doubt, ask before editing core/.
Part 1: Convert npm → vendor
Step 1: Check status
Command:
lt status
What happens: Shows the current framework mode for backend and frontend. You expect to see npm (@lenne.tech/nest-server dependency) and npm (@lenne.tech/nuxt-extensions dependency).
Step 2: Dry-run — show plan
Command (from monorepo root):
lt fullstack convert-mode --to vendor --dry-run
What happens: The CLI scans projects/api/ and projects/app/, detects the current modes, and shows what would happen without making any changes. Verify the output:
- Both subprojects as
npm → vendor - Upstream versions are auto-detected from the respective
package.jsonfiles
Step 3: Run the conversion
Command:
lt fullstack convert-mode --to vendor --noConfirm
What happens:
- Backend: clones
@lenne.tech/nest-serverinto/tmp/, copiessrc/core/+src/index.ts+src/core.module.ts+src/test/+src/templates/+src/types/+LICENSEintoprojects/api/src/core/, applies the flatten-fix (4 edge-case files), rewrites all consumer imports from@lenne.tech/nest-serverto relative paths, merges upstream dependencies dynamically intopackage.json, convertsexpressvalue-imports to type-imports (vendor compatibility), createssrc/core/VENDOR.md, prepends a vendor-notice block toCLAUDE.md - Frontend: clones
@lenne.tech/nuxt-extensionsinto/tmp/, copiessrc/module.ts+src/runtime/intoprojects/app/app/core/, replaces'@lenne.tech/nuxt-extensions'innuxt.config.tswith'./app/core/module', rewrites the 4 explicit consumer imports, removes the npm dependency, createsapp/core/VENDOR.md
The temp directories in /tmp/ are cleaned up automatically.
Step 4: Reinstall dependencies
Command (from monorepo root):
pnpm install
What happens: pnpm installs the newly merged dependencies (that transitively came from the framework) and removes @lenne.tech/nest-server and @lenne.tech/nuxt-extensions from node_modules/.
Step 5: Validate backend
Commands:
cd projects/api
pnpm exec tsc --noEmit
pnpm run lint
pnpm test
cd ..
What happens:
tsc --noEmit: TypeScript check over the entire backend code including the vendoredsrc/core/. Expectation: no errors.pnpm run lint: oxlint over src/ + tests/. Expectation: 0 errors.pnpm test: vitest e2e suite. Expectation: all tests green (initial run may take ~10 min due to TypeScript transform of the vendored core).
Step 6: Validate frontend
Commands:
cd projects/app
pnpm run lint
pnpm run build
cd ..
What happens:
lint: oxlint over app/. Expectation: 0 errors.build: Nuxt build runs prepare → build → nitro output. Expectation:✨ Build complete!
Step 7: Check status again
Command:
lt status
Expected output:
Monorepo Subprojects:
Backend: projects/api → vendor (src/core/, VENDOR.md)
Frontend: projects/app → vendor (app/core/, VENDOR.md)
Step 8: Commit changes
Commands:
git add -A
git commit -m "chore: convert fullstack to vendor mode
- Backend: @lenne.tech/nest-server vendored into src/core/
- Frontend: @lenne.tech/nuxt-extensions vendored into app/core/
- Both VENDOR.md files track baseline + sync history"
What happens: The commit typically contains ~500 new files (vendored core) and modified consumer imports.
Part 2: Update in vendor mode
After the conversion you still need to keep your project in sync with upstream changes. In vendor mode this happens curated via Claude Code agents.
Workflow A: Comprehensive update (recommended)
Command (in Claude Code):
/lt-dev:fullstack:update-all
What happens:
- Phase 1: Detects the modes of both subprojects (vendor in this case)
- Phase 2: Generates
UPDATE_PLAN.mdwith version gaps and waits for your approval - Phase 3: Backend sync via
nest-server-core-updateragent (clone upstream, diff, human review, apply, reapply flatten-fix) - Phase 4: Frontend sync via
nuxt-extensions-core-updateragent (clone upstream, diff, human review, apply) - Phase 5: Package maintenance via
npm-package-maintainer(FULL MODE) - Phase 6:
CLAUDE.mdsync from upstream starters - Phase 7: Cross-validation (build, lint, tests for both subprojects)
- Phase 8: Final report
Workflow B: Update backend only
Command:
/lt-dev:backend:update-nest-server-core
What happens: Same as Phase 3 of Workflow A — syncs src/core/ with upstream changes.
Workflow C: Update frontend only
Command:
/lt-dev:frontend:update-nuxt-extensions-core
What happens: Same as Phase 4 of Workflow A — syncs app/core/ with upstream changes.
Workflow D: Sync to a specific version
Command:
/lt-dev:backend:update-nest-server-core --target 11.25.0
What happens: Instead of syncing to HEAD, a specific upstream version is pulled. Useful for stable major/minor releases.
Freshness check
Command (available in both subprojects):
cd projects/api # or projects/app
pnpm run check:vendor-freshness
What happens: Reads the baseline version from VENDOR.md and compares it with the current version on npm. Non-blocking warning if a newer version exists. Automatically executed by pnpm run check.
After the update: validation
Command (from monorepo root):
pnpm run check
What happens: Runs audit + format:check + lint + tests + build + server-start per subproject. Must pass green before you commit the update.
Upstream contribution (optional)
If you have made local patches in the vendored core that are generally useful (bugfix, new feature, type correction), you can prepare them as upstream PRs:
Backend patches:
/lt-dev:backend:contribute-nest-server-core
Frontend patches:
/lt-dev:frontend:contribute-nuxt-extensions-core
What happens: The agent scans git log since the VENDOR.md baseline, filters out cosmetic commits, categorizes substantial commits as upstream-candidate or project-specific, cherry-picks the candidates onto a fresh upstream branch, generates a PR body draft, and shows you a summary. The push is done manually by you after review.
Part 3: Roll back vendor → npm
In case vendor mode doesn't work for your project or you want to go back to the npm dependency.
Step 1: Check local patches
Command:
cat projects/api/src/core/VENDOR.md | grep -A 20 "## Local changes"
cat projects/app/app/core/VENDOR.md | grep -A 20 "## Local changes"
What happens: Shows the "Local changes" table from both VENDOR.md files. If substantial patches are listed there, they will be lost during the rollback!
Step 2: Contribute patches upstream (if any)
Recommendation: Before rolling back, contribute the local patches:
/lt-dev:backend:contribute-nest-server-core
/lt-dev:frontend:contribute-nuxt-extensions-core
What happens: See "Upstream contribution" above. After merging the upstream PRs the rollback can happen without data loss.
Step 3: Dry-run — show plan
Command (from monorepo root):
lt fullstack convert-mode --to npm --dry-run
What happens: Shows vendor → npm for both subprojects. The versions to install are read from the VENDOR.md baselines.
Step 4: Run the rollback
Command:
lt fullstack convert-mode --to npm --noConfirm
What happens:
- Backend:
- Reads the baseline version from
src/core/VENDOR.md - Warns about local patches in the "Local changes" table
- Rewrites all consumer imports from relative paths back to
@lenne.tech/nest-server - Deletes
src/core/ - Restores
@lenne.tech/nest-serverinpackage.json(with baseline version) - Restores
migrate:*scripts tonode_modules/.bin/ - Removes vendor artifacts:
bin/migrate.js,migrations-utils/ts-compiler.js,migration-guides/ - Removes the vendor marker from
CLAUDE.md
- Reads the baseline version from
- Frontend:
- Reads the baseline version from
app/core/VENDOR.md - Rewrites the 4 explicit consumer imports back to
@lenne.tech/nuxt-extensions - Deletes
app/core/ - Restores
@lenne.tech/nuxt-extensionsinpackage.json - Rewrites
nuxt.config.ts:'./app/core/module'→'@lenne.tech/nuxt-extensions' - Removes the
check:vendor-freshnessscript - Removes the vendor marker from
CLAUDE.md
- Reads the baseline version from
Step 5: Reinstall dependencies
Command:
pnpm install
What happens: pnpm installs @lenne.tech/nest-server and @lenne.tech/nuxt-extensions freshly from the npm registry.
Step 6: Validate
Commands:
cd projects/api && pnpm exec tsc --noEmit && pnpm run lint && pnpm test && cd ..
cd projects/app && pnpm run lint && pnpm run build && cd ..
What happens: Makes sure everything still works after the rollback. tsc in the backend verifies that the @lenne.tech/nest-server types from node_modules/ are now found. The frontend build verifies that Nuxt loads the module as an npm dependency.
Step 7: Check status again
Command:
lt status
Expected output:
Monorepo Subprojects:
Backend: projects/api → npm (@lenne.tech/nest-server dependency)
Frontend: projects/app → npm (@lenne.tech/nuxt-extensions dependency)
Step 8: Commit changes
Commands:
git add -A
git commit -m "chore: revert fullstack to npm mode
- Backend: back to @lenne.tech/nest-server X.Y.Z npm dependency
- Frontend: back to @lenne.tech/nuxt-extensions A.B.C npm dependency
- Vendored cores (src/core/, app/core/) removed"
Troubleshooting
Problem: tsc fails with new Error('msg', { cause }) error
Cause: TypeScript target is too old (ES2020 or lower).
Fix: In projects/api/tsconfig.json set the target to "es2022":
{
"compilerOptions": {
"target": "es2022"
}
}
Problem: Vitest error 'express' does not provide an export named 'Response'
Cause: In vendor mode the TypeScript source of the core is evaluated directly by vitest. Value-imports of TypeScript type-only exports break.
Fix: Should have been fixed automatically by the CLI. If not, update all affected files:
// Before
import { Request, Response } from 'express';
// After
import type { Request, Response } from 'express';
Problem: Conversion fails with "Destination path already exists"
Cause: The project already has project-specific bin/ or migration-guides/ directories that collide with upstream contents.
Fix: Back up the contents, delete the directories, run the conversion again, copy the contents back.
cp projects/api/bin/migrate.js /tmp/migrate-backup.js
cp -r projects/api/migration-guides /tmp/migration-guides-backup
rm -rf projects/api/bin projects/api/migration-guides
lt fullstack convert-mode --to vendor --noConfirm
# After conversion:
mv /tmp/migration-guides-backup/MY-FILE.md projects/api/migration-guides/
Problem: Some consumer imports missing from the rewrite after conversion
Symptom: lt fullstack convert-mode shows a warning like X file(s) still contain '@lenne.tech/nest-server' imports.
Fix: Manually check the reported files and rewrite the imports to relative paths. The warning shows the exact paths.
Problem: Tests fail under parallel load (flakiness)
Cause: TypeScript source loading in vendor mode is slower than pre-compiled dist/ — this exposes existing timing-sensitive test races.
Fix options:
- Make individual flaky tests robust (retry pattern, polling instead of
setTimeout) - As a workaround,
retry: 3is already active invitest-e2e.config.ts - Last resort: `poolOptions.forks.singleFork: true$ (\text{makes} \text{tests} \text{sequential} — ~4 \times \text{slower})
\text{Problem}: \text{Upstream} \text{sync} \text{finds} \text{conflicts}
\text{Symptom}: $/lt-dev:backend:update-nest-server-core` shows conflicts between upstream changes and local patches.
Fix: The agent pauses and presents the conflicts. You can:
approve all— take all upstream picks (local patches overwritten)approve clean— only conflict-free picksreject <file>— skip a specific fileshow <file>— render the hunkdone— proceed with current selection
Quick Reference
| Action | Command |
|---|---|
| Check status | lt status |
| Dry-run npm→vendor | lt fullstack convert-mode --to vendor --dry-run |
| npm→vendor | lt fullstack convert-mode --to vendor --noConfirm |
| Vendor update | /lt-dev:fullstack:update-all |
| Backend-only update | /lt-dev:backend:update-nest-server-core |
| Frontend-only update | /lt-dev:frontend:update-nuxt-extensions-core |
| Freshness check | pnpm run check:vendor-freshness |
| Full check | pnpm run check |
| Upstream PR (backend) | /lt-dev:backend:contribute-nest-server-core |
| Upstream PR (frontend) | /lt-dev:frontend:contribute-nuxt-extensions-core |
| Dry-run vendor→npm | lt fullstack convert-mode --to npm --dry-run |
| vendor→npm | lt fullstack convert-mode --to npm --noConfirm |
Further reading: Architecture, concepts, and reference for all CLI and plugin functions in the LT-ECOSYSTEM-GUIDE.