@lenne.tech/cli 1.11.1 → 1.13.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.
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FrontendHelper = void 0;
13
+ const markdown_table_1 = require("../lib/markdown-table");
13
14
  /**
14
15
  * Frontend helper functions for project scaffolding
15
16
  * Provides reusable methods for setting up Nuxt and Angular frontends
@@ -273,9 +274,7 @@ class FrontendHelper {
273
274
  const { print } = this.toolbox;
274
275
  print.warning('');
275
276
  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));
277
+ const rows = localChangesSection[0].split('\n').filter((l) => /^\|\s*\d{4}-/.test(l));
279
278
  for (const row of rows.slice(0, 5)) {
280
279
  print.info(` ${row.trim()}`);
281
280
  }
@@ -307,8 +306,7 @@ class FrontendHelper {
307
306
  // Unhook freshness from check/check:fix/check:naf
308
307
  for (const scriptName of ['check', 'check:fix', 'check:naf']) {
309
308
  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, '');
309
+ scripts[scriptName] = scripts[scriptName].replace(/pnpm run check:vendor-freshness && /g, '');
312
310
  }
313
311
  }
314
312
  }
@@ -354,7 +352,7 @@ class FrontendHelper {
354
352
  */
355
353
  convertAppCloneToVendored(options) {
356
354
  return __awaiter(this, void 0, void 0, function* () {
357
- const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nuxt-extensions.git', } = options;
355
+ const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nuxt-extensions.git' } = options;
358
356
  const path = require('node:path');
359
357
  const { filesystem, system } = this.toolbox;
360
358
  const coreDir = path.join(dest, 'app', 'core');
@@ -501,14 +499,14 @@ class FrontendHelper {
501
499
  "var f=require('fs'),h=require('https');",
502
500
  "try{var c=f.readFileSync('app/core/VENDOR.md','utf8')}catch(e){process.exit(0)}",
503
501
  '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)}',
502
+ "if(!m){process.stderr.write(String.fromCharCode(9888)+' vendor-freshness: no baseline\\n');process.exit(0)}",
505
503
  'var v=m[1];',
506
504
  "h.get('https://registry.npmjs.org/@lenne.tech/nuxt-extensions/latest',function(r){",
507
505
  "var d='';r.on('data',function(c){d+=c});r.on('end',function(){",
508
- "try{var l=JSON.parse(d).version;",
506
+ 'try{var l=JSON.parse(d).version;',
509
507
  "if(v===l)console.log('vendor core up-to-date (v'+v+')');",
510
508
  "else process.stderr.write('vendor core v'+v+', latest v'+l+'\\n')",
511
- '}catch(e){}})}).on(\'error\',function(){});',
509
+ "}catch(e){}})}).on('error',function(){});",
512
510
  'setTimeout(function(){process.exit(0)},5000)',
513
511
  '"',
514
512
  ].join('');
@@ -525,7 +523,20 @@ class FrontendHelper {
525
523
  hookFreshness('check:fix');
526
524
  hookFreshness('check:naf');
527
525
  }
528
- filesystem.write(pkgPath, pkg, { jsonIndent: 2 });
526
+ // Sort dependency maps alphabetically so merged-in entries
527
+ // (e.g. upstream `@nuxt/kit`) end up in the expected position
528
+ // and the generated package.json passes oxfmt/format:check.
529
+ const sortObjectKeys = (obj) => {
530
+ if (!obj)
531
+ return obj;
532
+ return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
533
+ };
534
+ pkg.dependencies = sortObjectKeys(pkg.dependencies);
535
+ pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
536
+ pkg.peerDependencies = sortObjectKeys(pkg.peerDependencies);
537
+ // Ensure trailing newline — oxfmt with the starter's .editorconfig
538
+ // `insert_final_newline = true` requires it.
539
+ filesystem.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
529
540
  }
530
541
  }
531
542
  // ── 6. CLAUDE.md: prepend vendor marker + merge upstream sections ────
@@ -544,8 +555,8 @@ class FrontendHelper {
544
555
  'project code. There is **no** `@lenne.tech/nuxt-extensions` npm dependency.',
545
556
  '',
546
557
  '- **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\'`.',
558
+ "- **nuxt.config.ts** references `'./app/core/module'` instead of",
559
+ " `'@lenne.tech/nuxt-extensions'`.",
549
560
  '- **Baseline + patch log** live in `app/core/VENDOR.md`. Log any',
550
561
  ' substantial local change there so the `nuxt-extensions-core-updater`',
551
562
  ' agent can classify it at sync time.',
@@ -595,13 +606,41 @@ class FrontendHelper {
595
606
  '# @lenne.tech/nuxt-extensions (vendored)',
596
607
  '',
597
608
  '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.',
609
+ 'It is first-class project code, not a node_modules shadow copy — but it',
610
+ 'is **not a fork**. The copy exists so Claude Code (and humans) can read',
611
+ 'framework internals directly. Log substantial local changes in the',
612
+ '"Local changes" table below so the `nuxt-extensions-core-updater` agent',
613
+ 'can classify them at sync time.',
601
614
  '',
602
615
  'Unlike the backend (nest-server) vendoring, no flatten-fix is needed —',
603
616
  'the nuxt-extensions source structure is already flat.',
604
617
  '',
618
+ '## Modification Policy',
619
+ '',
620
+ 'Edit `app/core/` **only** when the change is generally useful to every',
621
+ '@lenne.tech/nuxt-extensions consumer:',
622
+ '',
623
+ '- Bugfixes that apply to every consumer',
624
+ '- Broad framework enhancements (new composables, better defaults, SSR fixes)',
625
+ '- Security vulnerability fixes',
626
+ '- Type/config compatibility fixes every consumer would hit',
627
+ '',
628
+ 'Everything else stays **outside** `app/core/`. Project-specific business',
629
+ 'rules, customer branding, and proprietary integrations belong in project',
630
+ 'code (`app/composables/`, `app/components/`, `app/middleware/`, plugin',
631
+ 'overrides).',
632
+ '',
633
+ 'Generally-useful changes **MUST** be submitted as an upstream PR to',
634
+ 'https://github.com/lenneTech/nuxt-extensions. Run',
635
+ '`/lt-dev:frontend:contribute-nuxt-extensions-core` to prepare it — the',
636
+ 'agent filters cosmetic commits, categorizes local changes as',
637
+ 'upstream-candidate vs. project-specific, and writes PR drafts for human',
638
+ "review. Letting useful fixes rot in one project's vendor tree is an",
639
+ 'anti-pattern: they belong upstream so every consumer benefits and the',
640
+ 'local patch disappears on the next sync.',
641
+ '',
642
+ 'When in doubt, ask before editing `app/core/`.',
643
+ '',
605
644
  '## Baseline',
606
645
  '',
607
646
  '- **Upstream-Repo:** https://github.com/lenneTech/nuxt-extensions',
@@ -612,22 +651,15 @@ class FrontendHelper {
612
651
  '',
613
652
  '## Sync history',
614
653
  '',
615
- '| Date | From | To | Notes |',
616
- '| ---- | ---- | -- | ----- |',
617
- `| ${today} | — | ${syncHistoryTo} | scaffolded by lt CLI |`,
654
+ ...(0, markdown_table_1.formatMarkdownTable)(['Date', 'From', 'To', 'Notes'], [[today, '—', syncHistoryTo, 'scaffolded by lt CLI']]),
618
655
  '',
619
656
  '## Local changes',
620
657
  '',
621
- '| Date | Commit | Scope | Reason | Status |',
622
- '| ---- | ------ | ----- | ------ | ------ |',
623
- '| — | — | (none, pristine) | initial vendor | — |',
658
+ ...(0, markdown_table_1.formatMarkdownTable)(['Date', 'Commit', 'Scope', 'Reason', 'Status'], [['—', '—', '(none, pristine)', 'initial vendor', '—']]),
624
659
  '',
625
660
  '## Upstream PRs',
626
661
  '',
627
- '| PR | Title | Commits | Status |',
628
- '| -- | ----- | ------- | ------ |',
629
- '| — | (none yet) | — | — |',
630
- '',
662
+ ...(0, markdown_table_1.formatMarkdownTable)(['PR', 'Title', 'Commits', 'Status'], [['—', '(none yet)', '—', '—']]),
631
663
  ].join('\n'));
632
664
  }
633
665
  // ── Post-conversion verification ─────────────────────────────────────
@@ -749,9 +781,7 @@ class FrontendHelper {
749
781
  if (skipPathContaining && absFile.includes(skipPathContaining))
750
782
  continue;
751
783
  const content = filesystem.read(absFile) || '';
752
- const matches = typeof needle === 'string'
753
- ? content.includes(needle)
754
- : needle.test(content);
784
+ const matches = typeof needle === 'string' ? content.includes(needle) : needle.test(content);
755
785
  if (matches) {
756
786
  stale.push(absFile.replace(`${appDir}/`, ''));
757
787
  }
@@ -800,10 +830,7 @@ class FrontendHelper {
800
830
  walkConsumerFiles(appDir) {
801
831
  const fs = require('node:fs');
802
832
  const path = require('node:path');
803
- const searchDirs = [
804
- path.join(appDir, 'app'),
805
- path.join(appDir, 'tests'),
806
- ];
833
+ const searchDirs = [path.join(appDir, 'app'), path.join(appDir, 'tests')];
807
834
  const allFiles = [];
808
835
  const walk = (dir) => {
809
836
  try {
@@ -119,8 +119,8 @@ class Git {
119
119
  const result = [];
120
120
  // Toolbox features
121
121
  const { system } = this.toolbox;
122
- // Get branches
123
- const branches = yield system.run('git fetch && git show-branch --list');
122
+ // Get branches (use short SSH timeout so fetch doesn't hang in offline environments)
123
+ const branches = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null; git show-branch --list');
124
124
  branches.split('\n').forEach((item) => {
125
125
  const matches = item.match(/\[(.*?)]/);
126
126
  if (matches) {
@@ -251,11 +251,10 @@ class Git {
251
251
  if (opts.spin) {
252
252
  searchSpin = spin(opts.spinText);
253
253
  }
254
- // Update infos
255
- const fetch = yield system.run('git fetch');
254
+ // Update infos (use short SSH timeout so fetch doesn't hang in offline environments)
255
+ const fetch = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true');
256
256
  if (fetch.length && !fetch.startsWith('remote')) {
257
257
  info(`Could not update infos ${fetch.length}`);
258
- process.exit(1);
259
258
  }
260
259
  // Search branch
261
260
  if (opts.exact) {
@@ -46,6 +46,7 @@ exports.Server = void 0;
46
46
  const crypto = __importStar(require("crypto"));
47
47
  const path_1 = require("path");
48
48
  const ts = __importStar(require("typescript"));
49
+ const markdown_table_1 = require("../lib/markdown-table");
49
50
  /**
50
51
  * Server helper functions
51
52
  */
@@ -749,6 +750,13 @@ class Server {
749
750
  catch (err) {
750
751
  return { method: result.method, path: dest, success: false };
751
752
  }
753
+ // Post-install format pass. processApiMode may have left whitespace
754
+ // artifacts (multi-line arrays/imports) that the formatter flags in
755
+ // format:check; oxfmt is only available after install, so we run it
756
+ // here.
757
+ if (apiMode) {
758
+ yield apiModeHelper.formatProject(dest);
759
+ }
752
760
  }
753
761
  return { method: result.method, path: dest, success: true };
754
762
  });
@@ -890,9 +898,11 @@ class Server {
890
898
  *
891
899
  * In vendor mode we additionally clone `@lenne.tech/nest-server` to
892
900
  * /tmp, copy its framework kernel (`src/core/`, `src/index.ts`,
893
- * `src/core.module.ts`, `src/test/`, `src/templates/`, `src/types/`,
894
- * `LICENSE`, `bin/migrate.js`) into the project at `src/core/`
895
- * applying the flatten-fix, remove `@lenne.tech/nest-server` from the
901
+ * `src/core.module.ts`, `src/test/`, `src/types/`, `LICENSE`,
902
+ * `bin/migrate.js`) into the project at `src/core/` applying the
903
+ * flatten-fix, place upstream `src/templates/` at `<project>/src/templates/`
904
+ * (outside core/ so the runtime resolver finds it at the same relative
905
+ * path as in npm mode), remove `@lenne.tech/nest-server` from the
896
906
  * project's `package.json`, merge the framework's transitive deps into
897
907
  * the project's own deps, and run an AST-based codemod that rewrites
898
908
  * every `from '@lenne.tech/nest-server'` import in consumer code
@@ -994,7 +1004,10 @@ class Server {
994
1004
  //
995
1005
  // Upstream layout: src/core/ (framework sub-dir) + src/index.ts +
996
1006
  // src/core.module.ts + src/test/ + src/templates/ + src/types/.
997
- // Target layout: everything flat under <project>/src/core/.
1007
+ // Target layout: most things flat under <project>/src/core/, with
1008
+ // one exception: src/templates/ stays at the same upstream location
1009
+ // (<project>/src/templates/) because the runtime email-template
1010
+ // resolver uses __dirname-relative lookup that must match npm mode.
998
1011
  //
999
1012
  // We WIPE the starter's (non-existent in npm mode) src/core/ first
1000
1013
  // just to guarantee idempotency when users run this twice.
@@ -1006,7 +1019,15 @@ class Server {
1006
1019
  [`${tmpClone}/src/index.ts`, `${coreDir}/index.ts`],
1007
1020
  [`${tmpClone}/src/core.module.ts`, `${coreDir}/core.module.ts`],
1008
1021
  [`${tmpClone}/src/test`, `${coreDir}/test`],
1009
- [`${tmpClone}/src/templates`, `${coreDir}/templates`],
1022
+ // src/templates/ stays OUTSIDE src/core/ at its upstream location so
1023
+ // the runtime template resolver (which computes
1024
+ // `__dirname + '../../../templates'` from within
1025
+ // src/core/modules/better-auth/) finds E-Mail templates at the same
1026
+ // relative path as in npm mode (node_modules/@lenne.tech/nest-server/
1027
+ // src/templates/). Keeping templates as first-class project files
1028
+ // outside core/ also lets projects customize them without touching
1029
+ // the vendored framework tree.
1030
+ [`${tmpClone}/src/templates`, `${dest}/src/templates`],
1010
1031
  [`${tmpClone}/src/types`, `${coreDir}/types`],
1011
1032
  [`${tmpClone}/LICENSE`, `${coreDir}/LICENSE`],
1012
1033
  ];
@@ -1395,14 +1416,14 @@ class Server {
1395
1416
  "var f=require('fs'),h=require('https');",
1396
1417
  "try{var c=f.readFileSync('src/core/VENDOR.md','utf8')}catch(e){process.exit(0)}",
1397
1418
  'var m=c.match(/Baseline-Version[^0-9]*(\\d+\\.\\d+\\.\\d+)/);',
1398
- 'if(!m){process.stderr.write(String.fromCharCode(9888)+\' vendor-freshness: no baseline\\n\');process.exit(0)}',
1419
+ "if(!m){process.stderr.write(String.fromCharCode(9888)+' vendor-freshness: no baseline\\n');process.exit(0)}",
1399
1420
  'var v=m[1];',
1400
1421
  "h.get('https://registry.npmjs.org/@lenne.tech/nest-server/latest',function(r){",
1401
1422
  "var d='';r.on('data',function(c){d+=c});r.on('end',function(){",
1402
- "try{var l=JSON.parse(d).version;",
1423
+ 'try{var l=JSON.parse(d).version;',
1403
1424
  "if(v===l)console.log('vendor core up-to-date (v'+v+')');",
1404
1425
  "else process.stderr.write('vendor core v'+v+', latest v'+l+'\\n')",
1405
- '}catch(e){}})}).on(\'error\',function(){});',
1426
+ "}catch(e){}})}).on('error',function(){});",
1406
1427
  'setTimeout(function(){process.exit(0)},5000)',
1407
1428
  '"',
1408
1429
  ].join('');
@@ -1417,7 +1438,8 @@ class Server {
1417
1438
  return;
1418
1439
  const installPrefix = 'pnpm install && ';
1419
1440
  if (existing.startsWith(installPrefix)) {
1420
- scripts[scriptName] = `${installPrefix}pnpm run check:vendor-freshness && ${existing.slice(installPrefix.length)}`;
1441
+ scripts[scriptName] =
1442
+ `${installPrefix}pnpm run check:vendor-freshness && ${existing.slice(installPrefix.length)}`;
1421
1443
  }
1422
1444
  else {
1423
1445
  scripts[scriptName] = `pnpm run check:vendor-freshness && ${existing}`;
@@ -1485,7 +1507,7 @@ class Server {
1485
1507
  ' * Vendor-mode stub for extras/sync-packages.mjs.',
1486
1508
  ' *',
1487
1509
  ' * The original script is designed for npm-mode projects where',
1488
- " * `@lenne.tech/nest-server` is an installed dependency and",
1510
+ ' * `@lenne.tech/nest-server` is an installed dependency and',
1489
1511
  ' * `pnpm run update` pulls the latest upstream deps into the',
1490
1512
  ' * project package.json.',
1491
1513
  ' *',
@@ -1547,7 +1569,7 @@ class Server {
1547
1569
  '',
1548
1570
  '- **Read framework code from `src/core/**`** — not from `node_modules/`.',
1549
1571
  '- **Generated imports use relative paths** to `src/core`, e.g.',
1550
- ' `import { CrudService } from \'../../../core\';`',
1572
+ " `import { CrudService } from '../../../core';`",
1551
1573
  ' The exact depth depends on the file location. `lt server module`',
1552
1574
  ' computes it automatically.',
1553
1575
  '- **Baseline + patch log** live in `src/core/VENDOR.md`. Log any',
@@ -1615,17 +1637,48 @@ class Server {
1615
1637
  '',
1616
1638
  'This directory is a curated vendor copy of the `core/` tree from',
1617
1639
  '@lenne.tech/nest-server. It is first-class project code, not a',
1618
- 'node_modules shadow copy. Edit freely; log substantial changes in',
1619
- 'the "Local changes" table below so the `nest-server-core-updater`',
1620
- 'agent can classify them at sync time.',
1640
+ 'node_modules shadow copy but it is **not a fork**. The copy',
1641
+ 'exists so Claude Code (and humans) can read framework internals',
1642
+ 'directly. Log substantial local changes in the "Local changes"',
1643
+ 'table below so the `nest-server-core-updater` agent can classify',
1644
+ 'them at sync time.',
1621
1645
  '',
1622
1646
  'The flatten-fix was applied during `lt fullstack init`: the',
1623
1647
  'upstream `src/index.ts`, `src/core.module.ts`, `src/test/`,',
1624
- '`src/templates/`, `src/types/`, and `LICENSE` were moved under',
1625
- '`src/core/` and their relative `./core/…` specifiers were',
1626
- 'stripped. See the init code in',
1648
+ '`src/types/`, and `LICENSE` were moved under `src/core/` and',
1649
+ 'their relative `./core/…` specifiers were stripped. The upstream',
1650
+ '`src/templates/` tree (E-Mail templates) was placed at the',
1651
+ 'project root `src/templates/` (outside `src/core/`) so the',
1652
+ 'runtime template resolver finds them at the same relative path',
1653
+ 'as in npm mode. See the init code in',
1627
1654
  '`lenneTech/cli/src/extensions/server.ts#convertCloneToVendored`.',
1628
1655
  '',
1656
+ '## Modification Policy',
1657
+ '',
1658
+ 'Edit `src/core/` **only** when the change is generally useful to every',
1659
+ '@lenne.tech/nest-server consumer:',
1660
+ '',
1661
+ '- Bugfixes that apply to every consumer',
1662
+ '- Broad framework enhancements',
1663
+ '- Security vulnerability fixes',
1664
+ '- Build/TypeScript compatibility fixes every consumer would hit',
1665
+ '',
1666
+ 'Everything else stays **outside** `src/core/`. Project-specific',
1667
+ 'business rules, customer enums, and proprietary integrations',
1668
+ 'belong in project code via modification, inheritance, extension,',
1669
+ 'or `ICoreModuleOverrides`.',
1670
+ '',
1671
+ 'Generally-useful changes **MUST** be submitted as an upstream PR',
1672
+ 'to https://github.com/lenneTech/nest-server. Run',
1673
+ '`/lt-dev:backend:contribute-nest-server-core` to prepare it — the',
1674
+ 'agent filters cosmetic commits, categorizes local changes as',
1675
+ 'upstream-candidate vs. project-specific, and writes PR drafts for',
1676
+ "human review. Letting useful fixes rot in one project's vendor",
1677
+ 'tree is an anti-pattern: they belong upstream so every consumer',
1678
+ 'benefits and the local patch disappears on the next sync.',
1679
+ '',
1680
+ 'When in doubt, ask before editing `src/core/`.',
1681
+ '',
1629
1682
  '## Baseline',
1630
1683
  '',
1631
1684
  '- **Upstream-Repo:** https://github.com/lenneTech/nest-server',
@@ -1636,22 +1689,15 @@ class Server {
1636
1689
  '',
1637
1690
  '## Sync history',
1638
1691
  '',
1639
- '| Date | From | To | Notes |',
1640
- '| ---- | ---- | -- | ----- |',
1641
- `| ${today} | — | ${syncHistoryTo} | scaffolded by lt CLI |`,
1692
+ ...(0, markdown_table_1.formatMarkdownTable)(['Date', 'From', 'To', 'Notes'], [[today, '—', syncHistoryTo, 'scaffolded by lt CLI']]),
1642
1693
  '',
1643
1694
  '## Local changes',
1644
1695
  '',
1645
- '| Date | Commit | Scope | Reason | Status |',
1646
- '| ---- | ------ | ----- | ------ | ------ |',
1647
- '| — | — | (none, pristine) | initial vendor | — |',
1696
+ ...(0, markdown_table_1.formatMarkdownTable)(['Date', 'Commit', 'Scope', 'Reason', 'Status'], [['—', '—', '(none, pristine)', 'initial vendor', '—']]),
1648
1697
  '',
1649
1698
  '## Upstream PRs',
1650
1699
  '',
1651
- '| PR | Title | Commits | Status |',
1652
- '| -- | ----- | ------- | ------ |',
1653
- '| — | (none yet) | — | — |',
1654
- '',
1700
+ ...(0, markdown_table_1.formatMarkdownTable)(['PR', 'Title', 'Commits', 'Status'], [['—', '(none yet)', '—', '—']]),
1655
1701
  ].join('\n'));
1656
1702
  }
1657
1703
  // ── Post-conversion verification ──────────────────────────────────────
@@ -1842,10 +1888,7 @@ class Server {
1842
1888
  if (!this.filesystem.exists(tsconfigPath)) {
1843
1889
  return;
1844
1890
  }
1845
- const EXCLUDE_ENTRIES = [
1846
- 'src/core/modules/migrate/templates/**/*.template.ts',
1847
- 'src/core/test/**/*.ts',
1848
- ];
1891
+ const EXCLUDE_ENTRIES = ['src/core/modules/migrate/templates/**/*.template.ts', 'src/core/test/**/*.ts'];
1849
1892
  try {
1850
1893
  // The upstream tsconfig files may contain comments — standard JSON parse
1851
1894
  // breaks on them. Use a regex-based patch as a fallback.
@@ -2080,9 +2123,7 @@ class Server {
2080
2123
  print.warning('');
2081
2124
  print.warning('⚠ VENDOR.md documents local patches in src/core/ that will be LOST:');
2082
2125
  // Extract non-header table rows
2083
- const rows = localChangesSection[0]
2084
- .split('\n')
2085
- .filter((l) => /^\|\s*\d{4}-/.test(l));
2126
+ const rows = localChangesSection[0].split('\n').filter((l) => /^\|\s*\d{4}-/.test(l));
2086
2127
  for (const row of rows.slice(0, 5)) {
2087
2128
  print.info(` ${row.trim()}`);
2088
2129
  }
@@ -2112,10 +2153,7 @@ class Server {
2112
2153
  for (const sourceFile of project.getSourceFiles()) {
2113
2154
  let modified = false;
2114
2155
  // Static imports + re-exports
2115
- for (const decl of [
2116
- ...sourceFile.getImportDeclarations(),
2117
- ...sourceFile.getExportDeclarations(),
2118
- ]) {
2156
+ for (const decl of [...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations()]) {
2119
2157
  const spec = decl.getModuleSpecifierValue();
2120
2158
  if (!spec)
2121
2159
  continue;
@@ -2182,7 +2220,8 @@ class Server {
2182
2220
  const migrateCompiler = 'ts:./node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/helpers/ts-compiler.js';
2183
2221
  const migrateStore = '--store ./migrations-utils/migrate.js --migrations-dir ./migrations';
2184
2222
  const migrateTemplate = './node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/templates/migration-project.template.ts';
2185
- scripts['migrate:create'] = `f() { migrate create "$1" --template-file ${migrateTemplate} --migrations-dir ./migrations --compiler ${migrateCompiler}; }; f`;
2223
+ scripts['migrate:create'] =
2224
+ `f() { migrate create "$1" --template-file ${migrateTemplate} --migrations-dir ./migrations --compiler ${migrateCompiler}; }; f`;
2186
2225
  scripts['migrate:up'] = `migrate up ${migrateStore} --compiler ${migrateCompiler}`;
2187
2226
  scripts['migrate:down'] = `migrate down ${migrateStore} --compiler ${migrateCompiler}`;
2188
2227
  scripts['migrate:list'] = `migrate list ${migrateStore} --compiler ${migrateCompiler}`;
@@ -2262,8 +2301,7 @@ class Server {
2262
2301
  let content = filesystem.read(gitignorePath) || '';
2263
2302
  content = content
2264
2303
  .split('\n')
2265
- .filter((line) => !line.includes('scripts/vendor/sync-results') &&
2266
- !line.includes('scripts/vendor/upstream-candidates'))
2304
+ .filter((line) => !line.includes('scripts/vendor/sync-results') && !line.includes('scripts/vendor/upstream-candidates'))
2267
2305
  .join('\n');
2268
2306
  filesystem.write(gitignorePath, content);
2269
2307
  }
@@ -2279,7 +2317,7 @@ class Server {
2279
2317
  for (const f of staleRelativeImports.slice(0, 10)) {
2280
2318
  print.info(` ${f}`);
2281
2319
  }
2282
- print.info('These imports must be manually rewritten to \'@lenne.tech/nest-server\'.');
2320
+ print.info("These imports must be manually rewritten to '@lenne.tech/nest-server'.");
2283
2321
  }
2284
2322
  });
2285
2323
  }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.createBrowserFetcher = createBrowserFetcher;
13
+ /**
14
+ * Headless-browser HTML fetcher for single-page applications.
15
+ *
16
+ * Mirrors the chrome-md content script's PageReadyDetector:
17
+ * waits for the network to settle, then returns the fully hydrated
18
+ * HTML so Defuddle can extract the real content instead of the
19
+ * pre-render shell.
20
+ *
21
+ * Uses `playwright-core` with a three-tier strategy:
22
+ * 1. System Chrome / Edge via `channel: 'chrome' | 'msedge'`.
23
+ * 2. Playwright's own bundled Chromium (if already installed).
24
+ * 3. Auto-install Playwright's Chromium (`npx playwright install
25
+ * chromium`) and retry — opt-in via `autoInstall`.
26
+ */
27
+ const child_process_1 = require("child_process");
28
+ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; lenneTech-CLI-Crawler/1.0; +https://lenne.tech)';
29
+ /**
30
+ * Try to construct a browser fetcher. Prefers a system Chrome /
31
+ * Edge via Playwright channels, falls back to Playwright's bundled
32
+ * Chromium, and (optionally) auto-installs Chromium on demand.
33
+ */
34
+ function createBrowserFetcher() {
35
+ return __awaiter(this, arguments, void 0, function* (options = {}) {
36
+ const log = options.onLog || (() => undefined);
37
+ const reasons = [];
38
+ const { chromium } = require('playwright-core');
39
+ // 1. System Chrome.
40
+ const chromeFetcher = yield launch(chromium, { channel: 'chrome' }, options, 'system-chrome').catch((error) => {
41
+ reasons.push(`channel:chrome: ${error.message}`);
42
+ return null;
43
+ });
44
+ if (chromeFetcher) {
45
+ log(`Browser engine: ${chromeFetcher.engine}`);
46
+ return chromeFetcher;
47
+ }
48
+ // 2. System Edge (Windows fallback, also common on macOS).
49
+ const edgeFetcher = yield launch(chromium, { channel: 'msedge' }, options, 'system-edge').catch((error) => {
50
+ reasons.push(`channel:msedge: ${error.message}`);
51
+ return null;
52
+ });
53
+ if (edgeFetcher) {
54
+ log(`Browser engine: ${edgeFetcher.engine}`);
55
+ return edgeFetcher;
56
+ }
57
+ // 3. Playwright's bundled Chromium.
58
+ const bundledFetcher = yield launch(chromium, {}, options, 'playwright-chromium').catch((error) => {
59
+ reasons.push(`playwright-chromium: ${error.message}`);
60
+ return null;
61
+ });
62
+ if (bundledFetcher) {
63
+ log(`Browser engine: ${bundledFetcher.engine}`);
64
+ return bundledFetcher;
65
+ }
66
+ // 4. Optional auto-install, then retry Playwright's chromium.
67
+ if (options.autoInstall) {
68
+ log('No browser available — installing Playwright chromium (one-time download, ~170 MB)…');
69
+ try {
70
+ yield runNpx(['playwright', 'install', 'chromium']);
71
+ const retry = yield launch(chromium, {}, options, 'playwright-chromium');
72
+ if (retry) {
73
+ log(`Browser engine: ${retry.engine}`);
74
+ return retry;
75
+ }
76
+ }
77
+ catch (error) {
78
+ reasons.push(`auto-install: ${error instanceof Error ? error.message : String(error)}`);
79
+ }
80
+ }
81
+ throw new Error([
82
+ 'Could not start a headless browser for SPA rendering.',
83
+ ...reasons.map((r) => ` - ${r}`),
84
+ '',
85
+ 'Fix one of these:',
86
+ ' 1. Install Google Chrome or Microsoft Edge (Playwright picks them up automatically).',
87
+ ' 2. Install Playwright browsers manually: `npx playwright install chromium`.',
88
+ ' 3. Re-run with --install-browser to let the CLI install them.',
89
+ ].join('\n'));
90
+ });
91
+ }
92
+ function launch(chromium, launchOptions, options, engineLabel) {
93
+ return __awaiter(this, void 0, void 0, function* () {
94
+ const browser = yield chromium.launch(Object.assign(Object.assign({}, launchOptions), { headless: true }));
95
+ const context = yield browser.newContext({
96
+ userAgent: options.userAgent || DEFAULT_USER_AGENT,
97
+ });
98
+ return {
99
+ close: () => __awaiter(this, void 0, void 0, function* () {
100
+ yield context.close();
101
+ yield browser.close();
102
+ }),
103
+ engine: engineLabel,
104
+ fetch: (url) => __awaiter(this, void 0, void 0, function* () {
105
+ var _a;
106
+ const page = yield context.newPage();
107
+ try {
108
+ yield page.goto(url, {
109
+ timeout: (_a = options.maxWaitMs) !== null && _a !== void 0 ? _a : 20000,
110
+ waitUntil: 'networkidle',
111
+ });
112
+ if (options.extraWaitMs) {
113
+ yield page.waitForTimeout(options.extraWaitMs);
114
+ }
115
+ return yield page.content();
116
+ }
117
+ finally {
118
+ yield page.close();
119
+ }
120
+ }),
121
+ };
122
+ });
123
+ }
124
+ /**
125
+ * Run an `npx` command, streaming its output to the current stdio.
126
+ * Resolves on exit code 0, rejects otherwise.
127
+ */
128
+ function runNpx(args) {
129
+ return new Promise((resolve, reject) => {
130
+ const child = (0, child_process_1.spawn)('npx', args, { shell: false, stdio: 'inherit' });
131
+ child.on('error', reject);
132
+ child.on('exit', (code) => {
133
+ if (code === 0)
134
+ resolve();
135
+ else
136
+ reject(new Error(`npx ${args.join(' ')} exited with code ${code}`));
137
+ });
138
+ });
139
+ }