@lenne.tech/cli 1.49.0 → 1.50.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.
@@ -43,6 +43,7 @@ const child_process_1 = require("child_process");
43
43
  const fs_1 = require("fs");
44
44
  const path_1 = require("path");
45
45
  const dev_project_1 = require("./dev-project");
46
+ const dev_state_1 = require("./dev-state");
46
47
  const dev_ticket_1 = require("./dev-ticket");
47
48
  /** Branch prefix `lt ticket start` creates worktrees on. */
48
49
  const TICKET_BRANCH_PREFIX = 'feat/';
@@ -188,9 +189,15 @@ function planOrphanTicketDbSweep(args) {
188
189
  * never auto-dropped).
189
190
  */
190
191
  function planRegistryPrune(registry) {
191
- return Object.entries(registry.projects)
192
- .filter(([, entry]) => entry.path && !(0, fs_1.existsSync)(entry.path))
193
- .map(([key]) => key);
192
+ return (Object.entries(registry.projects)
193
+ // A relative stored path is not evidence of anything: `existsSync` resolves
194
+ // it against the current process's cwd, so a perfectly live project reads as
195
+ // gone from any other directory — and pruning it reclaims its slug and its
196
+ // reserved internal ports for the next project that asks. "Cannot verify" is
197
+ // not "absent"; these are left alone until a registry write from inside the
198
+ // project replaces the path with an absolute one.
199
+ .filter(([, entry]) => (0, dev_state_1.isVerifiablePath)(entry.path) && !(0, fs_1.existsSync)(entry.path))
200
+ .map(([key]) => key));
194
201
  }
195
202
  /**
196
203
  * Orphaned smoke-test databases. The `/lt-dev:fullstack:smoke-test` command
@@ -218,12 +225,15 @@ function planSmokeTestDbSweep(args) {
218
225
  const entries = Object.entries(registry.projects);
219
226
  const smokeRunLive = entries.some(([key, entry]) => (key === exports.SMOKE_TEST_DB_PREFIX || key.startsWith(`${exports.SMOKE_TEST_DB_PREFIX}-`)) &&
220
227
  entry.path &&
221
- (0, fs_1.existsSync)(entry.path));
228
+ (!(0, dev_state_1.isVerifiablePath)(entry.path) || (0, fs_1.existsSync)(entry.path)));
222
229
  if (smokeRunLive)
223
230
  return [];
224
231
  const protectedNames = new Set();
225
232
  for (const [, entry] of entries) {
226
- if (entry.dbName && entry.path && (0, fs_1.existsSync)(entry.path)) {
233
+ // `!isVerifiablePath` → we cannot prove the checkout is gone, so the database
234
+ // keeps its protection. Erring the other way would expose a live project's DB
235
+ // to the orphan sweep on the strength of a path we know we cannot read.
236
+ if (entry.dbName && entry.path && (!(0, dev_state_1.isVerifiablePath)(entry.path) || (0, fs_1.existsSync)(entry.path))) {
227
237
  protectedNames.add(entry.dbName);
228
238
  protectedNames.add((0, dev_project_1.deriveTestDbName)(entry.dbName));
229
239
  }
@@ -51,6 +51,8 @@ const fs_1 = require("fs");
51
51
  const os_1 = require("os");
52
52
  const path_1 = require("path");
53
53
  const caddy_1 = require("./caddy");
54
+ const dev_process_1 = require("./dev-process");
55
+ const platform_1 = require("./platform");
54
56
  /**
55
57
  * Resolve the user's home directory in a test-overridable way.
56
58
  *
@@ -139,12 +141,12 @@ function installService() {
139
141
  ok: false,
140
142
  };
141
143
  }
142
- const caddyBin = opts.caddyBin || (yield resolveCaddyBin());
144
+ const caddyBin = opts.caddyBin || (yield resolveCaddyBin(opts.lookup));
143
145
  if (!caddyBin) {
144
146
  return {
145
147
  bootstrapped: false,
146
148
  created: false,
147
- message: 'caddy not found on PATH. Install with `brew install caddy` (macOS) or your package manager (Linux).',
149
+ message: caddyMissingMessage(),
148
150
  ok: false,
149
151
  };
150
152
  }
@@ -249,14 +251,19 @@ StandardError=append:${cfg.errFile}
249
251
  WantedBy=default.target
250
252
  `;
251
253
  }
252
- /** Resolve the absolute path of `caddy` via `which` so launchd has a guaranteed path. */
254
+ /**
255
+ * Absolute path of `caddy`, so launchd/systemd get a guaranteed path rather than
256
+ * a PATH lookup in an environment they do not share with the user's shell.
257
+ *
258
+ * Resolved in-process instead of by spawning `which`: there is no `which` on
259
+ * Windows, and `where.exe` prints every match rather than the first. The async
260
+ * signature is kept because callers await it and a future platform may need to
261
+ * ask something that does spawn.
262
+ */
253
263
  function resolveCaddyBin() {
254
- return __awaiter(this, void 0, void 0, function* () {
255
- const r = yield activeRunner('which', ['caddy']);
256
- if (!r.ok)
257
- return undefined;
258
- const line = r.stdout.split('\n').find((s) => s.trim().length > 0);
259
- return line ? line.trim() : undefined;
264
+ return __awaiter(this, arguments, void 0, function* (options = {}) {
265
+ var _a;
266
+ return (_a = (0, platform_1.findExecutable)('caddy', options)) !== null && _a !== void 0 ? _a : undefined;
260
267
  });
261
268
  }
262
269
  /** Inject a custom runner (tests). Pass `null` to reset to the real spawner. */
@@ -315,6 +322,16 @@ function waitForServiceReady() {
315
322
  return false;
316
323
  });
317
324
  }
