@bobfrankston/npmglobalize 1.0.220 → 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.
Files changed (5) hide show
  1. package/README.md +51 -9
  2. package/cli.js +6 -1
  3. package/lib.d.ts +27 -0
  4. package/lib.js +245 -20
  5. 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
- npmglobalize therefore allowlists **your own packages** on the global install
176
- it performs it just built and published them from your source, so they're
177
- trusted and leaves third-party packages gated:
178
-
179
- ```
180
- > npm install -g @bobfrankston/winpos@2.0.51 --allow-scripts @bobfrankston/winpos,@bobfrankston/msger,@bobfrankston/msgcommon
181
- ```
182
-
183
- Anything npm skips is reported rather than buried in the captured output:
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.
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
@@ -559,6 +559,33 @@ export declare function fixPuppeteerCorruptedCache(combined: string): {
559
559
  * Returns [] for an unscoped package: there's no way to tell an unscoped
560
560
  * package of ours from a third-party one by name alone. */
561
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>;
562
589
  /** Run a command and return success status */
563
590
  export declare function runCommand(cmd: string, args: string[], options?: {
564
591
  silent?: boolean;
package/lib.js CHANGED
@@ -4214,20 +4214,238 @@ export function ownScopePackages(dir) {
4214
4214
  walk(pkg, dir);
4215
4215
  return [...found];
4216
4216
  }
4217
- /** npm args allowing install scripts for our own packages only. Empty when
4218
- * npm predates the policy (it runs the scripts anyway) or the package is
4219
- * unscoped.
4220
- *
4221
- * Note: the CLI layer *replaces* the .npmrc layer rather than merging with
4222
- * it (first source with any config wins, per npm's resolve-allow-scripts),
4223
- * so passing this suppresses any allow-scripts entries in .npmrc for this
4224
- * install. Third-party approvals therefore belong on the command line too,
4225
- * not in .npmrc. */
4226
- function allowOwnScriptsArgs(dir) {
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) {
4227
4328
  if (!npmSupportsAllowScripts())
4228
4329
  return [];
4229
- const own = ownScopePackages(dir);
4230
- return own.length ? ['--allow-scripts', own.join(',')] : [];
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(', ')}`));
4231
4449
  }
4232
4450
  /** Install-script runners that fetch or select a prebuilt binary and only
4233
4451
  * compile when no prebuild matches the platform. A package using one of these
@@ -4301,9 +4519,10 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
4301
4519
  let attempt = 0;
4302
4520
  let cacheFixRounds = 0;
4303
4521
  // Our own packages are trusted — we just built and published them from
4304
- // this very directory — so their install scripts run. Everything else
4305
- // stays behind npm's allowScripts gate.
4306
- const allowArgs = allowOwnScriptsArgs(cwd);
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);
4307
4526
  while (true) {
4308
4527
  // 2026-09-01 10:15 EDT — Claude Code (Opus 5), at Bob's direction.
4309
4528
  // `--prefer-online`: this install always targets a version published
@@ -4623,13 +4842,15 @@ async function verifyWslGlobalBins(cwd) {
4623
4842
  }
4624
4843
  }
4625
4844
  export async function installInWsl(wslArgs, opts = {}) {
4626
- // Same trust rule as the Windows installs: allow our own packages'
4627
- // install scripts, leave third-party ones gated. Probed against WSL's
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
4628
4848
  // npm, which is a separate install from the Windows one.
4629
4849
  if (opts.cwd && wslArgs.includes('install') && await wslNpmSupportsAllowScripts()) {
4630
- const own = ownScopePackages(opts.cwd);
4631
- if (own.length)
4632
- wslArgs = [...wslArgs, '--allow-scripts', own.join(',')];
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(',')];
4633
4854
  }
4634
4855
  const runOnce = async () => {
4635
4856
  console.log(colors.cyan(`> wsl ${wslArgs.join(' ')}`));
@@ -6922,6 +7143,10 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6922
7143
  }
6923
7144
  if (!modulesOk)
6924
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 });
6925
7150
  }
6926
7151
  // Run build step if package.json has a build script (skip if CLI already built)
6927
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.220",
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.40",
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.40",
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",