@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.
@@ -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
- process.exit(0);
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 file and merge with CLI options (CLI takes precedence)
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 = spawnSync('npm', ['view', packageName, 'version'], {
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 = spawnSync('npm', ['view', `${packageName}@${version}`, 'version'], {
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 = spawnSync('npm', ['view', packageName, 'version'], {
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 = spawnSync('npm', ['view', packageName, 'name'], {
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 = spawnSync('npm', ['access', 'get', 'status', packageName], {
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 = spawnSync('npm', ['view', packageName, '--json'], {
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 = spawnSync('npm', ['view', packageName, 'name'], {
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 if (verbose) {
568
- console.log(colors.green(` ✓ ${name}@${targetVersion} exists on npm`));
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 = spawnSync('git', ['describe', '--tags', '--abbrev=0'], {
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 = spawnSync('git', ['tag', '-l', tag], {
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 = spawnSync('git', ['tag', '-d', tag], {
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 = spawnSync('git', ['tag', '-l'], {
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 = spawnSync('npm', ['view', `${pkgName}@${version}`, 'version'], {
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
- spawnSync(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '3', '/nobreak'] : ['3'], { stdio: 'pipe', shell: process.platform === 'win32' });
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 = spawnSync(cmd, args, {
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 = spawnSync(cmd, args, {
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 = spawnSync('git', ['config', '--global', '--add', 'safe.directory', '*'], {
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 = spawnSync(cmd, args, {
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 = spawnSync('npm', ['whoami'], {
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 = spawnSync('git', ['rev-parse', '--git-dir'], {
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 = spawnSync('git', ['config', '--global', '--add', 'safe.directory', '*'], {
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 = spawnSync('git', ['diff', '--cached', '--quiet'], { cwd });
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 = spawnSync('npm', ['audit', '--json'], {
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
- console.log(colors.yellow('Note: No bin field — this is a library, not a CLI tool.'));
1551
- console.log(colors.yellow('Running npm install -g . anyway (creates global link)...'));
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 ok = await confirm('Initialize git repository?', true);
1662
- if (!ok) {
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 ok = await confirm('No git remote configured. Create GitHub repository?', true);
1682
- if (!ok) {
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
- console.error(colors.red(`ERROR: Private packages must be scoped (e.g., @bobfrankston/${pkg.name})`));
1889
- return false;
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
- console.log(colors.yellow(` Consider using a scoped name: @bobfrankston/${pkg.name}`));
1947
- console.log(colors.yellow(` Or use --npm public to confirm public publishing`));
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.91",
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.2.1",
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
  }