@lenne.tech/cli 1.9.6 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +88 -3
  2. package/build/commands/config/validate.js +2 -0
  3. package/build/commands/frontend/convert-mode.js +198 -0
  4. package/build/commands/fullstack/convert-mode.js +368 -0
  5. package/build/commands/fullstack/init.js +150 -4
  6. package/build/commands/fullstack/update.js +177 -0
  7. package/build/commands/server/add-property.js +29 -2
  8. package/build/commands/server/convert-mode.js +197 -0
  9. package/build/commands/server/create.js +41 -3
  10. package/build/commands/server/module.js +58 -25
  11. package/build/commands/server/object.js +26 -5
  12. package/build/commands/server/permissions.js +20 -6
  13. package/build/commands/server/test.js +7 -1
  14. package/build/commands/status.js +94 -3
  15. package/build/config/vendor-frontend-runtime-deps.json +4 -0
  16. package/build/config/vendor-runtime-deps.json +9 -0
  17. package/build/extensions/api-mode.js +19 -3
  18. package/build/extensions/frontend-helper.js +652 -0
  19. package/build/extensions/server.js +1475 -3
  20. package/build/lib/framework-detection.js +167 -0
  21. package/build/lib/frontend-framework-detection.js +129 -0
  22. package/build/templates/nest-server-module/inputs/template-create.input.ts.ejs +1 -1
  23. package/build/templates/nest-server-module/inputs/template.input.ts.ejs +1 -1
  24. package/build/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs +1 -1
  25. package/build/templates/nest-server-module/template.controller.ts.ejs +1 -1
  26. package/build/templates/nest-server-module/template.model.ts.ejs +1 -1
  27. package/build/templates/nest-server-module/template.module.ts.ejs +1 -1
  28. package/build/templates/nest-server-module/template.resolver.ts.ejs +1 -1
  29. package/build/templates/nest-server-module/template.service.ts.ejs +1 -1
  30. package/build/templates/nest-server-object/template-create.input.ts.ejs +1 -1
  31. package/build/templates/nest-server-object/template.input.ts.ejs +1 -1
  32. package/build/templates/nest-server-object/template.object.ts.ejs +1 -1
  33. package/build/templates/nest-server-tests/tests.e2e-spec.ts.ejs +1 -1
  34. package/docs/LT-ECOSYSTEM-GUIDE.md +973 -0
  35. package/docs/VENDOR-MODE-WORKFLOW.md +471 -0
  36. package/docs/commands.md +196 -0
  37. package/docs/lt.config.md +9 -7
  38. package/package.json +17 -8
@@ -197,6 +197,658 @@ class FrontendHelper {
197
197
  return { method: result.method, path: dest, success: true };
198
198
  });
199
199
  }
