@bobfrankston/npmglobalize 1.0.221 → 1.0.222

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 +22 -12
  2. package/cli.js +18 -6
  3. package/lib.d.ts +2 -0
  4. package/lib.js +53 -12
  5. package/package.json +3 -3
package/README.md CHANGED
@@ -304,7 +304,7 @@ Notes:
304
304
  - **Git/GitHub only.** npm publish does not consume git commit messages; `npmchanges.md` lives in the git repo and on GitHub but is excluded from the published npm tarball (the standard `*.md` rule keeps only `README.md`).
305
305
  - If both `-m` and `.commitmsg` are present, `-m` wins and `.commitmsg` is left alone (not consumed).
306
306
  - If publish fails, `.commitmsg` is preserved for the next attempt.
307
- - Under `-local` (no publish) the file is still consumed — appended as `## v<version> (local) — <date>` and deleted, with no git commit. See [Local Install](#local-install--local).
307
+ - Under `-local-install` (no publish) the file is still consumed — appended as `## v<version> (local) — <date>` and deleted, with no git commit. See [Local Install](#local-install--local-install).
308
308
  - `.commitmsg` is auto-added to `.npmignore` (security pattern) so it never leaks into the tarball.
309
309
 
310
310
  ### 🔧 Git Integration & Error Recovery
@@ -467,9 +467,11 @@ not what you meant to publish, so npmglobalize lists the conflicted files and st
467
467
  ```
468
468
  -install, -i Install globally after publish (from registry)
469
469
  -link Install globally via symlink (npm install -g .)
470
- -local Local install only — skip transform/publish, just npm install -g .
471
- (persisted; a .commitmsg is still logged to npmchanges.md as "(local)")
472
- -global Clear a persisted -local in .globalize.json5 back to the normal
470
+ -local-install Install from the local directory only — skip transform/publish, just
471
+ npm install -g . with file: deps as-is (also -localinstall; -local is
472
+ the deprecated old spelling). Persisted; a .commitmsg is still logged
473
+ to npmchanges.md as "(local)"
474
+ -global Clear a persisted -local-install in .globalize.json5 — back to the normal
473
475
  transform/publish flow (publishes to npm again)
474
476
  -wsl Also install in WSL
475
477
  -once Don't persist flags to .globalize.json5
@@ -772,6 +774,10 @@ Cycle-safe via a shared visited set; each project is built at most once per run.
772
774
 
773
775
  This complements the existing publish cascade (which ensures version refs are correct) by closing the build-freshness gap that `npm install` alone left open.
774
776
 
777
+ #### `bin` shebang
778
+
779
+ Every file named in `package.json` `bin` must start with `#!/usr/bin/env node`. npm's Windows `.cmd` shim doesn't care, but the symlink npm creates on Linux, WSL and macOS execs the file directly, and without the shebang `sh` tries to run it and fails with a syntax error. Before each project is built, `npmglobalize` checks each `bin` file and adds the line if it is missing — in the `.ts` source when there is one (tsc carries it through to the emitted `.js`, and the touched source makes the freshness check rebuild), otherwise in the `.js` itself. A leading BOM, which would also defeat the shebang, is removed at the same time. A `bin` entry whose file does not exist yet is reported as a warning.
780
+
775
781
  #### Import maps (`importgen`) as a build step
776
782
 
777
783
  Browser projects that use [`importgen`](https://www.npmjs.com/package/@bobfrankston/importgen) have historically regenerated their import map from `.vscode/tasks.json`, which only runs when VS Code opens the folder — so a command-line or CI build could publish a stale map. `npmglobalize` treats the import map as a build product and moves the step into the package's own `build` script, where every build path picks it up.
@@ -838,7 +844,7 @@ Both `-flag` and `--flag` are accepted. Single-dash is the primary convention:
838
844
  ```bash
839
845
  npmglobalize -patch # same as --patch
840
846
  npmglobalize -np # same as --nopublish
841
- npmglobalize -local # same as --local
847
+ npmglobalize -local-install # same as --local-install
842
848
  ```
843
849
 
844
850
  ### Persistent vs One-Shot Flags
@@ -846,7 +852,7 @@ npmglobalize -local # same as --local
846
852
  Some flags are **persisted** to `.globalize.json5` when set from the CLI:
847
853
  - `-install`, `-link`, `-wsl`, `-files`, `-fix` — install/build preferences
848
854
  - `-np` (noPublish) — once set, prevents accidental publishes
849
- - `-local` — remembers "this project is local-only" (clear with `-global`)
855
+ - `-local-install` — remembers "this project is local-only" (clear with `-global`)
850
856
  - `-git`/`-npm` visibility
851
857
 
852
858
  Other flags are **one-shot** (never persisted):
@@ -859,14 +865,14 @@ Use `-once` to prevent any flag from persisting on that run:
859
865
  npmglobalize -np -once # No-publish this run only, don't remember it
860
866
  ```
861
867
 
862
- ### Local Install (`-local`)
868
+ ### Local Install (`-local-install`)
863
869
 
864
870
  Skip all transform/publish logic and just run `npm install -g .` with `file:` deps as-is. Use this when you want to install a CLI tool locally for your own use without publishing anything:
865
871
 
866
872
  ```bash
867
- npmglobalize -local # Install globally from local directory
868
- npmglobalize -local -wsl # Also install in WSL
869
- npmglobalize -global # Back to the normal transform/publish flow (publishes to npm)
873
+ npmglobalize -local-install # Install globally from the local directory
874
+ npmglobalize -local-install -wsl # Also install in WSL
875
+ npmglobalize -global # Back to the normal transform/publish flow (publishes to npm)
870
876
  ```
871
877
 
872
878
  This is useful for:
@@ -874,7 +880,11 @@ This is useful for:
874
880
  - Testing a CLI before publishing
875
881
  - Projects with `file:` deps that should stay as-is
876
882
 
877
- `-local` is persisted to `.globalize.json5` (`"local": true`), so a bare
883
+ `-localinstall` is accepted as well. The old spelling `-local` still works but prints a
884
+ deprecation note; it was renamed (2026-09-07) because "local" read as "local mode" rather
885
+ than "install from the local directory".
886
+
887
+ `-local-install` is persisted to `.globalize.json5` (`"local": true`), so a bare
878
888
  `npmglobalize` keeps doing local installs until you run `-global` (or edit the
879
889
  file). Nothing is committed, bumped, or published, but a `.commitmsg` is still
880
890
  consumed: it is appended to `npmchanges.md` under a
@@ -921,7 +931,7 @@ npmglobalize -install -wsl
921
931
  npmglobalize -link -wsl
922
932
 
923
933
  # Install locally without publishing (file: deps stay as-is)
924
- npmglobalize -local
934
+ npmglobalize -local-install
925
935
 
926
936
  # Restore original file: references
927
937
  npmglobalize -cleanup
package/cli.js CHANGED
@@ -70,9 +70,11 @@ Dependency Options:
70
70
  Install Options:
71
71
  -install, -i Global install after publish (from registry)
72
72
  -link Global install via symlink (npm install -g .)
73
- -local Local install only — skip transform/publish, just npm install -g .
74
- (persisted; a .commitmsg is still logged to npmchanges.md as "(local)")
75
- -global Clear a persisted -local in .globalize.json5 back to the normal
73
+ -local-install Install from the local directory only — skip transform/publish, just
74
+ npm install -g . with file: deps as-is (also -localinstall; -local is
75
+ the deprecated old spelling). Persisted; a .commitmsg is still logged
76
+ to npmchanges.md as "(local)"
77
+ -global Clear a persisted -local-install in .globalize.json5 — back to the normal
76
78
  transform/publish flow (publishes to npm again)
77
79
  -wsl Also install globally in WSL
78
80
  -freeze Freeze node_modules (replace symlinks with real copies for network shares)
@@ -157,8 +159,8 @@ Examples:
157
159
  npmglobalize -fix Fix security vulnerabilities
158
160
  npmglobalize -install -wsl Release + install on Windows and WSL (from registry)
159
161
  npmglobalize -link -wsl Release + link on Windows and WSL (symlink)
160
- npmglobalize -local Install locally with file: deps as-is (no publish)
161
- npmglobalize -local -wsl Local install on Windows and WSL
162
+ npmglobalize -local-install Install from the local directory, file: deps as-is (no publish)
163
+ npmglobalize -local-install -wsl Local install on Windows and WSL
162
164
  npmglobalize -global Leave local-only mode; publish to npm from now on
163
165
  npmglobalize -np Just transform, no publish (remembered in config)
164
166
  npmglobalize -cleanup Restore original dependencies
@@ -228,11 +230,21 @@ function parseArgs(args) {
228
230
  case '-cleanup':
229
231
  options.cleanup = true;
230
232
  break;
233
+ // 2026-09-07 — Claude Code (Fable 5.1), at Bob's direction: "-local is
234
+ // confusing" — it read like "local mode" / "local deps" rather than "install
235
+ // from the local directory". Renamed to -local-install (-localinstall also
236
+ // accepted). -local still works but says so, so muscle memory isn't broken
237
+ // and the old spelling is phased out rather than silently kept alive.
238
+ // The .globalize.json5 key stays "local" so existing configs keep working.
231
239
  case '-local':
240
+ console.log(colors.yellow('! -local is deprecated — use -local-install (same meaning: npm install -g . from the local directory, no publish)'));
241
+ // falls through
242
+ case '-local-install':
243
+ case '-localinstall':
232
244
  options.local = true;
233
245
  options.explicitKeys.add('local');
234
246
  break;
235
- // 2026-09-01 16:30 EDT — Claude Code (Opus 5), at Bob's direction. -local
247
+ // 2026-09-01 16:30 EDT — Claude Code (Opus 5), at Bob's direction. -local-install
236
248
  // persists to .globalize.json5 but had no counterpart, so leaving local-only
237
249
  // mode meant hand-editing the config. Same shape as -publish for -np.
238
250
  // 2026-09-01 15:00 EDT — Bob: "-global makes more sense, -nolocal is confusing";
package/lib.d.ts CHANGED
@@ -456,6 +456,8 @@ export declare function ensureBuildScript(cwd: string): Promise<{
456
456
  * was skipped because there was nothing to build / the user declined the
457
457
  * build-script prompt / output already up to date). Pass `forceBuild` to
458
458
  * skip the freshness check. */
459
+ /** Every `bin` entry gets a `#!/usr/bin/env node` first line, added to the source if missing. */
460
+ export declare function ensureBinShebang(cwd: string): void;
459
461
  export declare function buildProject(cwd: string, opts?: {
460
462
  verbose?: boolean;
461
463
  force?: boolean;
package/lib.js CHANGED
@@ -332,7 +332,7 @@ const BOOL_COMMENTS = {
332
332
  quiet: { on: 'Suppress npm warnings', off: 'Show npm warnings' },
333
333
  verbose: { on: 'Show detailed output', off: 'Normal output detail' },
334
334
  fix: { on: 'Auto-run npm audit fix (default)', off: 'Do NOT run npm audit fix (audit report still runs)' },
335
- local: { on: 'Local install only (skip transform/publish)', off: 'Normal transform/publish flow' },
335
+ local: { on: 'Local install only via -local-install (skip transform/publish)', off: 'Normal transform/publish flow' },
336
336
  noPublish: { on: 'Transform but don\'t publish', off: 'Publish normally' },
337
337
  freeze: { on: 'Freeze node_modules (replace symlinks with real copies)', off: 'Leave node_modules symlinks as-is' },
338
338
  usePaths: { on: 'Resolve file: deps from sibling checkouts', off: 'Resolve file: deps from npm, not siblings (standalone package)' },
@@ -444,7 +444,7 @@ export function writeConfig(dir, config, explicitKeys) {
444
444
  ['"gitVisibility": "private"', 'Git repo: private or public'],
445
445
  ['"npmVisibility": "private"', 'npm package: private or public'],
446
446
  ['"fix": true', 'true = auto-run npm audit fix (never --force); false = report only'],
447
- ['"local": false', 'true = local install only (skip transform/publish); clear with -global'],
447
+ ['"local": false', 'true = install from the local directory only, set by -local-install (skip transform/publish); clear with -global'],
448
448
  ['"noPublish": false', 'true = transform but don\'t publish'],
449
449
  ['"freeze": false', 'true = freeze node_modules (replace symlinks with real copies)'],
450
450
  ['"usePaths": true', 'true = resolve file: deps from siblings; false = use latest npm version (standalone)'],
@@ -3536,7 +3536,48 @@ export async function ensureBuildScript(cwd) {
3536
3536
  * was skipped because there was nothing to build / the user declined the
3537
3537
  * build-script prompt / output already up to date). Pass `forceBuild` to
3538
3538
  * skip the freshness check. */
3539
+ // 2026-09-07 — Claude Code (Fable 5.1), at Bob's direction: "When a program is
3540
+ // installed, at least locally, it needs a shebang for node. Make sure the main
3541
+ // program has a shebang." npm's Windows .cmd shim doesn't care, but the symlink
3542
+ // npm creates on Linux/WSL/macOS execs the file directly, so a bin entry
3543
+ // without `#!/usr/bin/env node` fails there with "syntax error" from sh. The
3544
+ // shebang belongs in the SOURCE (.ts): tsc carries it through to the emitted
3545
+ // .js, and editing the .ts bumps its mtime so the freshness check rebuilds.
3546
+ // Runs ahead of the build for that reason, and for every package in the
3547
+ // cascade, since a file: dep can be a CLI too.
3548
+ /** Every `bin` entry gets a `#!/usr/bin/env node` first line, added to the source if missing. */
3549
+ export function ensureBinShebang(cwd) {
3550
+ let pkg;
3551
+ try {
3552
+ pkg = readPackageJson(cwd);
3553
+ }
3554
+ catch {
3555
+ return;
3556
+ } // no package.json: nothing to check, the build reports that
3557
+ if (!pkg?.bin)
3558
+ return;
3559
+ const targets = typeof pkg.bin === 'string' ? [pkg.bin]
3560
+ : (typeof pkg.bin === 'object' ? Object.values(pkg.bin).filter((v) => typeof v === 'string') : []);
3561
+ for (const rel of targets) {
3562
+ const emitted = path.join(cwd, rel);
3563
+ // Prefer the TypeScript source the bin file is emitted from.
3564
+ const tsSibling = emitted.replace(/\.(m|c)?js$/i, '.$1ts');
3565
+ const file = tsSibling !== emitted && fs.existsSync(tsSibling) ? tsSibling : emitted;
3566
+ if (!fs.existsSync(file)) {
3567
+ console.log(colors.yellow(`! bin "${rel}" in ${pkg.name}: file not found (needs a #!/usr/bin/env node shebang once it exists)`));
3568
+ continue;
3569
+ }
3570
+ const raw = fs.readFileSync(file, 'utf-8');
3571
+ const body = raw.startsWith('') ? raw.slice(1) : raw; // a BOM ahead of #! defeats the shebang
3572
+ if (body.startsWith('#!'))
3573
+ continue;
3574
+ fs.writeFileSync(file, `#!/usr/bin/env node
3575
+ ${body}`, 'utf-8');
3576
+ console.log(colors.green(`+ Added #!/usr/bin/env node to ${path.relative(cwd, file)} (bin "${rel}")`));
3577
+ }
3578
+ }
3539
3579
  export async function buildProject(cwd, opts = {}) {
3580
+ ensureBinShebang(cwd);
3540
3581
  // Fix removed-in-TS7 tsconfig settings up front rather than waiting for a
3541
3582
  // build to fail on them. A deprecated `moduleResolution` is only a *hard*
3542
3583
  // error under the tsc that flags it (TS6+, and the editor's bundled
@@ -6529,15 +6570,15 @@ export function getToolVersion() {
6529
6570
  }
6530
6571
  }
6531
6572
  // 2026-09-01 16:30 EDT — Claude Code (Opus 5), at Bob's direction.
6532
- // Under -local the run returned before any of the .commitmsg handling, so the
6573
+ // Under -local-install the run returned before any of the .commitmsg handling, so the
6533
6574
  // notes a local-only package accumulated in .commitmsg were never recorded
6534
6575
  // anywhere — they sat in the file, and the next edit overwrote them. A local
6535
6576
  // install is still a change worth a history line, so the same npmchanges.md
6536
6577
  // gets the entry, tagged "(local)" because nothing was published or bumped
6537
6578
  // (several local runs may legitimately share one version number). No git
6538
- // activity: -local is the path taken when there is often no repo at all, and
6579
+ // activity: -local-install is the path taken when there is often no repo at all, and
6539
6580
  // when there is one the next real publish commits npmchanges.md with the rest.
6540
- /** Consume .commitmsg for a -local run: append to npmchanges.md, delete the file. */
6581
+ /** Consume .commitmsg for a -local-install run: append to npmchanges.md, delete the file. */
6541
6582
  function recordLocalChange(cwd, version, dryRun) {
6542
6583
  const commitMsgPath = path.join(cwd, '.commitmsg');
6543
6584
  if (!fs.existsSync(commitMsgPath))
@@ -6572,8 +6613,8 @@ function recordLocalChange(cwd, version, dryRun) {
6572
6613
  console.error(colors.yellow(` Warning: could not update npmchanges.md: ${err.message}`));
6573
6614
  }
6574
6615
  }
6575
- /** Perform local-only install (npm install -g .) — the single -local path, reached
6576
- * from the -local flag / config and from the git-init prompts' "local only" choice. */
6616
+ /** Perform local-only install (npm install -g .) — the single -local-install path, reached
6617
+ * from the -local-install flag / config and from the git-init prompts' "local only" choice. */
6577
6618
  async function doLocalInstall(cwd, options) {
6578
6619
  const { dryRun = false, wsl = false } = options;
6579
6620
  const pkg = readPackageJson(cwd);
@@ -6693,7 +6734,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6693
6734
  if (configOptions.show)
6694
6735
  settings.push('--show');
6695
6736
  if (configOptions.local)
6696
- settings.push('-local');
6737
+ settings.push('-local-install');
6697
6738
  if (configOptions.freeze)
6698
6739
  settings.push('-freeze');
6699
6740
  if (configOptions.usePaths === false)
@@ -6710,8 +6751,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6710
6751
  }
6711
6752
  }
6712
6753
  console.log('');
6713
- // -local: skip all transform/publish, just install from local directory with file: deps intact.
6714
- // -local is testing-only and must NOT mutate package.json — packages are meant to be installed
6754
+ // -local-install: skip all transform/publish, just install from local directory with file: deps intact.
6755
+ // -local-install is testing-only and must NOT mutate package.json — packages are meant to be installed
6715
6756
  // as published public packages by end users; the bin field is whatever the published shape is.
6716
6757
  // Library members (no bin) are silently skipped: nothing for global install to expose.
6717
6758
  // 2026-09-01 16:30 EDT — Claude Code (Opus 5): this was a verbatim copy of
@@ -9017,7 +9058,7 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
9017
9058
  console.log(`Packages (${packages.length}): ${packages.map(p => p.name).join(', ')}`);
9018
9059
  console.log(`Publish order: ${publishOrder.join(' → ')}`);
9019
9060
  console.log('');
9020
- // Warn if -local / -install are present at the workspace root: those are
9061
+ // Warn if -local-install / -install are present at the workspace root: those are
9021
9062
  // component-level concerns. In workspace mode they only act on members
9022
9063
  // that have a `bin` field; libraries are silently skipped. Telling the
9023
9064
  // user up front avoids the surprise of "I asked for install but most
@@ -9025,7 +9066,7 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
9025
9066
  const installSources = [];
9026
9067
  if (options.local || configOptions.local) {
9027
9068
  const src = configOptions.local && !options.local ? '.globalize.json5' : 'CLI';
9028
- installSources.push(`-local (${src})`);
9069
+ installSources.push(`-local-install (${src})`);
9029
9070
  }
9030
9071
  if (options.install || configOptions.install) {
9031
9072
  const src = configOptions.install && !options.install ? '.globalize.json5' : 'CLI';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.221",
3
+ "version": "1.0.222",
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.42",
35
+ "@bobfrankston/importgen": "^0.1.43",
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.42",
63
+ "@bobfrankston/importgen": "^0.1.43",
64
64
  "@bobfrankston/themecolors": "^0.1.9",
65
65
  "@bobfrankston/userconfig": "^1.0.11",
66
66
  "@npmcli/package-json": "^7.0.4",