@bobfrankston/npmglobalize 1.0.196 → 1.0.197
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/lib.d.ts +8 -0
- package/lib.js +171 -25
- package/package.json +3 -3
package/lib.d.ts
CHANGED
|
@@ -457,6 +457,14 @@ export declare function fixPuppeteerCorruptedCache(combined: string): {
|
|
|
457
457
|
repaired: string[];
|
|
458
458
|
removed: string[];
|
|
459
459
|
};
|
|
460
|
+
/** Every package in `dir`'s dependency closure that shares its scope — our
|
|
461
|
+
* own packages, published by this same tool. Walks node_modules for
|
|
462
|
+
* transitive own-scope deps, since a cascade dep can carry its own
|
|
463
|
+
* postinstall and only its installed copy lists its dependencies.
|
|
464
|
+
*
|
|
465
|
+
* Returns [] for an unscoped package: there's no way to tell an unscoped
|
|
466
|
+
* package of ours from a third-party one by name alone. */
|
|
467
|
+
export declare function ownScopePackages(dir: string): string[];
|
|
460
468
|
/** Run a command and return success status */
|
|
461
469
|
export declare function runCommand(cmd: string, args: string[], options?: {
|
|
462
470
|
silent?: boolean;
|
package/lib.js
CHANGED
|
@@ -3271,18 +3271,142 @@ export function fixPuppeteerCorruptedCache(combined) {
|
|
|
3271
3271
|
}
|
|
3272
3272
|
return { repaired: [], removed: [] };
|
|
3273
3273
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3274
|
+
// ─── allowScripts policy: trust our own packages, gate everyone else ──
|
|
3275
|
+
//
|
|
3276
|
+
// npm 11.17+ skips install-time lifecycle scripts (preinstall/install/
|
|
3277
|
+
// postinstall) for packages that aren't on an allowlist, and a global
|
|
3278
|
+
// install has no project package.json to record approvals in — so a
|
|
3279
|
+
// postinstall we wrote ourselves silently never runs while the install
|
|
3280
|
+
// still exits 0. (@bobfrankston/msger's postinstall copies its native
|
|
3281
|
+
// binary into the per-user bin dir its launcher reads from; skipping it
|
|
3282
|
+
// leaves a stale exe behind a green checkmark.)
|
|
3283
|
+
//
|
|
3284
|
+
// npmglobalize only ever installs a package it just built and published
|
|
3285
|
+
// from the user's own source, so own-scope packages are trusted. Third-
|
|
3286
|
+
// party deps are not: they stay gated and keep surfacing through
|
|
3287
|
+
// warnSkippedScripts. That's deliberately narrower than a blanket
|
|
3288
|
+
// --dangerously-allow-all-scripts — it keeps the warning meaningful
|
|
3289
|
+
// instead of turning it into dead code.
|
|
3290
|
+
//
|
|
3291
|
+
// The policy matcher has no scope wildcard (`@scope/*` matches nothing —
|
|
3292
|
+
// see arborist's script-allowed.js), so the allowlist must name every
|
|
3293
|
+
// own-scope package in the tree explicitly.
|
|
3294
|
+
let _allowScriptsSupported = null;
|
|
3295
|
+
let _wslAllowScriptsSupported = null;
|
|
3296
|
+
/** Probe output for a config key npm doesn't define. */
|
|
3297
|
+
const isUnknownConfig = (out) => {
|
|
3298
|
+
const v = out.trim();
|
|
3299
|
+
return v === '' || v === 'undefined';
|
|
3300
|
+
};
|
|
3301
|
+
/** Does this npm know the allowScripts policy (11.17+)? Older npm runs all
|
|
3302
|
+
* install scripts unconditionally, and passing the flag there just prints
|
|
3303
|
+
* "Unknown cli config" on every install. Probed once per process.
|
|
3304
|
+
*
|
|
3305
|
+
* Probes the boolean sibling flag rather than `allow-scripts` itself: an
|
|
3306
|
+
* unset list config also prints empty, which is indistinguishable from
|
|
3307
|
+
* "not supported". Both flags landed in the same npm release. */
|
|
3308
|
+
function npmSupportsAllowScripts() {
|
|
3309
|
+
if (_allowScriptsSupported === null) {
|
|
3310
|
+
const r = runCommand('npm', ['config', 'get', 'dangerously-allow-all-scripts'], { silent: true });
|
|
3311
|
+
_allowScriptsSupported = r.success && !isUnknownConfig(r.output);
|
|
3312
|
+
}
|
|
3313
|
+
return _allowScriptsSupported;
|
|
3314
|
+
}
|
|
3315
|
+
/** Same probe against WSL's npm, which is a separate install and can lag
|
|
3316
|
+
* behind the Windows one. */
|
|
3317
|
+
async function wslNpmSupportsAllowScripts() {
|
|
3318
|
+
if (_wslAllowScriptsSupported === null) {
|
|
3319
|
+
const r = await runCommandAsync('wsl', ['npm', 'config', 'get', 'dangerously-allow-all-scripts'], { silent: true });
|
|
3320
|
+
_wslAllowScriptsSupported = r.success && !isUnknownConfig(r.output);
|
|
3321
|
+
}
|
|
3322
|
+
return _wslAllowScriptsSupported;
|
|
3323
|
+
}
|
|
3324
|
+
/** Every package in `dir`'s dependency closure that shares its scope — our
|
|
3325
|
+
* own packages, published by this same tool. Walks node_modules for
|
|
3326
|
+
* transitive own-scope deps, since a cascade dep can carry its own
|
|
3327
|
+
* postinstall and only its installed copy lists its dependencies.
|
|
3328
|
+
*
|
|
3329
|
+
* Returns [] for an unscoped package: there's no way to tell an unscoped
|
|
3330
|
+
* package of ours from a third-party one by name alone. */
|
|
3331
|
+
export function ownScopePackages(dir) {
|
|
3332
|
+
let pkg;
|
|
3333
|
+
try {
|
|
3334
|
+
pkg = readPackageJson(dir);
|
|
3335
|
+
}
|
|
3336
|
+
catch {
|
|
3337
|
+
return [];
|
|
3338
|
+
}
|
|
3339
|
+
const name = pkg.name || '';
|
|
3340
|
+
const slash = name.indexOf('/');
|
|
3341
|
+
if (!name.startsWith('@') || slash < 2)
|
|
3342
|
+
return [];
|
|
3343
|
+
const scope = name.slice(0, slash + 1); // '@bobfrankston/'
|
|
3344
|
+
// npm hoists, so a dep's own deps usually sit in the root node_modules
|
|
3345
|
+
// rather than nested under it — check both.
|
|
3346
|
+
const resolveDep = (dep, from) => {
|
|
3347
|
+
for (const cand of [path.join(from, 'node_modules', dep), path.join(dir, 'node_modules', dep)]) {
|
|
3348
|
+
if (fs.existsSync(path.join(cand, 'package.json')))
|
|
3349
|
+
return cand;
|
|
3350
|
+
}
|
|
3351
|
+
return null;
|
|
3352
|
+
};
|
|
3353
|
+
const found = new Set([name]);
|
|
3354
|
+
const walk = (manifest, from) => {
|
|
3355
|
+
const deps = { ...(manifest.dependencies || {}), ...(manifest.optionalDependencies || {}) };
|
|
3356
|
+
for (const dep of Object.keys(deps)) {
|
|
3357
|
+
if (!dep.startsWith(scope) || found.has(dep))
|
|
3358
|
+
continue;
|
|
3359
|
+
found.add(dep);
|
|
3360
|
+
const depDir = resolveDep(dep, from);
|
|
3361
|
+
if (!depDir)
|
|
3362
|
+
continue; // not installed here — the name is still allowed
|
|
3363
|
+
try {
|
|
3364
|
+
walk(JSON.parse(fs.readFileSync(path.join(depDir, 'package.json'), 'utf-8')), depDir);
|
|
3365
|
+
}
|
|
3366
|
+
catch { /* unreadable dep manifest — the name is still allowed */ }
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
walk(pkg, dir);
|
|
3370
|
+
return [...found];
|
|
3371
|
+
}
|
|
3372
|
+
/** npm args allowing install scripts for our own packages only. Empty when
|
|
3373
|
+
* npm predates the policy (it runs the scripts anyway) or the package is
|
|
3374
|
+
* unscoped.
|
|
3375
|
+
*
|
|
3376
|
+
* Note: the CLI layer *replaces* the .npmrc layer rather than merging with
|
|
3377
|
+
* it (first source with any config wins, per npm's resolve-allow-scripts),
|
|
3378
|
+
* so passing this suppresses any allow-scripts entries in .npmrc for this
|
|
3379
|
+
* install. Third-party approvals therefore belong on the command line too,
|
|
3380
|
+
* not in .npmrc. */
|
|
3381
|
+
function allowOwnScriptsArgs(dir) {
|
|
3382
|
+
if (!npmSupportsAllowScripts())
|
|
3383
|
+
return [];
|
|
3384
|
+
const own = ownScopePackages(dir);
|
|
3385
|
+
return own.length ? ['--allow-scripts', own.join(',')] : [];
|
|
3386
|
+
}
|
|
3387
|
+
/** A skipped postinstall can leave a package broken at runtime (puppeteer
|
|
3388
|
+
* without its browser, native modules unbuilt), so surface the warning
|
|
3389
|
+
* instead of burying it in the captured output. Own-scope packages are
|
|
3390
|
+
* reported separately and louder: those were supposed to be allowed, so
|
|
3391
|
+
* one showing up here means the allowlist missed it. */
|
|
3392
|
+
function warnSkippedScripts(combined, allowed = []) {
|
|
3280
3393
|
const pkgs = [...combined.matchAll(/^npm warn allow-scripts\s+(\S+@[^\s(]+) \(/gm)];
|
|
3281
3394
|
if (!pkgs.length)
|
|
3282
3395
|
return;
|
|
3283
|
-
const
|
|
3284
|
-
|
|
3285
|
-
|
|
3396
|
+
const nameOf = (spec) => spec.replace(/@[^@]+$/, '');
|
|
3397
|
+
const first = allowed[0] || '';
|
|
3398
|
+
const scope = first.startsWith('@') ? first.slice(0, first.indexOf('/') + 1) : '';
|
|
3399
|
+
const ours = scope ? pkgs.filter(m => nameOf(m[1]).startsWith(scope)) : [];
|
|
3400
|
+
const others = pkgs.filter(m => !ours.includes(m));
|
|
3401
|
+
if (ours.length) {
|
|
3402
|
+
console.log(colors.red(` ⚠ Own-scope install scripts still skipped: ${ours.map(m => m[1]).join(', ')}`));
|
|
3403
|
+
console.log(colors.red(` npmglobalize allowlisted ${allowed.length} own package(s) but missed these — likely transitive deps not present in the local node_modules tree.`));
|
|
3404
|
+
}
|
|
3405
|
+
if (others.length) {
|
|
3406
|
+
const names = [...new Set(others.map(m => nameOf(m[1])))];
|
|
3407
|
+
console.log(colors.yellow(` ⚠ npm skipped install scripts for third-party packages: ${others.map(m => m[1]).join(', ')}`));
|
|
3408
|
+
console.log(colors.yellow(` Package may be broken at runtime. Allow with: npm install -g <pkg> --allow-scripts ${names.join(',')}`));
|
|
3409
|
+
}
|
|
3286
3410
|
}
|
|
3287
3411
|
/** Run npm install -g with retries for registry propagation delay.
|
|
3288
3412
|
* Brand-new packages (first-time publish) take much longer to become
|
|
@@ -3301,16 +3425,20 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
|
|
|
3301
3425
|
let result = { success: false, output: '', stderr: '' };
|
|
3302
3426
|
let attempt = 0;
|
|
3303
3427
|
let cacheFixRounds = 0;
|
|
3428
|
+
// Our own packages are trusted — we just built and published them from
|
|
3429
|
+
// this very directory — so their install scripts run. Everything else
|
|
3430
|
+
// stays behind npm's allowScripts gate.
|
|
3431
|
+
const allowArgs = allowOwnScriptsArgs(cwd);
|
|
3304
3432
|
while (true) {
|
|
3305
|
-
console.log(colors.cyan(`> npm install -g ${pkgSpec}`));
|
|
3306
|
-
result = await runCommandAsync('npm', ['install', '-g', pkgSpec], { cwd, silent: true });
|
|
3433
|
+
console.log(colors.cyan(`> npm install -g ${pkgSpec}${allowArgs.length ? ` ${allowArgs.join(' ')}` : ''}`));
|
|
3434
|
+
result = await runCommandAsync('npm', ['install', '-g', pkgSpec, ...allowArgs], { cwd, silent: true });
|
|
3307
3435
|
const combined = `${result.output}\n${result.stderr}`;
|
|
3308
3436
|
fullLog += `===== attempt ${attempt + 1}: npm install -g ${pkgSpec} =====\n${combined}\n`;
|
|
3309
3437
|
if (result.success) {
|
|
3310
3438
|
const added = combined.match(/^(?:added|changed|removed) \d+ packages?.*$/m);
|
|
3311
3439
|
if (added)
|
|
3312
3440
|
console.log(colors.dim(` ${added[0]}`));
|
|
3313
|
-
warnSkippedScripts(combined);
|
|
3441
|
+
warnSkippedScripts(combined, allowArgs.length ? allowArgs[1].split(',') : []);
|
|
3314
3442
|
// A truncated puppeteer browser extraction exits 0, so the install
|
|
3315
3443
|
// LOOKS fine while the cache is poisoned for every future install.
|
|
3316
3444
|
// Verify now, while the leftover archive still allows a repair.
|
|
@@ -3548,6 +3676,14 @@ async function waitForNpmVersionInWsl(spec, maxWaitMs = 180000) {
|
|
|
3548
3676
|
return false;
|
|
3549
3677
|
}
|
|
3550
3678
|
export async function installInWsl(wslArgs, opts = {}) {
|
|
3679
|
+
// Same trust rule as the Windows installs: allow our own packages'
|
|
3680
|
+
// install scripts, leave third-party ones gated. Probed against WSL's
|
|
3681
|
+
// npm, which is a separate install from the Windows one.
|
|
3682
|
+
if (opts.cwd && wslArgs.includes('install') && await wslNpmSupportsAllowScripts()) {
|
|
3683
|
+
const own = ownScopePackages(opts.cwd);
|
|
3684
|
+
if (own.length)
|
|
3685
|
+
wslArgs = [...wslArgs, '--allow-scripts', own.join(',')];
|
|
3686
|
+
}
|
|
3551
3687
|
const runOnce = async () => {
|
|
3552
3688
|
console.log(colors.cyan(`> wsl ${wslArgs.join(' ')}`));
|
|
3553
3689
|
const r = await runCommandAsync('wsl', wslArgs, { cwd: opts.cwd, silent: true });
|
|
@@ -4035,29 +4171,39 @@ function getAuthInstructions() {
|
|
|
4035
4171
|
return `
|
|
4036
4172
|
Authentication Options:
|
|
4037
4173
|
|
|
4038
|
-
1. ${colors.yellow('
|
|
4174
|
+
1. ${colors.yellow('Interactive login')} (recommended):
|
|
4175
|
+
- Run: ${colors.green('npm login')}
|
|
4176
|
+
- Web-based: completes 2FA in the browser and writes the token for you.
|
|
4177
|
+
- The path that survives npm's token changes (see the note below).
|
|
4178
|
+
|
|
4179
|
+
2. ${colors.yellow('Create a Granular Access Token')} (unattended use):
|
|
4039
4180
|
- Go to: https://www.npmjs.com/settings/[username]/tokens
|
|
4040
4181
|
- Click "Generate New Token" → "Granular Access Token"
|
|
4041
4182
|
- Set permissions: ${colors.green('Read and write')} for packages
|
|
4042
|
-
-
|
|
4043
|
-
-
|
|
4183
|
+
- Scope it to the packages you actually publish, and set an expiry
|
|
4184
|
+
- ${colors.red('Do NOT enable "Bypass 2FA"')} — npm is retiring it:
|
|
4185
|
+
· since early Aug 2026, bypass tokens no longer skip 2FA for token,
|
|
4186
|
+
package-access, maintainer, org or team changes
|
|
4187
|
+
· around Jan 2027, they lose direct publish entirely (read + stage
|
|
4188
|
+
only; publication needs human 2FA approval)
|
|
4044
4189
|
|
|
4045
|
-
|
|
4190
|
+
3. ${colors.yellow('Set token via environment variable')}:
|
|
4046
4191
|
- Set: ${colors.green('NPM_TOKEN=npm_xxx...')}
|
|
4047
4192
|
- Or run: ${colors.green('$env:NPM_TOKEN="npm_xxx..."')} (PowerShell)
|
|
4048
4193
|
|
|
4049
|
-
|
|
4194
|
+
4. ${colors.yellow('Set token in .npmrc')}:
|
|
4050
4195
|
- Edit: ${colors.green(npmrcPath)}
|
|
4051
4196
|
- Add: ${colors.green('//registry.npmjs.org/:_authToken=npm_xxx...')}
|
|
4052
4197
|
|
|
4053
|
-
|
|
4054
|
-
-
|
|
4055
|
-
|
|
4198
|
+
5. ${colors.yellow('Publishing from CI')}:
|
|
4199
|
+
- Prefer trusted publishing (OIDC) over any long-lived token — nothing to
|
|
4200
|
+
rotate, and it is the supported path once bypass tokens lose publish.
|
|
4056
4201
|
|
|
4057
4202
|
${colors.italic('Note: y:\\dev\\utils\\npmglobalize has set-npm-token.ps1 that may help fix token')}
|
|
4058
4203
|
${colors.italic(' problems, but no promises.')}
|
|
4059
4204
|
|
|
4060
|
-
Note:
|
|
4205
|
+
Note: unattended local publishing with a long-lived token is on a deprecation
|
|
4206
|
+
path. Plan for OIDC publishing from CI, or a publish step you approve with 2FA.
|
|
4061
4207
|
`;
|
|
4062
4208
|
}
|
|
4063
4209
|
function loadIgnorePatterns() {
|
|
@@ -4989,7 +5135,7 @@ async function doLocalInstall(cwd, options) {
|
|
|
4989
5135
|
console.log(' [dry-run] Would run: wsl npm install -g .');
|
|
4990
5136
|
return true;
|
|
4991
5137
|
}
|
|
4992
|
-
const result = await runCommandAsync('npm', ['install', '-g', '.'], { cwd, silent: false, showCommand: true });
|
|
5138
|
+
const result = await runCommandAsync('npm', ['install', '-g', '.', ...allowOwnScriptsArgs(cwd)], { cwd, silent: false, showCommand: true });
|
|
4993
5139
|
if (result.success) {
|
|
4994
5140
|
console.log(colors.green(`✓ Installed locally: ${pkgName}@${pkgVersion}`));
|
|
4995
5141
|
}
|
|
@@ -5000,7 +5146,7 @@ async function doLocalInstall(cwd, options) {
|
|
|
5000
5146
|
}
|
|
5001
5147
|
if (wsl) {
|
|
5002
5148
|
console.log(`Installing ${pkgName} in WSL (local)...`);
|
|
5003
|
-
const wslResult = await
|
|
5149
|
+
const wslResult = await installInWsl(['npm', 'install', '-g', '.'], { cwd });
|
|
5004
5150
|
if (wslResult.success) {
|
|
5005
5151
|
console.log(colors.green(`✓ Installed in WSL: ${pkgName}@${pkgVersion}`));
|
|
5006
5152
|
}
|
|
@@ -5105,7 +5251,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5105
5251
|
console.log(' [dry-run] Would run: wsl npm install -g .');
|
|
5106
5252
|
return true;
|
|
5107
5253
|
}
|
|
5108
|
-
const result = await runCommandAsync('npm', ['install', '-g', '.'], { cwd, silent: false, showCommand: true });
|
|
5254
|
+
const result = await runCommandAsync('npm', ['install', '-g', '.', ...allowOwnScriptsArgs(cwd)], { cwd, silent: false, showCommand: true });
|
|
5109
5255
|
if (result.success) {
|
|
5110
5256
|
console.log(colors.green(`✓ Installed locally: ${pkgName}@${pkgVersion}`));
|
|
5111
5257
|
}
|
|
@@ -5116,7 +5262,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5116
5262
|
}
|
|
5117
5263
|
if (wsl) {
|
|
5118
5264
|
console.log(`Installing ${pkgName} in WSL (local)...`);
|
|
5119
|
-
const wslResult =
|
|
5265
|
+
const wslResult = await installInWsl(['npm', 'install', '-g', '.'], { cwd });
|
|
5120
5266
|
if (wslResult.success) {
|
|
5121
5267
|
console.log(colors.green(`✓ Installed in WSL: ${pkgName}@${pkgVersion}`));
|
|
5122
5268
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/npmglobalize",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.197",
|
|
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.39",
|
|
36
36
|
"@bobfrankston/themecolors": "^0.1.8",
|
|
37
37
|
"@bobfrankston/userconfig": "^1.0.10",
|
|
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.39",
|
|
64
64
|
"@bobfrankston/themecolors": "^0.1.8",
|
|
65
65
|
"@bobfrankston/userconfig": "^1.0.10",
|
|
66
66
|
"@npmcli/package-json": "^7.0.4",
|