@bobfrankston/npmglobalize 1.0.227 → 1.0.228

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 +13 -1
  2. package/lib.d.ts +36 -1
  3. package/lib.js +188 -62
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -92,6 +92,8 @@ Before any transform/publish, `npmglobalize` walks the full `file:` dep graph an
92
92
 
93
93
  Errors abort (unless `--force`); warnings prompt to continue.
94
94
 
95
+ An unscoped dependency with private intent is reported differently: it is not a question. npmglobalize never renames a dependency to add a scope (the name is what every consumer imports by, and changing it is the dep owner's own project), so the dep cannot reach npm and neither can the package that depends on it. The first run says so in full, names the dep and its path, records it under `unscopedDepsNoted` in the consumer's `.globalize.json5`, and stops. Later runs print one line per dep and stop. Nothing is asked, because no answer could change the outcome. Fix it in the dep: scope the name in its own `package.json`, or set `npmVisibility: "public"` or `noprefix: true` in its `.globalize.json5`; the note clears itself on the next run. If the cascade reaches such a dep anyway (`-no-prescan`), it stops there with the same message. The rename offer still exists when you run npmglobalize *in* the unscoped package itself. (2026-09-14)
96
+
95
97
  Skip the prescan with `-no-prescan` / `-nps`.
96
98
 
97
99
  #### Workspace skip prescan
@@ -369,6 +371,11 @@ How would you like to set up git?
369
371
  5) Abort
370
372
  ```
371
373
 
374
+ The "Use local install only" line appears only for a package with a `bin` field.
375
+ A library has nothing for a global install to expose, so it is not offered the
376
+ option and the remaining lines are numbered without a gap. The same applies to
377
+ the "No git repository found" and "No git remote configured" menus.
378
+
372
379
  **Adopt** runs:
373
380
 
374
381
  ```bash
@@ -470,7 +477,8 @@ not what you meant to publish, so npmglobalize lists the conflicted files and st
470
477
  -update-deps, -ud Update package.json to latest versions (safe: minor/patch)
471
478
  -update-major Allow major version updates (breaking changes)
472
479
  -publish-deps Auto-publish file: dependencies (default)
473
- -pd Like -publish-deps, plus auto-yes to dep-cascade prompts (private only)
480
+ -pd Like -publish-deps, plus auto-yes to dep-cascade prompts (private only;
481
+ never adds a scope to a dep)
474
482
  -no-publish-deps, -npd Skip auto-publishing file: dependencies
475
483
  -no-prescan, -nps Skip upfront dep-graph prescan
476
484
  -force-publish Republish dependencies even if version exists
@@ -914,6 +922,10 @@ consumed: it is appended to `npmchanges.md` under a
914
922
  lost. Several local entries may share one version number — the date tells them
915
923
  apart. No git commit is made for it; the next real publish carries it along.
916
924
 
925
+ On a library (no `bin`) there is nothing to install, and the output says so: `Local mode:
926
+ <pkg> — library, nothing to install`. The `file:` deps are still built and the `.commitmsg`
927
+ is still consumed; only the install step does not exist. (2026-09-14)
928
+
917
929
  ### Private Packages
918
930
 
919
931
  Packages with `"private": true` in `package.json` skip the npm publish step. Dependencies are still transformed and restored — the publish is the only thing skipped.
