@lenne.tech/cli 1.50.0 → 1.50.2

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
  const path_1 = require("path");
13
+ const reinit_command_1 = require("../../lib/reinit-command");
13
14
  /**
14
15
  * Reinitialize npm packages
15
16
  */
@@ -83,8 +84,11 @@ const NewCommand = {
83
84
  if (system.which('rimraf')) {
84
85
  yield system.run(pm.globalInstall('rimraf'));
85
86
  }
86
- const lockfile = pm.getLockfileName(detectedPm);
87
- yield system.run(`cd ${(0, path_1.dirname)(path)} && rimraf ${lockfile} && rimraf node_modules && ${pm.cacheClean(detectedPm)} && ${pm.install(detectedPm)}`);
87
+ yield system.run((0, reinit_command_1.buildReinitCommand)({
88
+ dir: (0, path_1.dirname)(path),
89
+ install: pm.install(detectedPm),
90
+ lockfile: pm.getLockfileName(detectedPm),
91
+ }));
88
92
  reinitSpin.succeed();
89
93
  if (data.scripts && data.scripts['test:e2e']) {
90
94
  const testE2eSpin = spin('Run tests');
@@ -1,4 +1,4 @@
1
1
  {
2
- "$comment": "Upstream @lenne.tech/nest-server DEVDEPENDENCIES that must land in a vendored project's `dependencies`. Vendoring copies upstream `dependencies` verbatim and does not read `peerDependencies` at all, so a package the framework imports at runtime while keeping it in devDependencies would never reach the production runtime. Entries here are matched against upstream `devDependencies` only — see `isVendorRuntimeDep`, consulted in the `upstreamDevDeps` loop in `extensions/server.ts` — and promoted into `dependencies`, preferring the upstream peer RANGE over the exact devDep pin where one exists, so a generated project still receives patch-level security fixes. A package that appears ONLY in upstream `peerDependencies` cannot be promoted by this list: listing it here is a silent no-op. Two live cases: `find-file-up` (imported by the config loader, devDependency only), and the better-auth trio (declared as peerDependencies AND mirrored into devDependencies upstream; the mirror is what makes the promotion fire, so this list is the fallback if the starter ever stops declaring them itself).",
3
- "runtimeHelpers": ["@better-auth/core", "@better-auth/passkey", "better-auth", "find-file-up"]
2
+ "$comment": "Upstream @lenne.tech/nest-server DEVDEPENDENCIES that must land in a vendored project's `dependencies`. Vendoring copies upstream `dependencies` verbatim and does not read `peerDependencies` at all, so a package the framework imports at runtime while keeping it in devDependencies would never reach the production runtime. Entries here are matched against upstream `devDependencies` only — see `isVendorRuntimeDep`, consulted in the `upstreamDevDeps` loop in `extensions/server.ts` — and promoted into `dependencies`, preferring the upstream peer RANGE over the exact devDep pin where one exists, so a generated project still receives patch-level security fixes. A package that appears ONLY in upstream `peerDependencies` cannot be promoted by this list: listing it here is a silent no-op. One live case: the better-auth trio (declared as peerDependencies AND mirrored into devDependencies upstream; the mirror is what makes the promotion fire, so this list is the fallback if the starter ever stops declaring them itself). `find-file-up` was listed here until 2026-10-03 and never needed: no nest-server release from 8.x to 11.x imports it from src/ (only the dev script extras/sync-version.ts did), and 11.41.8 dropped the package altogether.",
3
+ "runtimeHelpers": ["@better-auth/core", "@better-auth/passkey", "better-auth"]
4
4
  }
@@ -43,21 +43,6 @@ class PackageManager {
43
43
  const manager = pm || this.detect();
44
44
  return manager;
45
45
  }
46
- /**
47
- * Get the cache clean command
48
- * @param pm - Override detected package manager
49
- */
50
- cacheClean(pm) {
51
- const manager = pm || this.detect();
52
- switch (manager) {
53
- case 'pnpm':
54
- return 'pnpm store prune';
55
- case 'yarn':
56
- return 'yarn cache clean';
57
- default:
58
- return 'npm cache clean --force';
59
- }
60
- }
61
46
  /**
62
47
  * Detect the package manager for the given directory
63
48
  * Uses caching per directory for performance
@@ -1919,8 +1919,8 @@ class Server {
1919
1919
  * dep in disguise that needs to live in `dependencies` after vendoring?
1920
1920
  *
1921
1921
  * `@lenne.tech/nest-server` keeps a few packages in devDependencies that
1922
- * the framework code imports at runtime (e.g. `find-file-up` in its
1923
- * config loader). When we vendor the framework source into a consumer
1922
+ * the framework code imports at runtime (e.g. the better-auth trio, which
1923
+ * it mirrors from peerDependencies). When we vendor the framework source into a consumer
1924
1924
  * project, those must end up in `dependencies` so the compiled/dist
1925
1925
  * runtime has them available.
1926
1926
  *
@@ -37,14 +37,23 @@ function describeLog(d) {
37
37
  }
38
38
  return [`Last lines of ${d.file}:`, ...d.tail.map((l) => ` ${l}`)];
39
39
  }
40
- /** Read `file` and keep its last `lines` non-empty lines. */
41
- function diagnoseLog(file, lines = 20) {
40
+ /**
41
+ * How much of a log's end is read. A dev log reached 1 GB (Vue warnings carry
42
+ * serialized vnode traces of several MB each), and reading all of it to show 20
43
+ * lines threw "Cannot create a string longer than 0x1fffffe8 characters".
44
+ */
45
+ const TAIL_BYTES = 64 * 1024;
46
+ /** Longest line shown: one of those traces would otherwise flood the terminal. */
47
+ const MAX_LINE_LENGTH = 500;
48
+ /** Read the end of `file` and keep its last `lines` non-empty lines. */
49
+ function diagnoseLog(file, lines = 20, maxBytes = TAIL_BYTES) {
42
50
  if (!(0, fs_1.existsSync)(file))
43
51
  return { file, size: -1, state: 'missing', tail: [] };
44
52
  const size = (0, fs_1.statSync)(file).size;
45
53
  if (size === 0)
46
54
  return { file, size, state: 'empty', tail: [] };
47
- return { file, size, state: 'ok', tail: tailLines((0, fs_1.readFileSync)(file, 'utf8'), lines) };
55
+ const tail = tailLines(readEnd(file, size, maxBytes), lines).map((l) => l.length > MAX_LINE_LENGTH ? `${l.slice(0, MAX_LINE_LENGTH)}…` : l);
56
+ return { file, size, state: 'ok', tail };
48
57
  }
49
58
  /**
50
59
  * Components whose pid is gone within `budgetMs` of their start.
@@ -89,3 +98,24 @@ function tailLines(content, n) {
89
98
  .filter((l) => l.trim().length > 0)
90
99
  .slice(-n);
91
100
  }
101
+ /**
102
+ * The last `maxBytes` of `file`. The first line of a cut window is partial and
103
+ * dropped, unless the window lies inside one line: then the fragment is kept,
104
+ * marked with a leading `…`.
105
+ */
106
+ function readEnd(file, size, maxBytes) {
107
+ const length = Math.min(size, maxBytes);
108
+ const buffer = Buffer.alloc(length);
109
+ const fd = (0, fs_1.openSync)(file, 'r');
110
+ try {
111
+ (0, fs_1.readSync)(fd, buffer, 0, length, size - length);
112
+ }
113
+ finally {
114
+ (0, fs_1.closeSync)(fd);
115
+ }
116
+ const text = buffer.toString('utf8');
117
+ if (length === size)
118
+ return text;
119
+ const rest = text.slice(text.indexOf('\n') + 1);
120
+ return text.includes('\n') && rest.trim() ? rest : `…${text}`;
121
+ }
@@ -180,7 +180,18 @@ function patchClaudeMd(file, options) {
180
180
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
181
181
  const before = content.slice(0, startIdx);
182
182
  const after = content.slice(endIdx + endMarker.length);
183
- next = before + block + after;
183
+ // Anything between the markers that is NOT this generated block is somebody's own writing,
184
+ // and it is kept — moved out in front of the block rather than overwritten.
185
+ //
186
+ // Replacing the span wholesale was silent data loss, observed in `offers`: a 29-line section
187
+ // of project documentation sat between the markers and one `lt dev up` deleted it, reporting
188
+ // only "updated CLAUDE.md URL block in 1 file(s)". Nothing about the markers suggests the
189
+ // span is owned — they are HTML comments, so they are invisible in every rendered view of the
190
+ // file, and a section appended after the preceding one lands inside them without anyone
191
+ // seeing it. The only reason it was noticed at all is that the file happened to be under
192
+ // review in the same minute.
193
+ const rescued = rescueForeignContent(content.slice(startIdx + startMarker.length, endIdx));
194
+ next = before + (rescued ? `${rescued}\n\n` : '') + block + after;
184
195
  }
185
196
  else {
186
197
  const sep = content.endsWith('\n\n') ? '' : content.endsWith('\n') ? '\n' : '\n\n';
@@ -427,3 +438,26 @@ function normaliseBridgeBlock(s) {
427
438
  .replace(/\s+/g, ' ')
428
439
  .trim();
429
440
  }
441
+ /**
442
+ * Pull out the sections between the lt-dev markers that this tool did not write.
443
+ *
444
+ * The generated block is one section, `## Local Development (lt dev)`. Every other `##` section
445
+ * found inside the markers was written by somebody else and is returned so the caller can place
446
+ * it outside the block. Whitespace-only remainders are dropped — that is the generated block's
447
+ * own padding, not content.
448
+ *
449
+ * Deliberately conservative about what counts as ours: a section is discarded only when its
450
+ * heading matches exactly. A renamed or hand-edited heading is treated as foreign and kept, which
451
+ * leaves a duplicate for a human to resolve. A duplicate is visible; a deletion is not.
452
+ */
453
+ function rescueForeignContent(span) {
454
+ const own = '## Local Development (lt dev)';
455
+ const sections = span.split(/\n(?=## )/);
456
+ // Compare the whole heading line, not a prefix: `## Local Development (lt dev) — notes` is a
457
+ // section somebody wrote next to ours, and a prefix match would delete it.
458
+ const foreign = sections
459
+ .filter((section) => section.trimStart().split(/\r?\n/, 1)[0].trim() !== own)
460
+ .map((section) => section.trim())
461
+ .filter(Boolean);
462
+ return foreign.join('\n\n');
463
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildReinitCommand = buildReinitCommand;
4
+ /**
5
+ * Shell command for `lt npm reinit` in a project without its own `reinit` script:
6
+ * remove the lockfile and node_modules, then install fresh.
7
+ *
8
+ * Deliberately no cache step. `pnpm store prune` empties the machine-wide store on APFS
9
+ * (clone imports leave every store file with link count 1, which is exactly what prune
10
+ * deletes), and `npm cache clean --force` / `yarn cache clean` wipe the shared caches the
11
+ * same way — every other project then downloads again, and a parallel install loses the
12
+ * files it is linking. A fresh resolution needs neither: deleting the lockfile is what
13
+ * makes the package manager ask the registry again, and pnpm verifies store integrity
14
+ * on every install.
15
+ */
16
+ function buildReinitCommand(opts) {
17
+ const { dir, install, lockfile } = opts;
18
+ return `cd ${dir} && rimraf ${lockfile} && rimraf node_modules && ${install}`;
19
+ }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // @lt-check-wrapper 3.13.1
2
+ // @lt-check-wrapper 3.14.1
3
3
  /**
4
4
  * Quiet, report-driven wrapper around the project `check` pipeline.
5
5
  *
@@ -33,7 +33,7 @@
33
33
  * scripts/check-wrapper-version.test.mjs fails when it drifts from
34
34
  * package.json. In a generated project it keeps the release it came from.
35
35
  */
36
- import { execFileSync, execSync, spawn } from 'node:child_process';
36
+ import { execFileSync, spawn } from 'node:child_process';
37
37
  import { readFileSync, realpathSync } from 'node:fs';
38
38
  import { dirname, join } from 'node:path';
39
39
  import { fileURLToPath } from 'node:url';
@@ -430,7 +430,21 @@ const IDLE_TIMEOUT_MS = (() => {
430
430
  const RUNNING = new Set();
431
431
 
432
432
  /**
433
- * How to kill a process tree on this platform.
433
+ * Whether a number may become a kill target at all.
434
+ *
435
+ * A spawn that failed leaves `child.pid` undefined. On POSIX that only made `pgrep` and
436
+ * `process.kill` throw, but on Windows it became `taskkill /PID undefined /T /F` — a kill
437
+ * command built from a word that is not a pid. And the small numbers are not processes you
438
+ * own: POSIX pid 1 is init/launchd, and 0 and -1 are the group and the kill(2) broadcast to
439
+ * every process of the user (the shape that rebooted a Mac on 2026-09-23). Windows reserves
440
+ * 0 (System Idle) and 4 (System). Nothing a check step spawns can have any of these pids.
441
+ */
442
+ export function isKillablePid(pid, platform = process.platform) {
443
+ return Number.isInteger(pid) && pid > (platform === 'win32' ? 4 : 1);
444
+ }
445
+
446
+ /**
447
+ * How to kill a process tree on this platform, or null when `pid` must not be touched.
434
448
  *
435
449
  * Windows has neither `pgrep` nor signals: `process.kill(pid, 'SIGTERM')` there ends the one
436
450
  * process and orphans its children, and `taskkill /T` alone was measured to leave the tree
@@ -445,46 +459,85 @@ const RUNNING = new Set();
445
459
  * Split out as a pure function so both branches can be tested from either platform.
446
460
  */
447
461
  export function killTreePlan(pid, signal, platform = process.platform) {
462
+ if (!isKillablePid(pid, platform)) return null;
448
463
  return platform === 'win32' ? { args: ['/PID', String(pid), '/T', '/F'], command: 'taskkill' } : { signal };
449
464
  }
450
465
 
451
- // Best-effort kill of a child's whole process tree (sh → pnpm → vitest →
452
- // fork workers). Killing only the direct child orphans the tree — exactly the
453
- // zombie workers a deadlock leaves behind. Children are collected via pgrep
454
- // and killed leaves-first.
455
- function killTree(child, signal = 'SIGTERM') {
456
- const plan = killTreePlan(child.pid, signal);
466
+ /**
467
+ * Kill `pid` and its whole process tree, with every effect injected.
468
+ *
469
+ * Injecting the platform alone is not enough: the effects would still run for real, against
470
+ * this machine, with whatever pid the test chose. So all four are required and nothing
471
+ * defaults — a test cannot reach a real `taskkill`, `pgrep` or `process.kill` by leaving one
472
+ * out. `killTree` below is the only caller that passes the real ones.
473
+ *
474
+ * - `run(command, args)` executes the Windows plan.
475
+ * - `childrenOf(pid)` returns the direct children of `pid` (POSIX).
476
+ * - `signal(pid, sig)` delivers `sig` to `pid` (POSIX).
477
+ *
478
+ * POSIX order is leaves first, so a parent cannot respawn a child that was just killed, and
479
+ * each pid is signalled at most once. Pids a lookup returns pass the same check as the root —
480
+ * a stray 1 in `pgrep` output must not become a signal to init.
481
+ */
482
+ export function killTreeWith(pid, sig, { childrenOf, platform, run, signal }) {
483
+ for (const [name, fn] of Object.entries({ childrenOf, run, signal })) {
484
+ if (typeof fn !== 'function') throw new TypeError(`killTreeWith: \`${name}\` must be injected`);
485
+ }
486
+ if (typeof platform !== 'string') throw new TypeError('killTreeWith: `platform` must be injected');
487
+ const plan = killTreePlan(pid, sig, platform);
488
+ if (!plan) return;
457
489
  if (plan.command) {
458
490
  try {
459
- execFileSync(plan.command, plan.args, { stdio: 'ignore' });
491
+ run(plan.command, plan.args);
460
492
  } catch {
461
493
  /* already gone, or taskkill refused — nothing further to try */
462
494
  }
463
495
  return;
464
496
  }
465
- const pids = [];
466
- const collect = (pid) => {
467
- pids.push(pid);
468
- let out = '';
497
+ const seen = new Set();
498
+ const order = [];
499
+ const collect = (p) => {
500
+ if (seen.has(p) || !isKillablePid(p, platform)) return;
501
+ seen.add(p);
502
+ let children = [];
469
503
  try {
470
- out = execSync(`pgrep -P ${pid}`, { stdio: ['ignore', 'pipe', 'ignore'] })
471
- .toString()
472
- .trim();
504
+ children = childrenOf(p);
473
505
  } catch {
474
506
  /* no children */
475
507
  }
476
- if (out) for (const p of out.split('\n')) collect(Number(p));
508
+ for (const c of children) collect(c);
509
+ order.push(p);
477
510
  };
478
- collect(child.pid);
479
- for (const pid of pids.reverse()) {
511
+ collect(pid);
512
+ for (const p of order) {
480
513
  try {
481
- process.kill(pid, signal);
514
+ signal(p, plan.signal);
482
515
  } catch {
483
516
  /* already gone */
484
517
  }
485
518
  }
486
519
  }
487
520
 
521
+ /** Direct children of `pid` via `pgrep -P`; empty when there are none (pgrep exits 1). */
522
+ function pgrepChildren(pid) {
523
+ const out = execFileSync('pgrep', ['-P', String(pid)], { stdio: ['ignore', 'pipe', 'ignore'] })
524
+ .toString()
525
+ .trim();
526
+ return out ? out.split('\n').map(Number) : [];
527
+ }
528
+
529
+ // Best-effort kill of a child's whole process tree (sh → pnpm → vitest →
530
+ // fork workers). Killing only the direct child orphans the tree — exactly the
531
+ // zombie workers a deadlock leaves behind.
532
+ function killTree(child, signal = 'SIGTERM') {
533
+ killTreeWith(child.pid, signal, {
534
+ childrenOf: pgrepChildren,
535
+ platform: process.platform,
536
+ run: (command, args) => execFileSync(command, args, { stdio: 'ignore' }),
537
+ signal: (pid, sig) => process.kill(pid, sig),
538
+ });
539
+ }
540
+
488
541
  // idleTimeoutMs > 0 arms the no-output watchdog for this child; 0 (the default)
489
542
  // runs it unwatched. Only callers that KNOW the child streams progress (test
490
543
  // steps) should pass a timeout — see runGroup.
@@ -1,13 +1,13 @@
1
1
  {
2
- "commit": "fcf7c83cffa37b09db88481833e7ccc0dd2fbe0f",
2
+ "commit": "26365f7c8acaa98bc48f360886055d0fe45c593d",
3
3
  "files": {
4
4
  "build-test-gate.mjs": "865bf1b9dd28151f53b9f2dfe4c3c46d29b9303fb1c2f497ce0e3136bcd37d71",
5
- "check.mjs": "fed669a625b56d907ab2e3693c2e5797a877c460588bd79f89249d88c568c178",
5
+ "check.mjs": "6be5506f5fb63f53c65ea438c67d810de1afe1f3299828ef4531c37cc2457d4b",
6
6
  "lib/ansi.mjs": "f997ee6e9bf1c7ee54ab612d4040fcc778064ec160eae19bf5c3960a75a6e2ff",
7
7
  "lib/audit-report.mjs": "503825c3342dec1ee5d8d70e5af21b73e798faee320426b2492190cde0801df2",
8
8
  "lib/workspace-packages.mjs": "ccdd133622a88186c924bc8271d35cf1f8357dc26d4a607998ca5e91fce5bf88"
9
9
  },
10
- "ref": "v3.13.1",
10
+ "ref": "v3.14.1",
11
11
  "repository": "lenneTech/lt-monorepo",
12
- "version": "3.13.1"
12
+ "version": "3.14.1"
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.50.0",
3
+ "version": "1.50.2",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -41,7 +41,7 @@
41
41
  "lint:fix": "eslint \"./src/**/*.{ts,js,vue}\" --fix",
42
42
  "prepublishOnly": "npm run build",
43
43
  "preversion": "npm run lint",
44
- "reinit": "npx rimraf package-lock.json && npx rimraf node_modules && npm cache clean --force && npm i && npm run build",
44
+ "reinit": "npx rimraf package-lock.json && npx rimraf node_modules && npm i && npm run build",
45
45
  "snapupdate": "jest --updateSnapshot",
46
46
  "start": "node bin/lt",
47
47
  "start:build": "npm run build && node bin/lt --compiled-build",
@@ -58,12 +58,13 @@
58
58
  "bin"
59
59
  ],
60
60
  "dependencies": {
61
- "@aws-sdk/client-s3": "3.1100.0",
61
+ "@aws-sdk/client-s3": "3.1146.0",
62
62
  "@lenne.tech/cli-plugin-helper": "0.0.14",
63
- "axios": "1.19.0",
63
+ "axios": "1.20.0",
64
64
  "bcrypt": "6.0.0",
65
65
  "cross-spawn": "7.0.6",
66
- "defuddle": "0.19.2",
66
+ "defuddle": "0.19.4",
67
+ "ejs": "6.0.1",
67
68
  "glob": "13.0.6",
68
69
  "gluegun": "5.2.2",
69
70
  "js-sha256": "1.0.0",
@@ -71,8 +72,8 @@
71
72
  "jsdom": "29.1.1",
72
73
  "jsonc-parser": "3.3.1",
73
74
  "lodash": "4.18.1",
74
- "open": "11.0.0",
75
- "playwright-core": "1.62.1",
75
+ "open": "11.0.4",
76
+ "playwright-core": "1.63.0",
76
77
  "ts-morph": "28.0.0",
77
78
  "ts-node": "10.9.2",
78
79
  "turndown": "7.2.4",
@@ -84,8 +85,8 @@
84
85
  "@emnapi/runtime": "Same as @emnapi/core."
85
86
  },
86
87
  "devDependencies": {
87
- "@emnapi/core": "^1.11.3",
88
- "@emnapi/runtime": "^1.11.3",
88
+ "@emnapi/core": "1.11.3",
89
+ "@emnapi/runtime": "1.11.3",
89
90
  "@lenne.tech/eslint-config-ts": "2.3.0",
90
91
  "@lenne.tech/npm-package-helper": "0.0.12",
91
92
  "@types/cross-spawn": "6.0.6",
@@ -93,33 +94,32 @@
93
94
  "@types/jest": "30.0.0",
94
95
  "@types/js-yaml": "4.0.9",
95
96
  "@types/jsdom": "28.0.1",
96
- "@types/lodash": "4.17.24",
97
- "@types/node": "26.1.2",
97
+ "@types/lodash": "4.17.25",
98
+ "@types/node": "26.6.4",
98
99
  "@types/turndown": "5.0.6",
99
- "ejs": "6.0.1",
100
- "eslint": "10.8.0",
100
+ "eslint": "10.12.0",
101
101
  "husky": "9.1.7",
102
- "jest": "30.4.2",
102
+ "jest": "30.5.2",
103
103
  "minimatch": "10.2.6",
104
- "prettier": "3.8.3",
104
+ "prettier": "3.9.9",
105
105
  "rimraf": "6.1.3",
106
- "ts-jest": "29.4.12"
106
+ "ts-jest": "29.4.14"
107
107
  },
108
108
  "//overrides": {
109
109
  "semver@*": "Force latest semver 7.x across all sub-deps; gluegun@5.2.2 pins semver@7.7.0 which is stale - remove once gluegun updates its dep.",
110
- "brace-expansion@<1.1.18": "Two DoS advisories: exponential-time expansion of consecutive non-expanding {} groups (GHSA-3jxr-9vmj-r5cp, high) and unbounded expansion length causing an OOM crash (GHSA-mh99-v99m-4gvg, high). Transitive via dotgitignore/eslint/fs-jetpack/glob/test-exclude/ts-morph > minimatch. One bounded key per affected major so each can only raise a vulnerable version, never cap a patched one - remove once minimatch requests the patched ranges.",
111
- "brace-expansion@>=2.0.0 <2.1.4": "Same advisories, 2.x line.",
112
- "brace-expansion@>=5.0.0 <5.0.9": "Same advisories, 5.x line. Floored at >=5.0.0 so a future 3.x/4.x dependency is not silently forced across two majors.",
113
- "//minimatch-note": "brace-expansion 1.x and 2.x are END OF LINE for GHSA-mh99-v99m-4gvg: the advisory range is <=5.0.7 across ALL majors and upstream patched ONLY 5.0.8+. 1.1.18 and 2.1.4 are the newest releases of their majors and remain vulnerable. Forcing brace-expansion 5.x globally is NOT an option: 5.x is ESM/tshy and exports an object ({ expand, ... }) while 1.x/2.x export the function itself, so minimatch 3.x/5.x would die on `expand is not a function`. The only real fix is to raise each CONSUMER off minimatch 3.x/5.x, which is what the scoped entries below do. Each was verified against the consumer's actual call site before being added. The eslint / @eslint/eslintrc / @eslint/config-array entries were REMOVED in 1.42.0: eslint 10 requests minimatch ^10.2.5 itself, config-array ^10.2.4, and eslintrc is no longer in the tree at all.",
114
- "minimatch@>=4 <10": "Bounded raise of every minimatch 4.x-9.x install to 10.2.6, whose brace-expansion ^5.0.8 is patched. Covers filelist (gluegun > ejs > jake > filelist, PRODUCTION-reachable, minimatch ^5.0.1), @typescript-eslint/typescript-estree (^9.0.4) and jest-config/jest-runtime/@jest/reporters (^9). Verified safe per consumer: filelist calls only the static helper `minimatch.match(files, pat, opts)`; typescript-estree calls the named `minimatch(filePath, pattern, { dot: true })`; the three jest packages declare minimatch but never require it. Floored at >=4 so the 3.x line is NOT swept in: minimatch 3.x is callable while 9.x/10.x export an object with __esModule but no `default` key, so sweeping it would break any consumer that calls the default export. Drop once these consumers request minimatch >=10 themselves.",
115
- "babel-plugin-istanbul > test-exclude@<8": "test-exclude 6.0.0 pins minimatch ^3.0.4 and glob ^7. test-exclude 8.0.0 uses minimatch ^10.2.2 + glob ^13 (the glob major this project already ships) and is still CJS with the same `module.exports = TestExclude` class shape, so babel-plugin-istanbul's _interopRequireDefault + `new TestExclude(opts)` keeps working. Drop once babel-plugin-istanbul widens its ^6.0.0 range.",
110
+ "brace-expansion@<1.1.21": "Five DoS advisories, all patched in the 1.x line by 1.1.21: exponential-time expansion of consecutive non-expanding {} groups (GHSA-3jxr-9vmj-r5cp, high, fixed 1.1.16), unbounded expansion length causing an OOM crash (GHSA-mh99-v99m-4gvg, high, fixed 1.1.17), uncontrolled recursion in parseCommaParts (GHSA-6j4f-fj2g-mc7p, high, fixed 1.1.19), uncontrolled recursion on nested brace groups (GHSA-qhr7-859c-m2p7, high, fixed 1.1.20) and quadratic-time `{a},b}` rewrite (GHSA-q2hr-2g5m-vwhr, medium, fixed 1.1.21). Raised from 1.1.18 on 2026-10-03: 1.1.18 was still open to the last three. Transitive via dotgitignore/eslint/fs-jetpack/glob/test-exclude/ts-morph > minimatch. Latent today: the minimatch raises below leave no 1.x install in the tree (only brace-expansion 5.0.12), so this key catches a future minimatch 3.x consumer. One bounded key per affected major so each can only raise a vulnerable version, never cap a patched one - remove once minimatch requests the patched ranges.",
111
+ "brace-expansion@>=2.0.0 <2.1.7": "Same five advisories, 2.x line (fixed 2.1.2 / 2.1.3 / 2.1.5 / 2.1.6 / 2.1.7). Raised from 2.1.4 on 2026-10-03. Latent today, like the 1.x key.",
112
+ "brace-expansion@>=5.0.0 <5.0.12": "Same advisories, 5.x line, plus three newer DoS advisories fixed only in 5.0.10-5.0.12 (GHSA-6j4f-fj2g-mc7p, GHSA-qhr7-859c-m2p7, GHSA-q2hr-2g5m-vwhr; all high). Reaches production via gluegun > fs-jetpack > minimatch. Floored at >=5.0.0 so a future 3.x/4.x dependency is not silently forced across two majors.",
113
+ "//minimatch-note": "Why the minimatch raises exist: when they were added, brace-expansion 1.x and 2.x were END OF LINE for GHSA-mh99-v99m-4gvg (advisory range <=5.0.7 across ALL majors, patched only 5.0.8+), so the only fix was to raise each CONSUMER off minimatch 3.x/5.x/9.x. That premise no longer holds: upstream has since backported every fix (GHSA-mh99 now reads <1.1.17 / >=2.0.0 <2.1.3, and 1.1.21 / 2.1.7 patch all five current advisories). Verified 2026-10-03 with fresh lockfile resolves: without `minimatch@>=4 <10` and `fs-jetpack > minimatch@<10` npm audit is still clean, the tree only gains minimatch 3.1.5 + 5.1.9 and brace-expansion 1.1.21 + 2.1.7 (7 packages). So both are now a DEDUPE measure (one minimatch 10.2.6, one brace-expansion 5.0.12), not a security fix - dropping them is a judgment call, it reopens no advisory. Forcing brace-expansion 5.x globally is still NOT an option: 5.x is ESM/tshy and exports an object ({ expand, ... }) while 1.x/2.x export the function itself, so minimatch 3.x/5.x would die on `expand is not a function`. Each scoped entry was verified against the consumer's actual call site before being added. The eslint / @eslint/eslintrc / @eslint/config-array entries were REMOVED in 1.42.0: eslint 10 requests minimatch ^10.2.5 itself, config-array ^10.2.4, and eslintrc is no longer in the tree at all.",
114
+ "minimatch@>=4 <10": "Bounded raise of every minimatch 4.x-9.x install to 10.2.6, whose brace-expansion ^5.0.8 is patched. As of 2026-10-03 the only consumer it still raises is filelist (gluegun > ejs 3.1.10 > jake > filelist, PRODUCTION-reachable, minimatch ^5.0.1): @typescript-eslint/typescript-estree 8.65 requests ^10.2.2 itself and the jest 30.5 packages no longer depend on minimatch. Verified safe: filelist calls only the static helper `minimatch.match(files, pat, opts)`. Floored at >=4 so the 3.x line is NOT swept in: minimatch 3.x is callable while 9.x/10.x export an object with __esModule but no `default` key, so sweeping it would break any consumer that calls the default export. Dedupe, not security (see //minimatch-note). Drop once filelist requests minimatch >=10.",
115
+ "babel-plugin-istanbul > test-exclude@<8": "Dedupe override, no longer a security fix. Added when babel-plugin-istanbul requested test-exclude ^6.0.0, whose 6.0.0 pins minimatch ^3.0.4 and glob ^7. Since jest 30.5 (babel-plugin-istanbul 8.0.0) the request is ^7.0.1, and test-exclude 7.0.2 already uses minimatch ^10.2.2: a fresh resolve without this entry audits clean (2026-10-03) but adds 23 packages (glob 10.5.0 and its subtree). 8.0.0 uses glob ^13, the major this project already ships, and is still CJS with the same `module.exports = TestExclude` class shape, so babel-plugin-istanbul 8's _interopRequireDefault + `new _testExclude.default(opts)` keeps working. Drop once babel-plugin-istanbul requests test-exclude ^8.",
116
116
  "fs-jetpack > minimatch@<10": "Same chain, PRODUCTION-reachable (gluegun > fs-jetpack). Safe: lib/utils/matcher.js does `require('minimatch').Minimatch` and uses only `new Minimatch(pattern, { matchBase, nocomment, nocase, dot })`, `.negate` and `.match()` - all verified against 10.x. Not fixable by upgrading fs-jetpack: gluegun@5.2.2 is the latest release and fs-jetpack@5.1.0 still requests minimatch ^5.1.0 (brace-expansion ^2.0.1, also unpatched)."
117
117
  },
118
118
  "overrides": {
119
119
  "semver@*": "7.8.5",
120
- "brace-expansion@<1.1.18": "1.1.18",
121
- "brace-expansion@>=2.0.0 <2.1.4": "2.1.4",
122
- "brace-expansion@>=5.0.0 <5.0.9": "5.0.9",
120
+ "brace-expansion@<1.1.21": "1.1.21",
121
+ "brace-expansion@>=2.0.0 <2.1.7": "2.1.7",
122
+ "brace-expansion@>=5.0.0 <5.0.12": "5.0.12",
123
123
  "fs-jetpack": {
124
124
  "minimatch@<10": "10.2.6"
125
125
  },