@bobfrankston/npmglobalize 1.0.215 → 1.0.217
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.
- package/README.md +39 -13
- package/cli.js +14 -2
- package/lib.d.ts +59 -11
- package/lib.js +285 -143
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,19 +142,24 @@ npmglobalize --update-major
|
|
|
142
142
|
|
|
143
143
|
### 🔒 Security Auditing
|
|
144
144
|
|
|
145
|
-
**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
Every run ends with an `npm audit` report. The **Release Summary** (or Transform Summary
|
|
146
|
+
with `-nopublish`) carries an `Audit:` row — `✓ no vulnerabilities`, the severity counts,
|
|
147
|
+
or `not run` with npm's reason (typically `ENOLOCK` when there is no lockfile). Critical
|
|
148
|
+
and high findings are listed one per package in the **Issues Summary** at the very end,
|
|
149
|
+
each with the advisory title and URL, how the package got in (`through …` / `pulled in
|
|
150
|
+
by …` / `direct dependency`), and what would clear it:
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
npmglobalize
|
|
153
|
-
|
|
152
|
+
- `fix: npm audit fix` — a compatible update exists; the next run's fix step takes it
|
|
153
|
+
- `fix: npm audit fix --force → node-gyp@13.0.2 (breaking — not run automatically)` — only a
|
|
154
|
+
semver-major bump clears it; npmglobalize never passes `--force`, that is your call
|
|
155
|
+
- `fix: none published yet` — wait for upstream
|
|
156
|
+
|
|
157
|
+
**Auto-fix** (`-fix`, **on by default**, remembered as `"fix": true` in `.globalize.json5`):
|
|
158
|
+
`npm audit fix` runs before the pre-release commit, so a changed lockfile is committed with
|
|
159
|
+
the release. It only ever installs semver-compatible updates.
|
|
154
160
|
|
|
155
|
-
**Disable audit**:
|
|
156
161
|
```bash
|
|
157
|
-
npmglobalize
|
|
162
|
+
npmglobalize -no-fix # report only, no fix attempt
|
|
158
163
|
```
|
|
159
164
|
|
|
160
165
|
### 🧩 Install Scripts (npm `allowScripts`)
|
|
@@ -409,8 +414,9 @@ not what you meant to publish, so npmglobalize lists the conflicted files and st
|
|
|
409
414
|
-no-publish-deps, -npd Skip auto-publishing file: dependencies
|
|
410
415
|
-no-prescan, -nps Skip upfront dep-graph prescan
|
|
411
416
|
-force-publish Republish dependencies even if version exists
|
|
412
|
-
-fix
|
|
413
|
-
|
|
417
|
+
-fix, -no-fix Run npm audit fix before the pre-release commit (default on,
|
|
418
|
+
never --force). The audit REPORT always runs; critical/high
|
|
419
|
+
findings are listed in the Issues Summary with a fix hint.
|
|
414
420
|
-no-use-paths, -nup Declare package standalone; do not resolve file: deps
|
|
415
421
|
from sibling checkouts (see Configuration File)
|
|
416
422
|
```
|
|
@@ -562,6 +568,23 @@ Settings can be saved in `.globalize.json5`:
|
|
|
562
568
|
|
|
563
569
|
Configuration persists across runs. CLI flags override config file.
|
|
564
570
|
|
|
571
|
+
### `.stale` — retired directories
|
|
572
|
+
|
|
573
|
+
A directory that is no longer live or maintained can be retired in place by
|
|
574
|
+
dropping a `.stale` file into it — empty, or better holding one line saying why
|
|
575
|
+
and when. It is a marker file, not a setting in `.globalize.json5`, because
|
|
576
|
+
stale is a property of the directory (most retired directories never had a
|
|
577
|
+
config) and a file is findable without parsing.
|
|
578
|
+
|
|
579
|
+
npmglobalize refuses to build, publish or install a directory marked `.stale`,
|
|
580
|
+
and refuses to cascade into a `file:` dependency that is marked. Both are
|
|
581
|
+
reported as **errors**, not skips: a live package that depends on a retired one
|
|
582
|
+
has a reference that needs fixing. Remove the marker to make the directory live
|
|
583
|
+
again.
|
|
584
|
+
|
|
585
|
+
`cleanprev.ps1` (in `%OneDrive%\xfer\bin`) strips `node_modules`, lock files and
|
|
586
|
+
tsc output from every `.stale` directory the same way it does for `prev/`.
|
|
587
|
+
|
|
565
588
|
### `usePaths` — Standalone packages
|
|
566
589
|
|
|
567
590
|
Default: `true`. Set to `false` (or pass `-no-use-paths` / `-nup`) to mark a
|
|
@@ -686,12 +709,15 @@ npmglobalize --dry-run # See what would happen
|
|
|
686
709
|
11. **Pushes** to git (with push-protection detection and auto-bypass)
|
|
687
710
|
12. **Installs** globally (if `--install`)
|
|
688
711
|
13. **Restores** file: references (if `--files`, default)
|
|
689
|
-
14. **Runs audit**
|
|
712
|
+
14. **Runs audit** — `Audit:` row in the Release Summary; critical/high findings explained in the Issues Summary
|
|
690
713
|
|
|
691
714
|
## Operational Details
|
|
692
715
|
|
|
693
716
|
### Build Cascade
|
|
694
717
|
|
|
718
|
+
Before anything is built, every project in the `file:` tree with missing deps gets an `npm install`. If one fails, the run **stops** there — the compiler/linker/npm lines that name the failure are printed (the full output goes to a log under `%TEMP%
|
|
719
|
+
pmglobalize-logs`), the failure is repeated in the Issues Summary, and nothing is built, published, or installed on top of a broken tree. `-force` continues anyway. A stale-lockfile / junction failure offers a clean reinstall (delete `node_modules/` + `package-lock.json`, retry once); a compiler or linker error does not, since a reinstall would reach the same line.
|
|
720
|
+
|
|
695
721
|
Before transforming or publishing anything, `npmglobalize` builds `file:` dependencies in topological order — deps before consumers — and then builds the target itself. This guarantees the target's `tsc` reads up-to-date `.d.ts` and `.js` from sibling checkouts even when a dep's source has changed since its last build.
|
|
696
722
|
|
|
697
723
|
For each project visited (the target and every transitive `file:` dep):
|
package/cli.js
CHANGED
|
@@ -60,7 +60,9 @@ Dependency Options:
|
|
|
60
60
|
to date (freshness check compares .ts/tsconfig
|
|
61
61
|
mtimes against emitted .js/.d.ts)
|
|
62
62
|
-force-publish Republish dependencies even if version exists
|
|
63
|
-
-fix
|
|
63
|
+
-fix, -no-fix Run npm audit fix before the pre-release commit (default on,
|
|
64
|
+
never --force). The audit REPORT always runs; critical/high
|
|
65
|
+
findings are listed in the Issues Summary with a fix hint.
|
|
64
66
|
-no-use-paths, -nup Declare this package standalone — file: deps shouldn't
|
|
65
67
|
be resolved from sibling checkouts (persisted as
|
|
66
68
|
usePaths:false in .globalize.json5; FYI for now)
|
|
@@ -596,7 +598,17 @@ export async function main() {
|
|
|
596
598
|
// Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
|
|
597
599
|
// whose source has changed since their last build.
|
|
598
600
|
if (!cliOptions.cleanup) {
|
|
599
|
-
|
|
601
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: a failed npm install used
|
|
602
|
+
// to be reported and then walked past — the run went on to "Build up to date" and a
|
|
603
|
+
// green "Installed locally" for a package with no node_modules. Stop here instead.
|
|
604
|
+
const modulesOk = await ensureFileDepModules(cwd, !!cliOptions.verbose);
|
|
605
|
+
if (!modulesOk && !cliOptions.force) {
|
|
606
|
+
console.error(colors.error('npm install failed — stopping before build/publish (-force continues anyway).'));
|
|
607
|
+
printBuildSummary();
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
if (!modulesOk)
|
|
611
|
+
console.log(colors.warn('Continuing with -force despite npm install failure...'));
|
|
600
612
|
const depsOk = await buildFileDepsTopologically(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force, forceBuild: !!cliOptions.forceBuild });
|
|
601
613
|
if (!depsOk && !cliOptions.force) {
|
|
602
614
|
printBuildSummary();
|
package/lib.d.ts
CHANGED
|
@@ -101,7 +101,8 @@ export interface GlobalizeOptions {
|
|
|
101
101
|
publicDeps?: boolean;
|
|
102
102
|
/** Force republish dependencies even if version exists on npm */
|
|
103
103
|
forcePublish?: boolean;
|
|
104
|
-
/** Run npm audit
|
|
104
|
+
/** Run `npm audit fix` before the pre-release commit (default true; never `--force`). The audit
|
|
105
|
+
* REPORT always runs regardless — this only controls the fix attempt. */
|
|
105
106
|
fix?: boolean;
|
|
106
107
|
/** Automatically fix version/tag mismatches */
|
|
107
108
|
fixTags?: boolean;
|
|
@@ -297,6 +298,15 @@ export type UnpublishedDep = {
|
|
|
297
298
|
* NOT `fs.realpathSync.native`: that would also resolve a junction the user is
|
|
298
299
|
* standing inside, which is a behaviour change nobody asked for. */
|
|
299
300
|
export declare function canonicalCase(target: string): string;
|
|
301
|
+
/** A directory retired in place carries a `.stale` marker file (Bob's convention,
|
|
302
|
+
* 2026-09-06; `cleanprev.ps1` strips such directories like a `prev/`). The file
|
|
303
|
+
* is empty or holds a line saying why and when. Returns that line (or a fixed
|
|
304
|
+
* phrase when empty), or null when the directory is not marked.
|
|
305
|
+
*
|
|
306
|
+
* 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. A marker file rather
|
|
307
|
+
* than a flag in .globalize.json5: stale is a property of a directory, most stale
|
|
308
|
+
* directories never had a config, and a file is findable without parsing. */
|
|
309
|
+
export declare function staleMarker(dir: string): string;
|
|
300
310
|
/** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
|
|
301
311
|
* the spec in package.json and the junction npm created in node_modules.
|
|
302
312
|
*
|
|
@@ -369,7 +379,10 @@ export declare function parseVersionTag(tag: string): number[] | null;
|
|
|
369
379
|
export declare function compareVersions(a: number[], b: number[]): number;
|
|
370
380
|
/** Fix version/tag mismatches */
|
|
371
381
|
export declare function fixVersionTagMismatch(cwd: string, pkg: any, verbose?: boolean): boolean;
|
|
372
|
-
|
|
382
|
+
/** Lines from a failed install that name the actual failure: linker / compiler / msbuild
|
|
383
|
+
* errors from a native addon build, and node-gyp's own verdict. npm's "npm error " prefix is
|
|
384
|
+
* stripped so a line is recognised whether npm relayed it or node-gyp printed it directly. */
|
|
385
|
+
export declare function distillBuildErrors(combined: string): string[];
|
|
373
386
|
/** Return declared deps (dependencies + devDependencies) that don't resolve from
|
|
374
387
|
* `pkgDir`. Skips `workspace:`/`link:` specs (handled by workspace tooling /
|
|
375
388
|
* rarely used). `file:` deps are checked: npm installs them as junctions
|
|
@@ -383,7 +396,7 @@ export declare function missingDeps(pkgDir: string, pkg: any): string[];
|
|
|
383
396
|
* "fresh clone, no `node_modules/`" and "dep added but `npm install` not
|
|
384
397
|
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
385
398
|
* Cycle-safe via the shared `visited` set. */
|
|
386
|
-
export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): Promise<
|
|
399
|
+
export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): Promise<boolean>;
|
|
387
400
|
/** Cheap freshness check so the build cascade can skip packages whose output is
|
|
388
401
|
* already current. Returns true only when provably up to date:
|
|
389
402
|
* - outDir projects: newest source (.ts/.tsx/.mts/.cts + tsconfig*.json) must
|
|
@@ -490,7 +503,7 @@ export declare function fixTs7Deprecations(cwd: string): {
|
|
|
490
503
|
export declare function ensureWorkspaceDepModules(rootDir: string, members: Array<{
|
|
491
504
|
dir: string;
|
|
492
505
|
pkg: any;
|
|
493
|
-
}>, verbose?: boolean): Promise<
|
|
506
|
+
}>, verbose?: boolean): Promise<boolean>;
|
|
494
507
|
/** Extract the lines a human needs from a failed npm command's output: the
|
|
495
508
|
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
496
509
|
* deduped. The full output belongs in a log file (saveNpmLog), not the terminal. */
|
|
@@ -613,13 +626,48 @@ export declare function promptChoice(message: string, choices: string[]): Promis
|
|
|
613
626
|
export declare function adoptExistingRemote(cwd: string, repoUrl: string, defaultBranch: string, dryRun: boolean): Promise<boolean>;
|
|
614
627
|
/** Initialize git repository */
|
|
615
628
|
export declare function initGit(cwd: string, visibility: 'private' | 'public', dryRun: boolean, allowTs?: boolean): Promise<boolean>;
|
|
616
|
-
/**
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
629
|
+
/** Vulnerability severity as reported by `npm audit --json` */
|
|
630
|
+
export type AuditSeverity = 'critical' | 'high' | 'moderate' | 'low' | 'info';
|
|
631
|
+
/** One vulnerable package from `npm audit --json` */
|
|
632
|
+
export interface AuditFinding {
|
|
633
|
+
/** Package name */
|
|
634
|
+
name: string;
|
|
635
|
+
severity: AuditSeverity;
|
|
636
|
+
/** Vulnerable version range, e.g. "<7.5.4" */
|
|
637
|
+
range: string;
|
|
638
|
+
/** Declared directly in this package.json (vs. pulled in transitively) */
|
|
639
|
+
isDirect: boolean;
|
|
640
|
+
/** Advisory titles + URLs. Empty when the package is vulnerable only because of a dep it uses. */
|
|
641
|
+
advisories: {
|
|
642
|
+
title: string;
|
|
643
|
+
url: string;
|
|
644
|
+
}[];
|
|
645
|
+
/** Vulnerable deps this package is affected THROUGH (npm's string `via` entries) */
|
|
646
|
+
via: string[];
|
|
647
|
+
/** Packages that are affected BECAUSE of this one (npm's `effects`) */
|
|
648
|
+
effects: string[];
|
|
649
|
+
/** 'plain' = `npm audit fix` clears it; 'major' = needs `--force` (breaking); 'none' = no fix published */
|
|
650
|
+
fix: 'plain' | 'major' | 'none';
|
|
651
|
+
/** The top-level package@version the fix would install (npm names it when the fix is an object) */
|
|
652
|
+
fixTarget: string;
|
|
653
|
+
}
|
|
654
|
+
/** Outcome of an `npm audit --json` run */
|
|
655
|
+
export interface AuditResult {
|
|
656
|
+
/** false when npm audit itself could not run (no lockfile, registry down...) — see error */
|
|
657
|
+
ran: boolean;
|
|
658
|
+
error: string;
|
|
659
|
+
counts: Record<AuditSeverity, number> & {
|
|
660
|
+
total: number;
|
|
661
|
+
};
|
|
662
|
+
findings: AuditFinding[];
|
|
663
|
+
}
|
|
664
|
+
/** Run `npm audit fix` (never `--force`: that installs semver-major bumps and can break the
|
|
665
|
+
* build; the summary tells the user when that is what it would take). Silent on purpose —
|
|
666
|
+
* the report that follows shows what is left. */
|
|
667
|
+
export declare function runNpmAuditFix(cwd: string): Promise<void>;
|
|
668
|
+
/** Run `npm audit --json` (read-only) and parse it into an AuditResult. Never throws; a run
|
|
669
|
+
* that could not happen comes back with ran=false and the reason in error. */
|
|
670
|
+
export declare function runNpmAudit(cwd: string): AuditResult;
|
|
623
671
|
/** Get the version of npmglobalize itself */
|
|
624
672
|
export declare function getToolVersion(): string;
|
|
625
673
|
export declare function globalize(cwd: string, options?: GlobalizeOptions, configOptions?: Partial<GlobalizeOptions>): Promise<boolean>;
|
package/lib.js
CHANGED
|
@@ -29,7 +29,6 @@ function spawnSafe(cmd, args, options = {}) {
|
|
|
29
29
|
return spawnSync(cmd, args, opts);
|
|
30
30
|
}
|
|
31
31
|
import readline from 'readline';
|
|
32
|
-
import { styleText } from 'util';
|
|
33
32
|
import libversion from 'libnpmversion';
|
|
34
33
|
import JSON5 from 'json5';
|
|
35
34
|
import { fileURLToPath } from 'url';
|
|
@@ -332,7 +331,7 @@ const BOOL_COMMENTS = {
|
|
|
332
331
|
force: { on: 'Continue despite git errors', off: 'Stop on git errors' },
|
|
333
332
|
quiet: { on: 'Suppress npm warnings', off: 'Show npm warnings' },
|
|
334
333
|
verbose: { on: 'Show detailed output', off: 'Normal output detail' },
|
|
335
|
-
fix: { on: 'Auto-run npm audit fix', off: 'Do NOT run npm audit fix' },
|
|
334
|
+
fix: { on: 'Auto-run npm audit fix (default)', off: 'Do NOT run npm audit fix (audit report still runs)' },
|
|
336
335
|
local: { on: 'Local install only (skip transform/publish)', off: 'Normal transform/publish flow' },
|
|
337
336
|
noPublish: { on: 'Transform but don\'t publish', off: 'Publish normally' },
|
|
338
337
|
freeze: { on: 'Freeze node_modules (replace symlinks with real copies)', off: 'Leave node_modules symlinks as-is' },
|
|
@@ -444,7 +443,7 @@ export function writeConfig(dir, config, explicitKeys) {
|
|
|
444
443
|
['"verbose": false', 'true = show detailed output'],
|
|
445
444
|
['"gitVisibility": "private"', 'Git repo: private or public'],
|
|
446
445
|
['"npmVisibility": "private"', 'npm package: private or public'],
|
|
447
|
-
['"fix":
|
|
446
|
+
['"fix": true', 'true = auto-run npm audit fix (never --force); false = report only'],
|
|
448
447
|
['"local": false', 'true = local install only (skip transform/publish); clear with -global'],
|
|
449
448
|
['"noPublish": false', 'true = transform but don\'t publish'],
|
|
450
449
|
['"freeze": false', 'true = freeze node_modules (replace symlinks with real copies)'],
|
|
@@ -1370,6 +1369,25 @@ function caseDiffers(a, b) {
|
|
|
1370
1369
|
const strip = (p) => p.replace(/[\\/]+$/, '');
|
|
1371
1370
|
return strip(a) !== strip(b);
|
|
1372
1371
|
}
|
|
1372
|
+
/** A directory retired in place carries a `.stale` marker file (Bob's convention,
|
|
1373
|
+
* 2026-09-06; `cleanprev.ps1` strips such directories like a `prev/`). The file
|
|
1374
|
+
* is empty or holds a line saying why and when. Returns that line (or a fixed
|
|
1375
|
+
* phrase when empty), or null when the directory is not marked.
|
|
1376
|
+
*
|
|
1377
|
+
* 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. A marker file rather
|
|
1378
|
+
* than a flag in .globalize.json5: stale is a property of a directory, most stale
|
|
1379
|
+
* directories never had a config, and a file is findable without parsing. */
|
|
1380
|
+
export function staleMarker(dir) {
|
|
1381
|
+
const marker = path.join(dir, '.stale');
|
|
1382
|
+
if (!fs.existsSync(marker))
|
|
1383
|
+
return null;
|
|
1384
|
+
let note = '';
|
|
1385
|
+
try {
|
|
1386
|
+
note = fs.readFileSync(marker, 'utf8').split(/\r?\n/).map(l => l.trim()).find(l => l) || '';
|
|
1387
|
+
}
|
|
1388
|
+
catch { /* unreadable note: still stale */ }
|
|
1389
|
+
return note || 'no reason recorded in .stale';
|
|
1390
|
+
}
|
|
1373
1391
|
/** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
|
|
1374
1392
|
* the spec in package.json and the junction npm created in node_modules.
|
|
1375
1393
|
*
|
|
@@ -2050,73 +2068,67 @@ export function fixVersionTagMismatch(cwd, pkg, verbose = false) {
|
|
|
2050
2068
|
function sleepSync(ms) {
|
|
2051
2069
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2052
2070
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
const
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
}
|
|
2106
|
-
/** Run `npm install` in `dir`; on failure, offer to delete `node_modules/`
|
|
2107
|
-
* and `package-lock.json` and retry once — the standard fix for arborist's
|
|
2108
|
-
* null-`package` crash on stale lockfile / junction state — before falling
|
|
2109
|
-
* back to the pnpm suggestion banner. Returns true if install succeeded.
|
|
2110
|
-
* `rmSync` unlinks junctions without descending, so file: sibling targets
|
|
2111
|
-
* are never touched. */
|
|
2071
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: the pnpm-eval banner that used
|
|
2072
|
+
// to live here ("npm install FAILED -- consider trying pnpm", plus a 60-second pause) is
|
|
2073
|
+
// gone. pnpm was evaluated and judged not feasible, so the suggestion was noise that
|
|
2074
|
+
// delayed the run and hid the real error. A failed install is now reported by
|
|
2075
|
+
// reportInstallFailure (the actual compiler/linker/npm lines, plus a log file) and stops
|
|
2076
|
+
// the run — see ensureFileDepModules and its callers.
|
|
2077
|
+
/** Lines from a failed install that name the actual failure: linker / compiler / msbuild
|
|
2078
|
+
* errors from a native addon build, and node-gyp's own verdict. npm's "npm error " prefix is
|
|
2079
|
+
* stripped so a line is recognised whether npm relayed it or node-gyp printed it directly. */
|
|
2080
|
+
export function distillBuildErrors(combined) {
|
|
2081
|
+
const out = [];
|
|
2082
|
+
const seen = new Set();
|
|
2083
|
+
for (const raw of combined.split('\n')) {
|
|
2084
|
+
const line = raw.replace(/^npm (?:error|ERR!)\s?/, '').trimEnd();
|
|
2085
|
+
if (!/fatal error|\berror (?:LNK|C|MSB|TS)\d+|gyp ERR! (?:build|configure|rebuild) error/.test(line))
|
|
2086
|
+
continue;
|
|
2087
|
+
if (seen.has(line))
|
|
2088
|
+
continue;
|
|
2089
|
+
seen.add(line);
|
|
2090
|
+
out.push(line);
|
|
2091
|
+
}
|
|
2092
|
+
return out.slice(0, 12);
|
|
2093
|
+
}
|
|
2094
|
+
/** Print why an `npm install` failed and keep the whole output in a log file. Native-build
|
|
2095
|
+
* errors (LNK1117 and friends) come first — they are the cause; npm's own "command failed"
|
|
2096
|
+
* lines only say that something did. Was: the first 5 lines of stderr, which for a node-gyp
|
|
2097
|
+
* failure are "npm error code 1 / path / command failed / command ..." and never the error. */
|
|
2098
|
+
function reportInstallFailure(dir, r, attempt) {
|
|
2099
|
+
const combined = [r.output, r.stderr].filter(Boolean).join('\n');
|
|
2100
|
+
const logFile = saveNpmLog(`install-${path.basename(dir)}`, combined);
|
|
2101
|
+
const buildErrors = distillBuildErrors(combined);
|
|
2102
|
+
const npmErrors = distillNpmErrors(combined).filter(l => !buildErrors.includes(l));
|
|
2103
|
+
console.error(colors.red(` ✗ npm install failed in ${dir}${attempt}`));
|
|
2104
|
+
for (const l of buildErrors)
|
|
2105
|
+
console.error(colors.red(` ${l}`));
|
|
2106
|
+
for (const l of npmErrors.slice(0, buildErrors.length > 0 ? 4 : 8))
|
|
2107
|
+
console.error(colors.dim(` ${l}`));
|
|
2108
|
+
if (logFile)
|
|
2109
|
+
console.error(colors.dim(` full output: ${logFile}`));
|
|
2110
|
+
const headline = buildErrors.find(l => /fatal error|\berror /.test(l))
|
|
2111
|
+
|| buildErrors[0]
|
|
2112
|
+
|| npmErrors.find(l => !/^(code \d+|path |command )/.test(l))
|
|
2113
|
+
|| 'see log';
|
|
2114
|
+
return { isBuildError: buildErrors.length > 0, headline, logFile };
|
|
2115
|
+
}
|
|
2116
|
+
/** Run `npm install` in `dir`. On failure, offer to delete `node_modules/` and
|
|
2117
|
+
* `package-lock.json` and retry once — the standard fix for arborist's null-`package`
|
|
2118
|
+
* crash on stale lockfile / junction state. Not offered when the output holds a
|
|
2119
|
+
* compiler / linker error: that is a broken native build, not a broken tree, and the
|
|
2120
|
+
* reinstall would spend a minute reaching the same line. Returns true if install
|
|
2121
|
+
* succeeded; a failure is recorded for the Issues Summary. `rmSync` unlinks junctions
|
|
2122
|
+
* without descending, so file: sibling targets are never touched. */
|
|
2112
2123
|
async function npmInstallWithCleanRetry(dir, verbose, context) {
|
|
2113
2124
|
let r = await runCommandAsync('npm', ['install'], { cwd: dir, silent: !verbose });
|
|
2114
2125
|
if (r.success)
|
|
2115
2126
|
return true;
|
|
2116
|
-
|
|
2117
|
-
if (
|
|
2118
|
-
console.error(colors.dim(
|
|
2119
|
-
|
|
2127
|
+
let failure = reportInstallFailure(dir, r, '');
|
|
2128
|
+
if (failure.isBuildError) {
|
|
2129
|
+
console.error(colors.dim(' (compiler/linker error — a clean reinstall would hit the same line, so not offering one)'));
|
|
2130
|
+
}
|
|
2131
|
+
else if (await confirm(` Delete node_modules/ and package-lock.json in ${dir} and retry npm install?`, true)) {
|
|
2120
2132
|
try {
|
|
2121
2133
|
fs.rmSync(path.join(dir, 'node_modules'), { recursive: true, force: true });
|
|
2122
2134
|
}
|
|
@@ -2131,11 +2143,9 @@ async function npmInstallWithCleanRetry(dir, verbose, context) {
|
|
|
2131
2143
|
console.log(colors.green(` ✓ clean reinstall succeeded in ${dir}`));
|
|
2132
2144
|
return true;
|
|
2133
2145
|
}
|
|
2134
|
-
|
|
2135
|
-
if (r.stderr)
|
|
2136
|
-
console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
|
|
2146
|
+
failure = reportInstallFailure(dir, r, ' after clean reinstall');
|
|
2137
2147
|
}
|
|
2138
|
-
|
|
2148
|
+
recordBuildIssue(path.basename(dir), 'error', `npm install failed (${context}): ${failure.headline}${failure.logFile ? ` — log: ${failure.logFile}` : ''}`);
|
|
2139
2149
|
return false;
|
|
2140
2150
|
}
|
|
2141
2151
|
/** Wait for a package version to appear on the npm registry.
|
|
@@ -2289,17 +2299,22 @@ function formatMissingReason(missing) {
|
|
|
2289
2299
|
* "fresh clone, no `node_modules/`" and "dep added but `npm install` not
|
|
2290
2300
|
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
2291
2301
|
* Cycle-safe via the shared `visited` set. */
|
|
2302
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction ("if the install failed why
|
|
2303
|
+
// continue"): returns false when any npm install in the tree failed. Every dep is still
|
|
2304
|
+
// attempted so one run reports all failures; the CALLER stops before building/publishing
|
|
2305
|
+
// on false (unless -force).
|
|
2292
2306
|
export async function ensureFileDepModules(cwd, verbose = false, visited = new Set()) {
|
|
2293
2307
|
const abs = path.resolve(cwd);
|
|
2294
2308
|
if (visited.has(abs))
|
|
2295
|
-
return;
|
|
2309
|
+
return true;
|
|
2296
2310
|
visited.add(abs);
|
|
2311
|
+
let ok = true;
|
|
2297
2312
|
let pkg;
|
|
2298
2313
|
try {
|
|
2299
2314
|
pkg = readPackageJson(cwd);
|
|
2300
2315
|
}
|
|
2301
2316
|
catch {
|
|
2302
|
-
return;
|
|
2317
|
+
return true; // no package.json here: nothing to install, nothing failed
|
|
2303
2318
|
}
|
|
2304
2319
|
const cwdMissing = missingDeps(abs, pkg);
|
|
2305
2320
|
if (cwdMissing.length > 0) {
|
|
@@ -2316,7 +2331,8 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
|
|
|
2316
2331
|
catch { /* best-effort */ }
|
|
2317
2332
|
}
|
|
2318
2333
|
console.log(colors.yellow(`↻ installing node_modules in ${pkg?.name || abs} (${formatMissingReason(cwdMissing)})`));
|
|
2319
|
-
await npmInstallWithCleanRetry(abs, verbose, `ensureFileDepModules (cwd): ${abs}`)
|
|
2334
|
+
if (!await npmInstallWithCleanRetry(abs, verbose, `ensureFileDepModules (cwd): ${abs}`))
|
|
2335
|
+
ok = false;
|
|
2320
2336
|
}
|
|
2321
2337
|
for (const key of ['dependencies', 'devDependencies']) {
|
|
2322
2338
|
const deps = pkg?.[key];
|
|
@@ -2328,6 +2344,15 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
|
|
|
2328
2344
|
const target = path.resolve(cwd, spec.slice('file:'.length));
|
|
2329
2345
|
if (!fs.existsSync(path.join(target, 'package.json')))
|
|
2330
2346
|
continue;
|
|
2347
|
+
// 2026-09-06 — Claude Code (Fable 5.1): never install into, or walk
|
|
2348
|
+
// through, a dep that is retired (`.stale`). Reported as an error so the
|
|
2349
|
+
// stale reference in package.json gets fixed rather than papered over.
|
|
2350
|
+
const targetStale = staleMarker(target);
|
|
2351
|
+
if (targetStale) {
|
|
2352
|
+
console.error(colors.red(`✗ file: dependency ${name} -> ${target} is marked .stale (${targetStale}); not installing into a retired directory.`));
|
|
2353
|
+
recordBuildIssue(pkg?.name || abs, 'error', `file: dependency ${name} (${target}) is marked .stale (${targetStale}).`);
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2331
2356
|
let targetPkg;
|
|
2332
2357
|
try {
|
|
2333
2358
|
targetPkg = readPackageJson(target);
|
|
@@ -2348,11 +2373,14 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
|
|
|
2348
2373
|
catch { /* best-effort */ }
|
|
2349
2374
|
}
|
|
2350
2375
|
console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target}) (${formatMissingReason(targetMissing)})`));
|
|
2351
|
-
await npmInstallWithCleanRetry(target, verbose, `ensureFileDepModules (file: dep ${name}): ${target}`)
|
|
2376
|
+
if (!await npmInstallWithCleanRetry(target, verbose, `ensureFileDepModules (file: dep ${name}): ${target}`))
|
|
2377
|
+
ok = false;
|
|
2352
2378
|
}
|
|
2353
|
-
await ensureFileDepModules(target, verbose, visited)
|
|
2379
|
+
if (!await ensureFileDepModules(target, verbose, visited))
|
|
2380
|
+
ok = false;
|
|
2354
2381
|
}
|
|
2355
2382
|
}
|
|
2383
|
+
return ok;
|
|
2356
2384
|
}
|
|
2357
2385
|
/** Cached major version of the global `tsc`. null = not yet probed,
|
|
2358
2386
|
* 0 = probe failed / tsc not found. TypeScript 6 stopped auto-including
|
|
@@ -3505,7 +3533,7 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
|
|
|
3505
3533
|
const deps = pkg?.[key];
|
|
3506
3534
|
if (!deps || typeof deps !== 'object')
|
|
3507
3535
|
continue;
|
|
3508
|
-
for (const [, spec] of Object.entries(deps)) {
|
|
3536
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
3509
3537
|
if (typeof spec !== 'string' || !spec.startsWith('file:'))
|
|
3510
3538
|
continue;
|
|
3511
3539
|
const target = path.resolve(cwd, spec.slice('file:'.length));
|
|
@@ -3514,6 +3542,18 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
|
|
|
3514
3542
|
const targetAbs = path.resolve(target);
|
|
3515
3543
|
if (visited.has(targetAbs))
|
|
3516
3544
|
continue;
|
|
3545
|
+
// 2026-09-06 — Claude Code (Fable 5.1): a retired (`.stale`) dep is not
|
|
3546
|
+
// built. Counts as a failed dep so the consumer's build stops here too,
|
|
3547
|
+
// unless -force says to carry on.
|
|
3548
|
+
const targetStale = staleMarker(target);
|
|
3549
|
+
if (targetStale) {
|
|
3550
|
+
console.error(colors.red(`✗ file: dependency ${name} -> ${target} is marked .stale (${targetStale}); not building a retired directory.`));
|
|
3551
|
+
recordBuildIssue(pkg?.name || abs, 'error', `file: dependency ${name} (${target}) is marked .stale (${targetStale}).`);
|
|
3552
|
+
allOk = false;
|
|
3553
|
+
if (!opts.force)
|
|
3554
|
+
return false;
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3517
3557
|
// Recurse first (deps before consumer)
|
|
3518
3558
|
const childOk = await buildFileDepsTopologically(target, opts, visited);
|
|
3519
3559
|
if (!childOk)
|
|
@@ -3730,7 +3770,7 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
|
|
|
3730
3770
|
rootPkg = readPackageJson(root);
|
|
3731
3771
|
}
|
|
3732
3772
|
catch {
|
|
3733
|
-
return;
|
|
3773
|
+
return true;
|
|
3734
3774
|
}
|
|
3735
3775
|
const allMissing = new Set();
|
|
3736
3776
|
for (const name of missingDeps(root, rootPkg))
|
|
@@ -3740,10 +3780,10 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
|
|
|
3740
3780
|
allMissing.add(name);
|
|
3741
3781
|
}
|
|
3742
3782
|
if (allMissing.size === 0)
|
|
3743
|
-
return;
|
|
3783
|
+
return true;
|
|
3744
3784
|
const list = [...allMissing];
|
|
3745
3785
|
console.log(colors.yellow(`↻ installing workspace node_modules in ${rootPkg?.name || path.basename(root)} (${formatMissingReason(list)})`));
|
|
3746
|
-
|
|
3786
|
+
return npmInstallWithCleanRetry(root, verbose, `ensureWorkspaceDepModules: ${root}`);
|
|
3747
3787
|
}
|
|
3748
3788
|
/** Extract the lines a human needs from a failed npm command's output: the
|
|
3749
3789
|
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
@@ -5979,54 +6019,140 @@ export async function initGit(cwd, visibility, dryRun, allowTs) {
|
|
|
5979
6019
|
}
|
|
5980
6020
|
return true;
|
|
5981
6021
|
}
|
|
5982
|
-
/**
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
6022
|
+
/** Run `npm audit fix` (never `--force`: that installs semver-major bumps and can break the
|
|
6023
|
+
* build; the summary tells the user when that is what it would take). Silent on purpose —
|
|
6024
|
+
* the report that follows shows what is left. */
|
|
6025
|
+
export async function runNpmAuditFix(cwd) {
|
|
6026
|
+
await runCommandAsync('npm', ['audit', 'fix'], { cwd, silent: true });
|
|
6027
|
+
}
|
|
6028
|
+
/** Classify npm's `fixAvailable` — true | false | { name, version, isSemVerMajor } */
|
|
6029
|
+
function classifyFix(fixAvailable) {
|
|
6030
|
+
if (fixAvailable === true)
|
|
6031
|
+
return { fix: 'plain', fixTarget: '' };
|
|
6032
|
+
if (fixAvailable && typeof fixAvailable === 'object') {
|
|
6033
|
+
const f = fixAvailable;
|
|
6034
|
+
const fixTarget = f.name ? `${f.name}@${f.version ?? '?'}` : '';
|
|
6035
|
+
return { fix: f.isSemVerMajor ? 'major' : 'plain', fixTarget };
|
|
6036
|
+
}
|
|
6037
|
+
return { fix: 'none', fixTarget: '' };
|
|
6038
|
+
}
|
|
6039
|
+
/** Run `npm audit --json` (read-only) and parse it into an AuditResult. Never throws; a run
|
|
6040
|
+
* that could not happen comes back with ran=false and the reason in error. */
|
|
6041
|
+
export function runNpmAudit(cwd) {
|
|
6042
|
+
const counts = { critical: 0, high: 0, moderate: 0, low: 0, info: 0, total: 0 };
|
|
6043
|
+
const result = { ran: false, error: '', counts, findings: [] };
|
|
6044
|
+
const proc = spawnSafe('npm', ['audit', '--json'], { cwd, encoding: 'utf-8', stdio: 'pipe', shell: true });
|
|
6045
|
+
// The JSON shape is npm's, not ours — narrowed field by field below, hence the any.
|
|
6046
|
+
let data;
|
|
5994
6047
|
try {
|
|
5995
|
-
|
|
5996
|
-
const m = auditData.metadata?.vulnerabilities || {};
|
|
5997
|
-
const critical = m.critical || 0;
|
|
5998
|
-
const high = m.high || 0;
|
|
5999
|
-
const moderate = m.moderate || 0;
|
|
6000
|
-
const low = m.low || 0;
|
|
6001
|
-
const total = critical + high + moderate + low + (m.info || 0);
|
|
6002
|
-
if (total > 0) {
|
|
6003
|
-
hasVulnerabilities = true;
|
|
6004
|
-
const parts = [];
|
|
6005
|
-
if (critical > 0)
|
|
6006
|
-
parts.push(colors.red(`${critical} critical`));
|
|
6007
|
-
if (high > 0)
|
|
6008
|
-
parts.push(colors.red(`${high} high`));
|
|
6009
|
-
if (moderate > 0)
|
|
6010
|
-
parts.push(colors.yellow(`${moderate} moderate`));
|
|
6011
|
-
if (low > 0)
|
|
6012
|
-
parts.push(`${low} low`);
|
|
6013
|
-
report = `${total} vulnerabilities`;
|
|
6014
|
-
// Only show if high/critical remain after fix
|
|
6015
|
-
if (critical > 0 || high > 0) {
|
|
6016
|
-
console.log(colors.red(` Audit: ${parts.join(', ')}`));
|
|
6017
|
-
}
|
|
6018
|
-
else if (verbose) {
|
|
6019
|
-
console.log(colors.dim(` Audit: ${parts.join(', ')}`));
|
|
6020
|
-
}
|
|
6021
|
-
}
|
|
6022
|
-
else {
|
|
6023
|
-
report = 'No vulnerabilities';
|
|
6024
|
-
}
|
|
6048
|
+
data = JSON.parse(proc.stdout || '');
|
|
6025
6049
|
}
|
|
6026
|
-
catch {
|
|
6027
|
-
|
|
6050
|
+
catch (error) {
|
|
6051
|
+
// Not a swallow: the failure becomes result.error, which the summary prints as "Audit: not run".
|
|
6052
|
+
const stderr = (proc.stderr || '').trim().split('\n')[0];
|
|
6053
|
+
result.error = stderr || `npm audit produced no JSON (${error.message})`;
|
|
6054
|
+
return result;
|
|
6028
6055
|
}
|
|
6029
|
-
|
|
6056
|
+
if (data?.error) {
|
|
6057
|
+
// npm reports its own failures (ENOLOCK, network...) as JSON on stdout
|
|
6058
|
+
result.error = [data.error.code, data.error.summary].filter(Boolean).join(': ');
|
|
6059
|
+
return result;
|
|
6060
|
+
}
|
|
6061
|
+
result.ran = true;
|
|
6062
|
+
const m = data.metadata?.vulnerabilities || {};
|
|
6063
|
+
for (const sev of ['critical', 'high', 'moderate', 'low', 'info']) {
|
|
6064
|
+
counts[sev] = m[sev] || 0;
|
|
6065
|
+
counts.total += counts[sev];
|
|
6066
|
+
}
|
|
6067
|
+
const vulns = data.vulnerabilities || {};
|
|
6068
|
+
for (const name of Object.keys(vulns)) {
|
|
6069
|
+
const v = vulns[name];
|
|
6070
|
+
const viaList = Array.isArray(v.via) ? v.via : [];
|
|
6071
|
+
const advisories = viaList
|
|
6072
|
+
.filter((x) => !!x && typeof x === 'object')
|
|
6073
|
+
.map(x => ({ title: x.title || '', url: x.url || '' }));
|
|
6074
|
+
const via = viaList.filter((x) => typeof x === 'string');
|
|
6075
|
+
result.findings.push({
|
|
6076
|
+
name,
|
|
6077
|
+
severity: v.severity,
|
|
6078
|
+
range: v.range || '',
|
|
6079
|
+
isDirect: !!v.isDirect,
|
|
6080
|
+
advisories,
|
|
6081
|
+
via,
|
|
6082
|
+
effects: Array.isArray(v.effects) ? v.effects : [],
|
|
6083
|
+
...classifyFix(v.fixAvailable)
|
|
6084
|
+
});
|
|
6085
|
+
}
|
|
6086
|
+
const order = ['critical', 'high', 'moderate', 'low', 'info'];
|
|
6087
|
+
result.findings.sort((a, b) => order.indexOf(a.severity) - order.indexOf(b.severity) || a.name.localeCompare(b.name));
|
|
6088
|
+
return result;
|
|
6089
|
+
}
|
|
6090
|
+
/** One-line explanation of a finding for the Issues Summary: what, why it is here, how to clear it. */
|
|
6091
|
+
function describeAuditFinding(f) {
|
|
6092
|
+
const parts = [`audit ${f.severity.toUpperCase()}: ${f.name} ${f.range}`.trim()];
|
|
6093
|
+
if (f.advisories.length > 0) {
|
|
6094
|
+
// One advisory named in full; the rest counted. tar alone carries 12 and a line per
|
|
6095
|
+
// title would bury the Issues Summary. The URL leads to the complete list.
|
|
6096
|
+
const a = f.advisories[0];
|
|
6097
|
+
const more = f.advisories.length - 1;
|
|
6098
|
+
parts.push(`${a.title}${a.url ? ` (${a.url})` : ''}${more > 0 ? ` +${more} more` : ''}`);
|
|
6099
|
+
}
|
|
6100
|
+
if (f.via.length > 0)
|
|
6101
|
+
parts.push(`through ${f.via.join(', ')}`);
|
|
6102
|
+
if (f.isDirect)
|
|
6103
|
+
parts.push('direct dependency');
|
|
6104
|
+
else if (f.effects.length > 0)
|
|
6105
|
+
parts.push(`pulled in by ${f.effects.join(', ')}`);
|
|
6106
|
+
switch (f.fix) {
|
|
6107
|
+
case 'plain':
|
|
6108
|
+
parts.push('fix: npm audit fix');
|
|
6109
|
+
break;
|
|
6110
|
+
case 'major':
|
|
6111
|
+
parts.push(`fix: npm audit fix --force → ${f.fixTarget || 'a semver-major bump'} (breaking — not run automatically)`);
|
|
6112
|
+
break;
|
|
6113
|
+
case 'none':
|
|
6114
|
+
parts.push('fix: none published yet');
|
|
6115
|
+
break;
|
|
6116
|
+
}
|
|
6117
|
+
return parts.join(' — ');
|
|
6118
|
+
}
|
|
6119
|
+
/** Run the audit report and put critical/high findings into the Issues Summary. Call once,
|
|
6120
|
+
* right before the end-of-run summary. */
|
|
6121
|
+
function auditForSummary(cwd, moduleName) {
|
|
6122
|
+
const audit = runNpmAudit(cwd);
|
|
6123
|
+
for (const f of audit.findings) {
|
|
6124
|
+
if (f.severity === 'critical' || f.severity === 'high') {
|
|
6125
|
+
recordBuildIssue(moduleName, 'warning', describeAuditFinding(f));
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
6128
|
+
return audit;
|
|
6129
|
+
}
|
|
6130
|
+
/** The "Audit:" row for the Release / Transform Summary. */
|
|
6131
|
+
function auditSummaryRow(audit, dryRun) {
|
|
6132
|
+
if (dryRun)
|
|
6133
|
+
return ` Audit: ${colors.dim('skipped (dry-run)')}`;
|
|
6134
|
+
if (!audit)
|
|
6135
|
+
return ` Audit: ${colors.dim('not run')}`;
|
|
6136
|
+
if (!audit.ran)
|
|
6137
|
+
return ` Audit: ${colors.yellow('not run')} ${colors.dim(audit.error)}`;
|
|
6138
|
+
if (audit.counts.total === 0)
|
|
6139
|
+
return ` Audit: ${colors.green('✓')} no vulnerabilities`;
|
|
6140
|
+
const c = audit.counts;
|
|
6141
|
+
const parts = [];
|
|
6142
|
+
if (c.critical > 0)
|
|
6143
|
+
parts.push(colors.red(`${c.critical} critical`));
|
|
6144
|
+
if (c.high > 0)
|
|
6145
|
+
parts.push(colors.red(`${c.high} high`));
|
|
6146
|
+
if (c.moderate > 0)
|
|
6147
|
+
parts.push(colors.yellow(`${c.moderate} moderate`));
|
|
6148
|
+
if (c.low > 0)
|
|
6149
|
+
parts.push(`${c.low} low`);
|
|
6150
|
+
if (c.info > 0)
|
|
6151
|
+
parts.push(`${c.info} info`);
|
|
6152
|
+
const serious = c.critical > 0 || c.high > 0;
|
|
6153
|
+
const mark = serious ? colors.red('✗') : colors.yellow('!');
|
|
6154
|
+
const hint = serious ? colors.dim(' (details in Issues Summary)') : '';
|
|
6155
|
+
return ` Audit: ${mark} ${parts.join(', ')}${hint}`;
|
|
6030
6156
|
}
|
|
6031
6157
|
/** Get the version of npmglobalize itself */
|
|
6032
6158
|
export function getToolVersion() {
|
|
@@ -6141,6 +6267,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6141
6267
|
publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
|
|
6142
6268
|
publicDeps = false, // -public-deps: cascade public visibility to all deps
|
|
6143
6269
|
noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs, adopt = false, importCheck = false } = options;
|
|
6270
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: a directory marked
|
|
6271
|
+
// `.stale` is retired. Refuse to build, publish or install it — and because the
|
|
6272
|
+
// dep cascade reaches deps through this same function, refuse to cascade into
|
|
6273
|
+
// one too. The refusal is an error, not a skip: a live package depending on a
|
|
6274
|
+
// stale one is a fact the user has to act on.
|
|
6275
|
+
const staleNote = staleMarker(cwd);
|
|
6276
|
+
if (staleNote) {
|
|
6277
|
+
console.error(colors.red(`✗ ${cwd} is marked .stale (${staleNote}). Not publishing a retired directory.`));
|
|
6278
|
+
recordBuildIssue(path.basename(cwd), 'error', `Directory is marked .stale (${staleNote}); remove the marker to publish it again.`);
|
|
6279
|
+
return false;
|
|
6280
|
+
}
|
|
6144
6281
|
// Show tool version only for recursive dep calls (CLI already prints it at startup)
|
|
6145
6282
|
const toolVersion = getToolVersion();
|
|
6146
6283
|
if (!options._fromWorkspace && !options._fromCli) {
|
|
@@ -6636,8 +6773,15 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6636
6773
|
// before doing anything else. Independent of build script presence — a dep
|
|
6637
6774
|
// with new declared deps still needs its own `npm install` even if it has
|
|
6638
6775
|
// no build step. CLI entrypoint already ran this, so skip when _fromCli.
|
|
6639
|
-
if (!options._fromCli && !dryRun)
|
|
6640
|
-
await ensureFileDepModules(cwd, verbose);
|
|
6776
|
+
if (!options._fromCli && !dryRun) {
|
|
6777
|
+
const modulesOk = await ensureFileDepModules(cwd, verbose);
|
|
6778
|
+
if (!modulesOk && !force) {
|
|
6779
|
+
console.error(colors.red(`npm install failed in ${pkg.name || cwd} — stopping before build/publish (-force continues anyway).`));
|
|
6780
|
+
return false;
|
|
6781
|
+
}
|
|
6782
|
+
if (!modulesOk)
|
|
6783
|
+
console.log(colors.yellow('Continuing with -force despite npm install failure...'));
|
|
6784
|
+
}
|
|
6641
6785
|
// Run build step if package.json has a build script (skip if CLI already built)
|
|
6642
6786
|
if (pkg.scripts?.build && !options._fromCli) {
|
|
6643
6787
|
console.log(`${timestamp()} Running build...`);
|
|
@@ -7275,19 +7419,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7275
7419
|
}
|
|
7276
7420
|
}
|
|
7277
7421
|
}
|
|
7278
|
-
//
|
|
7279
|
-
|
|
7422
|
+
// 2026-09-06 — Claude Code (Fable 5.1): fix step only here (before the pre-release commit,
|
|
7423
|
+
// so a changed lockfile is committed). The report moved next to the summaries so its
|
|
7424
|
+
// result is shown where the user is looking, not scrolled off under the git output.
|
|
7425
|
+
if (fix) {
|
|
7280
7426
|
if (!dryRun) {
|
|
7281
|
-
await
|
|
7427
|
+
await runNpmAuditFix(cwd);
|
|
7282
7428
|
}
|
|
7283
7429
|
else {
|
|
7284
|
-
console.log(' [dry-run] Would run npm audit');
|
|
7430
|
+
console.log(' [dry-run] Would run npm audit fix');
|
|
7285
7431
|
}
|
|
7286
7432
|
}
|
|
7287
|
-
else if (fix && !dryRun) {
|
|
7288
|
-
// Run fix even if no deps changed
|
|
7289
|
-
await runNpmAudit(cwd, fix, verbose);
|
|
7290
|
-
}
|
|
7291
7433
|
if (noPublish) {
|
|
7292
7434
|
console.log('Transform complete (--nopublish mode).');
|
|
7293
7435
|
if (freeze && !dryRun) {
|
|
@@ -7339,11 +7481,11 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7339
7481
|
console.log(` Package: ${colors.green(finalPkg2.name)}`);
|
|
7340
7482
|
console.log(` Version: ${colors.green('v' + finalPkg2.version)}`);
|
|
7341
7483
|
console.log(` Published: ${colors.red('✗')}`);
|
|
7484
|
+
console.log(auditSummaryRow(dryRun ? null : auditForSummary(cwd, finalPkg2.name), dryRun));
|
|
7342
7485
|
for (const s of surprises)
|
|
7343
7486
|
console.log(` ${colors.yellow('!')} ${s}`);
|
|
7344
7487
|
console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
7345
7488
|
console.log('');
|
|
7346
|
-
printPnpmSuggestionSummary();
|
|
7347
7489
|
return true;
|
|
7348
7490
|
}
|
|
7349
7491
|
// Skip if private
|
|
@@ -8421,11 +8563,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
8421
8563
|
// publish so the version recorded is the one that actually shipped.
|
|
8422
8564
|
await recordUpstream(cwd, dryRun, verbose);
|
|
8423
8565
|
console.log(`${timestamp()} Done!`);
|
|
8424
|
-
// Run final audit report if not already run
|
|
8425
|
-
const auditAlreadyRun = (fix || updateDeps) && (transformResult.transformed || alreadyTransformed || updateDeps);
|
|
8426
|
-
if (!auditAlreadyRun && (fix || updateDeps || transformResult.transformed) && !dryRun) {
|
|
8427
|
-
await runNpmAudit(cwd, false, verbose); // Just report, don't fix again
|
|
8428
|
-
}
|
|
8429
8566
|
// Print summary
|
|
8430
8567
|
console.log('');
|
|
8431
8568
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
@@ -8438,6 +8575,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
8438
8575
|
console.log(` Version: ${colors.green('v' + finalPkg.version)}`);
|
|
8439
8576
|
console.log(` Published: ${colors.green('✓')} (${accessLabel})`);
|
|
8440
8577
|
console.log(` Git pushed: ${colors.green('✓')}`);
|
|
8578
|
+
console.log(auditSummaryRow(dryRun ? null : auditForSummary(cwd, finalPkg.name), dryRun));
|
|
8441
8579
|
if (explicitKeys?.has('gitVisibility')) {
|
|
8442
8580
|
console.log(` Git visibility: ${colors.green(gitVisibility.toUpperCase())}`);
|
|
8443
8581
|
}
|
|
@@ -8486,7 +8624,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
8486
8624
|
}
|
|
8487
8625
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
8488
8626
|
console.log('');
|
|
8489
|
-
printPnpmSuggestionSummary();
|
|
8490
8627
|
// Only show "To use run" message if package provides commands (has bin field)
|
|
8491
8628
|
if (finalPkg.bin) {
|
|
8492
8629
|
const commandName = finalPkg.name.includes('/') ? finalPkg.name.split('/')[1] : finalPkg.name;
|
|
@@ -8699,7 +8836,13 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
8699
8836
|
// the case where a dep was added to a member package.json but `npm install`
|
|
8700
8837
|
// wasn't re-run — the builds would otherwise fail resolving the new dep.
|
|
8701
8838
|
if (!options.dryRun) {
|
|
8702
|
-
await ensureWorkspaceDepModules(rootDir, packages.map(p => ({ dir: p.dir, pkg: p.pkg })), !!options.verbose);
|
|
8839
|
+
const modulesOk = await ensureWorkspaceDepModules(rootDir, packages.map(p => ({ dir: p.dir, pkg: p.pkg })), !!options.verbose);
|
|
8840
|
+
if (!modulesOk && !options.force) {
|
|
8841
|
+
console.error(colors.red('npm install failed — stopping before build/publish (-force continues anyway).'));
|
|
8842
|
+
return { success: false, packages: [], publishOrder: [] };
|
|
8843
|
+
}
|
|
8844
|
+
if (!modulesOk)
|
|
8845
|
+
console.log(colors.yellow('Continuing with -force despite npm install failure...'));
|
|
8703
8846
|
}
|
|
8704
8847
|
// Prescan: decide which packages actually need processing so we don't waste
|
|
8705
8848
|
// time rebuilding+republishing ones with no relevant changes.
|
|
@@ -8840,7 +8983,6 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
|
|
|
8840
8983
|
}
|
|
8841
8984
|
console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
8842
8985
|
console.log('');
|
|
8843
|
-
printPnpmSuggestionSummary();
|
|
8844
8986
|
// Global install of the workspace root (monorepo CLI)
|
|
8845
8987
|
const { install = false, link = false, wsl = false, dryRun = false, verbose = false } = options;
|
|
8846
8988
|
const rootPkgFinal = readPackageJson(rootDir);
|