325
+ /** Install hint for the current platform — `brew` is not advice on Linux or Windows. */
326
+ function caddyMissingMessage() {
327
+ if (process.platform === 'darwin') {
328
+ return 'caddy not found on PATH. Install with `brew install caddy`.';
329
+ }
330
+ if (process.platform === 'win32') {
331
+ return 'caddy not found on PATH. Install with `winget install CaddyServer.Caddy` or `scoop install caddy`.';
332
+ }
333
+ return 'caddy not found on PATH. Install it with your package manager.';
334
+ }
318
335
  function defaultShellRunner(cmd, args) {
319
336
  return new Promise((resolve) => {
320
337
  var _a, _b;
@@ -396,13 +413,10 @@ function installLinux(paths, changed, existed) {
396
413
  };
397
414
  });
398
415
  }
416
+ /** See `caddy.ts#caddyDaemonRunning` for why this is not a `curl` any more. */
399
417
  function pingCaddyAdmin() {
400
- return new Promise((resolve) => {
401
- const child = (0, child_process_1.spawn)('curl', ['-fsS', '-o', '/dev/null', '--max-time', '1', 'http://127.0.0.1:2019/config/'], {
402
- stdio: ['ignore', 'ignore', 'ignore'],
403
- });
404
- child.on('error', () => resolve(false));
405
- child.on('close', (code) => resolve(code === 0));
418
+ return __awaiter(this, void 0, void 0, function* () {
419
+ return (yield (0, dev_process_1.httpStatus)('http://127.0.0.1:2019/config/', 1000)) !== null;
406
420
  });
407
421
  }
408
422
  function sleep(ms) {
@@ -18,6 +18,7 @@ exports.clearSession = clearSession;
18
18
  exports.detectSlugConflict = detectSlugConflict;
19
19
  exports.isPidAlive = isPidAlive;
20
20
  exports.isValidPid = isValidPid;
21
+ exports.isVerifiablePath = isVerifiablePath;
21
22
  exports.loadRegistry = loadRegistry;
22
23
  exports.loadSession = loadSession;
23
24
  exports.sameRealPath = sameRealPath;
@@ -142,13 +143,33 @@ function clearSession(root, sessionFile = SESSION_FILE) {
142
143
  */
143
144
  function detectSlugConflict(slug, root) {
144
145
  const entry = loadRegistry().projects[slug];
145
- if (!(entry === null || entry === void 0 ? void 0 : entry.path) || sameRealPath(entry.path, root))
146
+ // A relative stored path cannot be compared to anything: its base is lost, so
147
+ // it names a different directory from every cwd. Entries like that were written
148
+ // by `lt fullstack init` before the path was absolutised, and comparing one
149
+ // produced "slug 't-5' is also registered to another checkout: t-5". Claiming a
150
+ // conflict we cannot establish is worse than staying quiet — the next registry
151
+ // write from inside the project repairs the entry anyway.
152
+ if (!(entry === null || entry === void 0 ? void 0 : entry.path) || !isVerifiablePath(entry.path) || sameRealPath(entry.path, root))
146
153
  return null;
147
154
  const session = loadSession(entry.path);
148
155
  const otherSessionAlive = !!session && [session.pids.api, session.pids.app].some((p) => typeof p === 'number' && isPidAlive(p));
149
156
  return { otherPath: entry.path, otherSessionAlive };
150
157
  }
151
- /** Check whether a process with the given PID is currently alive. */
158
+ /**
159
+ * Check whether a process with the given PID is currently alive.
160
+ *
161
+ * Signal 0 delivers nothing; it only asks the kernel whether the target exists
162
+ * and may be signalled. The two failures mean opposite things, and collapsing
163
+ * them was a bug:
164
+ *
165
+ * - `ESRCH` — no such process. Dead.
166
+ * - `EPERM` — **the process EXISTS**, this user may not signal it.
167
+ *
168
+ * Reading `EPERM` as dead made `classifyComponentHealth` report a running
169
+ * component as `dead`, so `lt dev up` killed and restarted a healthy stack.
170
+ * Reproducible on any POSIX machine: `process.kill(1, 0)` throws `EPERM`.
171
+ * More likely on Windows, where an elevated shell is ordinary.
172
+ */
152
173
  function isPidAlive(pid) {
153
174
  if (!isValidPid(pid))
154
175
  return false;
@@ -156,14 +177,29 @@ function isPidAlive(pid) {
156
177
  process.kill(pid, 0);
157
178
  return true;
158
179
  }
159
- catch (_a) {
160
- return false;
180
+ catch (err) {
181
+ return (err === null || err === void 0 ? void 0 : err.code) === 'EPERM';
161
182
  }
162
183
  }
163
184
  /** Validate a PID — positive integer, within plausible range. */
164
185
  function isValidPid(pid) {
165
186
  return typeof pid === 'number' && Number.isInteger(pid) && pid > 0 && pid < 4194304;
166
187
  }
188
+ /**
189
+ * True when a stored registry path can be checked against the file system.
190
+ *
191
+ * A relative path in the registry has lost the directory it was relative TO, so
192
+ * `existsSync` / `realpathSync` answer about whatever directory the current
193
+ * process started in. Every such answer is meaningless, and two of them were
194
+ * actively harmful: `planRegistryPrune` reclaimed a live project's slug and
195
+ * reserved ports, and `detectSlugConflict` reported a project as colliding with
196
+ * itself. Legacy entries are therefore treated as "cannot determine" rather than
197
+ * as "gone"; running any `lt dev` command inside the project rewrites the entry
198
+ * with an absolute path and clears the condition.
199
+ */
200
+ function isVerifiablePath(storedPath) {
201
+ return !!storedPath && (0, path_1.isAbsolute)(storedPath);
202
+ }
167
203
  /** Load the central registry; returns an empty one if missing or unreadable. */
168
204
  function loadRegistry() {
169
205
  if (!(0, fs_1.existsSync)(REGISTRY_PATH))
@@ -1,31 +1,4 @@
1
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.TEST_NITRO_OUTPUT_DIR = exports.TEST_NUXT_BUILD_DIR = exports.TEST_INITIAL_ADMIN_ENV = void 0;
13
- exports.ensurePlaywrightBrowsers = ensurePlaywrightBrowsers;
14
- exports.autoShardCount = autoShardCount;
15
- exports.bringUpTestSession = bringUpTestSession;
16
- exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
17
- exports.buildTestAppEnv = buildTestAppEnv;
18
- exports.hasTestSession = hasTestSession;
19
- exports.isStackServing = isStackServing;
20
- exports.migrationFailedError = migrationFailedError;
21
- exports.resolveTestSession = resolveTestSession;
22
- exports.runShardedTestSession = runShardedTestSession;
23
- exports.shardReportDir = shardReportDir;
24
- exports.startTestApi = startTestApi;
25
- exports.tearDownAllTestSessions = tearDownAllTestSessions;
26
- exports.tearDownTestSession = tearDownTestSession;
27
- exports.testAppEntryCandidates = testAppEntryCandidates;
28
- exports.unreachableStackError = unreachableStackError;
29
2
  /**
30
3
  * Ephemeral, isolated test session for `lt dev test`.
31
4
  *
@@ -56,7 +29,33 @@ exports.unreachableStackError = unreachableStackError;
56
29
  * Teardown is idempotent and residue-free (processes, Caddy block, env bridge,
57
30
  * session file, registry entry), so a stale session is always safely reclaimed.
58
31
  */
59
- const child_process_1 = require("child_process");
32
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
33
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
34
+ return new (P || (P = Promise))(function (resolve, reject) {
35
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
36
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
37
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
38
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
39
+ });
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.TEST_NITRO_OUTPUT_DIR = exports.TEST_NUXT_BUILD_DIR = exports.TEST_INITIAL_ADMIN_ENV = void 0;
43
+ exports.ensurePlaywrightBrowsers = ensurePlaywrightBrowsers;
44
+ exports.autoShardCount = autoShardCount;
45
+ exports.bringUpTestSession = bringUpTestSession;
46
+ exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
47
+ exports.buildTestAppEnv = buildTestAppEnv;
48
+ exports.hasTestSession = hasTestSession;
49
+ exports.isStackServing = isStackServing;
50
+ exports.migrationFailedError = migrationFailedError;
51
+ exports.resolveTestSession = resolveTestSession;
52
+ exports.runShardedTestSession = runShardedTestSession;
53
+ exports.shardReportDir = shardReportDir;
54
+ exports.startTestApi = startTestApi;
55
+ exports.tearDownAllTestSessions = tearDownAllTestSessions;
56
+ exports.tearDownTestSession = tearDownTestSession;
57
+ exports.testAppEntryCandidates = testAppEntryCandidates;
58
+ exports.unreachableStackError = unreachableStackError;
60
59
  const fs_1 = require("fs");
61
60
  const os_1 = require("os");
62
61
  const path_1 = require("path");
@@ -69,6 +68,7 @@ const dev_package_manager_1 = require("./dev-package-manager");
69
68
  const dev_patches_1 = require("./dev-patches");
70
69
  const dev_process_1 = require("./dev-process");
71
70
  const dev_project_1 = require("./dev-project");
71
+ const platform_1 = require("./platform");
72
72
  const dev_state_1 = require("./dev-state");
73
73
  /**
74
74
  * Make sure the Playwright browser build the app's OWN playwright version needs
@@ -83,12 +83,22 @@ const dev_state_1 = require("./dev-state");
83
83
  * fails (offline), the suite still runs and Playwright's own error explains it.
84
84
  */
85
85
  function ensurePlaywrightBrowsers(appDir, pm, logInfo) {
86
+ const note = () => {
87
+ logInfo === null || logInfo === void 0 ? void 0 : logInfo('playwright install chromium failed — continuing (the suite reports missing browsers itself).');
88
+ };
86
89
  try {
87
90
  const args = pm.exec('playwright', ['install', 'chromium']);
88
- (0, child_process_1.execFileSync)(pm.bin, args, { cwd: appDir, stdio: 'ignore', timeout: 300000 });
91
+ // `spawnCmdSync` (spawnSync) does NOT throw on a non-zero exit, unlike the
92
+ // `execFileSync` this replaced — so the status is checked explicitly.
93
+ // Without it the failure would be silent and the suite would die later on a
94
+ // missing browser, which reads as a broken spec rather than a missing install.
95
+ const result = (0, platform_1.spawnCmdSync)(pm.bin, args, { cwd: appDir, stdio: 'ignore', timeout: 300000 });
96
+ if (result.error || result.status !== 0) {
97
+ note();
98
+ }
89
99
  }
90
100
  catch (_a) {
91
- logInfo === null || logInfo === void 0 ? void 0 : logInfo('playwright install chromium failed — continuing (the suite reports missing browsers itself).');
101
+ note();
92
102
  }
93
103
  }
94
104
  const TEST_API_LOG = 'api.test.log';
@@ -240,11 +250,14 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
240
250
  taken.add(apiPort);
241
251
  appPort = layout.appDir ? (0, dev_state_1.allocateInternalPort)(TEST_PORT_BASE, taken) : undefined;
242
252
  const portsToCheck = [apiPort, appPort].filter((p) => typeof p === 'number');
243
- const snap = yield (0, dev_process_1.listenSnapshot)(portsToCheck);
253
+ const probe = yield (0, dev_process_1.probePorts)(portsToCheck);
244
254
  for (const p of portsToCheck) {
245
- const r = snap.get(p);
246
- if (r)
247
- throw new Error(`test internal port ${p} already in use by ${r.command} (pid ${r.pid}).`);
255
+ if (!probe.bound.has(p))
256
+ continue;
257
+ const owner = probe.owners.get(p);
258
+ throw new Error(owner
259
+ ? `test internal port ${p} already in use by ${owner.command} (pid ${owner.pid}).`
260
+ : `test internal port ${p} already in use by an unidentified process.`);
248
261
  }
249
262
  // Reserve immediately (still under the lock) so a concurrent run sees these
250
263
  // ports as taken. PIDs are written to the session file after spawn, below.
@@ -405,10 +418,19 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
405
418
  // left to wait for. Being wrong here costs nothing — the timeout still bounds
406
419
  // the wait, so this only ever makes the abort arrive sooner.
407
420
  const gone = (pid) => pid === undefined || !(0, dev_state_1.isPidAlive)(pid);
421
+ // The probes go to the LOOPBACK address, not the public name: this is Node
422
+ // asking, and on Windows `*.localhost` does not resolve for Node at all — the
423
+ // waits would time out against a stack that is up, and `bringUpTestSession`
424
+ // aborts before Playwright ever starts. Probing the port directly also measures
425
+ // the component rather than the proxy, which is the better question here.
426
+ // The public name stays in the MESSAGES, because that is what the developer
427
+ // opens; the loopback pair is printed next to it above.
428
+ const appProbeUrl = (0, dev_env_1.internalUrl)(appPort) || appUrl;
429
+ const apiProbeUrl = (0, dev_env_1.internalUrl)(apiPort) || apiUrl;
408
430
  // Wait for the test App to answer.
409
431
  if (appUrl) {
410
432
  log.info(log.dim(`Waiting for ${appUrl} …`));
411
- const appReady = yield (0, dev_process_1.waitForHttp)(appUrl, 90000, isStackServing, () => gone(pids.app));
433
+ const appReady = yield (0, dev_process_1.waitForHttp)(appProbeUrl, 90000, isStackServing, () => gone(pids.app));
412
434
  if (!appReady)
413
435
  throw unreachableStackError('App', appUrl, appLogPath);
414
436
  }
@@ -418,7 +440,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
418
440
  // `ensureApiReachableOrSkip` guard (the API-readiness race).
419
441
  if (apiUrl) {
420
442
  log.info(log.dim(`Waiting for ${apiUrl}/meta …`));
421
- const apiReady = yield (0, dev_process_1.waitForHttp)(`${apiUrl}/meta`, 120000, isStackServing, () => gone(pids.api));
443
+ const apiReady = yield (0, dev_process_1.waitForHttp)(`${apiProbeUrl}/meta`, 120000, isStackServing, () => gone(pids.api));
422
444
  if (!apiReady)
423
445
  throw unreachableStackError('API', `${apiUrl}/meta`, apiLogPath);
424
446
  }
@@ -57,6 +57,7 @@ const dev_package_manager_1 = require("./dev-package-manager");
57
57
  const dev_patches_1 = require("./dev-patches");
58
58
  const dev_project_1 = require("./dev-project");
59
59
  const dev_state_1 = require("./dev-state");
60
+ const platform_1 = require("./platform");
60
61
  /** Marker file (under `.lt-dev/`) that tags a worktree with its ticket id. */
61
62
  const TICKET_MARKER = 'ticket';
62
63
  /** Where ticket databases live. Deliberately loopback-only — never a remote/prod host. */
@@ -281,8 +282,19 @@ function gitRefExists(repoDir, ref) {
281
282
  * for fresh scaffolds without a lockfile yet.
282
283
  */
283
284
  function installWorktreeDeps(dir) {
285
+ var _a;
284
286
  const pm = (0, dev_package_manager_1.pickPackageManager)(dir);
285
- (0, child_process_1.execFileSync)(pm.bin, pm.installArgs, { cwd: dir, stdio: 'inherit' });
287
+ // `spawnCmdSync` (spawnSync) does NOT throw on a non-zero exit, unlike the
288
+ // `execFileSync` this replaced. The contract here is to throw: the only caller
289
+ // (`lt ticket start`) catches and warns, and swallowing the failure would hand
290
+ // the developer a half-installed worktree with no hint that anything went wrong.
291
+ const result = (0, platform_1.spawnCmdSync)(pm.bin, pm.installArgs, { cwd: dir, stdio: 'inherit' });
292
+ if (result.error) {
293
+ throw result.error;
294
+ }
295
+ if (result.status !== 0) {
296
+ throw new Error(`${pm.bin} ${pm.installArgs.join(' ')} exited with ${(_a = result.status) !== null && _a !== void 0 ? _a : 'a signal'}`);
297
+ }
286
298
  }
287
299
  /**
288
300
  * True for ticket ids whose derived database would collide with a project-level
@@ -32,11 +32,16 @@ const fs_utils_1 = require("./fs-utils");
32
32
  * starter's assessed-advisory allowlist is destroyed rather than merely ignored,
33
33
  * and the generated project's very first pipeline goes red on an advisory that
34
34
  * was already justified upstream.
35
+ *
36
+ * `peerDependencyRules` (`{ allowedVersions: {...}, ignoreMissing: [...] }`) is
37
+ * the same trap one step later: the starter scope-allows known-stale peer ranges
38
+ * there, and without the hoist the root `check:peers` step fails the first
39
+ * `pnpm run check` of every generated project.
35
40
  */
36
41
  const OBJECT_FIELDS = ['overrides', 'allowBuilds'];
37
42
  const ARRAY_FIELDS = ['onlyBuiltDependencies', 'ignoredOptionalDependencies', 'minimumReleaseAgeExclude'];
38
- /** Objects whose values are arrays to be unioned, not replaced. */
39
- const NESTED_ARRAY_FIELDS = ['auditConfig'];
43
+ /** Objects whose inner arrays are unioned and inner maps merged, not replaced. */
44
+ const NESTED_ARRAY_FIELDS = ['auditConfig', 'peerDependencyRules'];
40
45
  /** The union of all three, in declaration order. Declared here, with its inputs,
41
46
  * because the comment-carrying helpers below default their `fields` parameter to it. */
42
47
  const WORKSPACE_SCOPED_PNPM_FIELDS = [...OBJECT_FIELDS, ...ARRAY_FIELDS, ...NESTED_ARRAY_FIELDS];
@@ -611,11 +616,13 @@ function mergePnpmFieldValue(field, rootValue, subValue) {
611
616
  const subArr = Array.isArray(subValue) ? subValue : [];
612
617
  return Array.from(new Set([...rootArr, ...subArr])).sort((a, b) => a.localeCompare(b));
613
618
  }
614
- // Nested (`auditConfig.ignoreGhsas` / `.ignoreCves`): union each inner array
615
- // instead of letting the sub-project's object replace the root's. A plain
616
- // key-by-key merge would drop every advisory the root had already justified.
619
+ // Nested (`auditConfig.ignoreGhsas`, `peerDependencyRules.allowedVersions`, …):
620
+ // union each inner array and merge each inner map instead of letting the
621
+ // sub-project's object replace the root's. A plain key-by-key merge would drop
622
+ // every advisory or peer rule the root had already justified.
617
623
  if (isNestedArrayField(field)) {
618
- const asObj = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
624
+ const isMap = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
625
+ const asObj = (v) => (isMap(v) ? v : {});
619
626
  const rootObj = asObj(rootValue);
620
627
  const subObj = asObj(subValue);
621
628
  const merged = Object.assign({}, rootObj);
@@ -625,6 +632,10 @@ function mergePnpmFieldValue(field, rootValue, subValue) {
625
632
  const b = Array.isArray(value) ? value : [];
626
633
  merged[key] = Array.from(new Set([...a, ...b])).sort((x, y) => x.localeCompare(y));
627
634
  }
635
+ else if (isMap(value) && isMap(merged[key])) {
636
+ const inner = Object.assign(Object.assign({}, merged[key]), value);
637
+ merged[key] = Object.fromEntries(Object.entries(inner).sort(([x], [y]) => x.localeCompare(y)));
638
+ }
628
639
  else {
629
640
  merged[key] = value;
630
641
  }
@@ -1,10 +1,44 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.findExecutable = findExecutable;
4
37
  exports.isWindows = isWindows;
38
+ exports.spawnCmd = spawnCmd;
5
39
  exports.spawnCmdSync = spawnCmdSync;
6
40
  exports.windowsAppData = windowsAppData;
7
- const cross_spawn_1 = require("cross-spawn");
41
+ const cross_spawn_1 = __importStar(require("cross-spawn"));
8
42
  const fs_1 = require("fs");
9
43
  const path_1 = require("path");
10
44
  const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
@@ -48,6 +82,24 @@ function findExecutable(name, options = {}) {
48
82
  function isWindows(platform = process.platform) {
49
83
  return platform === 'win32';
50
84
  }
85
+ /**
86
+ * `spawn` that also runs `.cmd`/`.bat` shims on Windows.
87
+ *
88
+ * The asynchronous counterpart of `spawnCmdSync`, with the same delegation and
89
+ * the same argument limit — see there. Needed because every long-running child
90
+ * the CLI starts (`pnpm run dev`, `pnpm start`, a Playwright run) is spawned
91
+ * asynchronously, and `pnpm` is `pnpm.cmd` on Windows: Node refuses to exec a
92
+ * `.cmd` directly (CVE-2024-27980, `EINVAL`), so without this nothing starts
93
+ * there at all.
94
+ *
95
+ * `detached` and `stdio` pass through unchanged, so a caller keeps its process
96
+ * group and its log-file descriptors. On Windows cross-spawn interposes
97
+ * `cmd.exe /c`, which means the reported pid is that of `cmd.exe` — the child
98
+ * tree, not a process group, is what a killer has to walk there.
99
+ */
100
+ function spawnCmd(command, args = [], options = {}) {
101
+ return (0, cross_spawn_1.default)(command, [...args], options);
102
+ }
51
103
  /**
52
104
  * `spawnSync` that also runs `.cmd`/`.bat` shims on Windows.
53
105
  *
@@ -91,14 +91,21 @@ JavaScript file: src/main.ts
91
91
  see [Debug.run.xml](.run/Debug.run.xml)
92
92
 
93
93
 
94
- ## Test & debug the NestServer package in this project
95
- Use [yalc](https://github.com/wclr/yalc) to include the NestJS server in the project.
94
+ <% if (props.vendor) { -%>
95
+ ## Working on the framework core
96
+ This project runs in vendor mode: the `@lenne.tech/nest-server` core is part of the source tree under `src/core/`, not an npm package, so there is nothing to link.
97
+
98
+ - Read `src/core/VENDOR.md` first. It records the upstream baseline and every local change to the core.
99
+ - Put project-specific code outside `src/core/`: extend, inherit from or override the core, never patch it for one project.
100
+ - Pull in upstream changes with `/lt-dev:backend:update-nest-server-core`, and propose generally useful core fixes upstream with `/lt-dev:backend:contribute-nest-server-core`.
96
101
 
97
- 1. clone [NestServer](https://github.com/lenneTech/nest-server): `git clone https://github.com/lenneTech/nest-server.git`
98
- 2. go to the nest-server folder (`cd nest-server`), install the packages via `npm i` and start the nest server in watch & yalc mode: `npm run watch`
99
- 3. link the nest server live package to this project via `npm run link:nest-server` and start the server: `npm start`
100
- 4. unlink the nest-server live package and use the normal package again when you are done: `npm run unlink:nest-server`
102
+ <% } else { -%>
103
+ ## Test & debug the NestServer package in this project
104
+ 1. Clone [NestServer](https://github.com/lenneTech/nest-server): `git clone https://github.com/lenneTech/nest-server.git`
105
+ 2. Link it into this project with `pnpm run link:nest-server` and start the server: `pnpm start`
106
+ 3. Switch back to the published package when you are done: `pnpm run unlink:nest-server`
101
107
 
108
+ <% } -%>
102
109
  ## Deployment with deploy.party
103
110
 
104
111
  This project is prepared for deployment with deploy.party.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.49.0",
3
+ "version": "1.50.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -134,6 +134,9 @@
134
134
  "rootDir": "__tests__",
135
135
  "testTimeout": 60000,
136
136
  "workerIdleMemoryLimit": "512MB",
137
+ "setupFilesAfterEnv": [
138
+ "<rootDir>/support/signal-guard.ts"
139
+ ],
137
140
  "testMatch": [
138
141
  "<rootDir>/*.test.ts"
139
142
  ],