@bobfrankston/npmglobalize 1.0.115 → 1.0.117

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 +19 -12
  2. package/cli.js +10 -0
  3. package/lib.d.ts +2 -0
  4. package/lib.js +102 -16
  5. package/package.json +3 -1
package/README.md CHANGED
@@ -127,6 +127,16 @@ npmglobalize --fix # Runs npm audit fix
127
127
  npmglobalize --no-fix
128
128
  ```
129
129
 
130
+ ### 🔑 OAuth Credentials Handling
131
+
132
+ `npmglobalize` automatically detects `credentials.json` files and handles them based on OAuth app type:
133
+
134
+ - **Desktop/installed apps** (`"installed"` key in JSON): The `client_secret` is just a public app registration ID, not a real secret. Google's own docs state: *"the client_secret is obviously not treated as a secret."* These files are kept in the repo — `!credentials.json` is added to `.gitignore`/`.npmignore` to override any broader ignore patterns.
135
+
136
+ - **Web apps** (`"web"` key in JSON): The `client_secret` is a real secret. These files are automatically added to `.gitignore`/`.npmignore` to prevent accidental exposure.
137
+
138
+ This distinction also drives the **push protection auto-bypass**: when GitHub blocks a push because it detects an OAuth client secret, `npmglobalize` checks the credential file type. For installed apps, it auto-bypasses (marking as `false_positive`) since the credential is public by design.
139
+
130
140
  ### 🔄 File Reference Management
131
141
 
132
142
  **Default behavior** (restore file: references after publish):
@@ -409,22 +419,19 @@ npmglobalize --dry-run # See what would happen
409
419
 
410
420
  ## Operational Details
411
421
 
412
- ### The `.dependencies` Backup Mechanism
422
+ ### The `.dependencies` Backup (Internal/Transient)
413
423
 
414
- When `npmglobalize` transforms `file:` references to npm versions, it stores the originals in a `.dependencies` field (and `.devDependencies`, etc.) inside `package.json`. This is the central safety mechanism it allows recovery from any failure during the publish cycle.
424
+ **You should never see `.dependencies` in your `package.json` under normal operation.** It is a temporary internal backup that exists only during the brief publish cycle and is removed automatically when the cycle completes.
415
425
 
416
- **How it works:**
417
- - Before transforming, the original `file:` entries are copied to `.dependencies`
418
- - After a successful publish, the originals are restored from `.dependencies` and the backup is removed
419
- - If the process crashes, is interrupted, or exits early (e.g., private package, `--nopublish`), `.dependencies` remains in `package.json`
420
- - On the **next run**, `transformDeps` detects `.dependencies`, restores the originals first, then re-transforms — so data is never lost
426
+ During publishing, `npmglobalize` temporarily replaces `file:` references with npm version strings. The original `file:` entries are stashed in `.dependencies` (and `.devDependencies`, etc.) so they can be restored afterward. Once the publish succeeds and `file:` paths are restored, `.dependencies` is deleted. A normal run leaves no trace of it.
421
427
 
422
- **Recovery commands:**
423
- ```bash
424
- npmglobalize -cleanup # Manually restore file: deps from .dependencies backup
425
- ```
428
+ **If you see `.dependencies` in your `package.json`, something went wrong** — the tool crashed, was killed, or the publish failed partway through. It is not a feature to rely on or edit manually.
429
+
430
+ **Recovery:**
431
+ - **Re-run `npmglobalize`**: It detects leftover `.dependencies`, restores the originals, and continues normally. Self-healing is automatic.
432
+ - **Manual restore**: `npmglobalize -cleanup` restores file: deps and removes the `.dependencies` backup.
426
433
 
427
- **Why not just restore immediately on error?** The `.dependencies` approach is deliberately persistent. If the tool crashes hard (killed process, power failure, npm timeout), there's no cleanup code to run. The backup in `package.json` survives because it was written before the risky operations began. The next run self-heals.
434
+ **Why a persistent backup?** If the tool crashes hard (killed process, power failure, npm timeout), there's no cleanup code to run. The backup in `package.json` survives because it was written before the risky operations began. The next run self-heals.
428
435
 
429
436
  ### Flag Conventions
430
437
 
package/cli.js CHANGED
@@ -42,6 +42,8 @@ Install Options:
42
42
  -wsl Also install globally in WSL
43
43
  -freeze Freeze node_modules (replace symlinks with real copies for network shares)
44
44
  -nofreeze Disable freeze
45
+ -importgen Run importgen to update import maps before publishing
46
+ -noimportgen Disable importgen
45
47
  -once Don't persist flags to .globalize.json5
46
48
 
47
49
  Mode Options:
@@ -297,6 +299,14 @@ function parseArgs(args) {
297
299
  case '-once':
298
300
  options.once = true;
299
301
  break;
302
+ case '-importgen':
303
+ options.importgen = true;
304
+ options.explicitKeys.add('importgen');
305
+ break;
306
+ case '-noimportgen':
307
+ options.importgen = false;
308
+ options.explicitKeys.add('importgen');
309
+ break;
300
310
  default:
301
311
  if (arg.startsWith('-')) {
302
312
  unrecognized.push(arg);
package/lib.d.ts CHANGED
@@ -72,6 +72,8 @@ export interface GlobalizeOptions {
72
72
  package?: boolean;
73
73
  /** Don't persist CLI flags to .globalize.json5 */
74
74
  once?: boolean;
75
+ /** Run importgen to update import maps before publishing */
76
+ importgen?: boolean;
75
77
  /** Local install only — skip transform/publish, just npm install -g . */
76
78
  local?: boolean;
77
79
  /** Freeze node_modules: replace symlinks/junctions with real copies for network share use */
package/lib.js CHANGED
@@ -14,6 +14,7 @@ import path from 'path';
14
14
  import { execSync, spawnSync } from 'child_process';
15
15
  import { readConfig as readUserConfig, writeConfig as writeUserConfig, configDir } from '@bobfrankston/userconfig';
16
16
  import { freezeDependencies } from '@bobfrankston/freezepak';
17
+ import { importgen as runImportgen } from '@bobfrankston/importgen';
17
18
  /** Wrapper for spawnSync that avoids DEP0190 (args + shell: true).
18
19
  * When shell is true, joins cmd+args into a single command string. */
19
20
  function spawnSafe(cmd, args, options = {}) {
@@ -1440,30 +1441,51 @@ function parsePushProtection(errorOutput, cwd) {
1440
1441
  return result;
1441
1442
  }
1442
1443
  result.detected = true;
1443
- // Split on secret-type headers: "—— Secret Type ————..."
1444
- const blocks = cleaned.split(/\u2014\u2014\s+/).slice(1);
1445
- for (const block of blocks) {
1446
- const typeMatch = block.match(/^(.+?)[\s\u2014]+/);
1447
- const type = typeMatch ? typeMatch[1].trim() : '';
1448
- if (!type)
1449
- continue;
1450
- const pathMatch = block.match(/path:\s+(\S+?)(?::(\d+))?\s/);
1451
- const file = pathMatch ? pathMatch[1].trim() : '';
1452
- const urlMatch = block.match(/(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/);
1453
- const unblockUrl = urlMatch ? urlMatch[1].trim() : '';
1454
- if (type && (file || unblockUrl)) {
1455
- result.secrets.push({ type, file, unblockUrl });
1444
+ // Match secret blocks using a single regex across any dash characters
1445
+ // Pattern: "-- Type Name ---..." then "path: file:line" then "unblock-secret/id" URL
1446
+ // Handles em-dash (—), en-dash (–), and regular dash (-)
1447
+ const secretRegex = /[-\u2014\u2013]{2,}\s+(.+?)\s+[-\u2014\u2013]{2,}[\s\S]*?path:\s+(\S+?)(?::\d+)?\s[\s\S]*?(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/g;
1448
+ let match;
1449
+ while ((match = secretRegex.exec(cleaned)) !== null) {
1450
+ result.secrets.push({
1451
+ type: match[1].trim(),
1452
+ file: match[2].trim(),
1453
+ unblockUrl: match[3].trim()
1454
+ });
1455
+ }
1456
+ // If regex didn't match (encoding issues), fall back to finding unblock URLs + paths
1457
+ if (result.secrets.length === 0) {
1458
+ const urlRegex = /(https:\/\/github\.com\/\S+\/unblock-secret\/\S+)/g;
1459
+ const pathRegex = /path:\s+(\S+?)(?::\d+)?\s/g;
1460
+ const urls = [];
1461
+ const files = [];
1462
+ let m;
1463
+ while ((m = urlRegex.exec(cleaned)) !== null)
1464
+ urls.push(m[1].trim());
1465
+ while ((m = pathRegex.exec(cleaned)) !== null)
1466
+ files.push(m[1].trim());
1467
+ // Try to detect secret type from text
1468
+ const hasOAuth = cleaned.toLowerCase().includes('oauth');
1469
+ const secretType = hasOAuth ? 'Google OAuth Credential' : 'Secret';
1470
+ for (let i = 0; i < Math.max(urls.length, files.length); i++) {
1471
+ result.secrets.push({
1472
+ type: secretType,
1473
+ file: files[i] || '',
1474
+ unblockUrl: urls[i] || ''
1475
+ });
1456
1476
  }
1457
1477
  }
1458
1478
  // Check if all detected secrets are from Google OAuth installed apps (safe to include)
1459
1479
  if (result.secrets.length > 0) {
1460
1480
  result.allInstalledOAuth = result.secrets.every(s => {
1461
- if (!s.type.toLowerCase().includes('oauth'))
1462
- return false;
1463
1481
  if (!s.file)
1464
1482
  return false;
1483
+ // Check by type name OR by inspecting the actual credential file
1465
1484
  const credType = detectCredentialsTypeFromFile(path.join(cwd, s.file));
1466
- return credType === 'installed';
1485
+ if (credType === 'installed')
1486
+ return true;
1487
+ // Also match if the type name mentions OAuth (even if file check failed)
1488
+ return s.type.toLowerCase().includes('oauth') && credType !== 'web';
1467
1489
  });
1468
1490
  }
1469
1491
  return result;
@@ -2534,6 +2556,27 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
2534
2556
  console.log(' [dry-run] Would run: npm run build');
2535
2557
  }
2536
2558
  }
2559
+ // Run importgen if enabled (update import maps in HTML files)
2560
+ if (options.importgen) {
2561
+ try {
2562
+ if (!dryRun) {
2563
+ const igResult = runImportgen(cwd);
2564
+ console.log(colors.green('✓ importgen updated import maps'));
2565
+ if (verbose && igResult.depDirs.length > 0) {
2566
+ console.log(` ${igResult.depDirs.length} dependency dirs resolved`);
2567
+ }
2568
+ }
2569
+ else {
2570
+ console.log(' [dry-run] Would run: importgen');
2571
+ }
2572
+ }
2573
+ catch (error) {
2574
+ // importgen throws if no HTML file found — that's fine, just skip
2575
+ if (verbose) {
2576
+ console.log(colors.dim(` importgen skipped: ${error.message}`));
2577
+ }
2578
+ }
2579
+ }
2537
2580
  // Pre-flight check: fix version/tag mismatches silently
2538
2581
  if (!dryRun) {
2539
2582
  fixVersionTagMismatch(cwd, pkg, verbose);
@@ -3823,6 +3866,49 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
3823
3866
  console.log(`Packages (${packages.length}): ${packages.map(p => p.name).join(', ')}`);
3824
3867
  console.log(`Publish order: ${publishOrder.join(' → ')}`);
3825
3868
  console.log('');
3869
+ // Check that workspace packages' deps are hoisted to root (needed for global install)
3870
+ const rootPkg = readPackageJson(rootDir);
3871
+ if (rootPkg.bin) {
3872
+ const wsNames = new Set(packages.map(p => p.name));
3873
+ const rootDeps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
3874
+ const missing = [];
3875
+ for (const pkgInfo of packages) {
3876
+ const deps = pkgInfo.pkg.dependencies || {};
3877
+ for (const [dep, ver] of Object.entries(deps)) {
3878
+ if (wsNames.has(dep))
3879
+ continue; // sibling workspace package — bundled
3880
+ if (dep in rootDeps)
3881
+ continue; // already in root
3882
+ if (missing.some(m => m.dep === dep))
3883
+ continue; // already flagged
3884
+ missing.push({ dep, version: ver, from: pkgInfo.name });
3885
+ }
3886
+ }
3887
+ if (missing.length > 0) {
3888
+ console.log(colors.yellow('⚠ Workspace packages have dependencies not in root package.json:'));
3889
+ for (const m of missing) {
3890
+ console.log(colors.yellow(` ${m.dep} (${m.version}) — from ${m.from}`));
3891
+ }
3892
+ console.log(colors.dim(' Global install (npm install -g) only installs root deps.'));
3893
+ if (!options.dryRun) {
3894
+ const hoist = await confirm('Add missing deps to root package.json?', true);
3895
+ if (hoist) {
3896
+ if (!rootPkg.dependencies)
3897
+ rootPkg.dependencies = {};
3898
+ for (const m of missing) {
3899
+ rootPkg.dependencies[m.dep] = m.version;
3900
+ console.log(colors.green(` + ${m.dep}: ${m.version}`));
3901
+ }
3902
+ writePackageJson(rootDir, rootPkg);
3903
+ console.log(colors.green('✓ Hoisted workspace deps to root package.json'));
3904
+ }
3905
+ }
3906
+ else {
3907
+ console.log(colors.dim(' [dry-run] Would offer to hoist missing deps'));
3908
+ }
3909
+ console.log('');
3910
+ }
3911
+ }
3826
3912
  // Handle --cleanup: restore deps for all packages and return
3827
3913
  if (options.cleanup) {
3828
3914
  console.log('Restoring workspace dependencies...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.115",
3
+ "version": "1.0.117",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@bobfrankston/freezepak": "^0.1.4",
35
+ "@bobfrankston/importgen": "^0.1.31",
35
36
  "@bobfrankston/userconfig": "^1.0.4",
36
37
  "@npmcli/package-json": "^7.0.4",
37
38
  "json5": "^2.2.3",
@@ -45,6 +46,7 @@
45
46
  },
46
47
  ".dependencies": {
47
48
  "@bobfrankston/freezepak": "file:../freezepak",
49
+ "@bobfrankston/importgen": "file:../importgen",
48
50
  "@bobfrankston/userconfig": "file:../userconfig",
49
51
  "@npmcli/package-json": "^7.0.4",
50
52
  "json5": "^2.2.3",