package/lib.d.ts CHANGED
@@ -91,7 +91,8 @@ export interface GlobalizeOptions {
91
91
  updateMajor?: boolean;
92
92
  /** Publish file: dependencies before converting them */
93
93
  publishDeps?: boolean;
94
- /** Auto-yes to dep-cascade prompts (add scope to make private, etc.); does NOT auto-yes public prompts */
94
+ /** Auto-yes to dep-cascade prompts (git setup, prescan warnings); does NOT auto-yes public
95
+ * prompts. Cannot add a scope to a dep: the cascade never renames a dependency (2026-09-14). */
95
96
  publishDepsYes?: boolean;
96
97
  /** Cascade npmVisibility:"public" to all transitive workspace/file: deps without prompting.
97
98
  * In workspace mode: fixpoint-promotes every workspace member reachable from a public consumer.
@@ -160,6 +161,12 @@ export interface GlobalizeOptions {
160
161
  * following each entry's own .globalize.json5. FYI for now: recorded and
161
162
  * displayed, nothing is updated automatically. */
162
163
  upstream?: UpstreamEntry[];
164
+ /** Bookkeeping, not a CLI option: unscoped `file:` deps this package was already told
165
+ * about (dep name → date first noted). The first run explains why the dep, and this
166
+ * package with it, will not reach npm until the dep's own package.json has a scope;
167
+ * later runs print one line. Nothing is asked either time, since no answer could
168
+ * change the outcome. Cleared automatically once the dep is scoped. (2026-09-14) */
169
+ unscopedDepsNoted?: Record<string, string>;
163
170
  /** Internal: signals this call is from workspace orchestrator */
164
171
  /** Skip the upfront dep-graph prescan */
165
172
  noPrescan?: boolean;
@@ -344,6 +351,9 @@ export declare function transformDeps(pkg: any, baseDir: string, verbose?: boole
344
351
  /** A problem discovered by the prescan. */
345
352
  export interface PrescanIssue {
346
353
  severity: 'error' | 'warning';
354
+ /** 'unscoped': an unscoped file: dep with private intent. Not an error and not a
355
+ * question — globalize reports it once, remembers it, and stops. */
356
+ kind?: 'unscoped';
347
357
  package: string;
348
358
  path: string;
349
359
  message: string;
@@ -360,6 +370,17 @@ export declare function findUndeclaredImports(cwd: string, pkg: any): Undeclared
360
370
  * user resolve all problems before starting the publish cascade instead of
361
371
  * being interrupted mid-run. */
362
372
  export declare function prescanDepGraph(baseDir: string, visited?: Set<string>, verbose?: boolean): PrescanIssue[];
373
+ /** Report unscoped `file:` deps found by the prescan, without asking anything.
374
+ *
375
+ * An unscoped dep with private intent cannot reach npm, and npmglobalize does not
376
+ * rename a dependency to fix that (the name is what every consumer imports by; scoping
377
+ * it is the dep owner's own project). So the consumer in `cwd` cannot be published
378
+ * either. Nothing the user could answer changes that, so there is no prompt: the first
379
+ * run explains it in full, records the dep in `unscopedDepsNoted` in the consumer's
380
+ * .globalize.json5, and later runs print one line per dep. Deps that have since been
381
+ * scoped drop out of the record automatically.
382
+ * 2026-09-14 17:05 EDT — Claude Code (Fable 5.1), at Bob's direction. */
383
+ export declare function reportUnscopedDeps(cwd: string, unscoped: PrescanIssue[]): void;
363
384
  /** Restore file: dependencies from .dependencies using a three-way merge that
364
385
  * preserves any external modifications made since the last transform. */
365
386
  export declare function restoreDeps(pkg: any, verbose?: boolean): boolean;
@@ -677,6 +698,20 @@ export declare function promptText(message: string, defaultValue?: string): Prom
677
698
  * answer resolved null and every caller fell through to its default action, so a typo
678
699
  * silently did something. Now it says what it didn't understand and asks again.
679
700
  */
701
+ /** One line of a numbered console menu. `action` is what the caller switches on;
702
+ * the number the user types is assigned at display time, so lines can be
703
+ * omitted without leaving gaps or shifting meanings. */
704
+ export interface MenuOption {
705
+ label: string;
706
+ action: string;
707
+ }
708
+ /** Show `header` and the options numbered 1..N in display order, then read a
709
+ * choice. Empty input takes the first option. Returns the chosen `action`.
710
+ * 2026-09-14 14:30 EDT — Claude Code (Fable 5.1), at Bob's direction. The git
711
+ * setup menus were hand-numbered strings with a fixed "Use local install only"
712
+ * line, so a library (no `bin`) was offered an install that doLocalInstall
713
+ * would then skip. Options are now data, so a line can be left out. */
714
+ export declare function promptMenu(header: string, options: MenuOption[]): Promise<string>;
680
715
  export declare function promptChoice(message: string, choices: string[]): Promise<string>;
681
716
  /** Initialize a local git repo by adopting history from an existing remote.
682
717
  * Preserves the working tree — local files appear as uncommitted changes
package/lib.js CHANGED
@@ -403,6 +403,9 @@ export function writeConfig(dir, config, explicitKeys) {
403
403
  if (config.upstream === undefined && existing.upstream !== undefined) {
404
404
  config = { ...config, upstream: existing.upstream };
405
405
  }
406
+ // unscopedDepsNoted is deliberately NOT carried across: reportUnscopedDeps clears it
407
+ // by omitting the key, and a publish-path writeConfig that drops it only means the
408
+ // full explanation prints once more. 2026-09-14
406
409
  const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'publicDeps', 'forcePublish', 'once', 'cleanNestedModules']);
407
410
  for (const [key, value] of Object.entries(config)) {
408
411
  if (omitKeys.has(key))
@@ -480,7 +483,8 @@ export function writeConfig(dir, config, explicitKeys) {
480
483
  ['"usePaths": true', 'true = resolve file: deps from siblings; false = use latest npm version (standalone)'],
481
484
  ['"allowTs": false', 'true = include .ts source in npm tarball (auto-true for noEmit projects)'],
482
485
  ['"importgen": auto', 'Detected from .vscode/tasks.json / import map in HTML; false = never ask'],
483
- ['"subProjects": auto', 'Sub-dirs with their own build script or tsconfig (e.g. ["client", "Sw"]); detected from .vscode/tasks.json; false = never ask']
486
+ ['"subProjects": auto', 'Sub-dirs with their own build script or tsconfig (e.g. ["client", "Sw"]); detected from .vscode/tasks.json; false = never ask'],
487
+ ['"unscopedDepsNoted": {}', 'Bookkeeping: unscoped file: deps already explained (name → date); cleared when the dep gets a scope']
484
488
  ];
485
489
  const refWidth = Math.max(...reference.map(([decl]) => decl.length));
486
490
  for (const [decl, note] of reference) {
@@ -1824,8 +1828,16 @@ export function prescanDepGraph(baseDir, visited = new Set(), verbose = false) {
1824
1828
  continue;
1825
1829
  }
1826
1830
  const depName = targetPkg.name || name;
1827
- // Unscoped + private-intent: will trigger a mid-cascade scope prompt
1828
- if (!depName.startsWith('@')) {
1831
+ // Unscoped + private-intent. This used to be a WARNING whose "continue" led
1832
+ // into the cascade prompting to rename the dep by adding a scope. The cascade
1833
+ // never renames a dependency now (see the _fromDep check in globalize), so it
1834
+ // is flagged `kind: 'unscoped'` and globalize reports it without asking anything.
1835
+ // Reported once per dep, not once per consumer edge: the recursion below
1836
+ // dedupes via `visited`, but this check ran before it, so a dep shared by four
1837
+ // packages printed four identical lines.
1838
+ // 2026-09-14 16:40 EDT — Claude Code (Fable 5.1), at Bob's direction: "It
1839
+ // shouldn't try to publish to a scope not in the package."
1840
+ if (!depName.startsWith('@') && !visited.has(fs.realpathSync(targetPath))) {
1829
1841
  const depConfig = readConfig(targetPath);
1830
1842
  const depVis = depConfig.npmVisibility
1831
1843
  ?? (targetPkg.publishConfig?.access === 'public' ? 'public' : undefined);
@@ -1833,10 +1845,11 @@ export function prescanDepGraph(baseDir, visited = new Set(), verbose = false) {
1833
1845
  if (depVis !== 'public' && !noprefix) {
1834
1846
  issues.push({
1835
1847
  severity: 'warning',
1848
+ kind: 'unscoped',
1836
1849
  package: depName,
1837
1850
  path: targetPath,
1838
- message: 'unscoped package private publishing will prompt for a scope',
1839
- suggestion: 'add a scope (e.g., @bobfrankston/) OR set npmVisibility="public" OR set noprefix=true in .globalize.json5'
1851
+ message: 'unscoped — not published to npm until its own package.json name has a scope',
1852
+ suggestion: 'scope it in ITS OWN package.json (e.g., @bobfrankston/), or set npmVisibility="public" or noprefix=true in its .globalize.json5'
1840
1853
  });
1841
1854
  }
1842
1855
  }
@@ -1846,6 +1859,60 @@ export function prescanDepGraph(baseDir, visited = new Set(), verbose = false) {
1846
1859
  }
1847
1860
  return issues;
1848
1861
  }
1862
+ /** Report unscoped `file:` deps found by the prescan, without asking anything.
1863
+ *
1864
+ * An unscoped dep with private intent cannot reach npm, and npmglobalize does not
1865
+ * rename a dependency to fix that (the name is what every consumer imports by; scoping
1866
+ * it is the dep owner's own project). So the consumer in `cwd` cannot be published
1867
+ * either. Nothing the user could answer changes that, so there is no prompt: the first
1868
+ * run explains it in full, records the dep in `unscopedDepsNoted` in the consumer's
1869
+ * .globalize.json5, and later runs print one line per dep. Deps that have since been
1870
+ * scoped drop out of the record automatically.
1871
+ * 2026-09-14 17:05 EDT — Claude Code (Fable 5.1), at Bob's direction. */
1872
+ export function reportUnscopedDeps(cwd, unscoped) {
1873
+ const config = readConfig(cwd);
1874
+ const noted = { ...(config.unscopedDepsNoted ?? {}) };
1875
+ if (unscoped.length === 0 && Object.keys(noted).length === 0)
1876
+ return; // nothing to say, nothing to forget
1877
+ let consumerName;
1878
+ try {
1879
+ consumerName = readPackageJson(cwd).name || path.basename(cwd);
1880
+ }
1881
+ catch {
1882
+ consumerName = path.basename(cwd);
1883
+ }
1884
+ const today = new Date().toISOString().slice(0, 10);
1885
+ let changed = false;
1886
+ for (const issue of unscoped) {
1887
+ const when = noted[issue.package];
1888
+ if (when) {
1889
+ console.log(colors.yellow(`${issue.package} still unscoped (noted ${when}) — ${consumerName} is not published to npm until ${issue.package} has a scope.`));
1890
+ continue;
1891
+ }
1892
+ console.log(colors.yellow(`${issue.package} is unscoped, so it will not be published to npm until the name in its package.json has a scope.`));
1893
+ console.log(colors.yellow(`${consumerName} depends on ${issue.package} and will not be published to npm either.`));
1894
+ console.log(colors.dim(` ${issue.package} is at ${issue.path}`));
1895
+ console.log(colors.dim(` → ${issue.suggestion}`));
1896
+ console.log(colors.dim(` Noted in ${path.join(cwd, '.globalize.json5')}; later runs print one line for ${issue.package}.`));
1897
+ noted[issue.package] = today;
1898
+ changed = true;
1899
+ }
1900
+ // Forget deps that are no longer reported: they were scoped (or made public) since.
1901
+ const stillUnscoped = new Set(unscoped.map(i => i.package));
1902
+ for (const name of Object.keys(noted)) {
1903
+ if (stillUnscoped.has(name))
1904
+ continue;
1905
+ delete noted[name];
1906
+ changed = true;
1907
+ }
1908
+ if (changed) {
1909
+ const next = { ...config };
1910
+ delete next.unscopedDepsNoted;
1911
+ if (Object.keys(noted).length)
1912
+ next.unscopedDepsNoted = noted;
1913
+ writeConfig(cwd, next);
1914
+ }
1915
+ }
1849
1916
  /** Restore file: dependencies from .dependencies using a three-way merge that
1850
1917
  * preserves any external modifications made since the last transform. */
1851
1918
  export function restoreDeps(pkg, verbose = false) {
@@ -5546,13 +5613,21 @@ export async function promptText(message, defaultValue) {
5546
5613
  });
5547
5614
  });
5548
5615
  }
