@bobfrankston/npmglobalize 1.0.91 → 1.0.93
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/.claude/settings.local.json +6 -1
- package/cli.js +11 -4
- package/lib.d.ts +11 -0
- package/lib.js +295 -35
- package/package.json +12 -2
|
@@ -25,7 +25,12 @@
|
|
|
25
25
|
"Bash(cmd.exe /c \"dir /a /b y:\\\\dev\\\\homecontrol\\\\utils\\\\addone\")",
|
|
26
26
|
"Bash(icacls:*)",
|
|
27
27
|
"Bash(npm install:*)",
|
|
28
|
-
"Bash(echo:*)"
|
|
28
|
+
"Bash(echo:*)",
|
|
29
|
+
"Bash(onboard --version:*)",
|
|
30
|
+
"Bash(onboard --help:*)",
|
|
31
|
+
"Bash(npmglobalize:*)",
|
|
32
|
+
"Bash(npm whoami:*)",
|
|
33
|
+
"Bash(where npmglobalize:*)"
|
|
29
34
|
]
|
|
30
35
|
}
|
|
31
36
|
}
|
package/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* npmglobalize CLI - Transform file: dependencies to npm versions for publishing
|
|
4
4
|
*/
|
|
5
|
-
import { globalize, globalizeWorkspace, readConfig, readPackageJson, writeConfig, writePackageJson, confirm } from './lib.js';
|
|
5
|
+
import { globalize, globalizeWorkspace, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, confirm } from './lib.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { styleText } from 'util';
|
|
@@ -402,11 +402,18 @@ export async function main() {
|
|
|
402
402
|
writePackageJson(cwd, pkg);
|
|
403
403
|
console.log(`Updated ${path.join(cwd, 'package.json')} scripts:`);
|
|
404
404
|
changes.forEach(c => console.log(` ${c}`));
|
|
405
|
-
|
|
405
|
+
// If no other flags need processing, exit; otherwise fall through
|
|
406
|
+
if (!cliOptions.install && !cliOptions.link && !cliOptions.local) {
|
|
407
|
+
process.exit(0);
|
|
408
|
+
}
|
|
406
409
|
}
|
|
407
|
-
// Load config
|
|
410
|
+
// Load config: global userconfig → per-project .globalize.json5 → CLI (each overrides previous)
|
|
411
|
+
const userNpmConfig = readUserNpmConfig();
|
|
412
|
+
const globalDefaults = {};
|
|
413
|
+
if (userNpmConfig.npmVisibility)
|
|
414
|
+
globalDefaults.npmVisibility = userNpmConfig.npmVisibility;
|
|
408
415
|
const configOptions = readConfig(cwd);
|
|
409
|
-
const options = { ...configOptions, ...cliOptions };
|
|
416
|
+
const options = { ...globalDefaults, ...configOptions, ...cliOptions };
|
|
410
417
|
// Persist explicitly set CLI flags to .globalize.json5
|
|
411
418
|
if (cliOptions.explicitKeys.size > 0 && !cliOptions.once) {
|
|
412
419
|
const persistable = { ...configOptions };
|
package/lib.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Current approach uses synchronous child_process calls for stability and simplicity.
|
|
10
10
|
* Consider library-based approach if async operations or cross-platform issues arise.
|
|
11
11
|
*/
|
|
12
|
+
import { NpmCommonConfig } from '@bobfrankston/userconfig';
|
|
12
13
|
/** Options for the globalize operation */
|
|
13
14
|
export interface GlobalizeOptions {
|
|
14
15
|
/** Bump type: patch (default), minor, major */
|
|
@@ -94,6 +95,14 @@ export interface WorkspaceResult {
|
|
|
94
95
|
}
|
|
95
96
|
/** Read and parse package.json from a directory */
|
|
96
97
|
export declare function readPackageJson(dir: string): any;
|
|
98
|
+
/** Global npm config from %USERPROFILE%\.userconfig\npm.json5 — via @bobfrankston/userconfig */
|
|
99
|
+
export type UserNpmConfig = NpmCommonConfig;
|
|
100
|
+
/** Get the .userconfig directory path (%USERPROFILE%\.userconfig) */
|
|
101
|
+
export declare function getUserConfigDir(): string;
|
|
102
|
+
/** Read global npm config from %USERPROFILE%\.userconfig\npm.json5 */
|
|
103
|
+
export declare function readUserNpmConfig(): UserNpmConfig;
|
|
104
|
+
/** Write global npm config to %USERPROFILE%\.userconfig\npm.json5 */
|
|
105
|
+
export declare function writeUserNpmConfig(config: UserNpmConfig): void;
|
|
97
106
|
/** Read .globalize.json5 config file */
|
|
98
107
|
export declare function readConfig(dir: string): Partial<GlobalizeOptions>;
|
|
99
108
|
/** Write .globalize.json5 config file */
|
|
@@ -205,6 +214,8 @@ export declare function getGitStatus(cwd: string): GitStatus;
|
|
|
205
214
|
export declare function validatePackageJson(pkg: any): string[];
|
|
206
215
|
/** Prompt user for confirmation */
|
|
207
216
|
export declare function confirm(message: string, defaultYes?: boolean): Promise<boolean>;
|
|
217
|
+
/** Prompt user for free text input */
|
|
218
|
+
export declare function promptText(message: string, defaultValue?: string): Promise<string>;
|
|
208
219
|
/** Prompt user for multiple choice */
|
|
209
220
|
export declare function promptChoice(message: string, choices: string[]): Promise<string | null>;
|
|
210
221
|
/** Initialize git repository */
|
package/lib.js
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
import fs from 'fs';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { execSync, spawnSync } from 'child_process';
|
|
15
|
+
import { readConfig as readUserConfig, writeConfig as writeUserConfig, configDir } from '@bobfrankston/userconfig';
|
|
16
|
+
/** Wrapper for spawnSync that avoids DEP0190 (args + shell: true).
|
|
17
|
+
* When shell is true, joins cmd+args into a single command string. */
|
|
18
|
+
function spawnSafe(cmd, args, options = {}) {
|
|
19
|
+
const opts = { ...options, encoding: 'utf-8' };
|
|
20
|
+
if (opts.shell && args.length > 0) {
|
|
21
|
+
// Join into a single command string to avoid DEP0190
|
|
22
|
+
const cmdStr = [cmd, ...args].map(a => /[\s"&|<>^]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a).join(' ');
|
|
23
|
+
return spawnSync(cmdStr, opts);
|
|
24
|
+
}
|
|
25
|
+
return spawnSync(cmd, args, opts);
|
|
26
|
+
}
|
|
15
27
|
import readline from 'readline';
|
|
16
28
|
import libversion from 'libnpmversion';
|
|
17
29
|
import JSON5 from 'json5';
|
|
@@ -77,6 +89,20 @@ export function readPackageJson(dir) {
|
|
|
77
89
|
throw new Error(`Failed to parse ${pkgPath}: ${error.message}`);
|
|
78
90
|
}
|
|
79
91
|
}
|
|
92
|
+
/** Get the .userconfig directory path (%USERPROFILE%\.userconfig) */
|
|
93
|
+
export function getUserConfigDir() {
|
|
94
|
+
return configDir;
|
|
95
|
+
}
|
|
96
|
+
/** Read global npm config from %USERPROFILE%\.userconfig\npm.json5 */
|
|
97
|
+
export function readUserNpmConfig() {
|
|
98
|
+
return readUserConfig();
|
|
99
|
+
}
|
|
100
|
+
/** Write global npm config to %USERPROFILE%\.userconfig\npm.json5 */
|
|
101
|
+
export function writeUserNpmConfig(config) {
|
|
102
|
+
const existing = readUserConfig();
|
|
103
|
+
const merged = { ...existing, ...config };
|
|
104
|
+
writeUserConfig(merged);
|
|
105
|
+
}
|
|
80
106
|
/** Read .globalize.json5 config file */
|
|
81
107
|
export function readConfig(dir) {
|
|
82
108
|
const configPath = path.join(dir, '.globalize.json5');
|
|
@@ -203,7 +229,7 @@ export function isFileRef(value) {
|
|
|
203
229
|
/** Get the latest version of a package from npm */
|
|
204
230
|
export function getLatestVersion(packageName) {
|
|
205
231
|
try {
|
|
206
|
-
const result =
|
|
232
|
+
const result = spawnSafe('npm', ['view', packageName, 'version'], {
|
|
207
233
|
encoding: 'utf-8',
|
|
208
234
|
stdio: 'pipe',
|
|
209
235
|
shell: true // Required on Windows to find npm.cmd
|
|
@@ -220,7 +246,7 @@ export function getLatestVersion(packageName) {
|
|
|
220
246
|
/** Check if a specific version of a package exists on npm */
|
|
221
247
|
export function checkVersionExists(packageName, version) {
|
|
222
248
|
try {
|
|
223
|
-
const result =
|
|
249
|
+
const result = spawnSafe('npm', ['view', `${packageName}@${version}`, 'version'], {
|
|
224
250
|
encoding: 'utf-8',
|
|
225
251
|
stdio: 'pipe',
|
|
226
252
|
shell: true // Required on Windows to find npm.cmd
|
|
@@ -234,7 +260,7 @@ export function checkVersionExists(packageName, version) {
|
|
|
234
260
|
/** Check if a package exists on npm (any version) */
|
|
235
261
|
export function checkPackageExists(packageName) {
|
|
236
262
|
try {
|
|
237
|
-
const result =
|
|
263
|
+
const result = spawnSafe('npm', ['view', packageName, 'version'], {
|
|
238
264
|
encoding: 'utf-8',
|
|
239
265
|
stdio: 'pipe',
|
|
240
266
|
shell: true // Required on Windows to find npm.cmd
|
|
@@ -252,14 +278,14 @@ export function checkNpmAccess(packageName) {
|
|
|
252
278
|
if (packageName.startsWith('@')) {
|
|
253
279
|
// First check if the package actually exists on npm
|
|
254
280
|
// npm access returns read-write for unpublished packages in owned scopes
|
|
255
|
-
const viewResult =
|
|
281
|
+
const viewResult = spawnSafe('npm', ['view', packageName, 'name'], {
|
|
256
282
|
encoding: 'utf-8',
|
|
257
283
|
stdio: 'pipe',
|
|
258
284
|
shell: true
|
|
259
285
|
});
|
|
260
286
|
if (viewResult.status === 0 && viewResult.stdout && viewResult.stdout.trim()) {
|
|
261
287
|
// Package exists - check public/private status
|
|
262
|
-
const accessResult =
|
|
288
|
+
const accessResult = spawnSafe('npm', ['access', 'get', 'status', packageName], {
|
|
263
289
|
encoding: 'utf-8',
|
|
264
290
|
stdio: 'pipe',
|
|
265
291
|
shell: true
|
|
@@ -273,7 +299,7 @@ export function checkNpmAccess(packageName) {
|
|
|
273
299
|
return 'restricted'; // Exists but not publicly accessible
|
|
274
300
|
}
|
|
275
301
|
// Package not viewable - check if it's restricted or unpublished
|
|
276
|
-
const checkResult =
|
|
302
|
+
const checkResult = spawnSafe('npm', ['view', packageName, '--json'], {
|
|
277
303
|
encoding: 'utf-8',
|
|
278
304
|
stdio: 'pipe',
|
|
279
305
|
shell: true
|
|
@@ -289,13 +315,36 @@ export function checkNpmAccess(packageName) {
|
|
|
289
315
|
return 'restricted'; // Default for scoped
|
|
290
316
|
}
|
|
291
317
|
else {
|
|
292
|
-
// Unscoped packages are always public if they exist
|
|
293
|
-
const result =
|
|
318
|
+
// Unscoped packages are always public if they exist — but only if we own them
|
|
319
|
+
const result = spawnSafe('npm', ['view', packageName, 'name'], {
|
|
294
320
|
encoding: 'utf-8',
|
|
295
321
|
stdio: 'pipe',
|
|
296
322
|
shell: true
|
|
297
323
|
});
|
|
298
324
|
if (result.status === 0 && result.stdout && result.stdout.trim()) {
|
|
325
|
+
// Package exists on npm — check if current user is a maintainer
|
|
326
|
+
const maintResult = spawnSafe('npm', ['view', packageName, 'maintainers', '--json'], {
|
|
327
|
+
encoding: 'utf-8',
|
|
328
|
+
stdio: 'pipe',
|
|
329
|
+
shell: true
|
|
330
|
+
});
|
|
331
|
+
if (maintResult.status === 0 && maintResult.stdout) {
|
|
332
|
+
const auth = checkNpmAuth();
|
|
333
|
+
if (auth.username) {
|
|
334
|
+
try {
|
|
335
|
+
const maintainers = JSON.parse(maintResult.stdout.trim());
|
|
336
|
+
const names = Array.isArray(maintainers)
|
|
337
|
+
? maintainers.map((m) => typeof m === 'string' ? m.replace(/ <.*/, '') : m.name)
|
|
338
|
+
: [];
|
|
339
|
+
if (!names.includes(auth.username)) {
|
|
340
|
+
return null; // Exists but we don't own it
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// Parse failed — fall through to return public
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
299
348
|
return 'public';
|
|
300
349
|
}
|
|
301
350
|
return null;
|
|
@@ -510,6 +559,55 @@ export function topologicalSort(graph) {
|
|
|
510
559
|
}
|
|
511
560
|
return result;
|
|
512
561
|
}
|
|
562
|
+
/** Check if a published npm package has broken deps (file: paths or unpublished transitive deps).
|
|
563
|
+
* Also checks local file: deps for unpublished versions. */
|
|
564
|
+
function hasUnpublishedTransitiveDeps(packageName, pkg, baseDir, verbose) {
|
|
565
|
+
// Check 1: Does the npm-published version have file: paths in its deps?
|
|
566
|
+
try {
|
|
567
|
+
const result = spawnSafe('npm', ['view', packageName, 'dependencies', '--json'], {
|
|
568
|
+
encoding: 'utf-8',
|
|
569
|
+
stdio: 'pipe',
|
|
570
|
+
shell: true
|
|
571
|
+
});
|
|
572
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
573
|
+
const npmDeps = JSON.parse(result.stdout.trim());
|
|
574
|
+
for (const [depName, depValue] of Object.entries(npmDeps)) {
|
|
575
|
+
if (isFileRef(depValue)) {
|
|
576
|
+
if (verbose) {
|
|
577
|
+
console.log(colors.yellow(` npm copy has file: dep ${depName} → ${depValue}`));
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// Can't check npm — fall through to local check
|
|
586
|
+
}
|
|
587
|
+
// Check 2: Do local file: deps have unpublished versions?
|
|
588
|
+
for (const key of DEP_KEYS) {
|
|
589
|
+
if (!pkg[key])
|
|
590
|
+
continue;
|
|
591
|
+
for (const [depName, depValue] of Object.entries(pkg[key])) {
|
|
592
|
+
if (!isFileRef(depValue))
|
|
593
|
+
continue;
|
|
594
|
+
try {
|
|
595
|
+
const depPath = resolveFilePath(depValue, baseDir);
|
|
596
|
+
const depPkg = readPackageJson(depPath);
|
|
597
|
+
if (!checkVersionExists(depName, depPkg.version)) {
|
|
598
|
+
if (verbose) {
|
|
599
|
+
console.log(colors.yellow(` transitive dep ${depName}@${depPkg.version} not on npm`));
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
513
611
|
/** Transform file: dependencies to npm versions */
|
|
514
612
|
export function transformDeps(pkg, baseDir, verbose = false, forcePublish = false) {
|
|
515
613
|
let transformed = false;
|
|
@@ -564,8 +662,15 @@ export function transformDeps(pkg, baseDir, verbose = false, forcePublish = fals
|
|
|
564
662
|
console.log(colors.red(` ⚠ ${name}@${targetVersion} not found on npm (local: ${value})`));
|
|
565
663
|
}
|
|
566
664
|
}
|
|
567
|
-
else
|
|
568
|
-
|
|
665
|
+
else {
|
|
666
|
+
if (verbose) {
|
|
667
|
+
console.log(colors.green(` ✓ ${name}@${targetVersion} exists on npm`));
|
|
668
|
+
}
|
|
669
|
+
// Check transitive file: deps — if any are unpublished, this dep needs republishing
|
|
670
|
+
if (hasUnpublishedTransitiveDeps(name, targetPkg, targetPath, verbose)) {
|
|
671
|
+
unpublished.push({ name, version: targetVersion, path: targetPath });
|
|
672
|
+
console.log(colors.yellow(` ⟳ ${name}@${targetVersion} has unpublished transitive deps — will republish`));
|
|
673
|
+
}
|
|
569
674
|
}
|
|
570
675
|
pkg[key][name] = npmVersion;
|
|
571
676
|
if (verbose) {
|
|
@@ -604,7 +709,7 @@ export function hasBackup(pkg) {
|
|
|
604
709
|
/** Get the latest git tag (if any) */
|
|
605
710
|
export function getLatestGitTag(cwd) {
|
|
606
711
|
try {
|
|
607
|
-
const result =
|
|
712
|
+
const result = spawnSafe('git', ['describe', '--tags', '--abbrev=0'], {
|
|
608
713
|
encoding: 'utf-8',
|
|
609
714
|
stdio: 'pipe',
|
|
610
715
|
cwd
|
|
@@ -621,7 +726,7 @@ export function getLatestGitTag(cwd) {
|
|
|
621
726
|
/** Check if a git tag exists */
|
|
622
727
|
export function gitTagExists(cwd, tag) {
|
|
623
728
|
try {
|
|
624
|
-
const result =
|
|
729
|
+
const result = spawnSafe('git', ['tag', '-l', tag], {
|
|
625
730
|
encoding: 'utf-8',
|
|
626
731
|
stdio: 'pipe',
|
|
627
732
|
cwd
|
|
@@ -635,7 +740,7 @@ export function gitTagExists(cwd, tag) {
|
|
|
635
740
|
/** Delete a git tag */
|
|
636
741
|
export function deleteGitTag(cwd, tag) {
|
|
637
742
|
try {
|
|
638
|
-
const result =
|
|
743
|
+
const result = spawnSafe('git', ['tag', '-d', tag], {
|
|
639
744
|
encoding: 'utf-8',
|
|
640
745
|
stdio: 'pipe',
|
|
641
746
|
cwd
|
|
@@ -649,7 +754,7 @@ export function deleteGitTag(cwd, tag) {
|
|
|
649
754
|
/** Get all git tags */
|
|
650
755
|
export function getAllGitTags(cwd) {
|
|
651
756
|
try {
|
|
652
|
-
const result =
|
|
757
|
+
const result = spawnSafe('git', ['tag', '-l'], {
|
|
653
758
|
encoding: 'utf-8',
|
|
654
759
|
stdio: 'pipe',
|
|
655
760
|
cwd
|
|
@@ -733,7 +838,7 @@ function waitForNpmVersion(pkgName, version, maxWaitMs = 60000) {
|
|
|
733
838
|
const maxAttempts = Math.ceil(maxWaitMs / interval);
|
|
734
839
|
process.stdout.write(`Waiting for ${pkgName}@${version} on npm registry`);
|
|
735
840
|
for (let i = 0; i < maxAttempts; i++) {
|
|
736
|
-
const result =
|
|
841
|
+
const result = spawnSafe('npm', ['view', `${pkgName}@${version}`, 'version'], {
|
|
737
842
|
shell: process.platform === 'win32',
|
|
738
843
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
739
844
|
encoding: 'utf-8'
|
|
@@ -743,7 +848,7 @@ function waitForNpmVersion(pkgName, version, maxWaitMs = 60000) {
|
|
|
743
848
|
return true;
|
|
744
849
|
}
|
|
745
850
|
process.stdout.write('.');
|
|
746
|
-
|
|
851
|
+
spawnSafe(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '3', '/nobreak'] : ['3'], { stdio: 'pipe', shell: process.platform === 'win32' });
|
|
747
852
|
}
|
|
748
853
|
process.stdout.write(' timed out\n');
|
|
749
854
|
return false;
|
|
@@ -758,7 +863,7 @@ export function runCommand(cmd, args, options = {}) {
|
|
|
758
863
|
if (!silent) {
|
|
759
864
|
console.log(colors.dim(`[DEBUG] Running: ${cmd} ${args.join(' ')}`));
|
|
760
865
|
}
|
|
761
|
-
const result =
|
|
866
|
+
const result = spawnSafe(cmd, args, {
|
|
762
867
|
encoding: 'utf-8',
|
|
763
868
|
stdio: silent ? 'pipe' : 'inherit',
|
|
764
869
|
cwd,
|
|
@@ -811,7 +916,7 @@ function getGitHubRepo(pkg) {
|
|
|
811
916
|
/** Run a command and throw on failure */
|
|
812
917
|
export function runCommandOrThrow(cmd, args, options = {}) {
|
|
813
918
|
const needsShell = cmd === 'npm' || cmd === 'npm.cmd' || cmd === 'gh';
|
|
814
|
-
const result =
|
|
919
|
+
const result = spawnSafe(cmd, args, {
|
|
815
920
|
encoding: 'utf-8',
|
|
816
921
|
stdio: 'pipe',
|
|
817
922
|
cwd: options.cwd,
|
|
@@ -826,7 +931,7 @@ export function runCommandOrThrow(cmd, args, options = {}) {
|
|
|
826
931
|
if (cmd === 'git' && stderr.includes('dubious ownership')) {
|
|
827
932
|
console.log(colors.yellow('Git "dubious ownership" error — directory owner SID differs from current user.'));
|
|
828
933
|
console.log(colors.yellow('Adding safe.directory \'*\' to global git config...'));
|
|
829
|
-
const fix =
|
|
934
|
+
const fix = spawnSafe('git', ['config', '--global', '--add', 'safe.directory', '*'], {
|
|
830
935
|
encoding: 'utf-8',
|
|
831
936
|
stdio: 'pipe',
|
|
832
937
|
shell: true
|
|
@@ -834,7 +939,7 @@ export function runCommandOrThrow(cmd, args, options = {}) {
|
|
|
834
939
|
if (fix.status === 0) {
|
|
835
940
|
console.log(colors.green('✓ Fixed. Retrying...'));
|
|
836
941
|
// Retry the original command
|
|
837
|
-
const retry =
|
|
942
|
+
const retry = spawnSafe(cmd, args, {
|
|
838
943
|
encoding: 'utf-8',
|
|
839
944
|
stdio: 'pipe',
|
|
840
945
|
cwd: options.cwd,
|
|
@@ -980,6 +1085,21 @@ export async function confirm(message, defaultYes = false) {
|
|
|
980
1085
|
});
|
|
981
1086
|
});
|
|
982
1087
|
}
|
|
1088
|
+
/** Prompt user for free text input */
|
|
1089
|
+
export async function promptText(message, defaultValue) {
|
|
1090
|
+
const rl = readline.createInterface({
|
|
1091
|
+
input: process.stdin,
|
|
1092
|
+
output: process.stdout
|
|
1093
|
+
});
|
|
1094
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
1095
|
+
return new Promise((resolve) => {
|
|
1096
|
+
rl.question(`${message}${suffix} `, (answer) => {
|
|
1097
|
+
rl.close();
|
|
1098
|
+
const a = answer.trim();
|
|
1099
|
+
resolve(a || defaultValue || '');
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
983
1103
|
/** Prompt user for multiple choice */
|
|
984
1104
|
export async function promptChoice(message, choices) {
|
|
985
1105
|
const rl = readline.createInterface({
|
|
@@ -1004,7 +1124,7 @@ function checkNpmAuth() {
|
|
|
1004
1124
|
try {
|
|
1005
1125
|
// Must use shell:true on Windows to find npm.cmd in PATH
|
|
1006
1126
|
// Must pass env: process.env to inherit NPM_TOKEN environment variable
|
|
1007
|
-
const result =
|
|
1127
|
+
const result = spawnSafe('npm', ['whoami'], {
|
|
1008
1128
|
encoding: 'utf-8',
|
|
1009
1129
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1010
1130
|
env: process.env,
|
|
@@ -1323,7 +1443,7 @@ export async function initGit(cwd, visibility, dryRun) {
|
|
|
1323
1443
|
// git init
|
|
1324
1444
|
runCommandOrThrow('git', ['init'], { cwd });
|
|
1325
1445
|
// Check for dubious ownership (git init succeeds but subsequent commands fail)
|
|
1326
|
-
const ownerCheck =
|
|
1446
|
+
const ownerCheck = spawnSafe('git', ['rev-parse', '--git-dir'], {
|
|
1327
1447
|
encoding: 'utf-8',
|
|
1328
1448
|
stdio: 'pipe',
|
|
1329
1449
|
cwd
|
|
@@ -1331,7 +1451,7 @@ export async function initGit(cwd, visibility, dryRun) {
|
|
|
1331
1451
|
if (ownerCheck.stderr && ownerCheck.stderr.includes('dubious ownership')) {
|
|
1332
1452
|
console.log(colors.yellow('Git "dubious ownership" error — directory owner SID differs from current user.'));
|
|
1333
1453
|
console.log(colors.yellow('Adding safe.directory \'*\' to global git config...'));
|
|
1334
|
-
const fix =
|
|
1454
|
+
const fix = spawnSafe('git', ['config', '--global', '--add', 'safe.directory', '*'], {
|
|
1335
1455
|
encoding: 'utf-8',
|
|
1336
1456
|
stdio: 'pipe',
|
|
1337
1457
|
shell: true
|
|
@@ -1349,7 +1469,7 @@ export async function initGit(cwd, visibility, dryRun) {
|
|
|
1349
1469
|
console.log(' ✓ Configured git for LF line endings');
|
|
1350
1470
|
runCommandOrThrow('git', ['add', '-A'], { cwd });
|
|
1351
1471
|
// Only commit if there are staged changes (repo may already have commits)
|
|
1352
|
-
const staged =
|
|
1472
|
+
const staged = spawnSafe('git', ['diff', '--cached', '--quiet'], { cwd });
|
|
1353
1473
|
if (staged.status !== 0) {
|
|
1354
1474
|
runCommandOrThrow('git', ['commit', '-m', 'Initial commit'], { cwd });
|
|
1355
1475
|
}
|
|
@@ -1389,7 +1509,7 @@ export function runNpmAudit(cwd, fix = false, verbose = false) {
|
|
|
1389
1509
|
}
|
|
1390
1510
|
}
|
|
1391
1511
|
// Always run audit to report status
|
|
1392
|
-
const auditResult =
|
|
1512
|
+
const auditResult = spawnSafe('npm', ['audit', '--json'], {
|
|
1393
1513
|
cwd,
|
|
1394
1514
|
encoding: 'utf-8',
|
|
1395
1515
|
stdio: 'pipe',
|
|
@@ -1475,6 +1595,67 @@ export function getToolVersion() {
|
|
|
1475
1595
|
return 'unknown';
|
|
1476
1596
|
}
|
|
1477
1597
|
}
|
|
1598
|
+
/** Offer to add a bin field if missing — returns true if bin was added or already existed */
|
|
1599
|
+
async function offerAddBin(cwd, pkg) {
|
|
1600
|
+
if (pkg.bin)
|
|
1601
|
+
return true;
|
|
1602
|
+
// Determine likely entry point
|
|
1603
|
+
const mainFile = pkg.main || 'index.js';
|
|
1604
|
+
const cmdName = (pkg.name || path.basename(cwd)).replace(/^@[^/]+\//, '');
|
|
1605
|
+
console.log(colors.yellow('No bin field — this package won\'t install as a CLI command.'));
|
|
1606
|
+
const choice = await promptChoice(`Add bin field to make it a CLI tool?\n 1) Yes, use "${cmdName}" → "${mainFile}" (default)\n 2) No, install as library link\n 3) Abort\nChoice:`, ['1', '2', '3', '']);
|
|
1607
|
+
if (choice === '3')
|
|
1608
|
+
return false;
|
|
1609
|
+
if (choice === '2') {
|
|
1610
|
+
console.log(colors.dim('Installing as library link (no bin)...'));
|
|
1611
|
+
return true;
|
|
1612
|
+
}
|
|
1613
|
+
// choice is '1' or '' (default)
|
|
1614
|
+
pkg.bin = { [cmdName]: mainFile };
|
|
1615
|
+
writePackageJson(cwd, pkg);
|
|
1616
|
+
console.log(colors.green(`✓ Added bin: { "${cmdName}": "${mainFile}" }`));
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
/** Perform local-only install (npm install -g .) — extracted for reuse from git-init prompts */
|
|
1620
|
+
async function doLocalInstall(cwd, options) {
|
|
1621
|
+
const { dryRun = false, wsl = false } = options;
|
|
1622
|
+
const pkg = readPackageJson(cwd);
|
|
1623
|
+
const pkgName = pkg.name || path.basename(cwd);
|
|
1624
|
+
const pkgVersion = pkg.version || '0.0.0';
|
|
1625
|
+
console.log(colors.blue(`Local install: ${pkgName}@${pkgVersion}`));
|
|
1626
|
+
console.log(colors.dim('Skipping transform/publish — installing with file: deps as-is'));
|
|
1627
|
+
if (!pkg.bin) {
|
|
1628
|
+
const proceed = await offerAddBin(cwd, pkg);
|
|
1629
|
+
if (!proceed)
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
if (dryRun) {
|
|
1633
|
+
console.log(' [dry-run] Would run: npm install -g .');
|
|
1634
|
+
if (wsl)
|
|
1635
|
+
console.log(' [dry-run] Would run: wsl npm install -g .');
|
|
1636
|
+
return true;
|
|
1637
|
+
}
|
|
1638
|
+
const result = runCommand('npm', ['install', '-g', '.'], { cwd, silent: false });
|
|
1639
|
+
if (result.success) {
|
|
1640
|
+
console.log(colors.green(`✓ Installed locally: ${pkgName}@${pkgVersion}`));
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
console.error(colors.red(`✗ Local install failed`));
|
|
1644
|
+
console.error(colors.yellow(' Try running manually: npm install -g .'));
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
if (wsl) {
|
|
1648
|
+
console.log(`Installing ${pkgName} in WSL (local)...`);
|
|
1649
|
+
const wslResult = runCommand('wsl', ['npm', 'install', '-g', '.'], { cwd, silent: false });
|
|
1650
|
+
if (wslResult.success) {
|
|
1651
|
+
console.log(colors.green(`✓ Installed in WSL: ${pkgName}@${pkgVersion}`));
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
console.error(colors.yellow('✗ WSL install failed (is npm installed in WSL?)'));
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1478
1659
|
export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
1479
1660
|
const { bump = 'patch', noPublish = false, cleanup = false, install = false, link = false, wsl = false, force = false, files = true, dryRun = false, quiet = true, verbose = false, init = false, gitVisibility = 'private', npmVisibility = 'private', message, conform = false, asis = false, updateDeps = false, updateMajor = false, publishDeps = true, // Default to publishing deps for safety
|
|
1480
1661
|
forcePublish = false, fix = false, fixTags = false, rebase = false, show = false, local = false } = options;
|
|
@@ -1547,8 +1728,9 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
1547
1728
|
console.log(colors.blue(`Local install: ${pkgName}@${pkgVersion}`));
|
|
1548
1729
|
console.log(colors.dim('Skipping transform/publish — installing with file: deps as-is'));
|
|
1549
1730
|
if (!pkg.bin) {
|
|
1550
|
-
|
|
1551
|
-
|
|
1731
|
+
const proceed = await offerAddBin(cwd, pkg);
|
|
1732
|
+
if (!proceed)
|
|
1733
|
+
return false;
|
|
1552
1734
|
}
|
|
1553
1735
|
if (dryRun) {
|
|
1554
1736
|
console.log(' [dry-run] Would run: npm install -g .');
|
|
@@ -1658,11 +1840,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
1658
1840
|
console.log(' [dry-run] Would initialize git repository');
|
|
1659
1841
|
}
|
|
1660
1842
|
else if (!init) {
|
|
1661
|
-
const
|
|
1662
|
-
if (
|
|
1843
|
+
const choice = await promptChoice('No git repository found. What would you like to do?\n 1) Initialize git repository (default)\n 2) Use local install only (skip git/publish)\n 3) Abort\nChoice:', ['1', '2', '3', '']);
|
|
1844
|
+
if (choice === '2') {
|
|
1845
|
+
console.log(colors.dim('Switching to local-only mode...'));
|
|
1846
|
+
writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
|
|
1847
|
+
return doLocalInstall(cwd, options);
|
|
1848
|
+
}
|
|
1849
|
+
if (choice === '3') {
|
|
1663
1850
|
console.log('Aborted. Run with --init to initialize.');
|
|
1664
1851
|
return false;
|
|
1665
1852
|
}
|
|
1853
|
+
// choice is '1' or '' (default)
|
|
1666
1854
|
const success = await initGit(cwd, gitVisibility, dryRun);
|
|
1667
1855
|
if (!success)
|
|
1668
1856
|
return false;
|
|
@@ -1678,8 +1866,13 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
1678
1866
|
else if (!gitStatus.hasRemote) {
|
|
1679
1867
|
// Git repo exists but no remote - need to create GitHub repo
|
|
1680
1868
|
if (!init) {
|
|
1681
|
-
const
|
|
1682
|
-
if (
|
|
1869
|
+
const choice = await promptChoice('No git remote configured. What would you like to do?\n 1) Create GitHub repository (default)\n 2) Use local install only (skip git/publish)\n 3) Abort\nChoice:', ['1', '2', '3', '']);
|
|
1870
|
+
if (choice === '2') {
|
|
1871
|
+
console.log(colors.dim('Switching to local-only mode...'));
|
|
1872
|
+
writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
|
|
1873
|
+
return doLocalInstall(cwd, options);
|
|
1874
|
+
}
|
|
1875
|
+
if (choice === '3') {
|
|
1683
1876
|
console.log('Aborted. Run with --init to set up GitHub repository.');
|
|
1684
1877
|
return false;
|
|
1685
1878
|
}
|
|
@@ -1885,8 +2078,40 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
1885
2078
|
if (effectiveNpmVisibility === 'private') {
|
|
1886
2079
|
// User explicitly wants private publication
|
|
1887
2080
|
if (!isScoped) {
|
|
1888
|
-
|
|
1889
|
-
|
|
2081
|
+
// Offer to add scope — check .userconfig first, then npm whoami
|
|
2082
|
+
const userConfig = readUserNpmConfig();
|
|
2083
|
+
const auth = checkNpmAuth();
|
|
2084
|
+
const defaultScope = userConfig.scope
|
|
2085
|
+
|| (auth.username ? `@${auth.username}` : undefined);
|
|
2086
|
+
const scopedExample = defaultScope ? `${defaultScope}/${pkg.name}` : `@scope/${pkg.name}`;
|
|
2087
|
+
console.log(colors.yellow(`Private packages must be scoped (e.g., ${scopedExample})`));
|
|
2088
|
+
if (dryRun) {
|
|
2089
|
+
console.log(' [dry-run] Would prompt to add scope');
|
|
2090
|
+
return false;
|
|
2091
|
+
}
|
|
2092
|
+
const addScope = await confirm(`Add scope to package name?`, true);
|
|
2093
|
+
if (!addScope) {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
const scope = await promptText('Scope:', defaultScope);
|
|
2097
|
+
if (!scope) {
|
|
2098
|
+
console.error(colors.red('No scope provided. Aborting.'));
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
// Normalize: ensure it starts with @
|
|
2102
|
+
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
2103
|
+
const newName = `${normalizedScope}/${pkg.name}`;
|
|
2104
|
+
pkg.name = newName;
|
|
2105
|
+
writePackageJson(cwd, pkg);
|
|
2106
|
+
console.log(colors.green(`✓ Renamed package to ${newName}`));
|
|
2107
|
+
// Save scope to .userconfig if not already there
|
|
2108
|
+
if (!userConfig.scope) {
|
|
2109
|
+
const saveScope = await confirm(`Save scope "${normalizedScope}" to global config (${getUserConfigDir()}\\npm.json5)?`, true);
|
|
2110
|
+
if (saveScope) {
|
|
2111
|
+
writeUserNpmConfig({ scope: normalizedScope });
|
|
2112
|
+
console.log(colors.green(`✓ Saved default scope to ${getUserConfigDir()}\\npm.json5`));
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
1890
2115
|
}
|
|
1891
2116
|
if (currentAccess === 'public') {
|
|
1892
2117
|
console.error(colors.red(`ERROR: Package '${pkg.name}' is currently PUBLIC on npm.`));
|
|
@@ -1941,10 +2166,45 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
1941
2166
|
console.log(colors.dim(` Use --npm public to make it public`));
|
|
1942
2167
|
}
|
|
1943
2168
|
else {
|
|
2169
|
+
// Unscoped new package — prompt to add scope (same as explicit-private path)
|
|
2170
|
+
const ucfg = readUserNpmConfig();
|
|
2171
|
+
const auth2 = checkNpmAuth();
|
|
2172
|
+
const defaultScope = ucfg.scope
|
|
2173
|
+
|| (auth2.username ? `@${auth2.username}` : undefined);
|
|
2174
|
+
const scopedExample = defaultScope ? `${defaultScope}/${pkg.name}` : `@scope/${pkg.name}`;
|
|
1944
2175
|
console.log(colors.yellow(`WARNING: Package '${pkg.name}' is unscoped and will be PUBLIC.`));
|
|
1945
2176
|
console.log(colors.yellow(` Unscoped packages cannot be private on npm.`));
|
|
1946
|
-
|
|
1947
|
-
|
|
2177
|
+
if (dryRun) {
|
|
2178
|
+
console.log(` [dry-run] Would prompt to add scope (e.g., ${scopedExample})`);
|
|
2179
|
+
}
|
|
2180
|
+
else {
|
|
2181
|
+
const addScope = await confirm(`Add scope to make it private (e.g., ${scopedExample})?`, true);
|
|
2182
|
+
if (addScope) {
|
|
2183
|
+
const scope = await promptText('Scope:', defaultScope);
|
|
2184
|
+
if (scope) {
|
|
2185
|
+
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
2186
|
+
const newName = `${normalizedScope}/${pkg.name}`;
|
|
2187
|
+
pkg.name = newName;
|
|
2188
|
+
writePackageJson(cwd, pkg);
|
|
2189
|
+
console.log(colors.green(`✓ Renamed package to ${newName}`));
|
|
2190
|
+
if (!ucfg.scope) {
|
|
2191
|
+
const saveScope = await confirm(`Save scope "${normalizedScope}" to global config (${getUserConfigDir()}\\npm.json5)?`, true);
|
|
2192
|
+
if (saveScope) {
|
|
2193
|
+
writeUserNpmConfig({ scope: normalizedScope });
|
|
2194
|
+
console.log(colors.green(`✓ Saved default scope to ${getUserConfigDir()}\\npm.json5`));
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
else {
|
|
2199
|
+
console.log(colors.yellow(` No scope provided. Continuing as public.`));
|
|
2200
|
+
console.log(colors.yellow(` Use --npm public to suppress this prompt.`));
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
else {
|
|
2204
|
+
console.log(colors.yellow(` Continuing as public.`));
|
|
2205
|
+
console.log(colors.yellow(` Use --npm public to suppress this prompt.`));
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
1948
2208
|
}
|
|
1949
2209
|
}
|
|
1950
2210
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/npmglobalize",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.93",
|
|
4
4
|
"description": "Transform file: dependencies to npm versions for publishing",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"author": "Bob Frankston",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@types/node": "^25.
|
|
25
|
+
"@types/node": "^25.3.0",
|
|
26
26
|
"@types/npm-package-arg": "^6.1.4",
|
|
27
27
|
"@types/pacote": "^11.1.8"
|
|
28
28
|
},
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"url": "https://github.com/BobFrankston/npmglobalize.git"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@bobfrankston/userconfig": "^1.0.3",
|
|
34
35
|
"@npmcli/package-json": "^7.0.4",
|
|
35
36
|
"json5": "^2.2.3",
|
|
36
37
|
"libnpmversion": "^8.0.3",
|
|
@@ -40,5 +41,14 @@
|
|
|
40
41
|
},
|
|
41
42
|
"publishConfig": {
|
|
42
43
|
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
".dependencies": {
|
|
46
|
+
"@bobfrankston/userconfig": "file:../userconfig",
|
|
47
|
+
"@npmcli/package-json": "^7.0.4",
|
|
48
|
+
"json5": "^2.2.3",
|
|
49
|
+
"libnpmversion": "^8.0.3",
|
|
50
|
+
"npm-registry-fetch": "^19.1.1",
|
|
51
|
+
"pacote": "^21.0.4",
|
|
52
|
+
"simple-git": "^3.30.0"
|
|
43
53
|
}
|
|
44
54
|
}
|