@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.
- package/.claude/settings.local.json +4 -1
- package/cli.js +11 -4
- package/lib.d.ts +13 -0
- package/lib.js +263 -33
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
568
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1551
|
-
|
|
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
|
|
1662
|
-
if (
|
|
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
|
|
1682
|
-
if (
|
|
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
|
-
|
|
1889
|
-
|
|
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
|
-
|
|
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
|
}
|