@bobfrankston/npmglobalize 1.0.91 → 1.0.92

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,10 @@
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:*)"
29
32
  ]
30
33
  }
31
34
  }
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
@@ -94,6 +94,17 @@ export interface WorkspaceResult {
94
94
  }
95
95
  /** Read and parse package.json from a directory */
96
96
  export declare function readPackageJson(dir: string): any;
97
+ /** Global npm config from %USERPROFILE%\.userconfig\npm.json5 */
98
+ export interface UserNpmConfig {
99
+ scope?: string;
100
+ npmVisibility?: 'private' | 'public';
101
+ }
102
+ /** Get the .userconfig directory path (%USERPROFILE%\.userconfig) */
103
+ export declare function getUserConfigDir(): string;
104
+ /** Read global npm config from %USERPROFILE%\.userconfig\npm.json5 */
105
+ export declare function readUserNpmConfig(): UserNpmConfig;
106
+ /** Write global npm config to %USERPROFILE%\.userconfig\npm.json5 */
107
+ export declare function writeUserNpmConfig(config: UserNpmConfig): void;
97
108
  /** Read .globalize.json5 config file */
98
109
  export declare function readConfig(dir: string): Partial<GlobalizeOptions>;
99
110
  /** Write .globalize.json5 config file */
@@ -205,6 +216,8 @@ export declare function getGitStatus(cwd: string): GitStatus;
205
216
  export declare function validatePackageJson(pkg: any): string[];
206
217
  /** Prompt user for confirmation */
207
218
  export declare function confirm(message: string, defaultYes?: boolean): Promise<boolean>;
219
+ /** Prompt user for free text input */
220
+ export declare function promptText(message: string, defaultValue?: string): Promise<string>;
208
221
  /** Prompt user for multiple choice */
209
222
  export declare function promptChoice(message: string, choices: string[]): Promise<string | null>;
210
223
  /** Initialize git repository */
package/lib.js CHANGED
@@ -12,6 +12,17 @@
12
12
  import fs from 'fs';
13
13
  import path from 'path';
14
14
  import { execSync, spawnSync } from 'child_process';