200
+ // ═══════════════════════════════════════════════════════════════════════
201
+ // Frontend Vendor Mode — @lenne.tech/nuxt-extensions
202
+ // ═══════════════════════════════════════════════════════════════════════
203
+ /**
204
+ * Convert an existing npm-mode frontend project to vendor mode.
205
+ *
206
+ * Validates that the project currently uses @lenne.tech/nuxt-extensions
207
+ * as an npm dependency, then delegates to convertAppCloneToVendored.
208
+ */
209
+ convertAppToVendorMode(options) {
210
+ return __awaiter(this, void 0, void 0, function* () {
211
+ const { dest, upstreamBranch, upstreamRepoUrl } = options;
212
+ const { isVendoredAppProject } = require('../lib/frontend-framework-detection');
213
+ if (isVendoredAppProject(dest)) {
214
+ throw new Error('Project is already in vendor mode (app/core/VENDOR.md exists).');
215
+ }
216
+ const pkg = this.toolbox.filesystem.read(`${dest}/package.json`, 'json');
217
+ if (!pkg) {
218
+ throw new Error('Cannot read package.json');
219
+ }
220
+ const allDeps = Object.assign(Object.assign({}, (pkg.dependencies || {})), (pkg.devDependencies || {}));
221
+ if (!allDeps['@lenne.tech/nuxt-extensions']) {
222
+ throw new Error('@lenne.tech/nuxt-extensions is not in dependencies or devDependencies. ' +
223
+ 'Is this an npm-mode lenne.tech frontend project?');
224
+ }
225
+ yield this.convertAppCloneToVendored({
226
+ dest,
227
+ upstreamBranch,
228
+ upstreamRepoUrl,
229
+ });
230
+ });
231
+ }
232
+ /**
233
+ * Convert an existing vendor-mode frontend project back to npm mode.
234
+ *
235
+ * Performs the inverse of convertAppCloneToVendored:
236
+ * 1. Read baseline version from VENDOR.md
237
+ * 2. Rewrite consumer imports from relative paths back to @lenne.tech/nuxt-extensions
238
+ * 3. Delete app/core/
239
+ * 4. Restore @lenne.tech/nuxt-extensions dependency in package.json
240
+ * 5. Rewrite nuxt.config.ts module entry
241
+ * 6. Remove vendor-specific scripts and CLAUDE.md marker
242
+ */
243
+ convertAppToNpmMode(options) {
244
+ return __awaiter(this, void 0, void 0, function* () {
245
+ var _a;
246
+ const { dest, targetVersion } = options;
247
+ const { filesystem } = this.toolbox;
248
+ const path = require('node:path');
249
+ const { isVendoredAppProject } = require('../lib/frontend-framework-detection');
250
+ if (!isVendoredAppProject(dest)) {
251
+ throw new Error('Project is not in vendor mode (app/core/VENDOR.md not found).');
252
+ }
253
+ const coreDir = path.join(dest, 'app', 'core');
254
+ // ── 1. Determine target version + warn about local patches ──────────
255
+ const vendorMd = filesystem.read(path.join(coreDir, 'VENDOR.md')) || '';
256
+ let version = targetVersion;
257
+ if (!version) {
258
+ const match = vendorMd.match(/Baseline-Version[^0-9]*(\d+\.\d+\.\d+\S*)/);
259
+ if (match) {
260
+ version = match[1];
261
+ }
262
+ }
263
+ if (!version) {
264
+ throw new Error('Cannot determine target version. Specify --version or ensure VENDOR.md has a Baseline-Version.');
265
+ }
266
+ // Warn if VENDOR.md documents local patches that will be lost
267
+ const localChangesSection = vendorMd.match(/## Local changes[\s\S]*?(?=## |$)/i);
268
+ if (localChangesSection) {
269
+ const hasRealPatches = localChangesSection[0].includes('|') &&
270
+ !localChangesSection[0].includes('(none, pristine)') &&
271
+ /\|\s*\d{4}-/.test(localChangesSection[0]);
272
+ if (hasRealPatches) {
273
+ const { print } = this.toolbox;
274
+ print.warning('');
275
+ print.warning('VENDOR.md documents local patches in app/core/ that will be LOST:');
276
+ const rows = localChangesSection[0]
277
+ .split('\n')
278
+ .filter((l) => /^\|\s*\d{4}-/.test(l));
279
+ for (const row of rows.slice(0, 5)) {
280
+ print.info(` ${row.trim()}`);
281
+ }
282
+ if (rows.length > 5) {
283
+ print.info(` ... and ${rows.length - 5} more`);
284
+ }
285
+ print.warning('Consider running /lt-dev:frontend:contribute-nuxt-extensions-core first to upstream them.');
286
+ print.warning('');
287
+ }
288
+ }
289
+ // ── 2. Rewrite consumer imports: relative → @lenne.tech/nuxt-extensions ─
290
+ this.rewriteConsumerImportsToNpm(dest);
291
+ // ── 3. Delete app/core/ ──────────────────────────────────────────────
292
+ if (filesystem.exists(coreDir)) {
293
+ filesystem.remove(coreDir);
294
+ }
295
+ // ── 4. Restore @lenne.tech/nuxt-extensions in package.json ──────────
296
+ const pkgPath = path.join(dest, 'package.json');
297
+ if (filesystem.exists(pkgPath)) {
298
+ const pkg = filesystem.read(pkgPath, 'json');
299
+ if (pkg && typeof pkg === 'object') {
300
+ if (!pkg.dependencies)
301
+ pkg.dependencies = {};
302
+ pkg.dependencies['@lenne.tech/nuxt-extensions'] = `^${version}`;
303
+ // Remove vendor-specific scripts
304
+ if (pkg.scripts && typeof pkg.scripts === 'object') {
305
+ const scripts = pkg.scripts;
306
+ delete scripts['check:vendor-freshness'];
307
+ // Unhook freshness from check/check:fix/check:naf
308
+ for (const scriptName of ['check', 'check:fix', 'check:naf']) {
309
+ if ((_a = scripts[scriptName]) === null || _a === void 0 ? void 0 : _a.includes('check:vendor-freshness')) {
310
+ scripts[scriptName] = scripts[scriptName]
311
+ .replace(/pnpm run check:vendor-freshness && /g, '');
312
+ }
313
+ }
314
+ }
315
+ filesystem.write(pkgPath, pkg, { jsonIndent: 2 });
316
+ }
317
+ }
318
+ // ── 5. Rewrite nuxt.config.ts module entry ──────────────────────────
319
+ this.rewriteNuxtConfig(dest, 'npm');
320
+ // ── 6. Clean CLAUDE.md vendor marker ─────────────────────────────────
321
+ const claudeMdPath = path.join(dest, 'CLAUDE.md');
322
+ if (filesystem.exists(claudeMdPath)) {
323
+ let content = filesystem.read(claudeMdPath) || '';
324
+ const markerStart = '<!-- lt-vendor-marker-frontend -->';
325
+ const markerEnd = '---';
326
+ if (content.includes(markerStart)) {
327
+ const startIdx = content.indexOf(markerStart);
328
+ const endIdx = content.indexOf(markerEnd, startIdx);
329
+ if (endIdx > startIdx) {
330
+ content = content.slice(0, startIdx) + content.slice(endIdx + markerEnd.length);
331
+ content = content.replace(/^\n+/, '');
332
+ filesystem.write(claudeMdPath, content);
333
+ }
334
+ }
335
+ }
336
+ // ── Post-conversion verification ─────────────────────────────────────
337
+ const stale = this.findStaleFrontendImports(dest, /from\s+['"]\..*\/core['"]/);
338
+ if (stale.length > 0) {
339
+ const { print } = this.toolbox;
340
+ print.warning(`${stale.length} file(s) still contain relative core imports after npm conversion:`);
341
+ for (const f of stale.slice(0, 10)) {
342
+ print.info(` ${f}`);
343
+ }
344
+ print.info('These imports must be manually rewritten to @lenne.tech/nuxt-extensions.');
345
+ }
346
+ });
347
+ }
348
+ /**
349
+ * Core vendoring pipeline for @lenne.tech/nuxt-extensions.
350
+ *
351
+ * Clones the upstream repo, copies module.ts + runtime/ into app/core/,
352
+ * rewrites nuxt.config.ts and explicit consumer imports, merges deps,
353
+ * creates VENDOR.md and patches CLAUDE.md.
354
+ */
355
+ convertAppCloneToVendored(options) {
356
+ return __awaiter(this, void 0, void 0, function* () {
357
+ const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nuxt-extensions.git', } = options;
358
+ const path = require('node:path');
359
+ const { filesystem, system } = this.toolbox;
360
+ const coreDir = path.join(dest, 'app', 'core');
361
+ // ── 1. Clone @lenne.tech/nuxt-extensions into temp dir ──────────────
362
+ const tmpClone = path.join(require('os').tmpdir(), `lt-vendor-nuxt-ext-${Date.now()}`);
363
+ const branchArg = upstreamBranch ? `--branch ${upstreamBranch} ` : '';
364
+ try {
365
+ yield system.run(`git clone --depth 1 ${branchArg}${upstreamRepoUrl} ${tmpClone}`);
366
+ }
367
+ catch (err) {
368
+ const raw = err.message || '';
369
+ const hints = [];
370
+ if (/Could not resolve host|getaddrinfo|ECONNREFUSED|Network is unreachable/i.test(raw)) {
371
+ hints.push('Network issue reaching github.com — check your connection or proxy settings.');
372
+ }
373
+ if (/Permission denied|authentication failed|publickey|403|401/i.test(raw)) {
374
+ hints.push('Authentication issue — the CLI uses an anonymous HTTPS clone; verify GitHub is reachable.');
375
+ }
376
+ if (upstreamBranch && /Remote branch .* not found|did not match any file\(s\) known to git/i.test(raw)) {
377
+ hints.push(`Upstream ref "${upstreamBranch}" does not exist. Check ${upstreamRepoUrl}/tags for valid refs. ` +
378
+ 'Note: nuxt-extensions tags have NO "v" prefix — use e.g. "1.5.3", not "v1.5.3".');
379
+ }
380
+ if (/already exists and is not an empty/i.test(raw)) {
381
+ hints.push(`Target ${tmpClone} already exists. rm -rf /tmp/lt-vendor-nuxt-ext-* and retry.`);
382
+ }
383
+ const hintBlock = hints.length > 0 ? `\n Hints:\n - ${hints.join('\n - ')}` : '';
384
+ throw new Error(`Failed to clone ${upstreamRepoUrl}${upstreamBranch ? ` (branch/tag: ${upstreamBranch})` : ''}.\n Raw git error: ${raw.trim()}${hintBlock}`);
385
+ }
386
+ // Snapshot upstream metadata
387
+ let upstreamDeps = {};
388
+ let upstreamDevDeps = {};
389
+ let upstreamPeerDeps = {};
390
+ let upstreamVersion = '';
391
+ try {
392
+ const upstreamPkg = filesystem.read(`${tmpClone}/package.json`, 'json');
393
+ if (upstreamPkg && typeof upstreamPkg === 'object') {
394
+ upstreamDeps = upstreamPkg.dependencies || {};
395
+ upstreamDevDeps = upstreamPkg.devDependencies || {};
396
+ upstreamPeerDeps = upstreamPkg.peerDependencies || {};
397
+ upstreamVersion = upstreamPkg.version || '';
398
+ }
399
+ }
400
+ catch (_a) {
401
+ // Best-effort
402
+ }
403
+ let upstreamClaudeMd = '';
404
+ try {
405
+ const c = filesystem.read(`${tmpClone}/CLAUDE.md`);
406
+ if (typeof c === 'string')
407
+ upstreamClaudeMd = c;
408
+ }
409
+ catch (_b) {
410
+ // Non-fatal
411
+ }
412
+ let upstreamCommit = '';
413
+ try {
414
+ const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
415
+ upstreamCommit = (sha || '').trim();
416
+ }
417
+ catch (_c) {
418
+ // Non-fatal
419
+ }
420
+ try {
421
+ // ── 2. Copy source files to app/core/ ─────────────────────────────
422
+ if (filesystem.exists(coreDir)) {
423
+ filesystem.remove(coreDir);
424
+ }
425
+ const copies = [
426
+ [`${tmpClone}/src/module.ts`, `${coreDir}/module.ts`],
427
+ [`${tmpClone}/src/index.ts`, `${coreDir}/index.ts`],
428
+ [`${tmpClone}/src/runtime`, `${coreDir}/runtime`],
429
+ [`${tmpClone}/LICENSE`, `${coreDir}/LICENSE`],
430
+ ];
431
+ for (const [from, to] of copies) {
432
+ if (filesystem.exists(from)) {
433
+ filesystem.copy(from, to);
434
+ }
435
+ }
436
+ }
437
+ finally {
438
+ // Always clean up temp clone
439
+ if (filesystem.exists(tmpClone)) {
440
+ filesystem.remove(tmpClone);
441
+ }
442
+ }
443
+ // No flatten-fix needed — nuxt-extensions source structure is already
444
+ // flat (module.ts + runtime/ at the same level). Unlike the backend
445
+ // (nest-server) where src/index.ts and src/core/ must be merged into
446
+ // one directory, nuxt-extensions keeps everything directly under src/.
447
+ // ── 3. Rewrite consumer explicit imports ─────────────────────────────
448
+ //
449
+ // Most consumer code uses Nuxt auto-imports (composables, utils,
450
+ // components) which the module.ts registers via addImports/addComponent.
451
+ // However, a few explicit imports exist:
452
+ // - Type imports: `from '@lenne.tech/nuxt-extensions'` (e.g. LtUser, LtUploadItem)
453
+ // - Testing imports: `from '@lenne.tech/nuxt-extensions/testing'`
454
+ //
455
+ // These must be rewritten to relative paths to app/core.
456
+ this.rewriteConsumerImportsToVendor(dest);
457
+ // ── 4. Rewrite nuxt.config.ts module entry ──────────────────────────
458
+ this.rewriteNuxtConfig(dest, 'vendor');
459
+ // ── 5. package.json: remove @lenne.tech/nuxt-extensions, merge deps ─
460
+ const pkgPath = path.join(dest, 'package.json');
461
+ if (filesystem.exists(pkgPath)) {
462
+ const pkg = filesystem.read(pkgPath, 'json');
463
+ if (pkg && typeof pkg === 'object') {
464
+ if (pkg.dependencies && typeof pkg.dependencies === 'object') {
465
+ delete pkg.dependencies['@lenne.tech/nuxt-extensions'];
466
+ }
467
+ if (pkg.devDependencies && typeof pkg.devDependencies === 'object') {
468
+ delete pkg.devDependencies['@lenne.tech/nuxt-extensions'];
469
+ }
470
+ // Merge upstream deps (mainly @nuxt/kit)
471
+ if (!pkg.dependencies)
472
+ pkg.dependencies = {};
473
+ const deps = pkg.dependencies;
474
+ for (const [depName, depVersion] of Object.entries(upstreamDeps)) {
475
+ if (depName === '@lenne.tech/nuxt-extensions')
476
+ continue;
477
+ if (!(depName in deps)) {
478
+ deps[depName] = depVersion;
479
+ }
480
+ }
481
+ // Promote any upstream devDeps flagged as runtime-needed (via
482
+ // vendor-frontend-runtime-deps.json) into dependencies. Currently
483
+ // empty for nuxt-extensions but reserved for future additions.
484
+ for (const [depName, depVersion] of Object.entries(upstreamDevDeps)) {
485
+ if (this.isFrontendVendorRuntimeDep(depName) && !(depName in deps)) {
486
+ deps[depName] = depVersion;
487
+ }
488
+ }
489
+ // Verify peer deps are present (they should already be from the starter)
490
+ for (const [depName] of Object.entries(upstreamPeerDeps)) {
491
+ if (!(depName in deps) && !(depName in (pkg.devDependencies || {}))) {
492
+ const { print } = this.toolbox;
493
+ print.warning(`Peer dependency ${depName} is missing — you may need to install it.`);
494
+ }
495
+ }
496
+ // Vendor freshness check script
497
+ if (pkg.scripts && typeof pkg.scripts === 'object') {
498
+ const scripts = pkg.scripts;
499
+ scripts['check:vendor-freshness'] = [
500
+ 'node -e "',
501
+ "var f=require('fs'),h=require('https');",
502
+ "try{var c=f.readFileSync('app/core/VENDOR.md','utf8')}catch(e){process.exit(0)}",
503
+ 'var m=c.match(/Baseline-Version[^0-9]*(\\d+\\.\\d+\\.\\d+)/);',
504
+ 'if(!m){process.stderr.write(String.fromCharCode(9888)+\' vendor-freshness: no baseline\\n\');process.exit(0)}',
505
+ 'var v=m[1];',
506
+ "h.get('https://registry.npmjs.org/@lenne.tech/nuxt-extensions/latest',function(r){",
507
+ "var d='';r.on('data',function(c){d+=c});r.on('end',function(){",
508
+ "try{var l=JSON.parse(d).version;",
509
+ "if(v===l)console.log('vendor core up-to-date (v'+v+')');",
510
+ "else process.stderr.write('vendor core v'+v+', latest v'+l+'\\n')",
511
+ '}catch(e){}})}).on(\'error\',function(){});',
512
+ 'setTimeout(function(){process.exit(0)},5000)',
513
+ '"',
514
+ ].join('');
515
+ // Hook freshness into check scripts
516
+ const hookFreshness = (scriptName) => {
517
+ const existing = scripts[scriptName];
518
+ if (!existing)
519
+ return;
520
+ if (existing.includes('check:vendor-freshness'))
521
+ return;
522
+ scripts[scriptName] = `pnpm run check:vendor-freshness && ${existing}`;
523
+ };
524
+ hookFreshness('check');
525
+ hookFreshness('check:fix');
526
+ hookFreshness('check:naf');
527
+ }
528
+ filesystem.write(pkgPath, pkg, { jsonIndent: 2 });
529
+ }
530
+ }
531
+ // ── 6. CLAUDE.md: prepend vendor marker + merge upstream sections ────
532
+ const claudeMdPath = path.join(dest, 'CLAUDE.md');
533
+ if (filesystem.exists(claudeMdPath)) {
534
+ const existing = filesystem.read(claudeMdPath) || '';
535
+ const marker = '<!-- lt-vendor-marker-frontend -->';
536
+ if (!existing.includes(marker)) {
537
+ const vendorBlock = [
538
+ marker,
539
+ '',
540
+ '# Vendor-Mode Notice (Frontend)',
541
+ '',
542
+ 'This frontend project runs in **vendor mode**: the `@lenne.tech/nuxt-extensions`',
543
+ 'module has been copied directly into `app/core/` as first-class',
544
+ 'project code. There is **no** `@lenne.tech/nuxt-extensions` npm dependency.',
545
+ '',
546
+ '- **Read framework code from `app/core/**`** — not from `node_modules/`.',
547
+ '- **nuxt.config.ts** references `\'./app/core/module\'` instead of',
548
+ ' `\'@lenne.tech/nuxt-extensions\'`.',
549
+ '- **Baseline + patch log** live in `app/core/VENDOR.md`. Log any',
550
+ ' substantial local change there so the `nuxt-extensions-core-updater`',
551
+ ' agent can classify it at sync time.',
552
+ '- **Update flow:** run `/lt-dev:frontend:update-nuxt-extensions-core`.',
553
+ '- **Contribute back:** run `/lt-dev:frontend:contribute-nuxt-extensions-core`.',
554
+ '- **Freshness check:** `pnpm run check:vendor-freshness` warns when',
555
+ ' upstream has a newer release than the baseline.',
556
+ '',
557
+ '---',
558
+ '',
559
+ ].join('\n');
560
+ filesystem.write(claudeMdPath, vendorBlock + existing);
561
+ }
562
+ }
563
+ // Merge upstream CLAUDE.md sections
564
+ if (upstreamClaudeMd && filesystem.exists(claudeMdPath)) {
565
+ const projectContent = filesystem.read(claudeMdPath) || '';
566
+ const upstreamSections = this.parseH2Sections(upstreamClaudeMd);
567
+ const projectSections = this.parseH2Sections(projectContent);
568
+ const newSections = [];
569
+ for (const [heading, body] of upstreamSections) {
570
+ if (heading === '__preamble__')
571
+ continue;
572
+ if (!projectSections.has(heading)) {
573
+ newSections.push(`## ${heading}\n\n${body.trim()}`);
574
+ }
575
+ }
576
+ if (newSections.length > 0) {
577
+ const separator = projectContent.endsWith('\n') ? '\n' : '\n\n';
578
+ filesystem.write(claudeMdPath, `${projectContent}${separator}${newSections.join('\n\n')}\n`);
579
+ }
580
+ }
581
+ // ── 7. VENDOR.md baseline ────────────────────────────────────────────
582
+ const vendorMdPath = path.join(coreDir, 'VENDOR.md');
583
+ if (!filesystem.exists(vendorMdPath)) {
584
+ const today = new Date().toISOString().slice(0, 10);
585
+ const versionLine = upstreamVersion
586
+ ? `- **Baseline-Version:** ${upstreamVersion}`
587
+ : '- **Baseline-Version:** (not detected)';
588
+ const commitLine = upstreamCommit
589
+ ? `- **Baseline-Commit:** \`${upstreamCommit}\``
590
+ : '- **Baseline-Commit:** (not detected)';
591
+ const syncHistoryTo = upstreamVersion
592
+ ? `${upstreamVersion}${upstreamCommit ? ` (\`${upstreamCommit.slice(0, 10)}\`)` : ''}`
593
+ : 'initial import';
594
+ filesystem.write(vendorMdPath, [
595
+ '# @lenne.tech/nuxt-extensions (vendored)',
596
+ '',
597
+ 'This directory is a curated vendor copy of `@lenne.tech/nuxt-extensions`.',
598
+ 'It is first-class project code, not a node_modules shadow copy.',
599
+ 'Edit freely; log substantial changes in the "Local changes" table below',
600
+ 'so the `nuxt-extensions-core-updater` agent can classify them at sync time.',
601
+ '',
602
+ 'Unlike the backend (nest-server) vendoring, no flatten-fix is needed —',
603
+ 'the nuxt-extensions source structure is already flat.',
604
+ '',
605
+ '## Baseline',
606
+ '',
607
+ '- **Upstream-Repo:** https://github.com/lenneTech/nuxt-extensions',
608
+ versionLine,
609
+ commitLine,
610
+ `- **Vendored am:** ${today}`,
611
+ '- **Vendored von:** lt CLI (`lt frontend convert-mode --to vendor`)',
612
+ '',
613
+ '## Sync history',
614
+ '',
615
+ '| Date | From | To | Notes |',
616
+ '| ---- | ---- | -- | ----- |',
617
+ `| ${today} | — | ${syncHistoryTo} | scaffolded by lt CLI |`,
618
+ '',
619
+ '## Local changes',
620
+ '',
621
+ '| Date | Commit | Scope | Reason | Status |',
622
+ '| ---- | ------ | ----- | ------ | ------ |',
623
+ '| — | — | (none, pristine) | initial vendor | — |',
624
+ '',
625
+ '## Upstream PRs',
626
+ '',
627
+ '| PR | Title | Commits | Status |',
628
+ '| -- | ----- | ------- | ------ |',
629
+ '| — | (none yet) | — | — |',
630
+ '',
631
+ ].join('\n'));
632
+ }
633
+ // ── Post-conversion verification ─────────────────────────────────────
634
+ // Only match actual import/from statements, not comments or strings
635
+ const staleImports = this.findStaleFrontendImports(dest, /(?:^|\s)(?:import|from)\s+['"][^'"]*@lenne\.tech\/nuxt-extensions/m, 'app/core/');
636
+ if (staleImports.length > 0) {
637
+ const { print } = this.toolbox;
638
+ print.warning(`${staleImports.length} file(s) still contain '@lenne.tech/nuxt-extensions' imports after vendor conversion:`);
639
+ for (const f of staleImports.slice(0, 10)) {
640
+ print.info(` ${f}`);
641
+ }
642
+ if (staleImports.length > 10) {
643
+ print.info(` ... and ${staleImports.length - 10} more`);
644
+ }
645
+ print.info('These imports must be manually rewritten to relative paths pointing to app/core.');
646
+ }
647
+ return { upstreamDeps };
648
+ });
649
+ }
650
+ // ═══════════════════════════════════════════════════════════════════════
651
+ // Private vendor helpers
652
+ // ═══════════════════════════════════════════════════════════════════════
653
+ /**
654
+ * Rewrite nuxt.config.ts module entry between npm and vendor mode.
655
+ *
656
+ * npm→vendor: '@lenne.tech/nuxt-extensions' → './app/core/module'
657
+ * vendor→npm: './app/core/module' → '@lenne.tech/nuxt-extensions'
658
+ */
659
+ rewriteNuxtConfig(appDir, mode) {
660
+ const path = require('node:path');
661
+ const { filesystem } = this.toolbox;
662
+ const configPath = path.join(appDir, 'nuxt.config.ts');
663
+ if (!filesystem.exists(configPath))
664
+ return;
665
+ let content = filesystem.read(configPath) || '';
666
+ if (mode === 'vendor') {
667
+ content = content.replace(/['"]@lenne\.tech\/nuxt-extensions['"]/g, "'./app/core/module'");
668
+ }
669
+ else {
670
+ content = content.replace(/['"]\.\/app\/core\/module['"]/g, "'@lenne.tech/nuxt-extensions'");
671
+ }
672
+ filesystem.write(configPath, content);
673
+ }
674
+ /**
675
+ * Rewrite consumer imports from npm specifiers to relative vendor paths.
676
+ *
677
+ * Handles both .ts and .vue files via regex replacement.
678
+ * Skips files inside app/core/ (the vendored framework itself).
679
+ */
680
+ rewriteConsumerImportsToVendor(appDir) {
681
+ const path = require('node:path');
682
+ const { filesystem } = this.toolbox;
683
+ const coreDir = path.join(appDir, 'app', 'core');
684
+ const coreDirWithSep = coreDir + path.sep;
685
+ const allFiles = this.walkConsumerFiles(appDir);
686
+ for (const absFile of allFiles) {
687
+ // Skip vendored framework files
688
+ if (absFile.startsWith(coreDirWithSep))
689
+ continue;
690
+ const content = filesystem.read(absFile);
691
+ if (!content)
692
+ continue;
693
+ if (!content.includes('@lenne.tech/nuxt-extensions'))
694
+ continue;
695
+ const fromDir = path.dirname(absFile);
696
+ let relToCore = path.relative(fromDir, coreDir).split(path.sep).join('/');
697
+ if (!relToCore.startsWith('.'))
698
+ relToCore = `./${relToCore}`;
699
+ let patched = content;
700
+ // Testing imports: @lenne.tech/nuxt-extensions/testing → relative/runtime/testing
701
+ patched = patched.replace(/from\s+['"]@lenne\.tech\/nuxt-extensions\/testing['"]/g, `from '${relToCore}/runtime/testing'`);
702
+ // Main imports: @lenne.tech/nuxt-extensions → relative core
703
+ patched = patched.replace(/from\s+['"]@lenne\.tech\/nuxt-extensions['"]/g, `from '${relToCore}'`);
704
+ if (patched !== content) {
705
+ filesystem.write(absFile, patched);
706
+ }
707
+ }
708
+ }
709
+ /**
710
+ * Rewrite consumer imports from relative vendor paths back to npm specifiers.
711
+ */
712
+ rewriteConsumerImportsToNpm(appDir) {
713
+ const path = require('node:path');
714
+ const { filesystem } = this.toolbox;
715
+ const coreDir = path.join(appDir, 'app', 'core');
716
+ const coreDirWithSep = coreDir + path.sep;
717
+ const allFiles = this.walkConsumerFiles(appDir);
718
+ for (const absFile of allFiles) {
719
+ if (absFile.startsWith(coreDirWithSep))
720
+ continue;
721
+ const content = filesystem.read(absFile);
722
+ if (!content)
723
+ continue;
724
+ // Check if file has any relative import pointing to the core dir
725
+ const fromDir = path.dirname(absFile);
726
+ const relToCore = path.relative(fromDir, coreDir).split(path.sep).join('/');
727
+ if (!content.includes(relToCore))
728
+ continue;
729
+ let patched = content;
730
+ // Testing imports: relative/runtime/testing → @lenne.tech/nuxt-extensions/testing
731
+ const testingPattern = new RegExp(`from\\s+['"]${relToCore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/runtime/testing['"]`, 'g');
732
+ patched = patched.replace(testingPattern, "from '@lenne.tech/nuxt-extensions/testing'");
733
+ // Main imports: relative core → @lenne.tech/nuxt-extensions
734
+ const corePattern = new RegExp(`from\\s+['"]${relToCore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
735
+ patched = patched.replace(corePattern, "from '@lenne.tech/nuxt-extensions'");
736
+ if (patched !== content) {
737
+ filesystem.write(absFile, patched);
738
+ }
739
+ }
740
+ }
741
+ /**
742
+ * Scan consumer files for stale imports matching a pattern.
743
+ */
744
+ findStaleFrontendImports(appDir, needle, skipPathContaining) {
745
+ const { filesystem } = this.toolbox;
746
+ const allFiles = this.walkConsumerFiles(appDir);
747
+ const stale = [];
748
+ for (const absFile of allFiles) {
749
+ if (skipPathContaining && absFile.includes(skipPathContaining))
750
+ continue;
751
+ const content = filesystem.read(absFile) || '';
752
+ const matches = typeof needle === 'string'
753
+ ? content.includes(needle)
754
+ : needle.test(content);
755
+ if (matches) {
756
+ stale.push(absFile.replace(`${appDir}/`, ''));
757
+ }
758
+ }
759
+ return stale;
760
+ }
761
+ /**
762
+ * Predicate: is a given upstream `devDependencies` key actually a runtime
763
+ * dep in disguise that needs to live in `dependencies` after vendoring?
764
+ *
765
+ * The list of such helpers lives in `src/config/vendor-frontend-runtime-deps.json`
766
+ * under the `runtimeHelpers` key. Adding a new helper is a data-only change
767
+ * (no CLI release required). If the config file is missing or unreadable,
768
+ * the predicate safely returns `false` for everything.
769
+ *
770
+ * Currently, nuxt-extensions has a minimal dependency graph (only `@nuxt/kit`
771
+ * as a direct runtime dep), so this list is typically empty. The mechanism
772
+ * exists for future-proofing: if upstream adds a devDep that the framework
773
+ * code imports at runtime, add it to the JSON and the next vendor conversion
774
+ * will promote it automatically.
775
+ */
776
+ isFrontendVendorRuntimeDep(pkgName) {
777
+ if (!this._vendorFrontendRuntimeHelpers) {
778
+ try {
779
+ const path = require('node:path');
780
+ const configPath = path.join(__dirname, '..', 'config', 'vendor-frontend-runtime-deps.json');
781
+ const raw = this.toolbox.filesystem.read(configPath, 'json');
782
+ const list = Array.isArray(raw === null || raw === void 0 ? void 0 : raw.runtimeHelpers) ? raw.runtimeHelpers : [];
783
+ this._vendorFrontendRuntimeHelpers = new Set(list.filter((e) => typeof e === 'string'));
784
+ }
785
+ catch (_a) {
786
+ this._vendorFrontendRuntimeHelpers = new Set();
787
+ }
788
+ }
789
+ return this._vendorFrontendRuntimeHelpers.has(pkgName);
790
+ }
791
+ /**
792
+ * Recursively walks `app/` and `tests/` directories under `appDir`,
793
+ * returning absolute paths to all `.ts` and `.vue` files.
794
+ *
795
+ * Shared helper for consumer-import codemods and stale-import scans.
796
+ * Uses native `fs.readdirSync` because gluegun's `filesystem.find()`
797
+ * returns paths relative to the jetpack cwd, which is unreliable
798
+ * when the CLI is invoked from arbitrary working directories.
799
+ */
800
+ walkConsumerFiles(appDir) {
801
+ const fs = require('node:fs');
802
+ const path = require('node:path');
803
+ const searchDirs = [
804
+ path.join(appDir, 'app'),
805
+ path.join(appDir, 'tests'),
806
+ ];
807
+ const allFiles = [];
808
+ const walk = (dir) => {
809
+ try {
810
+ const items = fs.readdirSync(dir, { withFileTypes: true });
811
+ for (const item of items) {
812
+ const fp = path.join(dir, item.name);
813
+ if (item.isDirectory()) {
814
+ walk(fp);
815
+ }
816
+ else if (item.isFile() && (fp.endsWith('.ts') || fp.endsWith('.vue'))) {
817
+ allFiles.push(fp);
818
+ }
819
+ }
820
+ }
821
+ catch (_a) {
822
+ // Directory doesn't exist or can't be read
823
+ }
824
+ };
825
+ for (const dir of searchDirs) {
826
+ walk(dir);
827
+ }
828
+ return allFiles;
829
+ }
830
+ /**
831
+ * Parse markdown content into H2 sections for section-level merge.
832
+ */
833
+ parseH2Sections(content) {
834
+ const sections = new Map();
835
+ const lines = content.split('\n');
836
+ let currentHeading = '__preamble__';
837
+ let currentBody = [];
838
+ for (const line of lines) {
839
+ const match = /^## (.+)$/.exec(line);
840
+ if (match) {
841
+ sections.set(currentHeading, currentBody.join('\n'));
842
+ currentHeading = match[1].trim();
843
+ currentBody = [];
844
+ }
845
+ else {
846
+ currentBody.push(line);
847
+ }
848
+ }
849
+ sections.set(currentHeading, currentBody.join('\n'));
850
+ return sections;
851
+ }
200
852
  }
201
853
  exports.FrontendHelper = FrontendHelper;
202
854
  /**