@bobfrankston/npmglobalize 1.0.223 → 1.0.225

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.
Files changed (4) hide show
  1. package/README.md +20 -2
  2. package/lib.d.ts +22 -2
  3. package/lib.js +77 -16
  4. package/package.json +3 -3
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` `allow-scripts` list, minus anything
211
- the project denied. Third-party packages you never approved stay gated:
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
@@ -188,8 +188,16 @@ export interface WorkspaceResult {
188
188
  }
189
189
  /** Read and parse package.json from a directory */
190
190
  export declare function readPackageJson(dir: string): any;
191
- /** Global npm config from %USERPROFILE%\.userconfig\npm.json5 — via @bobfrankston/userconfig */
192
- export type UserNpmConfig = NpmCommonConfig;
191
+ /** Global npm config, via @bobfrankston/userconfig.
192
+ *
193
+ * 2026-09-07 — Claude Code (Fable 5.1), at Bob's direction: the fields THIS app
194
+ * keeps in the store are typed here, not in userconfig. userconfig manages the
195
+ * low-level store and must know nothing about apps or their field names (Bob:
196
+ * "It is just the manager of the low level store"). `allowScripts` was briefly
197
+ * added to userconfig's NpmCommonConfig and has been moved back here. */
198
+ export type UserNpmConfig = NpmCommonConfig & {
199
+ allowScripts?: Record<string, boolean>; /** Remembered npm install-script decisions by package name: true = allow, false = deny */
200
+ };
193
201
  /** Get the .userconfig directory path (%USERPROFILE%\.userconfig) */
194
202
  export declare function getUserConfigDir(): string;
195
203
  /** Read global npm config from %USERPROFILE%\.userconfig\npm.json5 */
@@ -571,6 +579,18 @@ export declare function projectAllowScripts(dir: string): {
571
579
  allowed: string[];
572
580
  denied: string[];
573
581
  };
582
+ /** The user's remembered install-script decisions, by package name.
583
+ * `true` = approve, `false` = deny. Read from npm.json5 via userconfig. */
584
+ export declare function globalAllowScripts(): Record<string, boolean>;
585
+ /** Merge new decisions into the remembered set and persist it. */
586
+ export declare function rememberAllowScripts(decisions: Record<string, boolean>): void;
587
+ /** Project policy layered over the remembered user policy: a name the
588
+ * project decided keeps the project's answer; every other remembered
589
+ * name contributes its own. */
590
+ export declare function effectiveAllowScripts(dir: string): {
591
+ allowed: string[];
592
+ denied: string[];
593
+ };
574
594
  /** Bring the project's package.json `allowScripts` up to date with what is
575
595
  * installed, so `npm install` stops warning and — once npm starts enforcing
576
596
  * 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, and the
4377
- * user's .npmrc list — minus anything the project denies. Empty when npm
4378
- * predates the policy (it runs the scripts anyway) or nothing is allowed. */
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 project = projectAllowScripts(dir);
4383
- const names = [...new Set([...ownScopePackages(dir), ...project.allowed, ...npmrcAllowScripts()])]
4384
- .filter(n => !project.denied.includes(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}. Approvals are recorded in package.json "allowScripts" (name-only) and honored on the global install too.`));
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(', ')}${auto.length ? ` (${auto.length} own/local approved automatically)` : ''}`));
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
- const project = projectAllowScripts(opts.cwd);
4903
- const names = [...new Set([...ownScopePackages(opts.cwd), ...project.allowed])].filter(n => !project.denied.includes(n));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.223",
3
+ "version": "1.0.225",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "@bobfrankston/freezepak": "^0.1.9",
35
35
  "@bobfrankston/importgen": "^0.1.43",
36
36
  "@bobfrankston/themecolors": "^0.1.10",
37
- "@bobfrankston/userconfig": "^1.0.11",
37
+ "@bobfrankston/userconfig": "^1.0.12",
38
38
  "@npmcli/package-json": "^7.0.4",
39
39
  "json5": "^2.2.3",
40
40
  "libnpmversion": "^8.0.3",
@@ -62,7 +62,7 @@
62
62
  "@bobfrankston/freezepak": "^0.1.9",
63
63
  "@bobfrankston/importgen": "^0.1.43",
64
64
  "@bobfrankston/themecolors": "^0.1.10",
65
- "@bobfrankston/userconfig": "^1.0.11",
65
+ "@bobfrankston/userconfig": "^1.0.12",
66
66
  "@npmcli/package-json": "^7.0.4",
67
67
  "json5": "^2.2.3",
68
68
  "libnpmversion": "^8.0.3",