15
+ /** Wrapper for spawnSync that avoids DEP0190 (args + shell: true).
16
+ * When shell is true, joins cmd+args into a single command string. */
17
+ function spawnSafe(cmd, args, options = {}) {
18
+ const opts = { ...options, encoding: 'utf-8' };
19
+ if (opts.shell && args.length > 0) {
20
+ // Join into a single command string to avoid DEP0190
21
+ const cmdStr = [cmd, ...args].map(a => /[\s"&|<>^]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a).join(' ');
22
+ return spawnSync(cmdStr, opts);
23
+ }
24
+ return spawnSync(cmd, args, opts);
25
+ }
15
26
  import readline from 'readline';
16
27
  import libversion from 'libnpmversion';
17
28
  import JSON5 from 'json5';
@@ -77,6 +88,46 @@ export function readPackageJson(dir) {
77
88
  throw new Error(`Failed to parse ${pkgPath}: ${error.message}`);
78
89
  }
79
90
  }
91
+ /** Get the .userconfig directory path (%USERPROFILE%\.userconfig) */
92
+ export function getUserConfigDir() {
93
+ const userProfile = process.env.USERPROFILE || process.env.HOME || '~';
94
+ return path.join(userProfile, '.userconfig');
95
+ }
96
+ /** Read global npm config from %USERPROFILE%\.userconfig\npm.json5 */
97
+ export function readUserNpmConfig() {
98
+ const configPath = path.join(getUserConfigDir(), 'npm.json5');
99
+ if (!fs.existsSync(configPath)) {
100
+ return {};
101
+ }
102
+ try {
103
+ const content = fs.readFileSync(configPath, 'utf-8');
104
+ return JSON5.parse(content);
105
+ }
106
+ catch (error) {
107
+ console.warn(`Warning: Could not parse ${configPath}: ${error.message}`);
108
+ return {};
109
+ }
110
+ }
111
+ /** Write global npm config to %USERPROFILE%\.userconfig\npm.json5 */
112
+ export function writeUserNpmConfig(config) {
113
+ const configDir = getUserConfigDir();
114
+ if (!fs.existsSync(configDir)) {
115
+ fs.mkdirSync(configDir, { recursive: true });
116
+ }
117
+ const configPath = path.join(configDir, 'npm.json5');
118
+ // Read existing to merge
119
+ const existing = readUserNpmConfig();
120
+ const merged = { ...existing, ...config };
121
+ const lines = ['{'];
122
+ const entries = Object.entries(merged);
123
+ for (let i = 0; i < entries.length; i++) {
124
+ const [key, value] = entries[i];
125
+ const comma = i < entries.length - 1 ? ',' : '';
126
+ lines.push(` ${key}: ${JSON.stringify(value)}${comma}`);
127
+ }
128
+ lines.push('}');
129
+ fs.writeFileSync(configPath, lines.join('\n') + '\n', 'utf-8');
130
+ }
80
131
  /** Read .globalize.json5 config file */
81
132
  export function readConfig(dir) {
82
133
  const configPath = path.join(dir, '.globalize.json5');
@@ -203,7 +254,7 @@ export function isFileRef(value) {
203
254
  /** Get the latest version of a package from npm */
204
255
  export function getLatestVersion(packageName) {
205
256
  try {
206
- const result = spawnSync('npm', ['view', packageName, 'version'], {
257
+ const result = spawnSafe('npm', ['view', packageName, 'version'], {
207
258
  encoding: 'utf-8',
208
259
  stdio: 'pipe',
209
260
  shell: true // Required on Windows to find npm.cmd
@@ -220,7 +271,7 @@ export function getLatestVersion(packageName) {
220
271
  /** Check if a specific version of a package exists on npm */
221
272
  export function checkVersionExists(packageName, version) {
222
273
  try {
223
- const result = spawnSync('npm', ['view', `${packageName}@${version}`, 'version'], {
274
+ const result = spawnSafe('npm', ['view', `${packageName}@${version}`, 'version'], {
224
275
  encoding: 'utf-8',
225
276
  stdio: 'pipe',
226
277
  shell: true // Required on Windows to find npm.cmd
@@ -234,7 +285,7 @@ export function checkVersionExists(packageName, version) {
234
285
  /** Check if a package exists on npm (any version) */
235
286
  export function checkPackageExists(packageName) {
236
287
  try {
237
- const result = spawnSync('npm', ['view', packageName, 'version'], {
288
+ const result = spawnSafe('npm', ['view', packageName, 'version'], {
238
289
  encoding: 'utf-8',
239
290
  stdio: 'pipe',
240
291
  shell: true // Required on Windows to find npm.cmd
@@ -252,14 +303,14 @@ export function checkNpmAccess(packageName) {
252
303
  if (packageName.startsWith('@')) {
253
304
  // First check if the package actually exists on npm
254
305
  // npm access returns read-write for unpublished packages in owned scopes
255
- const viewResult = spawnSync('npm', ['view', packageName, 'name'], {
306
+ const viewResult = spawnSafe('npm', ['view', packageName, 'name'], {
256
307
  encoding: 'utf-8',
257
308
  stdio: 'pipe',
258
309
  shell: true
259
310
  });
260
311
  if (viewResult.status === 0 && viewResult.stdout && viewResult.stdout.trim()) {
261
312
  // Package exists - check public/private status
262
- const accessResult = spawnSync('npm', ['access', 'get', 'status', packageName], {
313
+ const accessResult = spawnSafe('npm', ['access', 'get', 'status', packageName], {
263
314
  encoding: 'utf-8',
264
315
  stdio: 'pipe',
265
316
  shell: true
@@ -273,7 +324,7 @@ export function checkNpmAccess(packageName) {
273
324
  return 'restricted'; // Exists but not publicly accessible
274
325
  }
275
326
  // Package not viewable - check if it's restricted or unpublished
276
- const checkResult = spawnSync('npm', ['view', packageName, '--json'], {
327
+ const checkResult = spawnSafe('npm', ['view', packageName, '--json'], {
277
328
  encoding: 'utf-8',
278
329
  stdio: 'pipe',
279
330
  shell: true
@@ -290,7 +341,7 @@ export function checkNpmAccess(packageName) {
290
341
  }
291
342
  else {
292
343
  // Unscoped packages are always public if they exist
293
- const result = spawnSync('npm', ['view', packageName, 'name'], {
344
+ const result = spawnSafe('npm', ['view', packageName, 'name'], {
294
345
  encoding: 'utf-8',
295
346
  stdio: 'pipe',
296
347
  shell: true
@@ -510,6 +561,55 @@ export function topologicalSort(graph) {
510
561
  }
511
562
  return result;
512
563
  }
564
+ /** Check if a published npm package has broken deps (file: paths or unpublished transitive deps).
565
+ * Also checks local file: deps for unpublished versions. */
566
+ function hasUnpublishedTransitiveDeps(packageName, pkg, baseDir, verbose) {
567
+ // Check 1: Does the npm-published version have file: paths in its deps?
568
+ try {
569
+ const result = spawnSafe('npm', ['view', packageName, 'dependencies', '--json'], {
570
+ encoding: 'utf-8',
571
+ stdio: 'pipe',
572
+ shell: true
573
+ });
574
+ if (result.status === 0 && result.stdout.trim()) {
575
+ const npmDeps = JSON.parse(result.stdout.trim());
576
+ for (const [depName, depValue] of Object.entries(npmDeps)) {
577
+ if (isFileRef(depValue)) {
578
+ if (verbose) {
579
+ console.log(colors.yellow(` npm copy has file: dep ${depName} → ${depValue}`));
580
+ }
581
+ return true;
582
+ }
583
+ }
584
+ }
585
+ }
586
+ catch {
587
+ // Can't check npm — fall through to local check
588
+ }
589
+ // Check 2: Do local file: deps have unpublished versions?
590
+ for (const key of DEP_KEYS) {
591
+ if (!pkg[key])
592
+ continue;
593
+ for (const [depName, depValue] of Object.entries(pkg[key])) {
594
+ if (!isFileRef(depValue))
595
+ continue;
596
+ try {
597
+ const depPath = resolveFilePath(depValue, baseDir);
598
+ const depPkg = readPackageJson(depPath);
599
+ if (!checkVersionExists(depName, depPkg.version)) {
600
+ if (verbose) {
601
+ console.log(colors.yellow(` transitive dep ${depName}@${depPkg.version} not on npm`));
602
+ }
603
+ return true;
604
+ }
605
+ }
606
+ catch {
607
+ return true;
608
+ }
609
+ }
610
+ }
611
+ return false;
612
+ }
513
613
  /** Transform file: dependencies to npm versions */
514
614
  export function transformDeps(pkg, baseDir, verbose = false, forcePublish = false) {
515
615
  let transformed = false;
@@ -564,8 +664,15 @@ export function transformDeps(pkg, baseDir, verbose = false, forcePublish = fals
564
664
  console.log(colors.red(` ⚠ ${name}@${targetVersion} not found on npm (local: ${value})`));
565
665
  }
566
666
  }
567
- else if (verbose) {
568
- console.log(colors.green(` ✓ ${name}@${targetVersion} exists on npm`));
667
+ else {
668
+ if (verbose) {
669
+ console.log(colors.green(` ✓ ${name}@${targetVersion} exists on npm`));
670
+ }
671
+ // Check transitive file: deps — if any are unpublished, this dep needs republishing
672
+ if (hasUnpublishedTransitiveDeps(name, targetPkg, targetPath, verbose)) {
673
+ unpublished.push({ name, version: targetVersion, path: targetPath });
674
+ console.log(colors.yellow(` ⟳ ${name}@${targetVersion} has unpublished transitive deps — will republish`));
675
+ }
569
676
  }
570
677
  pkg[key][name] = npmVersion;
571
678
  if (verbose) {
@@ -604,7 +711,7 @@ export function hasBackup(pkg) {
604
711
  /** Get the latest git tag (if any) */
605
712
  export function getLatestGitTag(cwd) {
606
713
  try {
607
- const result = spawnSync('git', ['describe', '--tags', '--abbrev=0'], {
714
+ const result = spawnSafe('git', ['describe', '--tags', '--abbrev=0'], {
608
715
  encoding: 'utf-8',
609
716
  stdio: 'pipe',
610
717
  cwd
@@ -621,7 +728,7 @@ export function getLatestGitTag(cwd) {
621
728
  /** Check if a git tag exists */
622
729
  export function gitTagExists(cwd, tag) {
623
730
  try {
624
- const result = spawnSync('git', ['tag', '-l', tag], {
731
+ const result = spawnSafe('git', ['tag', '-l', tag], {
625
732
  encoding: 'utf-8',
626
733
  stdio: 'pipe',
627
734
  cwd
@@ -635,7 +742,7 @@ export function gitTagExists(cwd, tag) {
635
742
  /** Delete a git tag */
636
743
  export function deleteGitTag(cwd, tag) {
637
744
  try {
638
- const result = spawnSync('git', ['tag', '-d', tag], {
745
+ const result = spawnSafe('git', ['tag', '-d', tag], {
639
746
  encoding: 'utf-8',
640
747
  stdio: 'pipe',
641
748
  cwd
@@ -649,7 +756,7 @@ export function deleteGitTag(cwd, tag) {
649
756
  /** Get all git tags */
650
757
  export function getAllGitTags(cwd) {
651
758
  try {
652
- const result = spawnSync('git', ['tag', '-l'], {
759
+ const result = spawnSafe('git', ['tag', '-l'], {
653
760
  encoding: 'utf-8',
654
761
  stdio: 'pipe',
655
762
  cwd
@@ -733,7 +840,7 @@ function waitForNpmVersion(pkgName, version, maxWaitMs = 60000) {
733
840
  const maxAttempts = Math.ceil(maxWaitMs / interval);
734
841
  process.stdout.write(`Waiting for ${pkgName}@${version} on npm registry`);
735
842
  for (let i = 0; i < maxAttempts; i++) {
736
- const result = spawnSync('npm', ['view', `${pkgName}@${version}`, 'version'], {
843
+ const result = spawnSafe('npm', ['view', `${pkgName}@${version}`, 'version'], {
737
844
  shell: process.platform === 'win32',
738
845
  stdio: ['pipe', 'pipe', 'pipe'],
739
846
  encoding: 'utf-8'
@@ -743,7 +850,7 @@ function waitForNpmVersion(pkgName, version, maxWaitMs = 60000) {
743
850
  return true;
744
851
  }
745
852
  process.stdout.write('.');
746
- spawnSync(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '3', '/nobreak'] : ['3'], { stdio: 'pipe', shell: process.platform === 'win32' });
853
+ spawnSafe(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '3', '/nobreak'] : ['3'], { stdio: 'pipe', shell: process.platform === 'win32' });
747
854
  }
748
855
  process.stdout.write(' timed out\n');
749
856
  return false;
@@ -758,7 +865,7 @@ export function runCommand(cmd, args, options = {}) {
758
865
  if (!silent) {
759
866
  console.log(colors.dim(`[DEBUG] Running: ${cmd} ${args.join(' ')}`));
760
867
  }
761
- const result = spawnSync(cmd, args, {
868
+ const result = spawnSafe(cmd, args, {
762
869
  encoding: 'utf-8',
763
870
  stdio: silent ? 'pipe' : 'inherit',
764
871
  cwd,
@@ -811,7 +918,7 @@ function getGitHubRepo(pkg) {
811
918
  /** Run a command and throw on failure */
812
919
  export function runCommandOrThrow(cmd, args, options = {}) {
813
920
  const needsShell = cmd === 'npm' || cmd === 'npm.cmd' || cmd === 'gh';
814
- const result = spawnSync(cmd, args, {
921
+ const result = spawnSafe(cmd, args, {
815
922
  encoding: 'utf-8',
816
923
  stdio: 'pipe',
817
924
  cwd: options.cwd,
@@ -826,7 +933,7 @@ export function runCommandOrThrow(cmd, args, options = {}) {
826
933
  if (cmd === 'git' && stderr.includes('dubious ownership')) {
827
934
  console.log(colors.yellow('Git "dubious ownership" error — directory owner SID differs from current user.'));
828
935
  console.log(colors.yellow('Adding safe.directory \'*\' to global git config...'));
829
- const fix = spawnSync('git', ['config', '--global', '--add', 'safe.directory', '*'], {
936
+ const fix = spawnSafe('git', ['config', '--global', '--add', 'safe.directory', '*'], {
830
937
  encoding: 'utf-8',
831
938
  stdio: 'pipe',
832
939
  shell: true
@@ -834,7 +941,7 @@ export function runCommandOrThrow(cmd, args, options = {}) {
834
941
  if (fix.status === 0) {
835
942
  console.log(colors.green('✓ Fixed. Retrying...'));
836
943
  // Retry the original command
837
- const retry = spawnSync(cmd, args, {
944
+ const retry = spawnSafe(cmd, args, {
838
945
  encoding: 'utf-8',
839
946
  stdio: 'pipe',
840
947
  cwd: options.cwd,
@@ -980,6 +1087,21 @@ export async function confirm(message, defaultYes = false) {
980
1087
  });
981
1088
  });
982
1089
  }
1090
+ /** Prompt user for free text input */
1091
+ export async function promptText(message, defaultValue) {
1092
+ const rl = readline.createInterface({
1093
+ input: process.stdin,
1094
+ output: process.stdout
1095
+ });
1096
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
1097
+ return new Promise((resolve) => {
1098
+ rl.question(`${message}${suffix} `, (answer) => {
1099
+ rl.close();
1100
+ const a = answer.trim();
1101
+ resolve(a || defaultValue || '');
1102
+ });
1103
+ });
1104
+ }
983
1105
  /** Prompt user for multiple choice */
984
1106
  export async function promptChoice(message, choices) {
985
1107
  const rl = readline.createInterface({
@@ -1004,7 +1126,7 @@ function checkNpmAuth() {
1004
1126
  try {
1005
1127
  // Must use shell:true on Windows to find npm.cmd in PATH
1006
1128
  // Must pass env: process.env to inherit NPM_TOKEN environment variable
1007
- const result = spawnSync('npm', ['whoami'], {
1129
+ const result = spawnSafe('npm', ['whoami'], {
1008
1130
  encoding: 'utf-8',
1009
1131
  stdio: ['ignore', 'pipe', 'pipe'],
1010
1132
  env: process.env,
@@ -1323,7 +1445,7 @@ export async function initGit(cwd, visibility, dryRun) {
1323
1445
  // git init
1324
1446
  runCommandOrThrow('git', ['init'], { cwd });
1325
1447
  // Check for dubious ownership (git init succeeds but subsequent commands fail)
1326
- const ownerCheck = spawnSync('git', ['rev-parse', '--git-dir'], {
1448
+ const ownerCheck = spawnSafe('git', ['rev-parse', '--git-dir'], {
1327
1449
  encoding: 'utf-8',
1328
1450
  stdio: 'pipe',
1329
1451
  cwd
@@ -1331,7 +1453,7 @@ export async function initGit(cwd, visibility, dryRun) {
1331
1453
  if (ownerCheck.stderr && ownerCheck.stderr.includes('dubious ownership')) {
1332
1454
  console.log(colors.yellow('Git "dubious ownership" error — directory owner SID differs from current user.'));
1333
1455
  console.log(colors.yellow('Adding safe.directory \'*\' to global git config...'));
1334
- const fix = spawnSync('git', ['config', '--global', '--add', 'safe.directory', '*'], {
1456
+ const fix = spawnSafe('git', ['config', '--global', '--add', 'safe.directory', '*'], {
1335
1457
  encoding: 'utf-8',
1336
1458
  stdio: 'pipe',
1337
1459
  shell: true
@@ -1349,7 +1471,7 @@ export async function initGit(cwd, visibility, dryRun) {
1349
1471
  console.log(' ✓ Configured git for LF line endings');
1350
1472
  runCommandOrThrow('git', ['add', '-A'], { cwd });
1351
1473
  // Only commit if there are staged changes (repo may already have commits)
1352
- const staged = spawnSync('git', ['diff', '--cached', '--quiet'], { cwd });
1474
+ const staged = spawnSafe('git', ['diff', '--cached', '--quiet'], { cwd });
1353
1475
  if (staged.status !== 0) {
1354
1476
  runCommandOrThrow('git', ['commit', '-m', 'Initial commit'], { cwd });
1355
1477
  }
@@ -1389,7 +1511,7 @@ export function runNpmAudit(cwd, fix = false, verbose = false) {
1389
1511
  }
1390
1512
  }
1391
1513
  // Always run audit to report status
1392
- const auditResult = spawnSync('npm', ['audit', '--json'], {
1514
+ const auditResult = spawnSafe('npm', ['audit', '--json'], {
1393
1515
  cwd,
1394
1516
  encoding: 'utf-8',
1395
1517
  stdio: 'pipe',
@@ -1475,6 +1597,67 @@ export function getToolVersion() {
1475
1597
  return 'unknown';
1476
1598
  }
1477
1599
  }
1600
+ /** Offer to add a bin field if missing — returns true if bin was added or already existed */
1601
+ async function offerAddBin(cwd, pkg) {
1602
+ if (pkg.bin)
1603
+ return true;
1604
+ // Determine likely entry point
1605
+ const mainFile = pkg.main || 'index.js';
1606
+ const cmdName = (pkg.name || path.basename(cwd)).replace(/^@[^/]+\//, '');
1607
+ console.log(colors.yellow('No bin field — this package won\'t install as a CLI command.'));
1608
+ 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', '']);
1609
+ if (choice === '3')
1610
+ return false;
1611
+ if (choice === '2') {
1612
+ console.log(colors.dim('Installing as library link (no bin)...'));
1613
+ return true;
1614
+ }
1615
+ // choice is '1' or '' (default)
1616
+ pkg.bin = { [cmdName]: mainFile };
1617
+ writePackageJson(cwd, pkg);
1618
+ console.log(colors.green(`✓ Added bin: { "${cmdName}": "${mainFile}" }`));
1619
+ return true;
1620
+ }
1621
+ /** Perform local-only install (npm install -g .) — extracted for reuse from git-init prompts */
1622
+ async function doLocalInstall(cwd, options) {
1623
+ const { dryRun = false, wsl = false } = options;
1624
+ const pkg = readPackageJson(cwd);
1625
+ const pkgName = pkg.name || path.basename(cwd);
1626
+ const pkgVersion = pkg.version || '0.0.0';
1627
+ console.log(colors.blue(`Local install: ${pkgName}@${pkgVersion}`));
1628
+ console.log(colors.dim('Skipping transform/publish — installing with file: deps as-is'));
1629
+ if (!pkg.bin) {
1630
+ const proceed = await offerAddBin(cwd, pkg);
1631
+ if (!proceed)
1632
+ return false;
1633
+ }
1634
+ if (dryRun) {
1635
+ console.log(' [dry-run] Would run: npm install -g .');
1636
+ if (wsl)
1637
+ console.log(' [dry-run] Would run: wsl npm install -g .');
1638
+ return true;
1639
+ }
1640
+ const result = runCommand('npm', ['install', '-g', '.'], { cwd, silent: false });
1641
+ if (result.success) {
1642
+ console.log(colors.green(`✓ Installed locally: ${pkgName}@${pkgVersion}`));
1643
+ }
1644
+ else {
1645
+ console.error(colors.red(`✗ Local install failed`));
1646
+ console.error(colors.yellow(' Try running manually: npm install -g .'));
1647
+ return false;
1648
+ }
1649
+ if (wsl) {
1650
+ console.log(`Installing ${pkgName} in WSL (local)...`);
1651
+ const wslResult = runCommand('wsl', ['npm', 'install', '-g', '.'], { cwd, silent: false });
1652
+ if (wslResult.success) {
1653
+ console.log(colors.green(`✓ Installed in WSL: ${pkgName}@${pkgVersion}`));
1654
+ }
1655
+ else {
1656
+ console.error(colors.yellow('✗ WSL install failed (is npm installed in WSL?)'));
1657
+ }
1658
+ }
1659
+ return true;
1660
+ }
1478
1661
  export async function globalize(cwd, options = {}, configOptions = {}) {
1479
1662
  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
1663
  forcePublish = false, fix = false, fixTags = false, rebase = false, show = false, local = false } = options;
@@ -1547,8 +1730,9 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
1547
1730
  console.log(colors.blue(`Local install: ${pkgName}@${pkgVersion}`));
1548
1731
  console.log(colors.dim('Skipping transform/publish — installing with file: deps as-is'));
1549
1732
  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)...'));
1733
+ const proceed = await offerAddBin(cwd, pkg);
1734
+ if (!proceed)
1735
+ return false;
1552
1736
  }
1553
1737
  if (dryRun) {
1554
1738
  console.log(' [dry-run] Would run: npm install -g .');
@@ -1658,11 +1842,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
1658
1842
  console.log(' [dry-run] Would initialize git repository');
1659
1843
  }
1660
1844
  else if (!init) {
1661
- const ok = await confirm('Initialize git repository?', true);
1662
- if (!ok) {
1845
+ 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', '']);
1846
+ if (choice === '2') {
1847
+ console.log(colors.dim('Switching to local-only mode...'));
1848
+ writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
1849
+ return doLocalInstall(cwd, options);
1850
+ }
1851
+ if (choice === '3') {
1663
1852
  console.log('Aborted. Run with --init to initialize.');
1664
1853
  return false;
1665
1854
  }
1855
+ // choice is '1' or '' (default)
1666
1856
  const success = await initGit(cwd, gitVisibility, dryRun);
1667
1857
  if (!success)
1668
1858
  return false;
@@ -1678,8 +1868,13 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
1678
1868
  else if (!gitStatus.hasRemote) {
1679
1869
  // Git repo exists but no remote - need to create GitHub repo
1680
1870
  if (!init) {
1681
- const ok = await confirm('No git remote configured. Create GitHub repository?', true);
1682
- if (!ok) {
1871
+ 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', '']);
1872
+ if (choice === '2') {
1873
+ console.log(colors.dim('Switching to local-only mode...'));
1874
+ writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
1875
+ return doLocalInstall(cwd, options);
1876
+ }
1877
+ if (choice === '3') {
1683
1878
  console.log('Aborted. Run with --init to set up GitHub repository.');
1684
1879
  return false;
1685
1880
  }
@@ -1885,8 +2080,40 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
1885
2080
  if (effectiveNpmVisibility === 'private') {
1886
2081
  // User explicitly wants private publication
1887
2082
  if (!isScoped) {
1888
- console.error(colors.red(`ERROR: Private packages must be scoped (e.g., @bobfrankston/${pkg.name})`));
1889
- return false;
2083
+ // Offer to add scope check .userconfig first, then npm whoami
2084
+ const userConfig = readUserNpmConfig();
2085
+ const auth = checkNpmAuth();
2086
+ const defaultScope = userConfig.scope
2087
+ || (auth.username ? `@${auth.username}` : undefined);
2088
+ const scopedExample = defaultScope ? `${defaultScope}/${pkg.name}` : `@scope/${pkg.name}`;
2089
+ console.log(colors.yellow(`Private packages must be scoped (e.g., ${scopedExample})`));
2090
+ if (dryRun) {
2091
+ console.log(' [dry-run] Would prompt to add scope');
2092
+ return false;
2093
+ }
2094
+ const addScope = await confirm(`Add scope to package name?`, true);
2095
+ if (!addScope) {
2096
+ return false;
2097
+ }
2098
+ const scope = await promptText('Scope:', defaultScope);
2099
+ if (!scope) {
2100
+ console.error(colors.red('No scope provided. Aborting.'));
2101
+ return false;
2102
+ }
2103
+ // Normalize: ensure it starts with @
2104
+ const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
2105
+ const newName = `${normalizedScope}/${pkg.name}`;
2106
+ pkg.name = newName;
2107
+ writePackageJson(cwd, pkg);
2108
+ console.log(colors.green(`✓ Renamed package to ${newName}`));
2109
+ // Save scope to .userconfig if not already there
2110
+ if (!userConfig.scope) {
2111
+ const saveScope = await confirm(`Save scope "${normalizedScope}" to global config (${getUserConfigDir()}\\npm.json5)?`, true);
2112
+ if (saveScope) {
2113
+ writeUserNpmConfig({ scope: normalizedScope });
2114
+ console.log(colors.green(`✓ Saved default scope to ${getUserConfigDir()}\\npm.json5`));
2115
+ }
2116
+ }
1890
2117
  }
1891
2118
  if (currentAccess === 'public') {
1892
2119
  console.error(colors.red(`ERROR: Package '${pkg.name}' is currently PUBLIC on npm.`));
@@ -1943,7 +2170,10 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
1943
2170
  else {
1944
2171
  console.log(colors.yellow(`WARNING: Package '${pkg.name}' is unscoped and will be PUBLIC.`));
1945
2172
  console.log(colors.yellow(` Unscoped packages cannot be private on npm.`));
1946
- console.log(colors.yellow(` Consider using a scoped name: @bobfrankston/${pkg.name}`));
2173
+ const ucfg = readUserNpmConfig();
2174
+ const auth2 = checkNpmAuth();
2175
+ const suggestedScope = ucfg.scope || (auth2.username ? `@${auth2.username}` : '@scope');
2176
+ console.log(colors.yellow(` Consider using a scoped name: ${suggestedScope}/${pkg.name}`));
1947
2177
  console.log(colors.yellow(` Or use --npm public to confirm public publishing`));
1948
2178
  }
1949
2179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.91",
3
+ "version": "1.0.92",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",