5549
- /**
5550
- * Prompt user for multiple choice. Re-asks until the answer is one of `choices`.
5551
- *
5552
- * 2026-09-01 Claude Code (Fable 5.1), at Bob's direction. Previously an unrecognized
5553
- * answer resolved null and every caller fell through to its default action, so a typo
5554
- * silently did something. Now it says what it didn't understand and asks again.
5555
- */
5616
+ /** Show `header` and the options numbered 1..N in display order, then read a
5617
+ * choice. Empty input takes the first option. Returns the chosen `action`.
5618
+ * 2026-09-14 14:30 EDT — Claude Code (Fable 5.1), at Bob's direction. The git
5619
+ * setup menus were hand-numbered strings with a fixed "Use local install only"
5620
+ * line, so a library (no `bin`) was offered an install that doLocalInstall
5621
+ * would then skip. Options are now data, so a line can be left out. */
5622
+ export function promptMenu(header, options) {
5623
+ const lines = options.map((o, i) => ` ${i + 1}) ${o.label}`);
5624
+ const message = `${header}\n${lines.join('\n')}\nChoice [1]:`;
5625
+ const valid = options.map((_, i) => String(i + 1));
5626
+ return promptChoice(message, [...valid, '']).then(choice => {
5627
+ const index = choice === '' ? 0 : Number(choice) - 1;
5628
+ return options[index].action;
5629
+ });
5630
+ }
5556
5631
  export function promptChoice(message, choices) {
5557
5632
  const rl = readline.createInterface({
5558
5633
  input: process.stdin,
@@ -6711,15 +6786,21 @@ async function doLocalInstall(cwd, options) {
6711
6786
  const pkg = readPackageJson(cwd);
6712
6787
  const pkgName = pkg.name || path.basename(cwd);
6713
6788
  const pkgVersion = pkg.version || '0.0.0';
6789
+ // A library has no bin, so there is nothing to install: say what actually happens
6790
+ // (deps built, notes recorded, no publish) instead of announcing an install and
6791
+ // then taking it back. 2026-09-14 17:50 EDT — Claude Code (Fable 5.1), at Bob's
6792
+ // direction: "why mention local install when there is no install".
6793
+ if (!pkg.bin) {
6794
+ console.log(colors.blue(`Local mode: ${pkgName}@${pkgVersion} — library, nothing to install`));
6795
+ console.log(colors.dim('Not transformed or published; file: deps left as-is'));
6796
+ console.log(colors.dim(' (switch back to the publish flow with -global)'));
6797
+ recordLocalChange(cwd, pkgVersion, dryRun);
6798
+ return true;
6799
+ }
6714
6800
  console.log(colors.blue(`Local install: ${pkgName}@${pkgVersion}`));
6715
6801
  console.log(colors.dim('Skipping transform/publish — installing with file: deps as-is'));
6716
6802
  console.log(colors.dim(' (switch back to the publish flow with -global)'));
6717
- // Record before the bin check: a library's notes are history too.
6718
6803
  recordLocalChange(cwd, pkgVersion, dryRun);
6719
- if (!pkg.bin) {
6720
- console.log(colors.dim(` (library — skipping global install)`));
6721
- return true;
6722
- }
6723
6804
  if (dryRun) {
6724
6805
  console.log(' [dry-run] Would run: npm install -g .');
6725
6806
  if (wsl)
@@ -6856,10 +6937,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6856
6937
  // any transform/publish, so the user can fix them once and run clean.
6857
6938
  // Only run at the top level (not during recursive cascade or workspace).
6858
6939
  if (!noPrescan && !cleanup && !options._fromDep && !options._fromWorkspace) {
6859
- const issues = prescanDepGraph(cwd, new Set(), verbose);
6940
+ const allIssues = prescanDepGraph(cwd, new Set(), verbose);
6941
+ // Unscoped deps are handled apart from the error/warning prompt below: nothing the
6942
+ // user answers could change the outcome, so nothing is asked. Report, remember,
6943
+ // stop. See reportUnscopedDeps.
6944
+ // 2026-09-14 17:05 EDT — Claude Code (Fable 5.1), at Bob's direction: "The message
6945
+ // should simply point it out won't be published to npm till the scope is fixed and
6946
+ // then remember I was warned. It shouldn't [need] a response because nothing would
6947
+ // be done."
6948
+ const unscoped = allIssues.filter(i => i.kind === 'unscoped');
6949
+ const issues = allIssues.filter(i => i.kind !== 'unscoped');
6860
6950
  if (issues.length > 0) {
6861
- const errors = issues.filter(i => i.severity === 'error');
6862
- const warnings = issues.filter(i => i.severity === 'warning');
6863
6951
  console.log(colors.yellow(`Prescan found ${issues.length} issue(s) in the file: dep graph:`));
6864
6952
  for (const i of issues) {
6865
6953
  const tag = i.severity === 'error' ? colors.red('ERROR') : colors.yellow('WARN');
@@ -6869,6 +6957,18 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6869
6957
  console.log(colors.dim(` → ${i.suggestion}`));
6870
6958
  }
6871
6959
  console.log('');
6960
+ }
6961
+ // Always called: with nothing unscoped it only forgets deps that were noted
6962
+ // earlier and have since been scoped.
6963
+ reportUnscopedDeps(cwd, unscoped);
6964
+ if (unscoped.length > 0) {
6965
+ // The run stops here regardless of the other issues, so the continue prompt
6966
+ // below is skipped: it would ask a question whose answer changes nothing.
6967
+ return false;
6968
+ }
6969
+ if (issues.length > 0) {
6970
+ const errors = issues.filter(i => i.severity === 'error');
6971
+ const warnings = issues.filter(i => i.severity === 'warning');
6872
6972
  if (errors.length > 0 && !force) {
6873
6973
  console.error(colors.red(`Prescan aborted: ${errors.length} error(s). Fix them or rerun with --force.`));
6874
6974
  return false;
@@ -7016,6 +7116,19 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
7016
7116
  const gitStatus = getGitStatus(cwd);
7017
7117
  // Handle init mode
7018
7118
  let justInitialized = false;
7119
+ // 2026-09-14 14:30 EDT — Claude Code (Fable 5.1), at Bob's direction. The git
7120
+ // setup menus below offer "Use local install only" — a global npm install of
7121
+ // this package. That is only meaningful for a CLI (a package with `bin`);
7122
+ // doLocalInstall skips a library anyway, so a library is not offered the line.
7123
+ const hasBin = !!readPackageJson(cwd).bin;
7124
+ const localInstallOption = hasBin
7125
+ ? [{ label: 'Use local install only (skip git/publish)', action: 'local' }]
7126
+ : [];
7127
+ const switchToLocalInstall = () => {
7128
+ console.log(colors.dim('Switching to local-only mode...'));
7129
+ writeConfig(cwd, { ...configOptions, localInstall: true }, new Set(['localInstall']));
7130
+ return doLocalInstall(cwd, options);
7131
+ };
7019
7132
  if (!gitStatus.isRepo) {
7020
7133
  console.log('No git repository found.');
7021
7134
  // Probe for an existing remote we could adopt history from.
@@ -7069,55 +7182,54 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
7069
7182
  // a reader who wanted the second line typed 2 and got something else. Bob:
7070
7183
  // "I type 2 when I mean a because that's what humans do." Options are now
7071
7184
  // numbered in display order; the ALL variant is 2, directly under the single.
7072
- let choice;
7185
+ // 2026-09-14 — Claude Code (Fable 5.1): menus are now promptMenu option
7186
+ // lists; the local-install line is present only for a package with `bin`.
7187
+ // The "No git repository found." header is printed once, above.
7188
+ let action;
7073
7189
  if (adoptable) {
7074
- choice = await promptChoice('How would you like to set up git?\n'
7075
- + ' 1) Adopt history from existing remote (recommended)\n'
7076
- + ' 2) Adopt for ALL remaining deps (don\'t ask again)\n'
7077
- + ' 3) Initialize fresh git repository\n'
7078
- + ' 4) Use local install only (skip git/publish)\n'
7079
- + ' 5) Abort\nChoice [1]:', ['1', '2', '3', '4', '5', '']);
7080
- if (choice === '4') {
7081
- console.log(colors.dim('Switching to local-only mode...'));
7082
- writeConfig(cwd, { ...configOptions, localInstall: true }, new Set(['localInstall']));
7083
- return doLocalInstall(cwd, options);
7084
- }
7085
- if (choice === '5') {
7190
+ action = await promptMenu('How would you like to set up git?', [
7191
+ { label: 'Adopt history from existing remote (recommended)', action: 'adopt' },
7192
+ { label: 'Adopt for ALL remaining deps (don\'t ask again)', action: 'adopt-all' },
7193
+ { label: 'Initialize fresh git repository', action: 'init' },
7194
+ ...localInstallOption,
7195
+ { label: 'Abort', action: 'abort' },
7196
+ ]);
7197
+ if (action === 'local')
7198
+ return switchToLocalInstall();
7199
+ if (action === 'abort') {
7086
7200
  console.log('Aborted. Run with --init to initialize.');
7087
7201
  return false;
7088
7202
  }
7089
- if (choice === '2')
7203
+ if (action === 'adopt-all')
7090
7204
  options.autoInit = true;
7091
- if (choice === '3') {
7205
+ if (action === 'init') {
7092
7206
  const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
7093
7207
  if (!success)
7094
7208
  return false;
7095
7209
  }
7096
7210
  else {
7097
- // '1', '2', '' default → adopt
7211
+ // 'adopt' or 'adopt-all'
7098
7212
  const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
7099
7213
  if (!success)
7100
7214
  return false;
7101
7215
  }
7102
7216
  }
7103
7217
  else {
7104
- choice = await promptChoice('No git repository found. What would you like to do?\n'
7105
- + ' 1) Initialize git repository\n'
7106
- + ' 2) Initialize for ALL remaining deps (don\'t ask again)\n'
7107
- + ' 3) Use local install only (skip git/publish)\n'
7108
- + ' 4) Abort\nChoice [1]:', ['1', '2', '3', '4', '']);
7109
- if (choice === '3') {
7110
- console.log(colors.dim('Switching to local-only mode...'));
7111
- writeConfig(cwd, { ...configOptions, localInstall: true }, new Set(['localInstall']));
7112
- return doLocalInstall(cwd, options);
7113
- }
7114
- if (choice === '4') {
7218
+ action = await promptMenu('What would you like to do?', [
7219
+ { label: 'Initialize git repository', action: 'init' },
7220
+ { label: 'Initialize for ALL remaining deps (don\'t ask again)', action: 'init-all' },
7221
+ ...localInstallOption,
7222
+ { label: 'Abort', action: 'abort' },
7223
+ ]);
7224
+ if (action === 'local')
7225
+ return switchToLocalInstall();
7226
+ if (action === 'abort') {
7115
7227
  console.log('Aborted. Run with --init to initialize.');
7116
7228
  return false;
7117
7229
  }
7118
- if (choice === '2')
7230
+ if (action === 'init-all')
7119
7231
  options.autoInit = true;
7120
- // choice is '1', '2', or '' (default) → init
7232
+ // 'init' or 'init-all'
7121
7233
  const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
7122
7234
  if (!success)
7123
7235
  return false;
@@ -7143,22 +7255,21 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
7143
7255
  else if (!gitStatus.hasRemote) {
7144
7256
  // Git repo exists but no remote - need to create GitHub repo
7145
7257
  if (!init && !options.autoInit && !publishDepsYes) {
7146
- // 2026-09-01 — Claude Code (Fable 5.1): renumbered from "1 a 2 3" (see note above).
7147
- const choice = await promptChoice('No git remote configured. What would you like to do?\n'
7148
- + ' 1) Create GitHub repository\n'
7149
- + ' 2) Create for ALL remaining deps (don\'t ask again)\n'
7150
- + ' 3) Use local install only (skip git/publish)\n'
7151
- + ' 4) Abort\nChoice [1]:', ['1', '2', '3', '4', '']);
7152
- if (choice === '3') {
7153
- console.log(colors.dim('Switching to local-only mode...'));
7154
- writeConfig(cwd, { ...configOptions, localInstall: true }, new Set(['localInstall']));
7155
- return doLocalInstall(cwd, options);
7156
- }
7157
- if (choice === '4') {
7258
+ // 2026-09-14 — Claude Code (Fable 5.1): promptMenu option list; local-install
7259
+ // line only for a package with `bin` (see hasBin above).
7260
+ const action = await promptMenu('No git remote configured. What would you like to do?', [
7261
+ { label: 'Create GitHub repository', action: 'create' },
7262
+ { label: 'Create for ALL remaining deps (don\'t ask again)', action: 'create-all' },
7263
+ ...localInstallOption,
7264
+ { label: 'Abort', action: 'abort' },
7265
+ ]);
7266
+ if (action === 'local')
7267
+ return switchToLocalInstall();
7268
+ if (action === 'abort') {
7158
7269
  console.log('Aborted. Run with --init to set up GitHub repository.');
7159
7270
  return false;
7160
7271
  }
7161
- if (choice === '2') {
7272
+ if (action === 'create-all') {
7162
7273
  options.autoInit = true;
7163
7274
  }
7164
7275
  }
@@ -7417,6 +7528,21 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
7417
7528
  publicConfirmed = true;
7418
7529
  }
7419
7530
  }
7531
+ // A dependency reached through the cascade is never renamed. Adding a scope changes the
7532
+ // name every consumer imports by, which is the dep owner's project, not a publish-time
7533
+ // side effect. Refuse and say what to fix; the top-level run on the package itself may
7534
+ // still offer the rename below, because there the user chose to work on that package.
7535
+ // 2026-09-14 16:40 EDT — Claude Code (Fable 5.1), at Bob's direction: "It shouldn't try
7536
+ // to publish to a scope not in the package. I need to fix the package and that's a
7537
+ // separate and messy project."
7538
+ // noprefix in the dep's .globalize.json5 is the same escape hatch the prescan honors.
7539
+ if (!isScoped && options._fromDep && effectiveNpmVisibility !== 'public' && currentAccess !== 'public'
7540
+ && readConfig(cwd).noprefix !== true) {
7541
+ console.error(colors.red(`Dependency '${pkg.name}' is unscoped and cannot be published privately.`));
7542
+ console.error(colors.red(` npmglobalize does not rename dependencies. Scope it in ${path.join(cwd, 'package.json')},`));
7543
+ console.error(colors.red(` or set npmVisibility="public" or noprefix=true in its .globalize.json5.`));
7544
+ return false;
7545
+ }
7420
7546
  if (effectiveNpmVisibility === 'private') {
7421
7547
  // User explicitly wants private publication
7422
7548
  if (!isScoped) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.227",
3
+ "version": "1.0.228",
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.13",
37
+ "@bobfrankston/userconfig": "^1.0.15",
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.13",
65
+ "@bobfrankston/userconfig": "^1.0.15",
66
66
  "@npmcli/package-json": "^7.0.4",
67
67
  "json5": "^2.2.3",
68
68
  "libnpmversion": "^8.0.3",