@bobfrankston/npmglobalize 1.0.223 → 1.0.224
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 +20 -2
- package/lib.d.ts +12 -0
- package/lib.js +77 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -200,6 +200,24 @@ node_modules check:
|
|
|
200
200
|
**name-only** (`--no-allow-scripts-pin`): a version pin would re-open the
|
|
201
201
|
question on every bump. Without a terminal (piped stdin) nothing is asked;
|
|
202
202
|
the pending packages are listed with the command to approve them.
|
|
203
|
+
- **Each answer is remembered for every project.** A decision about `sharp`'s
|
|
204
|
+
install script is about sharp, not about the project that asked first, so
|
|
205
|
+
`y`/`n`/`a` answers are stored once per user in your userconfig `npm.json5` (the file `@bobfrankston/userconfig` names as `configPath`; the prompt prints its location):
|
|
206
|
+
|
|
207
|
+
```json5
|
|
208
|
+
allowScripts: {
|
|
209
|
+
sharp: true,
|
|
210
|
+
koffi: true,
|
|
211
|
+
'some-pkg': false,
|
|
212
|
+
},
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The next project that meets a remembered package gets its `package.json`
|
|
216
|
+
entry written silently (`· install-script decisions applied from …npm.json5:
|
|
217
|
+
sharp allowed`). A project's own `allowScripts` entry wins over the
|
|
218
|
+
remembered one, so a per-project exception is just an edit to that
|
|
219
|
+
`package.json`. Edit or delete an entry in `npm.json5` to change your mind
|
|
220
|
+
everywhere.
|
|
203
221
|
- The field is written by npm itself, so a linked dep gets the key npm wants
|
|
204
222
|
for it — the `file:` spec, e.g. `"file:../../../../projects/com/com-wrapper": true`
|
|
205
223
|
(relative to the link's parent directory, not the project).
|
|
@@ -207,8 +225,8 @@ node_modules check:
|
|
|
207
225
|
The **global** install then allowlists your own packages — it just built and
|
|
208
226
|
published them from your source, so they're trusted — plus whatever the
|
|
209
227
|
project's `allowScripts` approved (a `file:` key is mapped to that dep's
|
|
210
|
-
published name) and your user `.npmrc`
|
|
211
|
-
|
|
228
|
+
published name), your remembered `npm.json5` approvals, and your user `.npmrc`
|
|
229
|
+
`allow-scripts` list, minus anything denied. Third-party packages you never approved stay gated:
|
|
212
230
|
|
|
213
231
|
```
|
|
214
232
|
> npm install -g @bobfrankston/winpos@2.0.51 --allow-scripts @bobfrankston/winpos,@bobfrankston/msger,@bobfrankston/msgcommon,sharp
|
package/lib.d.ts
CHANGED
|
@@ -571,6 +571,18 @@ export declare function projectAllowScripts(dir: string): {
|
|
|
571
571
|
allowed: string[];
|
|
572
572
|
denied: string[];
|
|
573
573
|
};
|
|
574
|
+
/** The user's remembered install-script decisions, by package name.
|
|
575
|
+
* `true` = approve, `false` = deny. Read from npm.json5 via userconfig. */
|
|
576
|
+
export declare function globalAllowScripts(): Record<string, boolean>;
|
|
577
|
+
/** Merge new decisions into the remembered set and persist it. */
|
|
578
|
+
export declare function rememberAllowScripts(decisions: Record<string, boolean>): void;
|
|
579
|
+
/** Project policy layered over the remembered user policy: a name the
|
|
580
|
+
* project decided keeps the project's answer; every other remembered
|
|
581
|
+
* name contributes its own. */
|
|
582
|
+
export declare function effectiveAllowScripts(dir: string): {
|
|
583
|
+
allowed: string[];
|
|
584
|
+
denied: string[];
|
|
585
|
+
};
|
|
574
586
|
/** Bring the project's package.json `allowScripts` up to date with what is
|
|
575
587
|
* installed, so `npm install` stops warning and — once npm starts enforcing
|
|
576
588
|
* the policy — keeps running the scripts this project depends on.
|
package/lib.js
CHANGED
|
@@ -14,7 +14,7 @@ import os from 'os';
|
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { execSync, spawn, spawnSync } from 'child_process';
|
|
16
16
|
import { builtinModules } from 'module';
|
|
17
|
-
import { readConfig as readUserConfig, writeConfig as writeUserConfig, configDir } from '@bobfrankston/userconfig';
|
|
17
|
+
import { readConfig as readUserConfig, writeConfig as writeUserConfig, configDir, configPath as userConfigPath } from '@bobfrankston/userconfig';
|
|
18
18
|
import { freezeDependencies } from '@bobfrankston/freezepak';
|
|
19
19
|
import { importgen as runImportgen } from '@bobfrankston/importgen';
|
|
20
20
|
/** Wrapper for spawnSync that avoids DEP0190 (args + shell: true).
|
|
@@ -4372,18 +4372,55 @@ function npmrcAllowScripts() {
|
|
|
4372
4372
|
return [];
|
|
4373
4373
|
return r.output.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
4374
4374
|
}
|
|
4375
|
+
// 2026-09-07 — Claude Code (Fable 5.1), at Bob's direction ("why not save the
|
|
4376
|
+
// accept scripts globally rather than asking me each time"). A yes/no about a
|
|
4377
|
+
// third-party package's install scripts is a decision about that PACKAGE, not
|
|
4378
|
+
// about the project that happened to ask first — sharp is sharp in whts and in
|
|
4379
|
+
// label-core. So every answer is remembered once, per user, in npm.json5
|
|
4380
|
+
// (`allowScripts: { sharp: true, "evil-pkg": false }`), and a project that
|
|
4381
|
+
// meets a remembered package gets its package.json field written without a
|
|
4382
|
+
// question. package.json stays the record npm itself reads; npm.json5 is the
|
|
4383
|
+
// memory that pre-answers it.
|
|
4384
|
+
/** The user's remembered install-script decisions, by package name.
|
|
4385
|
+
* `true` = approve, `false` = deny. Read from npm.json5 via userconfig. */
|
|
4386
|
+
export function globalAllowScripts() {
|
|
4387
|
+
const policy = readUserNpmConfig().allowScripts;
|
|
4388
|
+
return policy && typeof policy === 'object' ? { ...policy } : {};
|
|
4389
|
+
}
|
|
4390
|
+
/** Merge new decisions into the remembered set and persist it. */
|
|
4391
|
+
export function rememberAllowScripts(decisions) {
|
|
4392
|
+
if (!Object.keys(decisions).length)
|
|
4393
|
+
return;
|
|
4394
|
+
writeUserNpmConfig({ allowScripts: { ...globalAllowScripts(), ...decisions } });
|
|
4395
|
+
}
|
|
4375
4396
|
/** npm args allowing install scripts on a GLOBAL install: our own packages,
|
|
4376
|
-
* whatever the project's package.json `allowScripts` approves,
|
|
4377
|
-
* user's .npmrc list — minus
|
|
4378
|
-
*
|
|
4397
|
+
* whatever the project's package.json `allowScripts` approves, the user's
|
|
4398
|
+
* remembered approvals (npm.json5), and the user's .npmrc list — minus
|
|
4399
|
+
* anything denied. A project's explicit decision wins over the remembered
|
|
4400
|
+
* one. Empty when npm predates the policy (it runs the scripts anyway) or
|
|
4401
|
+
* nothing is allowed. */
|
|
4379
4402
|
function allowScriptsArgs(dir) {
|
|
4380
4403
|
if (!npmSupportsAllowScripts())
|
|
4381
4404
|
return [];
|
|
4382
|
-
const
|
|
4383
|
-
const names = [...new Set([...ownScopePackages(dir), ...
|
|
4384
|
-
.filter(n => !
|
|
4405
|
+
const { allowed, denied } = effectiveAllowScripts(dir);
|
|
4406
|
+
const names = [...new Set([...ownScopePackages(dir), ...allowed, ...npmrcAllowScripts()])]
|
|
4407
|
+
.filter(n => !denied.includes(n));
|
|
4385
4408
|
return names.length ? ['--allow-scripts', names.join(',')] : [];
|
|
4386
4409
|
}
|
|
4410
|
+
/** Project policy layered over the remembered user policy: a name the
|
|
4411
|
+
* project decided keeps the project's answer; every other remembered
|
|
4412
|
+
* name contributes its own. */
|
|
4413
|
+
export function effectiveAllowScripts(dir) {
|
|
4414
|
+
const project = projectAllowScripts(dir);
|
|
4415
|
+
const allowed = new Set(project.allowed);
|
|
4416
|
+
const denied = new Set(project.denied);
|
|
4417
|
+
for (const [name, ok] of Object.entries(globalAllowScripts())) {
|
|
4418
|
+
if (allowed.has(name) || denied.has(name))
|
|
4419
|
+
continue;
|
|
4420
|
+
(ok ? allowed : denied).add(name);
|
|
4421
|
+
}
|
|
4422
|
+
return { allowed: [...allowed], denied: [...denied] };
|
|
4423
|
+
}
|
|
4387
4424
|
/** Parse the text listing of `npm approve-scripts --allow-scripts-pending`:
|
|
4388
4425
|
* one indented `name@version (event: cmd; event: cmd)` line per node,
|
|
4389
4426
|
* the same package repeated when several versions are installed. */
|
|
@@ -4435,16 +4472,23 @@ export async function ensureAllowScripts(dir, opts = {}) {
|
|
|
4435
4472
|
if (!pending.length)
|
|
4436
4473
|
return;
|
|
4437
4474
|
const trusted = new Set([...ownScopePackages(dir), ...localDepDisplayNames(dir)]);
|
|
4438
|
-
const auto = pending.filter(p => trusted.has(p.name));
|
|
4439
|
-
const ask = pending.filter(p => !trusted.has(p.name));
|
|
4440
4475
|
const label = (p) => p.versions.length ? `${p.name}@${p.versions.join('|')}` : p.name;
|
|
4476
|
+
const auto = pending.filter(p => trusted.has(p.name));
|
|
4477
|
+
// 2026-09-07 — Claude Code (Fable 5.1), at Bob's direction: a package already
|
|
4478
|
+
// decided in npm.json5 is applied here without asking; only never-seen
|
|
4479
|
+
// third-party packages reach the prompt, and their answers are remembered.
|
|
4480
|
+
const remembered = globalAllowScripts();
|
|
4481
|
+
const rememberedYes = pending.filter(p => !trusted.has(p.name) && remembered[p.name] === true);
|
|
4482
|
+
const rememberedNo = pending.filter(p => !trusted.has(p.name) && remembered[p.name] === false);
|
|
4483
|
+
const ask = pending.filter(p => !trusted.has(p.name) && !(p.name in remembered));
|
|
4441
4484
|
if (opts.dryRun) {
|
|
4442
|
-
console.log(colors.dim(` [dry-run] allowScripts in ${pkgName}: would approve ${auto.map(label).join(', ') || '(none)'}; would ask about ${ask.map(label).join(', ') || '(none)'}`));
|
|
4485
|
+
console.log(colors.dim(` [dry-run] allowScripts in ${pkgName}: would approve ${[...auto, ...rememberedYes].map(label).join(', ') || '(none)'}; would deny ${rememberedNo.map(label).join(', ') || '(none)'}; would ask about ${ask.map(label).join(', ') || '(none)'}`));
|
|
4443
4486
|
return;
|
|
4444
4487
|
}
|
|
4445
|
-
const approve = auto.map(p => p.name);
|
|
4446
|
-
const deny =
|
|
4488
|
+
const approve = [...auto, ...rememberedYes].map(p => p.name);
|
|
4489
|
+
const deny = rememberedNo.map(p => p.name);
|
|
4447
4490
|
const skipped = [];
|
|
4491
|
+
const decisions = {};
|
|
4448
4492
|
if (ask.length) {
|
|
4449
4493
|
if (!process.stdin.isTTY) {
|
|
4450
4494
|
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(', ')}`));
|
|
@@ -4452,40 +4496,56 @@ export async function ensureAllowScripts(dir, opts = {}) {
|
|
|
4452
4496
|
skipped.push(...ask.map(label));
|
|
4453
4497
|
}
|
|
4454
4498
|
else {
|
|
4455
|
-
console.log(colors.cyan(`npm gates install scripts for ${ask.length} third-party package(s) in ${pkgName}.
|
|
4499
|
+
console.log(colors.cyan(`npm gates install scripts for ${ask.length} third-party package(s) in ${pkgName}. Each answer is recorded in package.json "allowScripts" (name-only), honored on the global install, and remembered for every project in ${userConfigPath}.`));
|
|
4456
4500
|
let allRemaining = false;
|
|
4457
4501
|
for (const p of ask) {
|
|
4458
4502
|
const prebuilt = PREBUILT_SCRIPT_RUNNERS.test(p.scripts) ? ' — prebuilt-binary fetcher, the package ships binaries' : '';
|
|
4459
4503
|
if (allRemaining) {
|
|
4460
4504
|
approve.push(p.name);
|
|
4505
|
+
decisions[p.name] = true;
|
|
4461
4506
|
continue;
|
|
4462
4507
|
}
|
|
4463
4508
|
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']);
|
|
4464
4509
|
switch (answer) {
|
|
4465
4510
|
case 'y':
|
|
4466
4511
|
approve.push(p.name);
|
|
4512
|
+
decisions[p.name] = true;
|
|
4467
4513
|
break;
|
|
4468
4514
|
case 'a':
|
|
4469
4515
|
approve.push(p.name);
|
|
4516
|
+
decisions[p.name] = true;
|
|
4470
4517
|
allRemaining = true;
|
|
4471
4518
|
break;
|
|
4472
4519
|
case 'n':
|
|
4473
4520
|
deny.push(p.name);
|
|
4521
|
+
decisions[p.name] = false;
|
|
4474
4522
|
break;
|
|
4475
4523
|
default:
|
|
4476
4524
|
skipped.push(label(p));
|
|
4477
4525
|
break; // 's', or EOF ('' from promptChoice)
|
|
4478
4526
|
}
|
|
4479
4527
|
}
|
|
4528
|
+
try {
|
|
4529
|
+
rememberAllowScripts(decisions);
|
|
4530
|
+
}
|
|
4531
|
+
catch (error) {
|
|
4532
|
+
// Not swallowed: the project's package.json still gets the answer below;
|
|
4533
|
+
// only the cross-project memory is lost, and the user is told so.
|
|
4534
|
+
console.log(colors.yellow(` ⚠ could not remember install-script decisions in ${userConfigPath}: ${error.message}`));
|
|
4535
|
+
}
|
|
4480
4536
|
}
|
|
4481
4537
|
}
|
|
4538
|
+
if (rememberedYes.length || rememberedNo.length) {
|
|
4539
|
+
console.log(colors.dim(` · install-script decisions applied from ${userConfigPath}: ${[...rememberedYes.map(p => `${p.name} allowed`), ...rememberedNo.map(p => `${p.name} denied`)].join(', ')}`));
|
|
4540
|
+
}
|
|
4482
4541
|
// `npm approve-scripts <name>` matches installed nodes by the same display
|
|
4483
4542
|
// name the pending listing used, and writes the right key for each kind
|
|
4484
4543
|
// (registry name, or the file: path for a linked dep).
|
|
4485
4544
|
if (approve.length) {
|
|
4486
4545
|
const r = await runCommandAsync('npm', ['approve-scripts', '--no-allow-scripts-pin', ...approve], { cwd: dir, silent: true });
|
|
4546
|
+
const autoNote = [auto.length ? `${auto.length} own/local` : '', rememberedYes.length ? `${rememberedYes.length} remembered` : ''].filter(Boolean).join(', ');
|
|
4487
4547
|
if (r.success)
|
|
4488
|
-
console.log(colors.green(` ✓ allowScripts: approved ${approve.join(', ')}${
|
|
4548
|
+
console.log(colors.green(` ✓ allowScripts: approved ${approve.join(', ')}${autoNote ? ` (${autoNote} approved automatically)` : ''}`));
|
|
4489
4549
|
else
|
|
4490
4550
|
console.log(colors.red(` ✗ npm approve-scripts failed: ${(r.stderr || r.output).trim().split('\n').find(l => /npm error/.test(l)) ?? 'see output'}`));
|
|
4491
4551
|
}
|
|
@@ -4899,8 +4959,9 @@ export async function installInWsl(wslArgs, opts = {}) {
|
|
|
4899
4959
|
// list is left out — WSL's npm has its own .npmrc. Probed against WSL's
|
|
4900
4960
|
// npm, which is a separate install from the Windows one.
|
|
4901
4961
|
if (opts.cwd && wslArgs.includes('install') && await wslNpmSupportsAllowScripts()) {
|
|
4902
|
-
|
|
4903
|
-
const
|
|
4962
|
+
// 2026-09-07 — Claude Code (Fable 5.1): remembered npm.json5 decisions apply in WSL too.
|
|
4963
|
+
const policy = effectiveAllowScripts(opts.cwd);
|
|
4964
|
+
const names = [...new Set([...ownScopePackages(opts.cwd), ...policy.allowed])].filter(n => !policy.denied.includes(n));
|
|
4904
4965
|
if (names.length)
|
|
4905
4966
|
wslArgs = [...wslArgs, '--allow-scripts', names.join(',')];
|
|
4906
4967
|
}
|