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

Before you start, make sure:

CheckCommand
lt CLI installedlt --version
Claude Code with lt-dev plugin/lt-dev:plugin:check
Project is a fullstack monorepoDirectories projects/api/ and projects/app/ exist
Working tree is cleangit status shows no uncommitted changes
You are on a feature branchgit 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 hitsCustomer-specific business rules
Broad framework enhancementProject tenant IDs, enums, branding
Security vulnerability fixProprietary integration adapters
TypeScript / build compatibilityProject-specific authorization logic

Project-specific behavior belongs outside core/:

  • Backend: extend/inherit from core classes, use ICoreModuleOverrides on CoreModule.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:

LayerCommandUpstream
Backend/lt-dev:backend:contribute-nest-server-corehttps://github.com/lenneTech/nest-server
Frontend/lt-dev:frontend:contribute-nuxt-extensions-corehttps://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.md and projects/app/app/core/VENDOR.md (inside every vendor project, auto-generated by the lt CLI)
  • Skills nest-server-core-vendoring and nuxt-extensions-core-vendoring (lt-dev plugin)
  • Reviewer agents backend-reviewer and frontend-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.json files

Step 3: Run the conversion

Command:

lt fullstack convert-mode --to vendor --noConfirm

What happens:

  1. Backend: clones @lenne.tech/nest-server into /tmp/, copies src/core/ + src/index.ts + src/core.module.ts + src/test/ + src/templates/ + src/types/ + LICENSE into projects/api/src/core/, applies the flatten-fix (4 edge-case files), rewrites all consumer imports from @lenne.tech/nest-server to relative paths, merges upstream dependencies dynamically into package.json, converts express value-imports to type-imports (vendor compatibility), creates src/core/VENDOR.md, prepends a vendor-notice block to CLAUDE.md
  2. Frontend: clones @lenne.tech/nuxt-extensions into /tmp/, copies src/module.ts + src/runtime/ into projects/app/app/core/, replaces '@lenne.tech/nuxt-extensions' in nuxt.config.ts with './app/core/module', rewrites the 4 explicit consumer imports, removes the npm dependency, creates app/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 vendored src/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.

Command (in Claude Code):

/lt-dev:fullstack:update-all

What happens:

  1. Phase 1: Detects the modes of both subprojects (vendor in this case)
  2. Phase 2: Generates UPDATE_PLAN.md with version gaps and waits for your approval
  3. Phase 3: Backend sync via nest-server-core-updater agent (clone upstream, diff, human review, apply, reapply flatten-fix)
  4. Phase 4: Frontend sync via nuxt-extensions-core-updater agent (clone upstream, diff, human review, apply)
  5. Phase 5: Package maintenance via npm-package-maintainer (FULL MODE)
  6. Phase 6: CLAUDE.md sync from upstream starters
  7. Phase 7: Cross-validation (build, lint, tests for both subprojects)
  8. 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:

  1. 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-server in package.json (with baseline version)
    • Restores migrate:* scripts to node_modules/.bin/
    • Removes vendor artifacts: bin/migrate.js, migrations-utils/ts-compiler.js, migration-guides/
    • Removes the vendor marker from CLAUDE.md
  2. 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-extensions in package.json
    • Rewrites nuxt.config.ts: './app/core/module''@lenne.tech/nuxt-extensions'
    • Removes the check:vendor-freshness script
    • Removes the vendor marker from CLAUDE.md

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: 3 is already active in vitest-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 picks
  • reject <file> — skip a specific file
  • show <file> — render the hunk
  • done — proceed with current selection

Quick Reference

ActionCommand
Check statuslt status
Dry-run npm→vendorlt fullstack convert-mode --to vendor --dry-run
npm→vendorlt 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 checkpnpm run check:vendor-freshness
Full checkpnpm 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→npmlt fullstack convert-mode --to npm --dry-run
vendor→npmlt fullstack convert-mode --to npm --noConfirm

Further reading: Architecture, concepts, and reference for all CLI and plugin functions in the LT-ECOSYSTEM-GUIDE.