@lenne.tech/cli 1.11.0 → 1.12.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.
@@ -30,6 +30,11 @@ const CLAUDE_SHORTCUTS = [
30
30
  command: 'claude --dangerously-skip-permissions --resume',
31
31
  description: 'Select and resume previous session',
32
32
  },
33
+ {
34
+ alias: 'cf',
35
+ command: 'LT_PLUGIN_HOOKS_SKIP=1 claude --dangerously-skip-permissions',
36
+ description: 'Start Claude Code in fast mode (skip lenne.tech plugin detect hooks)',
37
+ },
33
38
  ];
34
39
  /**
35
40
  * Install Claude Code shell shortcuts
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
12
13
  /**
13
14
  * Create a new fullstack workspace
14
15
  */
@@ -456,6 +457,14 @@ const NewCommand = {
456
457
  else {
457
458
  serverSpinner.warn('Nest Server Starter not integrated');
458
459
  }
460
+ // Hoist workspace-scoped pnpm config out of sub-projects. pnpm only
461
+ // honors `pnpm.overrides`, `pnpm.onlyBuiltDependencies`, and
462
+ // `pnpm.ignoredOptionalDependencies` at the workspace root; leaving
463
+ // them in projects/api/package.json or projects/app/package.json
464
+ // causes `WARN The field … was found in … This will not take
465
+ // effect. You should configure … at the root of the workspace
466
+ // instead.` and silently disables CVE overrides.
467
+ (0, hoist_workspace_pnpm_config_1.hoistWorkspacePnpmConfig)({ filesystem, projectDir, subProjects: ['projects/api', 'projects/app'] });
459
468
  // Install all packages
460
469
  const installSpinner = spin('Install all packages');
461
470
  try {
@@ -467,6 +476,18 @@ const NewCommand = {
467
476
  installSpinner.fail(`Failed to install packages: ${err.message}`);
468
477
  return;
469
478
  }
479
+ // Post-install format pass. processApiMode (run earlier in
480
+ // setupServerForFullstack) and convertAppCloneToVendored rewrite
481
+ // source files, leaving whitespace artifacts that oxfmt flags in
482
+ // `pnpm run format:check` (multi-line arrays/imports after region
483
+ // stripping, import-path rewrites that now fit single-line). The
484
+ // formatter is only available after install, so we normalize here.
485
+ if (apiMode && filesystem.isDirectory(`${projectDir}/projects/api`)) {
486
+ yield toolbox.apiMode.formatProject(`${projectDir}/projects/api`);
487
+ }
488
+ if (isNuxt && filesystem.isDirectory(`${projectDir}/projects/app`)) {
489
+ yield toolbox.apiMode.formatProject(`${projectDir}/projects/app`);
490
+ }
470
491
  // Create initial commit after everything is set up
471
492
  try {
472
493
  yield system.run(`cd ${projectDir} && git add . && git commit -m "Initial commit"`);
@@ -126,10 +126,7 @@ const NewCommand = {
126
126
  info(colors.dim('─'.repeat(60)));
127
127
  info('');
128
128
  // Detect frontend project
129
- const appCandidates = [
130
- (0, path_1.join)(cwd, 'projects', 'app'),
131
- (0, path_1.join)(cwd, 'packages', 'app'),
132
- ].filter((p) => Boolean(p));
129
+ const appCandidates = [(0, path_1.join)(cwd, 'projects', 'app'), (0, path_1.join)(cwd, 'packages', 'app')].filter((p) => Boolean(p));
133
130
  let appDir;
134
131
  for (const candidate of appCandidates) {
135
132
  if (filesystem.exists((0, path_1.join)(candidate, 'nuxt.config.ts')) || filesystem.exists((0, path_1.join)(candidate, 'package.json'))) {
@@ -45,8 +45,8 @@ const NewCommand = {
45
45
  error('No current branch!');
46
46
  return;
47
47
  }
48
- // Check remote
49
- const remoteBranch = yield system.run(`git ls-remote --heads origin ${branch}`);
48
+ // Check remote (use short SSH timeout so ls-remote doesn't hang in offline environments)
49
+ const remoteBranch = yield system.run(`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git ls-remote --heads origin ${branch} 2>/dev/null || true`);
50
50
  if (!remoteBranch) {
51
51
  error(`No remote branch ${branch} found!`);
52
52
  return;
@@ -47,8 +47,8 @@ const NewCommand = {
47
47
  info('');
48
48
  info(`Current branch: ${branch}`);
49
49
  info('');
50
- // Fetch to see incoming changes
51
- yield run('git fetch');
50
+ // Fetch to see incoming changes (use short SSH timeout so it doesn't hang offline)
51
+ yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true');
52
52
  // Check for incoming commits
53
53
  const incomingCommits = yield run(`git log ${branch}..origin/${branch} --oneline 2>/dev/null || echo ""`);
54
54
  const commits = (incomingCommits === null || incomingCommits === void 0 ? void 0 : incomingCommits.trim().split('\n').filter((c) => c)) || [];
@@ -78,7 +78,7 @@ const NewCommand = {
78
78
  const timer = startTimer();
79
79
  // Update
80
80
  const updateSpin = spin(`Update branch ${branch}`);
81
- yield run('git fetch && git pull --rebase');
81
+ yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true && GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git pull --rebase');
82
82
  updateSpin.succeed();
83
83
  // Install packages (unless skipped) with correctly detected package manager (supports monorepo lockfiles)
84
84
  if (!skipInstall) {
@@ -434,9 +434,7 @@ const NewCommand = {
434
434
  // path to src/core whose depth depends on the model file location.
435
435
  // We search for BOTH forms so this works regardless of how the file
436
436
  // was originally generated.
437
- const vendoredSpec = (0, framework_detection_1.isVendoredProject)(path)
438
- ? (0, framework_detection_1.getFrameworkImportSpecifier)(path, modelPath)
439
- : null;
437
+ const vendoredSpec = (0, framework_detection_1.isVendoredProject)(path) ? (0, framework_detection_1.getFrameworkImportSpecifier)(path, modelPath) : null;
440
438
  let existingImports = moduleFile.getImportDeclaration('@lenne.tech/nest-server');
441
439
  if (!existingImports && vendoredSpec) {
442
440
  existingImports = moduleFile.getImportDeclaration(vendoredSpec);
@@ -410,7 +410,7 @@ const NewCommand = {
410
410
  // plus the `from '@nestjs/common'` clause. The replacement wedges
411
411
  // `, forwardRef` in between.
412
412
  yield patching.patch(serverModule, {
413
- insert: "$1, forwardRef$2",
413
+ insert: '$1, forwardRef$2',
414
414
  replace: /(import\s*\{\s*[^}]*?)(\s*\}\s*from\s+['"]@nestjs\/common['"])/,
415
415
  });
416
416
  }
@@ -1,9 +1,5 @@
1
1
  {
2
2
  "$schema": "./vendor-runtime-deps.schema.json",
3
3
  "description": "Upstream @lenne.tech/nest-server devDependencies that are actually needed at runtime in a consumer project. When vendoring, these entries are promoted from upstream devDependencies into the project's dependencies so production runtime has them available.",
4
- "runtimeHelpers": [
5
- "find-file-up"
6
- ]
4
+ "runtimeHelpers": ["find-file-up"]
7
5
  }
8
- </content>
9
- </invoke>
@@ -55,6 +55,50 @@ class ApiMode {
55
55
  this.filesystem.remove((0, path_1.join)(projectPath, 'scripts', 'strip-api-mode-markers.mjs'));
56
56
  // Remove strip-markers script from package.json
57
57
  this.removeScriptFromPackageJson(projectPath, 'strip-markers');
58
+ // NOTE: auto-format of the stripped files happens separately in
59
+ // `formatProject()`, which MUST be called by the caller AFTER
60
+ // `pnpm install`. At this point the project's formatter (oxfmt) is
61
+ // not yet on disk, so running it here would silently no-op.
62
+ });
63
+ }
64
+ /**
65
+ * Run the project's `format` (or `format:fix`) npm script, if it exists.
66
+ * Call this AFTER the project's dependencies have been installed —
67
+ * otherwise the formatter (e.g. oxfmt) isn't available yet and the
68
+ * pass silently no-ops.
69
+ *
70
+ * Used after region stripping to normalize whitespace artifacts the
71
+ * formatter would otherwise flag in `format:check` (e.g. collapsing
72
+ * `providers: [\n X,\n]` to `providers: [X]` once graphql items were
73
+ * removed). Failures are non-fatal so a misbehaving formatter never
74
+ * blocks init.
75
+ */
76
+ formatProject(projectPath) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ var _a, _b, _c;
79
+ const pkgPath = (0, path_1.join)(projectPath, 'package.json');
80
+ const pkgRaw = this.filesystem.read(pkgPath);
81
+ if (!pkgRaw)
82
+ return;
83
+ let pkg;
84
+ try {
85
+ pkg = JSON.parse(pkgRaw);
86
+ }
87
+ catch (_d) {
88
+ return;
89
+ }
90
+ const scripts = (_a = pkg.scripts) !== null && _a !== void 0 ? _a : {};
91
+ const formatScript = scripts.format ? 'format' : scripts['format:fix'] ? 'format:fix' : null;
92
+ if (!formatScript)
93
+ return;
94
+ const { pm, system } = this.toolbox;
95
+ const runner = (_c = (_b = pm === null || pm === void 0 ? void 0 : pm.run) === null || _b === void 0 ? void 0 : _b.call(pm, formatScript, pm.detect(projectPath))) !== null && _c !== void 0 ? _c : `pnpm run ${formatScript}`;
96
+ try {
97
+ yield system.run(`cd "${projectPath}" && ${runner}`);
98
+ }
99
+ catch (_e) {
100
+ // Non-fatal: the user can run format manually if this misbehaves.
101
+ }
58
102
  });
59
103
  }
60
104
  /**
@@ -143,13 +187,77 @@ class ApiMode {
143
187
  if (!content) {
144
188
  continue;
145
189
  }
146
- const processed = this.processFileRegions(content, removeMarker, keepMarker);
190
+ // Special-case config.env.ts in REST mode: simply deleting the
191
+ // `graphQl: { … }` property is not enough — `CoreModule.forRoot`
192
+ // treats `graphQl === undefined` as enabled and tries to build
193
+ // the GraphQL schema anyway (which then fails on core models
194
+ // like CoreHealthCheckResult that reference the JSON scalar).
195
+ // Replace each stripped `// #region graphql … // #endregion
196
+ // graphql` block with an explicit `graphQl: false,` so GraphQL
197
+ // is cleanly disabled.
198
+ let processed;
199
+ if (removeMarker === 'graphql' && file.endsWith('/config.env.ts')) {
200
+ processed = this.replaceGraphqlRegionsWithDisabled(content, keepMarker);
201
+ }
202
+ else {
203
+ processed = this.processFileRegions(content, removeMarker, keepMarker);
204
+ }
147
205
  if (processed !== content) {
148
206
  this.filesystem.write(file, processed);
149
207
  }
150
208
  }
151
209
  }
152
210
  }
211
+ /**
212
+ * Like `processFileRegions` with removeMarker='graphql', but a
213
+ * stripped region that contains a `graphQl:` property assignment is
214
+ * replaced with `graphQl: false,` (at the region's indent) instead of
215
+ * being deleted outright. Other graphql-regions (e.g. wrapping
216
+ * `execAfterInit: 'pnpm run docs:bootstrap'`) are deleted as usual.
217
+ *
218
+ * Rationale: `CoreModule.forRoot` treats `options.graphQl === undefined`
219
+ * as enabled, so dropping the property silently keeps GraphQL active
220
+ * and later crashes when the schema references scalars that were
221
+ * purged in REST mode.
222
+ *
223
+ * Preserves keepMarker behaviour (strip marker lines only, keep content).
224
+ */
225
+ replaceGraphqlRegionsWithDisabled(content, keepMarker) {
226
+ const lines = content.split('\n');
227
+ const result = [];
228
+ let inRegion = false;
229
+ let regionIndent = '';
230
+ let regionBuffer = [];
231
+ for (const line of lines) {
232
+ const trimmed = line.trim();
233
+ if (trimmed === '// #region graphql') {
234
+ inRegion = true;
235
+ regionIndent = line.slice(0, line.indexOf('//'));
236
+ regionBuffer = [];
237
+ continue;
238
+ }
239
+ if (trimmed === '// #endregion graphql') {
240
+ inRegion = false;
241
+ // Only emit a replacement if the stripped block actually
242
+ // contained a `graphQl:` assignment.
243
+ if (regionBuffer.some((l) => /\bgraphQl\s*:/.test(l))) {
244
+ result.push(`${regionIndent}graphQl: false,`);
245
+ }
246
+ regionBuffer = [];
247
+ continue;
248
+ }
249
+ if (inRegion) {
250
+ regionBuffer.push(line);
251
+ continue;
252
+ }
253
+ // Keep-marker lines (e.g. `// #region rest`) are dropped; content between them stays.
254
+ if (trimmed === `// #region ${keepMarker}` || trimmed === `// #endregion ${keepMarker}`) {
255
+ continue;
256
+ }
257
+ result.push(line);
258
+ }
259
+ return result.join('\n');
260
+ }
153
261
  /**
154
262
  * Process region markers in file content
155
263
  */
@@ -261,8 +369,20 @@ class ApiMode {
261
369
  }
262
370
  catch (_b) {
263
371
  // If ts-morph is not available or fails, fall back to regex
264
- this.modifyConfigEnvForRestFallback(projectPath);
265
372
  }
373
+ // Safety net: always run the regex fallback too. ts-morph only
374
+ // traverses direct ObjectLiteralExpression properties, so configs
375
+ // that wrap env-blocks in helper functions (e.g. `local:
376
+ // localConfig(...)`) are skipped silently. The regex is idempotent
377
+ // — if ts-morph already replaced `graphQl: {...}` with
378
+ // `graphQl: false` it's a no-op, but if ts-morph missed a wrapped
379
+ // occurrence the regex catches it. Without this, REST-mode
380
+ // projects built from a starter that lacks explicit
381
+ // `// #region graphql` markers would end up with `graphQl:
382
+ // undefined`, which `CoreModule.forRoot` treats as ENABLED, and
383
+ // the GraphQL schema build crashes on core models that still
384
+ // reference the JSON scalar.
385
+ this.modifyConfigEnvForRestFallback(projectPath);
266
386
  });
267
387
  }
268
388
  /**
@@ -389,9 +509,7 @@ class ApiMode {
389
509
  importLineSet.add(j);
390
510
  }
391
511
  }
392
- const codeContent = lines
393
- .map((line, idx) => (importLineSet.has(idx) ? '' : line))
394
- .join('\n');
512
+ const codeContent = lines.map((line, idx) => (importLineSet.has(idx) ? '' : line)).join('\n');
395
513
  // Check each import
396
514
  const linesToRemove = new Set();
397
515
  for (const imp of importLines) {
@@ -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) {