@lenne.tech/cli 1.42.0 → 1.44.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.
@@ -154,23 +154,46 @@ const DoctorCommand = {
154
154
  else if (gs.file && gs.hasDbReset) {
155
155
  line('OK', colors.green, 'global-setup allow-list is ticket + shard safe');
156
156
  }
157
- // 7.5 check.mjs drift: the root wrapper is canonical (bundled with the
157
+ // 7.5 check wrapper drift: the root wrapper is canonical (bundled with the
158
158
  // CLI, synced by `lt fullstack update`). A diverged copy silently
159
159
  // misses fixes (idle-watchdog, install hoisting, summed test
160
160
  // metrics) — surface it instead of letting copies drift apart.
161
+ //
162
+ // Checked over the WHOLE copy set, not just `check.mjs`: the wrapper
163
+ // imports siblings, so a missing or stale one makes `check` die with
164
+ // ERR_MODULE_NOT_FOUND (or an import mismatch) before running a single
165
+ // step. Reporting only on `check.mjs` meant doctor printed a green
166
+ // "matches the canonical CLI version" for a project whose `check` was
167
+ // completely broken — and doctor is the tool people reach for exactly
168
+ // then.
161
169
  try {
162
170
  const { readFileSync: read } = yield Promise.resolve().then(() => __importStar(require('fs')));
163
171
  const { join: j } = yield Promise.resolve().then(() => __importStar(require('path')));
164
- const projectCheck = j(layout.root, 'scripts', 'check.mjs');
172
+ const { resolveCopySet } = yield Promise.resolve().then(() => __importStar(require('../../lib/heal-check-wrapper')));
165
173
  const bundledCheck = j(__dirname, '..', '..', 'templates', 'check', 'check.mjs');
166
- if (filesystem.exists(projectCheck) && filesystem.exists(bundledCheck)) {
167
- if (read(projectCheck, 'utf8') === read(bundledCheck, 'utf8')) {
168
- line('OK', colors.green, 'scripts/check.mjs matches the canonical CLI version');
174
+ if (filesystem.exists(bundledCheck)) {
175
+ const missing = [];
176
+ const drifted = [];
177
+ for (const { rel, source } of resolveCopySet(bundledCheck)) {
178
+ const target = j(layout.root, rel);
179
+ if (!filesystem.exists(target)) {
180
+ missing.push(rel);
181
+ }
182
+ else if (read(target, 'utf8') !== read(source, 'utf8')) {
183
+ drifted.push(rel);
184
+ }
169
185
  }
170
- else {
171
- line('WARN', colors.yellow, 'scripts/check.mjs differs from the canonical CLI version');
186
+ if (missing.length > 0) {
187
+ line('ERROR', colors.red, `check wrapper incomplete missing ${missing.join(', ')}`);
188
+ line('ERROR', colors.red, ' `pnpm run check` cannot start; run `lt fullstack update` to install it');
189
+ }
190
+ else if (drifted.length > 0) {
191
+ line('WARN', colors.yellow, `${drifted.join(', ')} differs from the canonical CLI version`);
172
192
  line('WARN', colors.yellow, ' run `lt fullstack update` to sync it (skips uncommitted local edits)');
173
193
  }
194
+ else if (filesystem.exists(j(layout.root, 'scripts', 'check.mjs'))) {
195
+ line('OK', colors.green, 'check wrapper matches the canonical CLI version');
196
+ }
174
197
  }
175
198
  }
176
199
  catch (_a) {
@@ -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.help = void 0;
13
+ const fail_run_1 = require("../../lib/fail-run");
13
14
  const workspace_integration_1 = require("../../lib/workspace-integration");
14
15
  /**
15
16
  * Add an API (`projects/api/`) to a fullstack workspace that currently
@@ -68,6 +69,7 @@ const NewCommand = {
68
69
  info('Add API to fullstack workspace');
69
70
  toolbox.tools.nonInteractiveHint('lt fullstack add-api --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor> [--api-branch <ref>] [--next] [--dry-run] --noConfirm');
70
71
  if (!(yield git.gitInstalled())) {
72
+ (0, fail_run_1.failRun)(toolbox);
71
73
  return;
72
74
  }
73
75
  const ltConfig = config.loadConfig();
@@ -126,10 +128,12 @@ const NewCommand = {
126
128
  const layout = (0, workspace_integration_1.detectWorkspaceLayout)(workspaceDir, filesystem);
127
129
  if (!layout.hasWorkspace) {
128
130
  error(`No fullstack workspace detected at "${workspaceDir}". Expected pnpm-workspace.yaml, package.json#workspaces, or a projects/ directory. Use \`lt fullstack init\` for a fresh workspace.`);
131
+ (0, fail_run_1.failRun)(toolbox);
129
132
  return;
130
133
  }
131
134
  if (layout.hasApi) {
132
135
  error(`An API already exists at "${workspaceDir}/projects/api". Remove it first or use \`lt fullstack init\` in a fresh directory.`);
136
+ (0, fail_run_1.failRun)(toolbox);
133
137
  return;
134
138
  }
135
139
  // Resolve api mode (CLI > experimental override > config > global > interactive/default).
@@ -177,6 +181,7 @@ const NewCommand = {
177
181
  }
178
182
  else if (cliFrameworkMode) {
179
183
  error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
184
+ (0, fail_run_1.failRun)(toolbox);
180
185
  return;
181
186
  }
182
187
  else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
@@ -282,6 +287,7 @@ const NewCommand = {
282
287
  });
283
288
  if (!apiResult.success) {
284
289
  apiSpinner.fail(`Failed to set up API: ${apiResult.path}`);
290
+ (0, fail_run_1.failRun)(toolbox);
285
291
  return;
286
292
  }
287
293
  apiSpinner.succeed(`API integrated (${apiResult.method})`);
@@ -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.help = void 0;
13
+ const fail_run_1 = require("../../lib/fail-run");
13
14
  const workspace_integration_1 = require("../../lib/workspace-integration");
14
15
  /**
15
16
  * Add a frontend app (`projects/app/`) to a fullstack workspace that
@@ -57,6 +58,7 @@ const NewCommand = {
57
58
  info('Add app to fullstack workspace');
58
59
  toolbox.tools.nonInteractiveHint('lt fullstack add-app --frontend <nuxt|angular> [--frontend-branch <ref>] [--next] [--dry-run] --noConfirm');
59
60
  if (!(yield git.gitInstalled())) {
61
+ (0, fail_run_1.failRun)(toolbox);
60
62
  return;
61
63
  }
62
64
  const ltConfig = config.loadConfig();
@@ -110,10 +112,12 @@ const NewCommand = {
110
112
  const layout = (0, workspace_integration_1.detectWorkspaceLayout)(workspaceDir, filesystem);
111
113
  if (!layout.hasWorkspace) {
112
114
  error(`No fullstack workspace detected at "${workspaceDir}". Expected pnpm-workspace.yaml, package.json#workspaces, or a projects/ directory. Use \`lt fullstack init\` for a fresh workspace.`);
115
+ (0, fail_run_1.failRun)(toolbox);
113
116
  return;
114
117
  }
115
118
  if (layout.hasApp) {
116
119
  error(`An app already exists at "${workspaceDir}/projects/app". Remove it first or use \`lt fullstack init\` in a fresh directory.`);
120
+ (0, fail_run_1.failRun)(toolbox);
117
121
  return;
118
122
  }
119
123
  // Resolve frontend.
@@ -123,6 +127,7 @@ const NewCommand = {
123
127
  }
124
128
  else if (cliFrontend) {
125
129
  error('Invalid --frontend option. Use "angular" or "nuxt".');
130
+ (0, fail_run_1.failRun)(toolbox);
126
131
  return;
127
132
  }
128
133
  else if (configFrontend === 'angular' || configFrontend === 'nuxt') {
@@ -150,6 +155,7 @@ const NewCommand = {
150
155
  }
151
156
  else if (cliFrontendFrameworkMode) {
152
157
  error(`Invalid --frontend-framework-mode value "${cliFrontendFrameworkMode}". Use "npm" or "vendor".`);
158
+ (0, fail_run_1.failRun)(toolbox);
153
159
  return;
154
160
  }
155
161
  else if (configFrontendFrameworkMode === 'npm' || configFrontendFrameworkMode === 'vendor') {
@@ -225,6 +231,7 @@ const NewCommand = {
225
231
  });
226
232
  if (!result.success) {
227
233
  appSpinner.fail(`Failed to set up ${frontend} frontend: ${result.path}`);
234
+ (0, fail_run_1.failRun)(toolbox);
228
235
  return;
229
236
  }
230
237
  appSpinner.succeed(`${frontend} integrated (${result.method})`);
@@ -16,6 +16,7 @@ const gluegun_1 = require("gluegun");
16
16
  const caddy_1 = require("../../lib/caddy");
17
17
  const dev_migrate_helper_1 = require("../../lib/dev-migrate-helper");
18
18
  const dev_project_1 = require("../../lib/dev-project");
19
+ const fail_run_1 = require("../../lib/fail-run");
19
20
  const package_name_1 = require("../../lib/package-name");
20
21
  const vendor_claude_md_1 = require("../../lib/vendor-claude-md");
21
22
  const workspace_integration_1 = require("../../lib/workspace-integration");
@@ -38,9 +39,10 @@ const NewCommand = {
38
39
  // Info
39
40
  info('Create a new fullstack workspace');
40
41
  // Hint for non-interactive callers (e.g. Claude Code)
41
- toolbox.tools.nonInteractiveHint('lt fullstack init --name <name> --frontend <nuxt|angular> --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor> [--framework-upstream-branch <ref>] [--next: implies nuxt-base-starter#next unless --frontend-branch overrides] [--dry-run] --noConfirm');
42
+ toolbox.tools.nonInteractiveHint('lt fullstack init --name <name> --frontend <nuxt|angular> --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor: workspace-wide, applies to API and frontend> [--frontend-framework-mode <npm|vendor>: overrides --framework-mode for the frontend only] [--framework-upstream-branch <ref>] [--next: implies nuxt-base-starter#next unless --frontend-branch overrides] [--dry-run] --noConfirm');
42
43
  // Check git
43
44
  if (!(yield git.gitInstalled())) {
45
+ (0, fail_run_1.failRun)(toolbox);
44
46
  return;
45
47
  }
46
48
  // Load configuration
@@ -92,6 +94,7 @@ const NewCommand = {
92
94
  if (cwdLayout.hasApi && cwdLayout.hasApp) {
93
95
  error('Workspace already has both projects/api and projects/app — nothing to add.');
94
96
  info('Use `lt fullstack add-api --help-json` or `lt fullstack add-app --help-json` to inspect options.');
97
+ (0, fail_run_1.failRun)(toolbox);
95
98
  return;
96
99
  }
97
100
  if (cwdLayout.hasApp && !cwdLayout.hasApi) {
@@ -124,6 +127,7 @@ const NewCommand = {
124
127
  if (filesystem.exists(projectDir)) {
125
128
  info('');
126
129
  error(`There's already a folder named "${projectDir}" here.`);
130
+ (0, fail_run_1.failRun)(toolbox);
127
131
  return;
128
132
  }
129
133
  // Determine frontend with priority: CLI > config > interactive
@@ -132,6 +136,7 @@ const NewCommand = {
132
136
  frontend = cliFrontend === 'angular' ? 'angular' : cliFrontend === 'nuxt' ? 'nuxt' : null;
133
137
  if (!frontend) {
134
138
  error('Invalid frontend option. Use "angular" or "nuxt".');
139
+ (0, fail_run_1.failRun)(toolbox);
135
140
  return;
136
141
  }
137
142
  }
@@ -220,6 +225,7 @@ const NewCommand = {
220
225
  }
221
226
  else if (cliFrameworkMode) {
222
227
  error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
228
+ (0, fail_run_1.failRun)(toolbox);
223
229
  return;
224
230
  }
225
231
  else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
@@ -245,20 +251,34 @@ const NewCommand = {
245
251
  }
246
252
  // ── Frontend framework mode ─────────────────────────────────────────
247
253
  const configFrontendFrameworkMode = (_0 = (_z = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _z === void 0 ? void 0 : _z.fullstack) === null || _0 === void 0 ? void 0 : _0.frontendFrameworkMode;
254
+ // Precedence: frontend-specific CLI flag > workspace-wide CLI flag >
255
+ // frontend-specific config > workspace-wide config > vendor default.
256
+ //
257
+ // `--framework-mode` is the workspace-wide default and MUST propagate here.
258
+ // Without that inheritance, `--framework-mode npm` silently produced an
259
+ // npm API next to a vendored frontend — visible only as the M1..M3 steps in
260
+ // a `--dry-run` plan, or afterwards as an unexpected `app/core/` tree.
261
+ // Consumers who genuinely want mixed modes pass `--frontend-framework-mode`.
248
262
  let frontendFrameworkMode;
249
263
  if (cliFrontendFrameworkMode === 'npm' || cliFrontendFrameworkMode === 'vendor') {
250
264
  frontendFrameworkMode = cliFrontendFrameworkMode;
251
265
  }
252
266
  else if (cliFrontendFrameworkMode) {
253
267
  error(`Invalid --frontend-framework-mode value "${cliFrontendFrameworkMode}". Use "npm" or "vendor".`);
268
+ (0, fail_run_1.failRun)(toolbox);
254
269
  return;
255
270
  }
271
+ else if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
272
+ frontendFrameworkMode = cliFrameworkMode;
273
+ info(`Using frontend framework mode from --framework-mode: ${frontendFrameworkMode}`);
274
+ }
256
275
  else if (configFrontendFrameworkMode === 'npm' || configFrontendFrameworkMode === 'vendor') {
257
276
  frontendFrameworkMode = configFrontendFrameworkMode;
258
277
  info(`Using frontend framework mode from lt.config: ${frontendFrameworkMode}`);
259
278
  }
260
- else if (noConfirm) {
261
- frontendFrameworkMode = 'vendor';
279
+ else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
280
+ frontendFrameworkMode = configFrameworkMode;
281
+ info(`Using frontend framework mode from lt.config frameworkMode: ${frontendFrameworkMode}`);
262
282
  }
263
283
  else {
264
284
  // Default to vendor without asking (unless user sets it explicitly)
@@ -403,11 +423,13 @@ const NewCommand = {
403
423
  }
404
424
  catch (err) {
405
425
  workspaceSpinner.fail(`Failed to clone monorepo: ${err.message}`);
426
+ (0, fail_run_1.failRun)(toolbox);
406
427
  return;
407
428
  }
408
429
  // Check for directory
409
430
  if (!filesystem.isDirectory(`./${projectDir}`)) {
410
431
  workspaceSpinner.fail(`The directory "${projectDir}" could not be created.`);
432
+ (0, fail_run_1.failRun)(toolbox);
411
433
  return;
412
434
  }
413
435
  workspaceSpinner.succeed(`Create fullstack workspace with ${frontend} in ${projectDir} for ${name} created`);
@@ -467,6 +489,7 @@ const NewCommand = {
467
489
  }
468
490
  catch (err) {
469
491
  error(`Failed to initialize git: ${err.message}`);
492
+ (0, fail_run_1.failRun)(toolbox);
470
493
  return;
471
494
  }
472
495
  // Add remote if push is configured
@@ -476,6 +499,7 @@ const NewCommand = {
476
499
  }
477
500
  catch (err) {
478
501
  error(`Failed to add remote: ${err.message}`);
502
+ (0, fail_run_1.failRun)(toolbox);
479
503
  return;
480
504
  }
481
505
  }
@@ -503,6 +527,7 @@ const NewCommand = {
503
527
  }
504
528
  if (!frontendResult.success) {
505
529
  error(`Failed to set up ${frontend} frontend: ${frontendResult.path}`);
530
+ (0, fail_run_1.failRun)(toolbox);
506
531
  return;
507
532
  }
508
533
  // Patch frontend .env with project-specific values (skip for linked templates)
@@ -546,6 +571,7 @@ const NewCommand = {
546
571
  });
547
572
  if (!apiResult.success) {
548
573
  serverSpinner.fail(`Failed to set up API: ${apiResult.path}`);
574
+ (0, fail_run_1.failRun)(toolbox);
549
575
  return;
550
576
  }
551
577
  // Auto-run `bun run rename <projectDir>` for the experimental nest-base
@@ -638,6 +664,7 @@ const NewCommand = {
638
664
  }
639
665
  catch (err) {
640
666
  installSpinner.fail(`Failed to install packages: ${err.message}`);
667
+ (0, fail_run_1.failRun)(toolbox);
641
668
  return;
642
669
  }
643
670
  }
@@ -673,6 +700,7 @@ const NewCommand = {
673
700
  }
674
701
  catch (err) {
675
702
  error(`Failed to create initial commit: ${err.message}`);
703
+ (0, fail_run_1.failRun)(toolbox);
676
704
  return;
677
705
  }
678
706
  // Push to remote if configured
@@ -682,6 +710,7 @@ const NewCommand = {
682
710
  }
683
711
  catch (err) {
684
712
  error(`Failed to push to remote: ${err.message}`);
713
+ (0, fail_run_1.failRun)(toolbox);
685
714
  return;
686
715
  }
687
716
  }
@@ -198,7 +198,20 @@ const NewCommand = {
198
198
  const changedCheck = (0, heal_check_wrapper_1.healCheckWrapper)(cwd, checkAsset);
199
199
  if (changedCheck.length > 0) {
200
200
  info('');
201
- success(` Installed/updated the check wrapper: ${changedCheck.join(', ')}`);
201
+ // A skip entry is NOT a success — it means the wrapper stayed on its old
202
+ // version. Reporting the whole list through `success()` painted a refusal
203
+ // green.
204
+ const skipped = changedCheck.filter((entry) => entry.includes('skipped'));
205
+ const applied = changedCheck.filter((entry) => !entry.includes('skipped'));
206
+ if (applied.length > 0) {
207
+ success(` Installed/updated the check wrapper: ${applied.join(', ')}`);
208
+ info(' `check` now serialises build/typecheck against the test suites,');
209
+ info(' so it takes longer in wall-clock but no longer destabilises API e2e runs.');
210
+ info(' The wrapper imports its siblings — keep them together, or `check` will not start.');
211
+ }
212
+ for (const entry of skipped) {
213
+ warning(` Check wrapper NOT updated: ${entry}`);
214
+ }
202
215
  }
203
216
  // ── Self-heal: keep `.lt-dev/` out of git ──────────────────────────────
204
217
  //
@@ -209,6 +222,16 @@ const NewCommand = {
209
222
  info('');
210
223
  success(' Added `.lt-dev/` to .gitignore');
211
224
  }
225
+ // ── Self-heal: keep the check's isolated Nuxt build dir out of git ──────
226
+ //
227
+ // The check wrapper pins `NUXT_BUILD_DIR=.nuxt-check` so it never writes the
228
+ // `.nuxt/` a parked `nuxt dev` reads. Current starters already ignore it
229
+ // (via their `.nuxt-*` glob); projects scaffolded before that glob do not,
230
+ // and a build dir is a plausible place for a resolved runtimeConfig to be
231
+ // committed by accident. Idempotent.
232
+ if ((0, dev_patches_1.addToGitignore)(cwd, '.nuxt-check')) {
233
+ success(' Added `.nuxt-check` to .gitignore');
234
+ }
212
235
  // ── Self-heal: repair the vendor-mode migration store ──────────────────
213
236
  //
214
237
  // `migrations-utils/migrate.js` is written ONCE, at conversion time. Projects
@@ -46,7 +46,7 @@ const NewCommand = {
46
46
  return;
47
47
  }
48
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`);
49
+ const remoteBranch = yield system.run(`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="\${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;
@@ -48,7 +48,7 @@ const NewCommand = {
48
48
  info(`Current branch: ${branch}`);
49
49
  info('');
50
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');
51
+ yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${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_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');
81
+ yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null || true && GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${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) {
@@ -13,6 +13,7 @@ exports.FrontendHelper = void 0;
13
13
  const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
14
14
  const markdown_table_1 = require("../lib/markdown-table");
15
15
  const strip_comments_1 = require("../lib/strip-comments");
16
+ const strip_vendor_schema_augmentation_1 = require("../lib/strip-vendor-schema-augmentation");
16
17
  const vendor_claude_md_1 = require("../lib/vendor-claude-md");
17
18
  /**
18
19
  * Frontend helper functions for project scaffolding
@@ -520,6 +521,24 @@ class FrontendHelper {
520
521
  this.rewriteConsumerImportsToVendor(dest);
521
522
  // ── 4. Rewrite nuxt.config.ts module entry ──────────────────────────
522
523
  this.rewriteNuxtConfig(dest, 'vendor');
524
+ // ── 4b. Drop the core's `nuxt/schema` runtime-config augmentation ────
525
+ //
526
+ // Harmless inside node_modules, poisonous as project source: it augments the
527
+ // same interface under both `nuxt/schema` and `@nuxt/schema` (the former
528
+ // re-exports the latter) and closes a cycle with Nuxt's generated
529
+ // runtime-config types. TS2310 — suppressed by `skipLibCheck`, so all a
530
+ // developer sees is every `config.public.*` typed `unknown`. See the lib for
531
+ // the measurement.
532
+ const strippedAugmentation = (0, strip_vendor_schema_augmentation_1.stripVendorSchemaAugmentation)({ coreDir, filesystem });
533
+ if (strippedAugmentation.touched.length > 0) {
534
+ this.toolbox.print.info(` vendored core: removed the nuxt/schema runtime-config augmentation from ${strippedAugmentation.touched.length} file(s) — keeps config.public.* typed`);
535
+ }
536
+ // A block the transform could not process is left in place, which means the
537
+ // TS2310 bug ships with the project. Silence there would be the worst of both
538
+ // worlds: the conversion reports success and the typing is broken anyway.
539
+ for (const warning of strippedAugmentation.warnings) {
540
+ this.toolbox.print.warning(` ⚠ vendored core: ${warning}`);
541
+ }
523
542
  // ── 5. package.json: remove @lenne.tech/nuxt-extensions, merge deps ─
524
543
  const pkgPath = path.join(dest, 'package.json');
525
544
  if (filesystem.exists(pkgPath)) {
@@ -120,7 +120,7 @@ class Git {
120
120
  // Toolbox features
121
121
  const { system } = this.toolbox;
122
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');
123
+ const branches = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${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) {
@@ -197,6 +197,27 @@ class Git {
197
197
  /**
198
198
  * Check if git is installed (cached for performance)
199
199
  */
200
+ /**
201
+ * Why the git calls in this repo spell `GIT_SSH_COMMAND="\${GIT_SSH_COMMAND:-…}"` (shell default)
202
+ * rather than assigning it outright.
203
+ *
204
+ * These commands do a best-effort `git fetch` to see whether the branch is
205
+ * behind. They set `BatchMode=yes` so ssh fails instead of PROMPTING — but an
206
+ * agent that stalls is not a prompt. On a 1Password-backed machine the agent is
207
+ * reachable and every signature needs an interactive approval; unattended it
208
+ * waits and then reports `communication with agent failed`. Measured: **61 s per
209
+ * fetch**, so `lt git update --dry-run` took 62 s and `lt git create --dry-run`
210
+ * 123 s (two fetches). `ConnectTimeout` does not bound it — that covers the TCP
211
+ * connect, not the agent.
212
+ *
213
+ * Waiting for a human to approve a key is legitimate for an interactive command,
214
+ * so the default is unchanged. What was wrong is that the assignment was
215
+ * UNCONDITIONAL: it overrode a caller who had deliberately configured ssh,
216
+ * including a test harness trying to make the behaviour deterministic. The
217
+ * `:-` default respects an existing value and keeps the old behaviour when there
218
+ * is none. `IdentityAgent=none` in the caller's env then drops the same fetch to
219
+ * ~1 s with a clean `Permission denied (publickey)`.
220
+ */
200
221
  gitInstalled() {
201
222
  return __awaiter(this, void 0, void 0, function* () {
202
223
  // Return cached result if available
@@ -252,7 +273,7 @@ class Git {
252
273
  searchSpin = spin(opts.spinText);
253
274
  }
254
275
  // 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');
276
+ const fetch = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null || true');
256
277
  if (fetch.length && !fetch.startsWith('remote')) {
257
278
  info(`Could not update infos ${fetch.length}`);
258
279
  }
@@ -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 adopt_upstream_build_allowlist_1 = require("../lib/adopt-upstream-build-allowlist");
49
50
  const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
50
51
  const markdown_table_1 = require("../lib/markdown-table");
51
52
  const strip_comments_1 = require("../lib/strip-comments");
@@ -1021,6 +1022,22 @@ class Server {
1021
1022
  // Best-effort — without the lockfile, the closure step falls back to
1022
1023
  // the upstream package.json ranges (still better than nothing).
1023
1024
  }
1025
+ // Snapshot the upstream pnpm-workspace.yaml. It carries `allowBuilds`, the map
1026
+ // that decides which packages may run install scripts — and vendoring is what
1027
+ // makes the framework's entries the PROJECT's problem: step 5b turns the core's
1028
+ // import closure into direct dependencies, so packages nest-server knew about
1029
+ // (bullmq → msgpackr → msgpackr-extract) become the project's own. pnpm 11 aborts
1030
+ // the install outright on an unlisted one, so a missing entry is an unusable
1031
+ // project rather than a warning. This file is NOT in nest-server's npm tarball,
1032
+ // so the clone is the only moment it can be read at all.
1033
+ let upstreamWorkspaceYaml = '';
1034
+ try {
1035
+ upstreamWorkspaceYaml = filesystem.read(`${tmpClone}/pnpm-workspace.yaml`) || '';
1036
+ }
1037
+ catch (_c) {
1038
+ // Best-effort: an older framework revision may predate the file. The project
1039
+ // then keeps exactly the allowlist its own template shipped.
1040
+ }
1024
1041
  // Snapshot the upstream CLAUDE.md for section-merge into projects/api/CLAUDE.md.
1025
1042
  // The nest-server CLAUDE.md contains framework-specific instructions that
1026
1043
  // Claude Code needs to work correctly with the vendored source (API conventions,
@@ -1033,7 +1050,7 @@ class Server {
1033
1050
  upstreamClaudeMd = claudeMdContent;
1034
1051
  }
1035
1052
  }
1036
- catch (_c) {
1053
+ catch (_d) {
1037
1054
  // Non-fatal — if missing, the project CLAUDE.md just won't get upstream sections.
1038
1055
  }
1039
1056
  // Snapshot the upstream commit SHA for traceability in VENDOR.md.
@@ -1042,7 +1059,7 @@ class Server {
1042
1059
  const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
1043
1060
  upstreamCommit = (sha || '').trim();
1044
1061
  }
1045
- catch (_d) {
1062
+ catch (_e) {
1046
1063
  // Non-fatal — VENDOR.md will just show an empty SHA.
1047
1064
  }
1048
1065
  try {
@@ -1325,7 +1342,7 @@ class Server {
1325
1342
  }
1326
1343
  }
1327
1344
  }
1328
- catch (_e) {
1345
+ catch (_f) {
1329
1346
  // skip unreadable file
1330
1347
  }
1331
1348
  }
@@ -1503,6 +1520,14 @@ class Server {
1503
1520
  upstreamDeps,
1504
1521
  upstreamDevDeps,
1505
1522
  });
1523
+ // The closure above just made framework-only packages direct dependencies of
1524
+ // this project. Their build-script decisions have to come along, or the first
1525
+ // `pnpm install` stops on ERR_PNPM_IGNORED_BUILDS. Additive only — a decision
1526
+ // the project already made is never overwritten.
1527
+ const adoptedBuilds = (0, adopt_upstream_build_allowlist_1.adoptUpstreamBuildAllowlist)({ dest, filesystem, upstreamWorkspaceYaml });
1528
+ if (adoptedBuilds.length > 0) {
1529
+ this.toolbox.print.info(` vendored core allowlist: adopted ${adoptedBuilds.length} build decision(s) from nest-server → ${adoptedBuilds.join(', ')}`);
1530
+ }
1506
1531
  // Add a script to run the local bin/migrate.js. The starter's
1507
1532
  // existing migrate:* scripts are already correct for npm mode; we
1508
1533
  // need them pointing at the local bin + local ts-compiler.