@bobfrankston/npmglobalize 1.0.227 → 1.0.229
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/README.md +14 -2
- package/lib.d.ts +36 -1
- package/lib.js +209 -142
- 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 same holds when you run npmglobalize *in* an unscoped package: it used to ask "Add scope to package name?", and now it warns and assumes no, since a rename changes what every consumer imports. Publishing an unscoped package PUBLIC on purpose takes an explicit `-npm public` or `noprefix: true`. (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
|
+
npmglobalize never adds a scope to any package)
|
|
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
|
|
@@ -785,7 +793,7 @@ Before transforming or publishing anything, `npmglobalize` builds `file:` depend
|
|
|
785
793
|
For each project visited (the target and every transitive `file:` dep):
|
|
786
794
|
|
|
787
795
|
- If `tsconfig.json` is missing or has `"noEmit": true` → **skip** (not a TypeScript build), unless the `build` script runs `importgen` — a plain-JS browser app still needs its import map regenerated.
|
|
788
|
-
- If `tsconfig.json` exists but `package.json` has no `"build"` script → **
|
|
796
|
+
- If `tsconfig.json` exists but `package.json` has no `"build"` script → **add** `"build": "tsc"` without asking (2026-09-14; it used to prompt) (plus a `tsc -p <dir>` per sub-project — see below). Decline and that project is skipped.
|
|
789
797
|
- Otherwise → run `npm run build`. A failure halts the cascade unless `-force` is passed.
|
|
790
798
|
|
|
791
799
|
Cycle-safe via a shared visited set; each project is built at most once per run.
|
|
@@ -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 (
|
|
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
|
|
1828
|
-
|
|
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
|
|
1839
|
-
suggestion: '
|
|
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) {
|
|
@@ -3537,12 +3604,17 @@ export async function ensureBuildScript(cwd) {
|
|
|
3537
3604
|
const subs = await readySubProjects(cwd, readConfig(cwd));
|
|
3538
3605
|
const tscParts = ['tsc', ...subs.map(subProjectBuildCommand)];
|
|
3539
3606
|
const newScript = [...(existingBuild ? [existingBuild] : []), ...tscParts].join(' && ');
|
|
3607
|
+
// No question here: a TypeScript project that emits needs a build script, and
|
|
3608
|
+
// "tsc" is the only sensible one, so the answer was always yes. The prompt cost
|
|
3609
|
+
// a keystroke per package for no decision. The importgen and sub-project prompts
|
|
3610
|
+
// above stay, since those rewrite a build script that already works.
|
|
3611
|
+
// 2026-09-14 18:05 EDT — Claude Code (Fable 5.1), at Bob's direction ("is there
|
|
3612
|
+
// a command option to avoid having to answer questions like whether to update
|
|
3613
|
+
// tsconfig?" — and "-yes is too general", so the question is gone instead).
|
|
3540
3614
|
console.log(colors.yellow(`TypeScript project has no "build" script in ${pkg.name || cwd}`));
|
|
3541
3615
|
for (const s of subs)
|
|
3542
3616
|
console.log(colors.yellow(` sub-project ${s.dir}/ — ${s.reason}`));
|
|
3543
|
-
|
|
3544
|
-
if (!addIt)
|
|
3545
|
-
return null;
|
|
3617
|
+
console.log(colors.cyan(` Adding "build": "${newScript}" to ${pkg.name || path.basename(cwd)}'s package.json`));
|
|
3546
3618
|
if (!pkg.scripts)
|
|
3547
3619
|
pkg.scripts = {};
|
|
3548
3620
|
pkg.scripts.build = newScript;
|
|
@@ -5546,13 +5618,21 @@ export async function promptText(message, defaultValue) {
|
|
|
5546
5618
|
});
|
|
5547
5619
|
});
|
|
5548
5620
|
}
|
|
5549
|
-
/**
|
|
5550
|
-
*
|
|
5551
|
-
*
|
|
5552
|
-
*
|
|
5553
|
-
*
|
|
5554
|
-
*
|
|
5555
|
-
|
|
5621
|
+
/** Show `header` and the options numbered 1..N in display order, then read a
|
|
5622
|
+
* choice. Empty input takes the first option. Returns the chosen `action`.
|
|
5623
|
+
* 2026-09-14 14:30 EDT — Claude Code (Fable 5.1), at Bob's direction. The git
|
|
5624
|
+
* setup menus were hand-numbered strings with a fixed "Use local install only"
|
|
5625
|
+
* line, so a library (no `bin`) was offered an install that doLocalInstall
|
|
5626
|
+
* would then skip. Options are now data, so a line can be left out. */
|
|
5627
|
+
export function promptMenu(header, options) {
|
|
5628
|
+
const lines = options.map((o, i) => ` ${i + 1}) ${o.label}`);
|
|
5629
|
+
const message = `${header}\n${lines.join('\n')}\nChoice [1]:`;
|
|
5630
|
+
const valid = options.map((_, i) => String(i + 1));
|
|
5631
|
+
return promptChoice(message, [...valid, '']).then(choice => {
|
|
5632
|
+
const index = choice === '' ? 0 : Number(choice) - 1;
|
|
5633
|
+
return options[index].action;
|
|
5634
|
+
});
|
|
5635
|
+
}
|
|
5556
5636
|
export function promptChoice(message, choices) {
|
|
5557
5637
|
const rl = readline.createInterface({
|
|
5558
5638
|
input: process.stdin,
|
|
@@ -6711,15 +6791,21 @@ async function doLocalInstall(cwd, options) {
|
|
|
6711
6791
|
const pkg = readPackageJson(cwd);
|
|
6712
6792
|
const pkgName = pkg.name || path.basename(cwd);
|
|
6713
6793
|
const pkgVersion = pkg.version || '0.0.0';
|
|
6794
|
+
// A library has no bin, so there is nothing to install: say what actually happens
|
|
6795
|
+
// (deps built, notes recorded, no publish) instead of announcing an install and
|
|
6796
|
+
// then taking it back. 2026-09-14 17:50 EDT — Claude Code (Fable 5.1), at Bob's
|
|
6797
|
+
// direction: "why mention local install when there is no install".
|
|
6798
|
+
if (!pkg.bin) {
|
|
6799
|
+
console.log(colors.blue(`Local mode: ${pkgName}@${pkgVersion} — library, nothing to install`));
|
|
6800
|
+
console.log(colors.dim('Not transformed or published; file: deps left as-is'));
|
|
6801
|
+
console.log(colors.dim(' (switch back to the publish flow with -global)'));
|
|
6802
|
+
recordLocalChange(cwd, pkgVersion, dryRun);
|
|
6803
|
+
return true;
|
|
6804
|
+
}
|
|
6714
6805
|
console.log(colors.blue(`Local install: ${pkgName}@${pkgVersion}`));
|
|
6715
6806
|
console.log(colors.dim('Skipping transform/publish — installing with file: deps as-is'));
|
|
6716
6807
|
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
6808
|
recordLocalChange(cwd, pkgVersion, dryRun);
|
|
6719
|
-
if (!pkg.bin) {
|
|
6720
|
-
console.log(colors.dim(` (library — skipping global install)`));
|
|
6721
|
-
return true;
|
|
6722
|
-
}
|
|
6723
6809
|
if (dryRun) {
|
|
6724
6810
|
console.log(' [dry-run] Would run: npm install -g .');
|
|
6725
6811
|
if (wsl)
|
|
@@ -6856,10 +6942,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6856
6942
|
// any transform/publish, so the user can fix them once and run clean.
|
|
6857
6943
|
// Only run at the top level (not during recursive cascade or workspace).
|
|
6858
6944
|
if (!noPrescan && !cleanup && !options._fromDep && !options._fromWorkspace) {
|
|
6859
|
-
const
|
|
6945
|
+
const allIssues = prescanDepGraph(cwd, new Set(), verbose);
|
|
6946
|
+
// Unscoped deps are handled apart from the error/warning prompt below: nothing the
|
|
6947
|
+
// user answers could change the outcome, so nothing is asked. Report, remember,
|
|
6948
|
+
// stop. See reportUnscopedDeps.
|
|
6949
|
+
// 2026-09-14 17:05 EDT — Claude Code (Fable 5.1), at Bob's direction: "The message
|
|
6950
|
+
// should simply point it out won't be published to npm till the scope is fixed and
|
|
6951
|
+
// then remember I was warned. It shouldn't [need] a response because nothing would
|
|
6952
|
+
// be done."
|
|
6953
|
+
const unscoped = allIssues.filter(i => i.kind === 'unscoped');
|
|
6954
|
+
const issues = allIssues.filter(i => i.kind !== 'unscoped');
|
|
6860
6955
|
if (issues.length > 0) {
|
|
6861
|
-
const errors = issues.filter(i => i.severity === 'error');
|
|
6862
|
-
const warnings = issues.filter(i => i.severity === 'warning');
|
|
6863
6956
|
console.log(colors.yellow(`Prescan found ${issues.length} issue(s) in the file: dep graph:`));
|
|
6864
6957
|
for (const i of issues) {
|
|
6865
6958
|
const tag = i.severity === 'error' ? colors.red('ERROR') : colors.yellow('WARN');
|
|
@@ -6869,6 +6962,18 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6869
6962
|
console.log(colors.dim(` → ${i.suggestion}`));
|
|
6870
6963
|
}
|
|
6871
6964
|
console.log('');
|
|
6965
|
+
}
|
|
6966
|
+
// Always called: with nothing unscoped it only forgets deps that were noted
|
|
6967
|
+
// earlier and have since been scoped.
|
|
6968
|
+
reportUnscopedDeps(cwd, unscoped);
|
|
6969
|
+
if (unscoped.length > 0) {
|
|
6970
|
+
// The run stops here regardless of the other issues, so the continue prompt
|
|
6971
|
+
// below is skipped: it would ask a question whose answer changes nothing.
|
|
6972
|
+
return false;
|
|
6973
|
+
}
|
|
6974
|
+
if (issues.length > 0) {
|
|
6975
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
6976
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
6872
6977
|
if (errors.length > 0 && !force) {
|
|
6873
6978
|
console.error(colors.red(`Prescan aborted: ${errors.length} error(s). Fix them or rerun with --force.`));
|
|
6874
6979
|
return false;
|
|
@@ -7016,6 +7121,19 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7016
7121
|
const gitStatus = getGitStatus(cwd);
|
|
7017
7122
|
// Handle init mode
|
|
7018
7123
|
let justInitialized = false;
|
|
7124
|
+
// 2026-09-14 14:30 EDT — Claude Code (Fable 5.1), at Bob's direction. The git
|
|
7125
|
+
// setup menus below offer "Use local install only" — a global npm install of
|
|
7126
|
+
// this package. That is only meaningful for a CLI (a package with `bin`);
|
|
7127
|
+
// doLocalInstall skips a library anyway, so a library is not offered the line.
|
|
7128
|
+
const hasBin = !!readPackageJson(cwd).bin;
|
|
7129
|
+
const localInstallOption = hasBin
|
|
7130
|
+
? [{ label: 'Use local install only (skip git/publish)', action: 'local' }]
|
|
7131
|
+
: [];
|
|
7132
|
+
const switchToLocalInstall = () => {
|
|
7133
|
+
console.log(colors.dim('Switching to local-only mode...'));
|
|
7134
|
+
writeConfig(cwd, { ...configOptions, localInstall: true }, new Set(['localInstall']));
|
|
7135
|
+
return doLocalInstall(cwd, options);
|
|
7136
|
+
};
|
|
7019
7137
|
if (!gitStatus.isRepo) {
|
|
7020
7138
|
console.log('No git repository found.');
|
|
7021
7139
|
// Probe for an existing remote we could adopt history from.
|
|
@@ -7069,55 +7187,54 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7069
7187
|
// a reader who wanted the second line typed 2 and got something else. Bob:
|
|
7070
7188
|
// "I type 2 when I mean a because that's what humans do." Options are now
|
|
7071
7189
|
// numbered in display order; the ALL variant is 2, directly under the single.
|
|
7072
|
-
|
|
7190
|
+
// 2026-09-14 — Claude Code (Fable 5.1): menus are now promptMenu option
|
|
7191
|
+
// lists; the local-install line is present only for a package with `bin`.
|
|
7192
|
+
// The "No git repository found." header is printed once, above.
|
|
7193
|
+
let action;
|
|
7073
7194
|
if (adoptable) {
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
}
|
|
7085
|
-
if (choice === '5') {
|
|
7195
|
+
action = await promptMenu('How would you like to set up git?', [
|
|
7196
|
+
{ label: 'Adopt history from existing remote (recommended)', action: 'adopt' },
|
|
7197
|
+
{ label: 'Adopt for ALL remaining deps (don\'t ask again)', action: 'adopt-all' },
|
|
7198
|
+
{ label: 'Initialize fresh git repository', action: 'init' },
|
|
7199
|
+
...localInstallOption,
|
|
7200
|
+
{ label: 'Abort', action: 'abort' },
|
|
7201
|
+
]);
|
|
7202
|
+
if (action === 'local')
|
|
7203
|
+
return switchToLocalInstall();
|
|
7204
|
+
if (action === 'abort') {
|
|
7086
7205
|
console.log('Aborted. Run with --init to initialize.');
|
|
7087
7206
|
return false;
|
|
7088
7207
|
}
|
|
7089
|
-
if (
|
|
7208
|
+
if (action === 'adopt-all')
|
|
7090
7209
|
options.autoInit = true;
|
|
7091
|
-
if (
|
|
7210
|
+
if (action === 'init') {
|
|
7092
7211
|
const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
|
|
7093
7212
|
if (!success)
|
|
7094
7213
|
return false;
|
|
7095
7214
|
}
|
|
7096
7215
|
else {
|
|
7097
|
-
// '
|
|
7216
|
+
// 'adopt' or 'adopt-all'
|
|
7098
7217
|
const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
|
|
7099
7218
|
if (!success)
|
|
7100
7219
|
return false;
|
|
7101
7220
|
}
|
|
7102
7221
|
}
|
|
7103
7222
|
else {
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
}
|
|
7114
|
-
if (choice === '4') {
|
|
7223
|
+
action = await promptMenu('What would you like to do?', [
|
|
7224
|
+
{ label: 'Initialize git repository', action: 'init' },
|
|
7225
|
+
{ label: 'Initialize for ALL remaining deps (don\'t ask again)', action: 'init-all' },
|
|
7226
|
+
...localInstallOption,
|
|
7227
|
+
{ label: 'Abort', action: 'abort' },
|
|
7228
|
+
]);
|
|
7229
|
+
if (action === 'local')
|
|
7230
|
+
return switchToLocalInstall();
|
|
7231
|
+
if (action === 'abort') {
|
|
7115
7232
|
console.log('Aborted. Run with --init to initialize.');
|
|
7116
7233
|
return false;
|
|
7117
7234
|
}
|
|
7118
|
-
if (
|
|
7235
|
+
if (action === 'init-all')
|
|
7119
7236
|
options.autoInit = true;
|
|
7120
|
-
//
|
|
7237
|
+
// 'init' or 'init-all'
|
|
7121
7238
|
const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
|
|
7122
7239
|
if (!success)
|
|
7123
7240
|
return false;
|
|
@@ -7143,22 +7260,21 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7143
7260
|
else if (!gitStatus.hasRemote) {
|
|
7144
7261
|
// Git repo exists but no remote - need to create GitHub repo
|
|
7145
7262
|
if (!init && !options.autoInit && !publishDepsYes) {
|
|
7146
|
-
// 2026-09-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
return
|
|
7156
|
-
|
|
7157
|
-
if (choice === '4') {
|
|
7263
|
+
// 2026-09-14 — Claude Code (Fable 5.1): promptMenu option list; local-install
|
|
7264
|
+
// line only for a package with `bin` (see hasBin above).
|
|
7265
|
+
const action = await promptMenu('No git remote configured. What would you like to do?', [
|
|
7266
|
+
{ label: 'Create GitHub repository', action: 'create' },
|
|
7267
|
+
{ label: 'Create for ALL remaining deps (don\'t ask again)', action: 'create-all' },
|
|
7268
|
+
...localInstallOption,
|
|
7269
|
+
{ label: 'Abort', action: 'abort' },
|
|
7270
|
+
]);
|
|
7271
|
+
if (action === 'local')
|
|
7272
|
+
return switchToLocalInstall();
|
|
7273
|
+
if (action === 'abort') {
|
|
7158
7274
|
console.log('Aborted. Run with --init to set up GitHub repository.');
|
|
7159
7275
|
return false;
|
|
7160
7276
|
}
|
|
7161
|
-
if (
|
|
7277
|
+
if (action === 'create-all') {
|
|
7162
7278
|
options.autoInit = true;
|
|
7163
7279
|
}
|
|
7164
7280
|
}
|
|
@@ -7417,45 +7533,33 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7417
7533
|
publicConfirmed = true;
|
|
7418
7534
|
}
|
|
7419
7535
|
}
|
|
7536
|
+
// An unscoped name cannot be published privately, and npmglobalize does not rename a
|
|
7537
|
+
// package to fix that: the name is what every consumer imports by, so adding a scope
|
|
7538
|
+
// is the package owner's own project, not a publish-time side effect. This used to
|
|
7539
|
+
// ask "Add scope to package name?"; now it warns and assumes no, for a dependency in
|
|
7540
|
+
// the cascade and for the package itself alike. Publishing unscoped as PUBLIC takes an
|
|
7541
|
+
// explicit -npm public, or noprefix: true in .globalize.json5 (the escape hatch the
|
|
7542
|
+
// prescan honors too).
|
|
7543
|
+
// 2026-09-14 18:10 EDT — Claude Code (Fable 5.1), at Bob's direction: "It shouldn't try
|
|
7544
|
+
// to publish to a scope not in the package" and, on the prompt in the package itself,
|
|
7545
|
+
// "For libraries that is problematic since other programs are affected so just warn
|
|
7546
|
+
// and assume no."
|
|
7547
|
+
const unscopedAllowed = readConfig(cwd).noprefix === true;
|
|
7548
|
+
if (!isScoped && effectiveNpmVisibility !== 'public' && currentAccess !== 'public' && !unscopedAllowed) {
|
|
7549
|
+
const scopeHint = readUserNpmConfig().scope || '@scope';
|
|
7550
|
+
console.log(colors.yellow(`${pkg.name} is unscoped, so it cannot be published privately. Not publishing until the name in ${path.join(cwd, 'package.json')} has a scope (e.g., ${scopeHint}/${pkg.name}).`));
|
|
7551
|
+
console.log(colors.dim(` npmglobalize does not rename packages. To publish it unscoped and PUBLIC on purpose: -npm public, or noprefix: true in .globalize.json5.`));
|
|
7552
|
+
return false;
|
|
7553
|
+
}
|
|
7420
7554
|
if (effectiveNpmVisibility === 'private') {
|
|
7421
7555
|
// User explicitly wants private publication
|
|
7422
7556
|
if (!isScoped) {
|
|
7423
|
-
//
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
console.log(colors.yellow(`Private packages must be scoped (e.g., ${scopedExample})`));
|
|
7430
|
-
if (dryRun) {
|
|
7431
|
-
console.log(' [dry-run] Would prompt to add scope');
|
|
7432
|
-
return false;
|
|
7433
|
-
}
|
|
7434
|
-
const addScope = await confirm(`Add scope to package name?`, true);
|
|
7435
|
-
if (!addScope) {
|
|
7436
|
-
return false;
|
|
7437
|
-
}
|
|
7438
|
-
const scope = await promptText('Scope:', defaultScope);
|
|
7439
|
-
if (!scope) {
|
|
7440
|
-
console.error(colors.red('No scope provided. Aborting.'));
|
|
7441
|
-
return false;
|
|
7442
|
-
}
|
|
7443
|
-
// Normalize: ensure it starts with @
|
|
7444
|
-
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
7445
|
-
const newName = `${normalizedScope}/${pkg.name}`;
|
|
7446
|
-
pkg.name = newName;
|
|
7447
|
-
isScoped = true;
|
|
7448
|
-
currentAccess = checkNpmAccess(newName);
|
|
7449
|
-
writePackageJson(cwd, pkg);
|
|
7450
|
-
console.log(colors.green(`✓ Renamed package to ${newName}`));
|
|
7451
|
-
// Save scope to .userconfig if not already there
|
|
7452
|
-
if (!userConfig.scope) {
|
|
7453
|
-
const saveScope = await confirm(`Save scope "${normalizedScope}" to global config (${getUserConfigDir()}\\npm.json5)?`, true);
|
|
7454
|
-
if (saveScope) {
|
|
7455
|
-
writeUserNpmConfig({ scope: normalizedScope });
|
|
7456
|
-
console.log(colors.green(`✓ Saved default scope to ${getUserConfigDir()}\\npm.json5`));
|
|
7457
|
-
}
|
|
7458
|
-
}
|
|
7557
|
+
// Only reachable with noprefix: true (the unscoped guard above returned
|
|
7558
|
+
// otherwise). noprefix says "publish unscoped", private says "restricted",
|
|
7559
|
+
// and npm cannot do both. The rename prompt that used to live here is gone
|
|
7560
|
+
// (2026-09-14, see the guard above).
|
|
7561
|
+
console.log(colors.yellow(`${pkg.name} is unscoped (noprefix) and npm cannot restrict an unscoped package. Not publishing; use -npm public to publish it unscoped and PUBLIC.`));
|
|
7562
|
+
return false;
|
|
7459
7563
|
}
|
|
7460
7564
|
if (currentAccess === 'public') {
|
|
7461
7565
|
console.log(colors.yellow(`Package '${pkg.name}' is currently PUBLIC on npm. Converting to private...`));
|
|
@@ -7527,47 +7631,10 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7527
7631
|
console.log(colors.dim(` Use --npm public to make it public`));
|
|
7528
7632
|
}
|
|
7529
7633
|
else {
|
|
7530
|
-
// Unscoped new package
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|| (auth2.username ? `@${auth2.username}` : undefined);
|
|
7535
|
-
const scopedExample = defaultScope ? `${defaultScope}/${pkg.name}` : `@scope/${pkg.name}`;
|
|
7536
|
-
console.log(colors.yellow(`WARNING: Package '${pkg.name}' is unscoped and will be PUBLIC.`));
|
|
7537
|
-
console.log(colors.yellow(` Unscoped packages cannot be private on npm.`));
|
|
7538
|
-
if (dryRun) {
|
|
7539
|
-
console.log(` [dry-run] Would prompt to add scope (e.g., ${scopedExample})`);
|
|
7540
|
-
}
|
|
7541
|
-
else {
|
|
7542
|
-
const addScope = await confirm(`Add scope to make it private (e.g., ${scopedExample})?`, true);
|
|
7543
|
-
if (addScope) {
|
|
7544
|
-
const scope = await promptText('Scope:', defaultScope);
|
|
7545
|
-
if (scope) {
|
|
7546
|
-
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
7547
|
-
const newName = `${normalizedScope}/${pkg.name}`;
|
|
7548
|
-
pkg.name = newName;
|
|
7549
|
-
isScoped = true;
|
|
7550
|
-
currentAccess = checkNpmAccess(newName);
|
|
7551
|
-
writePackageJson(cwd, pkg);
|
|
7552
|
-
console.log(colors.green(`✓ Renamed package to ${newName}`));
|
|
7553
|
-
if (!ucfg.scope) {
|
|
7554
|
-
const saveScope = await confirm(`Save scope "${normalizedScope}" to global config (${getUserConfigDir()}\\npm.json5)?`, true);
|
|
7555
|
-
if (saveScope) {
|
|
7556
|
-
writeUserNpmConfig({ scope: normalizedScope });
|
|
7557
|
-
console.log(colors.green(`✓ Saved default scope to ${getUserConfigDir()}\\npm.json5`));
|
|
7558
|
-
}
|
|
7559
|
-
}
|
|
7560
|
-
}
|
|
7561
|
-
else {
|
|
7562
|
-
console.log(colors.yellow(` No scope provided. Continuing as public.`));
|
|
7563
|
-
console.log(colors.yellow(` Use --npm public to suppress this prompt.`));
|
|
7564
|
-
}
|
|
7565
|
-
}
|
|
7566
|
-
else {
|
|
7567
|
-
console.log(colors.yellow(` Continuing as public.`));
|
|
7568
|
-
console.log(colors.yellow(` Use --npm public to suppress this prompt.`));
|
|
7569
|
-
}
|
|
7570
|
-
}
|
|
7634
|
+
// Unscoped new package. Only reachable with noprefix: true (the unscoped
|
|
7635
|
+
// guard above returned otherwise), so publishing it PUBLIC is the stated
|
|
7636
|
+
// intent. The rename prompt that used to live here is gone (2026-09-14).
|
|
7637
|
+
console.log(colors.yellow(`Package '${pkg.name}' is unscoped (noprefix) and will publish as PUBLIC — npm cannot restrict an unscoped package.`));
|
|
7571
7638
|
}
|
|
7572
7639
|
}
|
|
7573
7640
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/npmglobalize",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.229",
|
|
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.
|
|
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.
|
|
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",
|