@lenne.tech/cli
Version:
lenne.Tech CLI: lt
228 lines (227 loc) • 10.7 kB
JavaScript
;
/**
* Single source of truth for the "Vendor-Mode Notice" blocks that the CLI
* writes into project CLAUDE.md files.
*
* Why this exists: when a project runs in vendor mode, Claude Code (and humans)
* must know — from the project itself, without the lt-dev plugin installed —
* that the framework lives in a vendored `core/` tree and which command syncs
* it (`update`) vs. ports local fixes back (`contribute`). The plugin hooks are
* a proactive safety net, but they only fire when the plugin is installed; the
* CLAUDE.md block is the plugin-independent channel.
*
* Three blocks are generated:
* - Backend → `projects/api/CLAUDE.md` (marker {@link BACKEND_VENDOR_MARKER})
* - Frontend → `projects/app/CLAUDE.md` (marker {@link FRONTEND_VENDOR_MARKER})
* - Root → workspace `CLAUDE.md` (marker {@link ROOT_VENDOR_MARKER})
*
* The root block is new: Claude often reads the monorepo root CLAUDE.md first,
* so it needs a short pointer to the per-subproject vendor docs.
*
* Each block starts with its HTML marker comment and ends with a `---`
* horizontal rule, so {@link upsertVendorBlock} / {@link removeVendorBlock} can
* find and replace exactly the generated region without touching hand-written
* content below it.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ROOT_VENDOR_MARKER = exports.FRONTEND_VENDOR_MARKER = exports.BACKEND_VENDOR_MARKER = void 0;
exports.buildBackendVendorBlock = buildBackendVendorBlock;
exports.buildFrontendVendorBlock = buildFrontendVendorBlock;
exports.buildRootVendorBlock = buildRootVendorBlock;
exports.hasVendorBlock = hasVendorBlock;
exports.healVendorClaudeMd = healVendorClaudeMd;
exports.insertVendorBlockIfMissing = insertVendorBlockIfMissing;
exports.removeVendorBlock = removeVendorBlock;
exports.upsertVendorBlock = upsertVendorBlock;
exports.BACKEND_VENDOR_MARKER = '<!-- lt-vendor-marker -->';
exports.FRONTEND_VENDOR_MARKER = '<!-- lt-vendor-marker-frontend -->';
exports.ROOT_VENDOR_MARKER = '<!-- lt-vendor-marker-root -->';
/**
* Vendor-mode notice for the backend api project (`projects/api/CLAUDE.md`).
*/
function buildBackendVendorBlock() {
return block([
exports.BACKEND_VENDOR_MARKER,
'',
'# Vendor-Mode Notice',
'',
'This api project runs in **vendor mode**: the `@lenne.tech/nest-server`',
'core/ tree has been copied directly into `src/core/` as first-class',
'project code. There is **no** `@lenne.tech/nest-server` npm dependency.',
'',
'- **Read framework code from `src/core/**`** — not from `node_modules/`.',
'- **Generated imports use relative paths** to `src/core`, e.g.',
" `import { CrudService } from '../../../core';`",
' The exact depth depends on the file location. `lt server module`',
' computes it automatically.',
'- **Baseline + patch log** live in `src/core/VENDOR.md`. Log any',
' substantial local change there so the `nest-server-core-updater`',
' agent can classify it at sync time.',
'- **Update flow:** run `/lt-dev:backend:update-nest-server-core` (the',
' agent clones upstream, computes a delta, and presents a review). The',
' update also raises npm packages to at least the upstream baseline',
' (via `/lt-dev:maintenance:maintain`).',
'- **Contribute back:** run `/lt-dev:backend:contribute-nest-server-core`',
' to propose local fixes as upstream PRs.',
'- **Freshness check:** `pnpm run check:vendor-freshness` warns (non-',
' blockingly) when upstream has a newer release than the baseline.',
]);
}
/**
* Vendor-mode notice for the frontend app project (`projects/app/CLAUDE.md`).
*/
function buildFrontendVendorBlock() {
return block([
exports.FRONTEND_VENDOR_MARKER,
'',
'# Vendor-Mode Notice (Frontend)',
'',
'This frontend project runs in **vendor mode**: the `@lenne.tech/nuxt-extensions`',
'module has been copied directly into `app/core/` as first-class',
'project code. There is **no** `@lenne.tech/nuxt-extensions` npm dependency.',
'',
'- **Read framework code from `app/core/**`** — not from `node_modules/`.',
"- **nuxt.config.ts** references `'./app/core/module'` instead of",
" `'@lenne.tech/nuxt-extensions'`.",
'- **Baseline + patch log** live in `app/core/VENDOR.md`. Log any',
' substantial local change there so the `nuxt-extensions-core-updater`',
' agent can classify it at sync time.',
'- **Update flow:** run `/lt-dev:frontend:update-nuxt-extensions-core`. The',
' update also raises npm packages to at least the upstream baseline',
' (via `/lt-dev:maintenance:maintain`).',
'- **Contribute back:** run `/lt-dev:frontend:contribute-nuxt-extensions-core`.',
'- **Freshness check:** `pnpm run check:vendor-freshness` warns when',
' upstream has a newer release than the baseline.',
]);
}
/**
* Vendor-mode notice for the monorepo root (`<workspace>/CLAUDE.md`).
*
* Tailored to which halves are vendored so Claude sees only the relevant
* commands. At least one of `backend` / `frontend` must be true; otherwise the
* caller should {@link removeVendorBlock} instead of writing an empty notice.
*/
function buildRootVendorBlock(opts) {
const { backend, frontend } = opts;
const lines = [
exports.ROOT_VENDOR_MARKER,
'',
'# Vendor-Mode Notice (Monorepo)',
'',
'This workspace runs at least one framework in **vendor mode** — the',
'framework source is vendored directly into the project tree instead of',
'being an npm dependency. Read framework code from the vendored `core/`',
'trees, not from `node_modules/`.',
'',
'**Vendored frameworks:**',
];
if (backend) {
lines.push('- **Backend** (`@lenne.tech/nest-server`): `projects/api/src/core/` —', ' details in `projects/api/CLAUDE.md` and `projects/api/src/core/VENDOR.md`.');
}
if (frontend) {
lines.push('- **Frontend** (`@lenne.tech/nuxt-extensions`): `projects/app/app/core/` —', ' details in `projects/app/CLAUDE.md` and `projects/app/app/core/VENDOR.md`.');
}
lines.push('', '**Update** (sync from upstream; also raises npm packages to at least the', 'upstream baseline via `/lt-dev:maintenance:maintain`):');
if (backend) {
lines.push('- Backend: `/lt-dev:backend:update-nest-server-core`');
}
if (frontend) {
lines.push('- Frontend: `/lt-dev:frontend:update-nuxt-extensions-core`');
}
lines.push('', '**Contribute back** generally-useful core fixes as upstream PRs:');
if (backend) {
lines.push('- Backend: `/lt-dev:backend:contribute-nest-server-core`');
}
if (frontend) {
lines.push('- Frontend: `/lt-dev:frontend:contribute-nuxt-extensions-core`');
}
lines.push('', 'Project-specific code never goes into a `core/` tree — see each', "subproject's VENDOR.md Modification Policy.");
return block(lines);
}
/** True when `content` already contains the given vendor marker. */
function hasVendorBlock(content, marker) {
return content.includes(marker);
}
/**
* Bring every CLAUDE.md in a workspace in line with its current vendor state:
* upsert the matching notice block where a framework is vendored, remove it
* where it is not. Idempotent — running it on an already-correct workspace
* changes nothing.
*
* This is what makes `lt fullstack update` able to *heal* pre-existing or
* drifted vendor projects (e.g. ones scaffolded before the root notice existed,
* or whose notice fell out of date).
*
* @returns the list of CLAUDE.md paths that were actually modified.
*/
function healVendorClaudeMd(fs, state) {
const changed = [];
const apply = (path, marker, desiredBlock) => {
if (!fs.exists(path)) {
return;
}
const content = fs.read(path) || '';
const next = desiredBlock ? upsertVendorBlock(content, marker, desiredBlock) : removeVendorBlock(content, marker);
if (next !== content) {
fs.write(path, next);
changed.push(path);
}
};
if (state.apiDir) {
apply(joinPath(state.apiDir, 'CLAUDE.md'), exports.BACKEND_VENDOR_MARKER, state.backendVendor ? buildBackendVendorBlock() : null);
}
if (state.appDir) {
apply(joinPath(state.appDir, 'CLAUDE.md'), exports.FRONTEND_VENDOR_MARKER, state.frontendVendor ? buildFrontendVendorBlock() : null);
}
if (state.workspaceRoot) {
const anyVendor = state.backendVendor || state.frontendVendor;
apply(joinPath(state.workspaceRoot, 'CLAUDE.md'), exports.ROOT_VENDOR_MARKER, anyVendor ? buildRootVendorBlock({ backend: state.backendVendor, frontend: state.frontendVendor }) : null);
}
return changed;
}
/**
* Insert the block at the very top of the file **only when it is missing**.
* Used during conversion so a hand-customized existing block is never clobbered.
*/
function insertVendorBlockIfMissing(content, marker, newBlock) {
if (content.includes(marker)) {
return content;
}
return newBlock + content;
}
/**
* Remove the generated block (marker through the first `---`) and trim leading
* blank lines. Used when converting a project back to npm mode.
*/
function removeVendorBlock(content, marker) {
if (!content.includes(marker)) {
return content;
}
return content.replace(blockRegex(marker), '').replace(/^\n+/, '');
}
/**
* Insert the block if missing, or replace the existing generated region with the
* current canonical block (idempotent self-heal). Used by `lt fullstack update`
* to bring pre-existing / drifted projects up to the current notice.
*/
function upsertVendorBlock(content, marker, newBlock) {
if (!content.includes(marker)) {
return newBlock + content;
}
return content.replace(blockRegex(marker), newBlock);
}
/** Join block lines into the canonical `marker … --- ` shape (ends with `---\n`). */
function block(lines) {
return [...lines, '', '---', ''].join('\n');
}
/** Build the regex that matches an existing block from its marker to the first `---`. */
function blockRegex(marker) {
return new RegExp(`${escapeRegExp(marker)}[\\s\\S]*?---\\s*\\n?`);
}
/** Escape a string for safe use inside a RegExp. */
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function joinPath(dir, file) {
return dir.endsWith('/') ? `${dir}${file}` : `${dir}/${file}`;
}