@bobfrankston/npmglobalize 1.0.219 → 1.0.221
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 +52 -10
- package/cli.js +6 -1
- package/lib.d.ts +27 -10
- package/lib.js +312 -34
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -172,15 +172,57 @@ exits 0 — e.g. `@bobfrankston/msger`'s postinstall copies its native binary
|
|
|
172
172
|
into the per-user bin dir its launcher reads from, and skipping it leaves a
|
|
173
173
|
stale exe behind a green checkmark.
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
175
|
+
A **project** install is the opposite case: npm *rejects* `--allow-scripts`
|
|
176
|
+
there and reads the policy from the `allowScripts` field of the root
|
|
177
|
+
`package.json` (maintained by `npm approve-scripts` / `npm deny-scripts`),
|
|
178
|
+
falling back to `allow-scripts` in `.npmrc` only when the field is absent.
|
|
179
|
+
Until that field covers everything installed, every `npm install` ends with a
|
|
180
|
+
`npm warn allow-scripts … not yet covered by allowScripts` block.
|
|
181
|
+
|
|
182
|
+
npmglobalize keeps that field current, once per run, right after the
|
|
183
|
+
node_modules check:
|
|
184
|
+
|
|
185
|
+
- **Own-scope packages and `file:` deps are approved without asking** — they
|
|
186
|
+
are your source.
|
|
187
|
+
- **Each third-party package is asked about**, showing the script npm would
|
|
188
|
+
run and whether it is a prebuilt-binary fetcher:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
npm gates install scripts for 2 third-party package(s) in @bobfrankston/whts. Approvals are recorded in package.json "allowScripts" (name-only) and honored on the global install too.
|
|
192
|
+
Allow install scripts for sharp@0.33.5 (install: node install/check)? [y]es / [n]o, deny / [a]ll remaining / [s]kip for now: y
|
|
193
|
+
Allow install scripts for koffi@2.14.1|2.16.1 (install: node src/cnoke/cnoke.js --prebuild) — prebuilt-binary fetcher, the package ships binaries? [y]es / [n]o, deny / [a]ll remaining / [s]kip for now: y
|
|
194
|
+
✓ allowScripts: approved @bobfrankston/msger, com-wrapper, sharp, koffi (2 own/local approved automatically)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`y` approves, `n` records a deny (`"pkg": false`, npm's own mechanism — the
|
|
198
|
+
question is not repeated; delete the entry to reconsider), `a` approves
|
|
199
|
+
everything remaining, `s` leaves it pending for next time. Approvals are
|
|
200
|
+
**name-only** (`--no-allow-scripts-pin`): a version pin would re-open the
|
|
201
|
+
question on every bump. Without a terminal (piped stdin) nothing is asked;
|
|
202
|
+
the pending packages are listed with the command to approve them.
|
|
203
|
+
- The field is written by npm itself, so a linked dep gets the key npm wants
|
|
204
|
+
for it — the `file:` spec, e.g. `"file:../../../../projects/com/com-wrapper": true`
|
|
205
|
+
(relative to the link's parent directory, not the project).
|
|
206
|
+
|
|
207
|
+
The **global** install then allowlists your own packages — it just built and
|
|
208
|
+
published them from your source, so they're trusted — plus whatever the
|
|
209
|
+
project's `allowScripts` approved (a `file:` key is mapped to that dep's
|
|
210
|
+
published name) and your user `.npmrc` `allow-scripts` list, minus anything
|
|
211
|
+
the project denied. Third-party packages you never approved stay gated:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
> npm install -g @bobfrankston/winpos@2.0.51 --allow-scripts @bobfrankston/winpos,@bobfrankston/msger,@bobfrankston/msgcommon,sharp
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
npm takes the allowlist from the *first* source that has one rather than
|
|
218
|
+
merging — CLI flag, then `package.json`, then `.npmrc` — which is why the
|
|
219
|
+
`.npmrc` list is folded into the flag here, and why npm prints
|
|
220
|
+
`.npmrc allow-scripts setting is being ignored because package.json declares
|
|
221
|
+
its own allowScripts field` on project installs once the field exists. That
|
|
222
|
+
warning is npm's layering at work, not a problem.
|
|
223
|
+
|
|
224
|
+
Anything npm skips on the global install is reported rather than buried in
|
|
225
|
+
the captured output:
|
|
184
226
|
|
|
185
227
|
```
|
|
186
228
|
· Skipped koffi@2.16.3 install script — prebuilt-binary fetcher, the package ships binaries; normally harmless.
|
|
@@ -756,7 +798,7 @@ As with import maps, these have historically been built only by a second `.vscod
|
|
|
756
798
|
Before building, `npmglobalize` looks for sub-projects, in this order:
|
|
757
799
|
|
|
758
800
|
1. `.vscode/tasks.json` has a task that runs `tsc` or `importgen` with `"options": { "cwd": "${workspaceFolder}/client" }`, or runs `tsc` with `-p`/`--project` naming a sub-directory (authoritative — it's how the project is actually built today; the task's `label` is quoted back in the prompt).
|
|
759
|
-
2. An immediate sub-directory
|
|
801
|
+
2. An immediate sub-directory that is a build of its own: a `package.json` with a `build` script or an import map to regenerate (a sub-package gets its build script set up the same way the root does — importgen, `tsc`, its own sub-projects — before the root delegates to it), or a `tsconfig.json` the root `tsconfig.json` does not already compile. A `package.json` that only holds dependencies for files the root `tsc` compiles (`whts/pdfToImg`) is not a sub-project. Build output and vendored trees (`node_modules`, `prev`, `dist`, `built`, `out`, `wwwroot`, `coverage`, `temp`, `preflight`, dot-directories) are never scanned.
|
|
760
802
|
|
|
761
803
|
Nothing is detected when the root `tsconfig.json` uses project `references` — that build graph belongs to `tsc -b` and isn't second-guessed.
|
|
762
804
|
|
package/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* npmglobalize CLI - Transform file: dependencies to npm versions for publishing
|
|
4
4
|
*/
|
|
5
|
-
import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations, fixTs7Deprecations, canonicalCase } from './lib.js';
|
|
5
|
+
import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, ensureAllowScripts, buildProject, buildFileDepsTopologically, reportTs7Deprecations, fixTs7Deprecations, canonicalCase } from './lib.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { colors } from './colors.js';
|
|
@@ -609,6 +609,11 @@ export async function main() {
|
|
|
609
609
|
}
|
|
610
610
|
if (!modulesOk)
|
|
611
611
|
console.log(colors.warn('Continuing with -force despite npm install failure...'));
|
|
612
|
+
// 2026-09-07 — Claude Code (Fable 5.1), at Bob's direction: keep package.json
|
|
613
|
+
// "allowScripts" current — own/file: deps approved, third-party ones asked about —
|
|
614
|
+
// so npm's install-script gate stops warning and the global install honors it.
|
|
615
|
+
if (modulesOk)
|
|
616
|
+
await ensureAllowScripts(cwd, { dryRun: !!cliOptions.dryRun, verbose: !!cliOptions.verbose });
|
|
612
617
|
const depsOk = await buildFileDepsTopologically(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force, forceBuild: !!cliOptions.forceBuild });
|
|
613
618
|
if (!depsOk && !cliOptions.force) {
|
|
614
619
|
printBuildSummary();
|
package/lib.d.ts
CHANGED
|
@@ -397,16 +397,6 @@ export declare function missingDeps(pkgDir: string, pkg: any): string[];
|
|
|
397
397
|
* re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
|
|
398
398
|
* Cycle-safe via the shared `visited` set. */
|
|
399
399
|
export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): Promise<boolean>;
|
|
400
|
-
/** Cheap freshness check so the build cascade can skip packages whose output is
|
|
401
|
-
* already current. Returns true only when provably up to date:
|
|
402
|
-
* - outDir projects: newest source (.ts/.tsx/.mts/.cts + tsconfig*.json) must
|
|
403
|
-
* not be newer than the newest file under outDir.
|
|
404
|
-
* - side-by-side projects: every source must have an emitted sibling
|
|
405
|
-
* (.js/.jsx/.mjs/.cjs or declaration) at least as new.
|
|
406
|
-
* Conservative: project references, allowJs, missing outputs, or unreadable
|
|
407
|
-
* tsconfig all report stale (→ build). package.json mtime is deliberately
|
|
408
|
-
* ignored — npmglobalize itself rewrites it around every publish, which would
|
|
409
|
-
* otherwise force a rebuild on every run. */
|
|
410
400
|
export declare function isBuildUpToDate(cwd: string): boolean;
|
|
411
401
|
/** Detect whether a package is an importgen project — a browser app whose HTML
|
|
412
402
|
* carries a generated `<script type="importmap">`. Signals, in order:
|
|
@@ -569,6 +559,33 @@ export declare function fixPuppeteerCorruptedCache(combined: string): {
|
|
|
569
559
|
* Returns [] for an unscoped package: there's no way to tell an unscoped
|
|
570
560
|
* package of ours from a third-party one by name alone. */
|
|
571
561
|
export declare function ownScopePackages(dir: string): string[];
|
|
562
|
+
/** The project's recorded install-script policy, as registry names. Keys in
|
|
563
|
+
* `allowScripts` are `name`, `name@version`, or a `file:` / absolute path;
|
|
564
|
+
* path keys are mapped to the target's manifest name so an approval given
|
|
565
|
+
* to a linked dep carries over to the global install, where that same dep
|
|
566
|
+
* arrives from the registry under its published name. */
|
|
567
|
+
export declare function projectAllowScripts(dir: string): {
|
|
568
|
+
allowed: string[];
|
|
569
|
+
denied: string[];
|
|
570
|
+
};
|
|
571
|
+
/** Bring the project's package.json `allowScripts` up to date with what is
|
|
572
|
+
* installed, so `npm install` stops warning and — once npm starts enforcing
|
|
573
|
+
* the policy — keeps running the scripts this project depends on.
|
|
574
|
+
*
|
|
575
|
+
* Own-scope packages and `file:` deps are approved without asking: they are
|
|
576
|
+
* this user's source. Anything else is asked about, one package at a time,
|
|
577
|
+
* showing the script npm would run. "no" records a deny (npm's own
|
|
578
|
+
* mechanism, `npm deny-scripts`) so the question isn't repeated; "skip"
|
|
579
|
+
* leaves it pending for next time. Approvals are name-only
|
|
580
|
+
* (`--no-allow-scripts-pin`): a version pin would re-open the question on
|
|
581
|
+
* every bump, which is exactly the churn this exists to remove.
|
|
582
|
+
*
|
|
583
|
+
* Runs once per project root, not per cascade dep: npm reads the policy
|
|
584
|
+
* from the root package.json only. ~1.5s (arborist loads the actual tree). */
|
|
585
|
+
export declare function ensureAllowScripts(dir: string, opts?: {
|
|
586
|
+
dryRun?: boolean;
|
|
587
|
+
verbose?: boolean;
|
|
588
|
+
}): Promise<void>;
|
|
572
589
|
/** Run a command and return success status */
|
|
573
590
|
export declare function runCommand(cmd: string, args: string[], options?: {
|
|
574
591
|
silent?: boolean;
|
package/lib.js
CHANGED
|
@@ -2888,6 +2888,35 @@ function newestMtimeUnder(dir) {
|
|
|
2888
2888
|
* tsconfig all report stale (→ build). package.json mtime is deliberately
|
|
2889
2889
|
* ignored — npmglobalize itself rewrites it around every publish, which would
|
|
2890
2890
|
* otherwise force a rebuild on every run. */
|
|
2891
|
+
/** The directories a parsed tsconfig's `exclude` keeps out of the compile, as
|
|
2892
|
+
* lower-cased relative paths. Matches on the literal path prefix (glob tails
|
|
2893
|
+
* like "tests/**" reduce to "tests"). */
|
|
2894
|
+
function tsconfigExcludedDirs(tsconfig) {
|
|
2895
|
+
return (Array.isArray(tsconfig?.exclude) ? tsconfig.exclude : [])
|
|
2896
|
+
.filter((x) => typeof x === 'string')
|
|
2897
|
+
.map((x) => x.replace(/^\.\//, '').replace(/[\\/]?\*.*$/, '').replace(/[\\/]+$/, '').toLowerCase())
|
|
2898
|
+
.filter((x) => x.length > 0 && !x.includes('*'));
|
|
2899
|
+
}
|
|
2900
|
+
/** True when the root tsconfig in `cwd` already compiles the sources under
|
|
2901
|
+
* `rel`: it exists, doesn't narrow the compile with `files`/`include`, and
|
|
2902
|
+
* doesn't `exclude` the directory. Conservative — a root that uses `include`
|
|
2903
|
+
* is treated as not covering the sub-directory. */
|
|
2904
|
+
function rootTsconfigCompiles(cwd, rel) {
|
|
2905
|
+
let tsconfig;
|
|
2906
|
+
try {
|
|
2907
|
+
tsconfig = JSON5.parse(fs.readFileSync(path.join(cwd, 'tsconfig.json'), 'utf-8'));
|
|
2908
|
+
}
|
|
2909
|
+
catch {
|
|
2910
|
+
return false;
|
|
2911
|
+
}
|
|
2912
|
+
if (Array.isArray(tsconfig.files) || Array.isArray(tsconfig.include))
|
|
2913
|
+
return false;
|
|
2914
|
+
const target = normalizeProjectPath(rel).toLowerCase();
|
|
2915
|
+
const parts = target.split('/');
|
|
2916
|
+
const excludes = tsconfigExcludedDirs(tsconfig);
|
|
2917
|
+
// Excluding a parent excludes everything under it.
|
|
2918
|
+
return !parts.some((_, i) => excludes.includes(parts.slice(0, i + 1).join('/')));
|
|
2919
|
+
}
|
|
2891
2920
|
export function isBuildUpToDate(cwd) {
|
|
2892
2921
|
let tsconfig;
|
|
2893
2922
|
try {
|
|
@@ -2901,12 +2930,8 @@ export function isBuildUpToDate(cwd) {
|
|
|
2901
2930
|
return false;
|
|
2902
2931
|
const outDir = typeof co.outDir === 'string' ? path.resolve(cwd, co.outDir) : null;
|
|
2903
2932
|
// tsconfig "exclude" dirs aren't compiled, so their .ts files never get
|
|
2904
|
-
// outputs — don't count them as sources.
|
|
2905
|
-
|
|
2906
|
-
const excludes = (Array.isArray(tsconfig.exclude) ? tsconfig.exclude : [])
|
|
2907
|
-
.filter((x) => typeof x === 'string')
|
|
2908
|
-
.map((x) => x.replace(/^\.\//, '').replace(/[\\/]?\*.*$/, '').replace(/[\\/]+$/, '').toLowerCase())
|
|
2909
|
-
.filter((x) => x.length > 0 && !x.includes('*'));
|
|
2933
|
+
// outputs — don't count them as sources.
|
|
2934
|
+
const excludes = tsconfigExcludedDirs(tsconfig);
|
|
2910
2935
|
const sources = [];
|
|
2911
2936
|
const collect = (dir, rel) => {
|
|
2912
2937
|
let entries;
|
|
@@ -3198,13 +3223,31 @@ function resolveOnDiskCase(base, rel) {
|
|
|
3198
3223
|
}
|
|
3199
3224
|
return out.length ? out.join('/') : null;
|
|
3200
3225
|
}
|
|
3201
|
-
/** Classify `rel` under `cwd`: a package (its own package.json
|
|
3202
|
-
* script
|
|
3203
|
-
* sub-project, or null when it
|
|
3226
|
+
/** Classify `rel` under `cwd`: a package (its own package.json with a build
|
|
3227
|
+
* script, or an importgen project — `ensureBuildScript` sets its script up),
|
|
3228
|
+
* a plain tsconfig sub-project the root tsc doesn't compile, or null when it
|
|
3229
|
+
* is neither.
|
|
3230
|
+
* 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. whts/pdfToImg has
|
|
3231
|
+
* a package.json that only holds puppeteer/sharp for a file the root tsc
|
|
3232
|
+
* already compiles; treating every package.json as a sub-package gave it a
|
|
3233
|
+
* `build: tsc` it never needed and a second compile under its stale tsconfig.
|
|
3234
|
+
* A package.json is a build of its own only when it has something the root
|
|
3235
|
+
* can't do — its own build script, or an import map to regenerate. */
|
|
3204
3236
|
function subProjectKind(cwd, rel) {
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3237
|
+
const dir = path.join(cwd, rel);
|
|
3238
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
3239
|
+
let pkg = {};
|
|
3240
|
+
try {
|
|
3241
|
+
pkg = readPackageJson(dir);
|
|
3242
|
+
}
|
|
3243
|
+
catch { /* unreadable — treat as having no build script */ }
|
|
3244
|
+
const hasBuild = typeof pkg?.scripts?.build === 'string' && pkg.scripts.build.trim();
|
|
3245
|
+
if (hasBuild || detectImportgen(dir))
|
|
3246
|
+
return 'package';
|
|
3247
|
+
}
|
|
3248
|
+
if (!fs.existsSync(path.join(dir, 'tsconfig.json')))
|
|
3249
|
+
return null;
|
|
3250
|
+
return rootTsconfigCompiles(cwd, rel) ? null : 'tsconfig';
|
|
3208
3251
|
}
|
|
3209
3252
|
/** The command the root build script runs for a sub-project. */
|
|
3210
3253
|
function subProjectBuildCommand(s) {
|
|
@@ -3505,7 +3548,7 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
3505
3548
|
// tsconfig.json, which makes the freshness check correctly read as stale.
|
|
3506
3549
|
const tsconfigSnapshot = new Map();
|
|
3507
3550
|
const issueMark = markBuildIssues();
|
|
3508
|
-
|
|
3551
|
+
let migratedUpFront = fs.existsSync(path.join(cwd, 'tsconfig.json')) && migrateTsconfigDeprecations(cwd, tsconfigSnapshot);
|
|
3509
3552
|
const setup = await ensureBuildScript(cwd);
|
|
3510
3553
|
if (!setup)
|
|
3511
3554
|
return true;
|
|
@@ -3514,6 +3557,16 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
3514
3557
|
// freshness — one the user declined to wire in isn't this build's business.
|
|
3515
3558
|
const finalBuild = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '';
|
|
3516
3559
|
const wiredSubs = detectSubProjects(cwd).filter(s => scriptBuildsSubProject(finalBuild, s.dir));
|
|
3560
|
+
// 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction.
|
|
3561
|
+
// The sub-projects this build runs get the same TS7 migration as the root.
|
|
3562
|
+
// whts: `tsc -p pdfToImg` failed on pdfToImg/tsconfig.json's
|
|
3563
|
+
// `moduleResolution: node10` (TS5107) and the retry below re-migrated only
|
|
3564
|
+
// the root chain, which had nothing left to fix — so no retry ever happened.
|
|
3565
|
+
const migrateSubProjects = () => wiredSubs
|
|
3566
|
+
.map(s => migrateTsconfigDeprecations(path.join(cwd, s.dir), tsconfigSnapshot))
|
|
3567
|
+
.some(Boolean);
|
|
3568
|
+
if (migrateSubProjects())
|
|
3569
|
+
migratedUpFront = true;
|
|
3517
3570
|
// A migration just changed how modules resolve — never report that as fresh;
|
|
3518
3571
|
// the rebuild is what validates it (and what the revert path above needs).
|
|
3519
3572
|
if (!opts.forceBuild && !migratedUpFront && isBuildUpToDate(cwd) && areSubProjectsUpToDate(cwd, wiredSubs)
|
|
@@ -3529,7 +3582,7 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
3529
3582
|
// We don't silence with ignoreDeprecations — the retry may now surface real
|
|
3530
3583
|
// resolution errors, which are genuine bugs to fix, not deprecations.
|
|
3531
3584
|
const out = (buildResult.stderr || '') + (buildResult.output || '');
|
|
3532
|
-
if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd, tsconfigSnapshot)) {
|
|
3585
|
+
if (/error TS510[17]\b/.test(out) && [migrateTsconfigDeprecations(cwd, tsconfigSnapshot), migrateSubProjects()].some(Boolean)) {
|
|
3533
3586
|
buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
3534
3587
|
}
|
|
3535
3588
|
}
|
|
@@ -4161,20 +4214,238 @@ export function ownScopePackages(dir) {
|
|
|
4161
4214
|
walk(pkg, dir);
|
|
4162
4215
|
return [...found];
|
|
4163
4216
|
}
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4217
|
+
// 2026-09-07 — Claude Code (Fable 5.1), at Bob's direction ("I want you to handle
|
|
4218
|
+
// this", after `npm un itemgen` in whts warned about com-wrapper, msger and sharp).
|
|
4219
|
+
// The project side of the policy. npm REJECTS `--allow-scripts` on a project-scoped
|
|
4220
|
+
// install (EALLOWSCRIPTS), so the only place a project's approvals can live is the
|
|
4221
|
+
// `allowScripts` field of its package.json, maintained by `npm approve-scripts`.
|
|
4222
|
+
// npmglobalize now keeps that field current (ensureAllowScripts) and feeds it back
|
|
4223
|
+
// into the global install's `--allow-scripts` flag (allowScriptsArgs), so a package
|
|
4224
|
+
// is trusted once, in one place, for both kinds of install.
|
|
4225
|
+
/** Names of the project's `file:` dependencies as npm displays them in the
|
|
4226
|
+
* allowScripts pending list. A linked dep has no registry identity, so npm
|
|
4227
|
+
* falls back to the link target's directory name (`com-wrapper` for
|
|
4228
|
+
* `@bobfrankston/com-wrapper` at ../../projects/com/com-wrapper); the
|
|
4229
|
+
* manifest name is included too in case that fallback ever changes. */
|
|
4230
|
+
function localDepDisplayNames(dir) {
|
|
4231
|
+
let pkg;
|
|
4232
|
+
try {
|
|
4233
|
+
pkg = readPackageJson(dir);
|
|
4234
|
+
}
|
|
4235
|
+
catch {
|
|
4236
|
+
return [];
|
|
4237
|
+
}
|
|
4238
|
+
const names = [];
|
|
4239
|
+
for (const key of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
4240
|
+
const deps = pkg[key];
|
|
4241
|
+
if (!deps || typeof deps !== 'object')
|
|
4242
|
+
continue;
|
|
4243
|
+
for (const spec of Object.values(deps)) {
|
|
4244
|
+
if (typeof spec !== 'string' || !spec.startsWith('file:'))
|
|
4245
|
+
continue;
|
|
4246
|
+
const target = path.resolve(dir, spec.slice('file:'.length));
|
|
4247
|
+
names.push(path.basename(target));
|
|
4248
|
+
try {
|
|
4249
|
+
const name = readPackageJson(target).name;
|
|
4250
|
+
if (name)
|
|
4251
|
+
names.push(name);
|
|
4252
|
+
}
|
|
4253
|
+
catch { /* target missing or unreadable — the basename alone still identifies it */ }
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
return names;
|
|
4257
|
+
}
|
|
4258
|
+
/** The project's recorded install-script policy, as registry names. Keys in
|
|
4259
|
+
* `allowScripts` are `name`, `name@version`, or a `file:` / absolute path;
|
|
4260
|
+
* path keys are mapped to the target's manifest name so an approval given
|
|
4261
|
+
* to a linked dep carries over to the global install, where that same dep
|
|
4262
|
+
* arrives from the registry under its published name. */
|
|
4263
|
+
export function projectAllowScripts(dir) {
|
|
4264
|
+
const allowed = [];
|
|
4265
|
+
const denied = [];
|
|
4266
|
+
let pkg;
|
|
4267
|
+
try {
|
|
4268
|
+
pkg = readPackageJson(dir);
|
|
4269
|
+
}
|
|
4270
|
+
catch {
|
|
4271
|
+
return { allowed, denied };
|
|
4272
|
+
}
|
|
4273
|
+
const policy = pkg.allowScripts;
|
|
4274
|
+
if (!policy || typeof policy !== 'object')
|
|
4275
|
+
return { allowed, denied };
|
|
4276
|
+
// A `file:` key written by `npm approve-scripts` is the link's saved spec,
|
|
4277
|
+
// which npm records relative to the LINK's parent directory — verified on
|
|
4278
|
+
// whts: package.json says `file:../../projects/com/com-wrapper`, the policy
|
|
4279
|
+
// key came out `file:../../../../projects/com/com-wrapper`, i.e. relative to
|
|
4280
|
+
// node_modules/@bobfrankston/. A hand-written key is relative to the
|
|
4281
|
+
// project. Try both bases.
|
|
4282
|
+
const bases = new Set([dir]);
|
|
4283
|
+
for (const key of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
4284
|
+
for (const [name, spec] of Object.entries(pkg[key] || {})) {
|
|
4285
|
+
if (typeof spec === 'string' && spec.startsWith('file:'))
|
|
4286
|
+
bases.add(path.dirname(path.join(dir, 'node_modules', name)));
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
const manifestName = (rel) => {
|
|
4290
|
+
for (const base of bases) {
|
|
4291
|
+
try {
|
|
4292
|
+
return readPackageJson(path.resolve(base, rel)).name;
|
|
4293
|
+
}
|
|
4294
|
+
catch { /* not this base — try the next; the caller skips the key when none has it */ }
|
|
4295
|
+
}
|
|
4296
|
+
return null;
|
|
4297
|
+
};
|
|
4298
|
+
for (const [key, value] of Object.entries(policy)) {
|
|
4299
|
+
let name;
|
|
4300
|
+
if (key.startsWith('file:') || path.isAbsolute(key)) {
|
|
4301
|
+
name = manifestName(key.startsWith('file:') ? key.slice('file:'.length) : key);
|
|
4302
|
+
}
|
|
4303
|
+
else {
|
|
4304
|
+
// strip a trailing @version; a scoped name keeps its leading @
|
|
4305
|
+
name = key.replace(/(.)@[^@]*$/, '$1');
|
|
4306
|
+
}
|
|
4307
|
+
if (!name)
|
|
4308
|
+
continue;
|
|
4309
|
+
(value === false ? denied : allowed).push(name);
|
|
4310
|
+
}
|
|
4311
|
+
return { allowed, denied };
|
|
4312
|
+
}
|
|
4313
|
+
/** Names from the user's own `allow-scripts` npm config (.npmrc layers).
|
|
4314
|
+
* npm's resolver takes the FIRST layer with any config and ignores the
|
|
4315
|
+
* rest, so a CLI flag would silently suppress these — merging them into the
|
|
4316
|
+
* flag keeps `npm config set allow-scripts=… --location=user` meaningful. */
|
|
4317
|
+
function npmrcAllowScripts() {
|
|
4318
|
+
const r = runCommand('npm', ['config', 'get', 'allow-scripts'], { silent: true });
|
|
4319
|
+
if (!r.success || isUnknownConfig(r.output))
|
|
4320
|
+
return [];
|
|
4321
|
+
return r.output.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
4322
|
+
}
|
|
4323
|
+
/** npm args allowing install scripts on a GLOBAL install: our own packages,
|
|
4324
|
+
* whatever the project's package.json `allowScripts` approves, and the
|
|
4325
|
+
* user's .npmrc list — minus anything the project denies. Empty when npm
|
|
4326
|
+
* predates the policy (it runs the scripts anyway) or nothing is allowed. */
|
|
4327
|
+
function allowScriptsArgs(dir) {
|
|
4174
4328
|
if (!npmSupportsAllowScripts())
|
|
4175
4329
|
return [];
|
|
4176
|
-
const
|
|
4177
|
-
|
|
4330
|
+
const project = projectAllowScripts(dir);
|
|
4331
|
+
const names = [...new Set([...ownScopePackages(dir), ...project.allowed, ...npmrcAllowScripts()])]
|
|
4332
|
+
.filter(n => !project.denied.includes(n));
|
|
4333
|
+
return names.length ? ['--allow-scripts', names.join(',')] : [];
|
|
4334
|
+
}
|
|
4335
|
+
/** Parse the text listing of `npm approve-scripts --allow-scripts-pending`:
|
|
4336
|
+
* one indented `name@version (event: cmd; event: cmd)` line per node,
|
|
4337
|
+
* the same package repeated when several versions are installed. */
|
|
4338
|
+
function parsePendingScripts(output) {
|
|
4339
|
+
const byName = new Map();
|
|
4340
|
+
for (const line of output.split('\n')) {
|
|
4341
|
+
const m = line.match(/^ (\S+?)(?:@([^@\s]+))? \((.*)\)\s*$/);
|
|
4342
|
+
if (!m)
|
|
4343
|
+
continue;
|
|
4344
|
+
const [, name, version, scripts] = m;
|
|
4345
|
+
const entry = byName.get(name) ?? { name, versions: [], scripts };
|
|
4346
|
+
if (version)
|
|
4347
|
+
entry.versions.push(version);
|
|
4348
|
+
byName.set(name, entry);
|
|
4349
|
+
}
|
|
4350
|
+
return [...byName.values()];
|
|
4351
|
+
}
|
|
4352
|
+
/** Bring the project's package.json `allowScripts` up to date with what is
|
|
4353
|
+
* installed, so `npm install` stops warning and — once npm starts enforcing
|
|
4354
|
+
* the policy — keeps running the scripts this project depends on.
|
|
4355
|
+
*
|
|
4356
|
+
* Own-scope packages and `file:` deps are approved without asking: they are
|
|
4357
|
+
* this user's source. Anything else is asked about, one package at a time,
|
|
4358
|
+
* showing the script npm would run. "no" records a deny (npm's own
|
|
4359
|
+
* mechanism, `npm deny-scripts`) so the question isn't repeated; "skip"
|
|
4360
|
+
* leaves it pending for next time. Approvals are name-only
|
|
4361
|
+
* (`--no-allow-scripts-pin`): a version pin would re-open the question on
|
|
4362
|
+
* every bump, which is exactly the churn this exists to remove.
|
|
4363
|
+
*
|
|
4364
|
+
* Runs once per project root, not per cascade dep: npm reads the policy
|
|
4365
|
+
* from the root package.json only. ~1.5s (arborist loads the actual tree). */
|
|
4366
|
+
export async function ensureAllowScripts(dir, opts = {}) {
|
|
4367
|
+
if (!npmSupportsAllowScripts())
|
|
4368
|
+
return;
|
|
4369
|
+
let pkgName;
|
|
4370
|
+
try {
|
|
4371
|
+
pkgName = readPackageJson(dir).name;
|
|
4372
|
+
}
|
|
4373
|
+
catch {
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
const listing = await runCommandAsync('npm', ['approve-scripts', '--allow-scripts-pending'], { cwd: dir, silent: true });
|
|
4377
|
+
if (!listing.success) {
|
|
4378
|
+
if (opts.verbose)
|
|
4379
|
+
console.log(colors.dim(` (could not list pending install scripts in ${pkgName}: ${(listing.stderr || listing.output).trim().split('\n')[0]})`));
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
const pending = parsePendingScripts(listing.output);
|
|
4383
|
+
if (!pending.length)
|
|
4384
|
+
return;
|
|
4385
|
+
const trusted = new Set([...ownScopePackages(dir), ...localDepDisplayNames(dir)]);
|
|
4386
|
+
const auto = pending.filter(p => trusted.has(p.name));
|
|
4387
|
+
const ask = pending.filter(p => !trusted.has(p.name));
|
|
4388
|
+
const label = (p) => p.versions.length ? `${p.name}@${p.versions.join('|')}` : p.name;
|
|
4389
|
+
if (opts.dryRun) {
|
|
4390
|
+
console.log(colors.dim(` [dry-run] allowScripts in ${pkgName}: would approve ${auto.map(label).join(', ') || '(none)'}; would ask about ${ask.map(label).join(', ') || '(none)'}`));
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
const approve = auto.map(p => p.name);
|
|
4394
|
+
const deny = [];
|
|
4395
|
+
const skipped = [];
|
|
4396
|
+
if (ask.length) {
|
|
4397
|
+
if (!process.stdin.isTTY) {
|
|
4398
|
+
console.log(colors.yellow(` ⚠ ${ask.length} third-party package(s) in ${pkgName} have install scripts not yet covered by allowScripts (no terminal to ask): ${ask.map(label).join(', ')}`));
|
|
4399
|
+
console.log(colors.dim(` npm approve-scripts --no-allow-scripts-pin <pkg> or npm deny-scripts <pkg>`));
|
|
4400
|
+
skipped.push(...ask.map(label));
|
|
4401
|
+
}
|
|
4402
|
+
else {
|
|
4403
|
+
console.log(colors.cyan(`npm gates install scripts for ${ask.length} third-party package(s) in ${pkgName}. Approvals are recorded in package.json "allowScripts" (name-only) and honored on the global install too.`));
|
|
4404
|
+
let allRemaining = false;
|
|
4405
|
+
for (const p of ask) {
|
|
4406
|
+
const prebuilt = PREBUILT_SCRIPT_RUNNERS.test(p.scripts) ? ' — prebuilt-binary fetcher, the package ships binaries' : '';
|
|
4407
|
+
if (allRemaining) {
|
|
4408
|
+
approve.push(p.name);
|
|
4409
|
+
continue;
|
|
4410
|
+
}
|
|
4411
|
+
const answer = await promptChoice(` Allow install scripts for ${label(p)} (${p.scripts})${prebuilt}? [y]es / [n]o, deny / [a]ll remaining / [s]kip for now:`, ['y', 'n', 'a', 's']);
|
|
4412
|
+
switch (answer) {
|
|
4413
|
+
case 'y':
|
|
4414
|
+
approve.push(p.name);
|
|
4415
|
+
break;
|
|
4416
|
+
case 'a':
|
|
4417
|
+
approve.push(p.name);
|
|
4418
|
+
allRemaining = true;
|
|
4419
|
+
break;
|
|
4420
|
+
case 'n':
|
|
4421
|
+
deny.push(p.name);
|
|
4422
|
+
break;
|
|
4423
|
+
default:
|
|
4424
|
+
skipped.push(label(p));
|
|
4425
|
+
break; // 's', or EOF ('' from promptChoice)
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
// `npm approve-scripts <name>` matches installed nodes by the same display
|
|
4431
|
+
// name the pending listing used, and writes the right key for each kind
|
|
4432
|
+
// (registry name, or the file: path for a linked dep).
|
|
4433
|
+
if (approve.length) {
|
|
4434
|
+
const r = await runCommandAsync('npm', ['approve-scripts', '--no-allow-scripts-pin', ...approve], { cwd: dir, silent: true });
|
|
4435
|
+
if (r.success)
|
|
4436
|
+
console.log(colors.green(` ✓ allowScripts: approved ${approve.join(', ')}${auto.length ? ` (${auto.length} own/local approved automatically)` : ''}`));
|
|
4437
|
+
else
|
|
4438
|
+
console.log(colors.red(` ✗ npm approve-scripts failed: ${(r.stderr || r.output).trim().split('\n').find(l => /npm error/.test(l)) ?? 'see output'}`));
|
|
4439
|
+
}
|
|
4440
|
+
if (deny.length) {
|
|
4441
|
+
const r = await runCommandAsync('npm', ['deny-scripts', ...deny], { cwd: dir, silent: true });
|
|
4442
|
+
if (r.success)
|
|
4443
|
+
console.log(colors.yellow(` ✓ allowScripts: denied ${deny.join(', ')} (remove the entry from package.json allowScripts to reconsider)`));
|
|
4444
|
+
else
|
|
4445
|
+
console.log(colors.red(` ✗ npm deny-scripts failed: ${(r.stderr || r.output).trim().split('\n').find(l => /npm error/.test(l)) ?? 'see output'}`));
|
|
4446
|
+
}
|
|
4447
|
+
if (skipped.length)
|
|
4448
|
+
console.log(colors.dim(` · install scripts still unreviewed (asked again next run): ${skipped.join(', ')}`));
|
|
4178
4449
|
}
|
|
4179
4450
|
/** Install-script runners that fetch or select a prebuilt binary and only
|
|
4180
4451
|
* compile when no prebuild matches the platform. A package using one of these
|
|
@@ -4248,9 +4519,10 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
|
|
|
4248
4519
|
let attempt = 0;
|
|
4249
4520
|
let cacheFixRounds = 0;
|
|
4250
4521
|
// Our own packages are trusted — we just built and published them from
|
|
4251
|
-
// this very directory — so their install scripts run
|
|
4252
|
-
//
|
|
4253
|
-
|
|
4522
|
+
// this very directory — so their install scripts run, along with whatever
|
|
4523
|
+
// the project's package.json allowScripts approved (ensureAllowScripts)
|
|
4524
|
+
// and the user's .npmrc list. Everything else stays behind npm's gate.
|
|
4525
|
+
const allowArgs = allowScriptsArgs(cwd);
|
|
4254
4526
|
while (true) {
|
|
4255
4527
|
// 2026-09-01 10:15 EDT — Claude Code (Opus 5), at Bob's direction.
|
|
4256
4528
|
// `--prefer-online`: this install always targets a version published
|
|
@@ -4570,13 +4842,15 @@ async function verifyWslGlobalBins(cwd) {
|
|
|
4570
4842
|
}
|
|
4571
4843
|
}
|
|
4572
4844
|
export async function installInWsl(wslArgs, opts = {}) {
|
|
4573
|
-
// Same trust rule as the Windows installs:
|
|
4574
|
-
//
|
|
4845
|
+
// Same trust rule as the Windows installs: our own packages plus the
|
|
4846
|
+
// project's package.json approvals, minus its denies. The Windows .npmrc
|
|
4847
|
+
// list is left out — WSL's npm has its own .npmrc. Probed against WSL's
|
|
4575
4848
|
// npm, which is a separate install from the Windows one.
|
|
4576
4849
|
if (opts.cwd && wslArgs.includes('install') && await wslNpmSupportsAllowScripts()) {
|
|
4577
|
-
const
|
|
4578
|
-
|
|
4579
|
-
|
|
4850
|
+
const project = projectAllowScripts(opts.cwd);
|
|
4851
|
+
const names = [...new Set([...ownScopePackages(opts.cwd), ...project.allowed])].filter(n => !project.denied.includes(n));
|
|
4852
|
+
if (names.length)
|
|
4853
|
+
wslArgs = [...wslArgs, '--allow-scripts', names.join(',')];
|
|
4580
4854
|
}
|
|
4581
4855
|
const runOnce = async () => {
|
|
4582
4856
|
console.log(colors.cyan(`> wsl ${wslArgs.join(' ')}`));
|
|
@@ -6869,6 +7143,10 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6869
7143
|
}
|
|
6870
7144
|
if (!modulesOk)
|
|
6871
7145
|
console.log(colors.yellow('Continuing with -force despite npm install failure...'));
|
|
7146
|
+
// 2026-09-07 — Claude Code (Fable 5.1): same allowScripts upkeep the CLI
|
|
7147
|
+
// entrypoint does; the global install below reads the result.
|
|
7148
|
+
if (modulesOk)
|
|
7149
|
+
await ensureAllowScripts(cwd, { verbose });
|
|
6872
7150
|
}
|
|
6873
7151
|
// Run build step if package.json has a build script (skip if CLI already built)
|
|
6874
7152
|
if (pkg.scripts?.build && !options._fromCli) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/npmglobalize",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.221",
|
|
4
4
|
"description": "Transform file: dependencies to npm versions for publishing",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@bobfrankston/freezepak": "^0.1.9",
|
|
35
|
-
"@bobfrankston/importgen": "^0.1.
|
|
35
|
+
"@bobfrankston/importgen": "^0.1.42",
|
|
36
36
|
"@bobfrankston/themecolors": "^0.1.9",
|
|
37
37
|
"@bobfrankston/userconfig": "^1.0.11",
|
|
38
38
|
"@npmcli/package-json": "^7.0.4",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
".transformedSnapshot": {
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@bobfrankston/freezepak": "^0.1.9",
|
|
63
|
-
"@bobfrankston/importgen": "^0.1.
|
|
63
|
+
"@bobfrankston/importgen": "^0.1.42",
|
|
64
64
|
"@bobfrankston/themecolors": "^0.1.9",
|
|
65
65
|
"@bobfrankston/userconfig": "^1.0.11",
|
|
66
66
|
"@npmcli/package-json": "^7.0.4",
|