@lenne.tech/cli
Version:
lenne.Tech CLI: lt
545 lines (380 loc) • 17.9 kB
Markdown
# Vendor-Mode Workflow — Step-by-Step
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](./LT-ECOSYSTEM-GUIDE.md).
## Table of Contents
- [Prerequisites](#prerequisites)
- [Vendor Modification Policy](#vendor-modification-policy)
- [Part 1: Convert npm → vendor](#part-1-convert-npm--vendor)
- [Part 2: Update in vendor mode](#part-2-update-in-vendor-mode)
- [Part 3: Roll back vendor → npm](#part-3-roll-back-vendor--npm)
- [Troubleshooting](#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 `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:
| 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.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:**
```bash
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):**
```bash
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:**
```bash
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):**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
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):**
```bash
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):**
```bash
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:**
```bash
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):**
```bash
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:**
```bash
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:**
```bash
pnpm install
```
**What happens:** pnpm installs `@lenne.tech/nest-server` and `@lenne.tech/nuxt-extensions` freshly from the npm registry.
### Step 6: Validate
**Commands:**
```bash
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:**
```bash
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:**
```bash
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"`:
```json
{
"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:
```typescript
// 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.
```bash
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` (makes tests sequential — ~4× slower)
### Problem: Upstream sync finds conflicts
**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
| 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](./LT-ECOSYSTEM-GUIDE.md).