@bobfrankston/npmglobalize 1.0.80 → 1.0.82

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.
Files changed (5) hide show
  1. package/README.md +72 -19
  2. package/cli.js +51 -75
  3. package/lib.d.ts +2 -0
  4. package/lib.js +34 -8
  5. package/package.json +1 -1
package/README.md CHANGED
@@ -224,29 +224,36 @@ Publishing requires being on a branch so commits and tags can be properly tracke
224
224
 
225
225
  ## Command Reference
226
226
 
227
+ <!-- NOTE: Keep this in sync with the --help output in cli.ts printHelp().
228
+ The README expands on options with examples and context;
229
+ cli.ts is the concise quick-reference. Update both when adding/changing flags. -->
230
+
227
231
  ### Release Options
228
232
  ```
229
- --patch Bump patch version (default: 1.0.0 → 1.0.1)
230
- --minor Bump minor version (1.0.0 → 1.1.0)
231
- --major Bump major version (1.0.0 → 2.0.0)
232
- --nopublish, -np Just transform, don't publish
233
- --cleanup Restore file: dependencies from backup
233
+ --patch Bump patch version (default)
234
+ --minor Bump minor version
235
+ --major Bump major version
236
+ --nopublish, -np Just transform, don't publish
237
+ --cleanup Restore file: dependencies from backup
238
+ -m, --message <msg> Custom commit message (forces release even without changes)
234
239
  ```
235
240
 
236
241
  ### Dependency Options
237
242
  ```
238
- --update-deps Update package.json to latest versions (safe: minor/patch)
239
- --update-major Allow major version updates (breaking changes)
240
- --no-publish-deps, -npd Skip auto-publishing file: dependencies
241
- --force-publish Republish dependencies even if version exists
242
- --fix Run npm audit fix after transformation
243
- --no-fix Don't run npm audit
243
+ --update-deps, -ud Update package.json to latest versions (safe: minor/patch)
244
+ --update-major Allow major version updates (breaking changes)
245
+ --no-publish-deps, -npd Skip auto-publishing file: dependencies
246
+ --force-publish Republish dependencies even if version exists
247
+ --fix Run npm audit fix after transformation
248
+ --no-fix Don't run npm audit
244
249
  ```
245
250
 
246
251
  ### Install Options
247
252
  ```
248
- --install, -i Install globally after publish (Windows)
253
+ --install, -i Install globally after publish (from registry)
254
+ --link Install globally via symlink (npm install -g .)
249
255
  --wsl Also install in WSL
256
+ --once Don't persist flags to .globalize.json5
250
257
  ```
251
258
 
252
259
  ### Mode Options
@@ -258,14 +265,23 @@ Publishing requires being on a branch so commits and tags can be properly tracke
258
265
  ### Git/npm Visibility
259
266
  ```
260
267
  --git private Make git repo private (default)
261
- --git public Make git repo public
262
- --npm private Mark package private (skip publish) (default)
268
+ --git public Make git repo public (requires confirmation)
269
+ --npm private Mark npm package private (skip publish) (default)
263
270
  --npm public Publish to npm
264
271
  ```
265
272
 
273
+ ### Workspace Options
274
+ ```
275
+ -w, --workspace <pkg> Filter to specific package (repeatable)
276
+ --no-workspace Disable workspace mode at a workspace root
277
+ --continue-on-error Continue if a package fails in workspace mode
278
+ ```
279
+
280
+ Workspace mode is auto-detected when run from a root with `"private": true` and a `workspaces` field.
281
+
266
282
  ### Other Options
267
283
  ```
268
- --init Initialize git/npm if needed (creates .gitignore, .npmignore,
284
+ --init Initialize git/npm if needed (creates .gitignore, .npmignore,
269
285
  .gitattributes, and configures git for LF line endings)
270
286
  --force Continue despite git errors
271
287
  --dry-run Preview what would happen
@@ -273,13 +289,41 @@ Publishing requires being on a branch so commits and tags can be properly tracke
273
289
  --verbose Show detailed output
274
290
  --conform Update .gitignore/.npmignore/.gitattributes to best practices
275
291
  and configure git for LF line endings (fixes existing repos)
276
- --asis Skip ignore file checks
292
+ --asis Skip ignore file checks (or set "asis": true in .globalize.json5)
277
293
  --fix-tags Automatically fix version/tag mismatches
278
294
  --rebase Automatically rebase if local is behind remote
279
- --help, -h Show help
280
- --version, -v Show version
295
+ --show Show package.json dependency changes
296
+ --package, -pkg Update package.json scripts to use npmglobalize (see below)
297
+ -h, --help Show help
298
+ -v, --version Show version
299
+ ```
300
+
301
+ ## Using in package.json
302
+
303
+ You can wire npmglobalize into your package.json `scripts` so that `npm run release` handles publishing:
304
+
305
+ ```json
306
+ {
307
+ "scripts": {
308
+ "release": "npmglobalize"
309
+ }
310
+ }
281
311
  ```
282
312
 
313
+ The `--package` (`-pkg`) flag does this automatically — it adds a `release` script (renaming any existing `release`/`installer` scripts to `old-release`/`old-installer`):
314
+
315
+ ```bash
316
+ npmglobalize --package # Sets up "release": "npmglobalize" in package.json
317
+ ```
318
+
319
+ After that, publishing is just:
320
+ ```bash
321
+ npm run release # Same as running npmglobalize directly
322
+ npm run release -- --minor # Pass flags through
323
+ ```
324
+
325
+ You can also combine it with `.globalize.json5` for persistent options so `npm run release` always uses your preferred settings (install, visibility, etc.).
326
+
283
327
  ## Configuration File
284
328
 
285
329
  Settings can be saved in `.globalize.json5`:
@@ -375,12 +419,21 @@ npmglobalize -np --update-deps
375
419
  # Force republish all dependencies
376
420
  npmglobalize --force-publish --update-major
377
421
 
378
- # Release + install on Windows and WSL
422
+ # Release + install on Windows and WSL (from registry)
379
423
  npmglobalize --install --wsl
380
424
 
425
+ # Release + link on Windows and WSL (symlink)
426
+ npmglobalize --link --wsl
427
+
381
428
  # Restore original file: references
382
429
  npmglobalize --cleanup
383
430
 
431
+ # Initialize new git repo + release
432
+ npmglobalize --init
433
+
434
+ # Migrate package.json scripts to use npmglobalize
435
+ npmglobalize --package
436
+
384
437
  # Preview what would happen
385
438
  npmglobalize --dry-run --verbose
386
439
  ```
package/cli.js CHANGED
@@ -5,41 +5,10 @@
5
5
  import { globalize, globalizeWorkspace, readConfig, readPackageJson, writeConfig, writePackageJson } from './lib.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
- import { fileURLToPath } from 'url';
9
8
  import { styleText } from 'util';
10
9
  import JSON5 from 'json5';
11
- // Check if JS files are up-to-date with TS files
12
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
- // Check all .ts files in the directory
14
- const tsFiles = ['lib.ts', 'cli.ts', 'index.ts'];
15
- const outOfSync = [];
16
- for (const tsFile of tsFiles) {
17
- const tsPath = path.join(__dirname, tsFile);
18
- const jsPath = path.join(__dirname, tsFile.replace('.ts', '.js'));
19
- if (fs.existsSync(tsPath) && fs.existsSync(jsPath)) {
20
- const tsStat = fs.statSync(tsPath);
21
- const jsStat = fs.statSync(jsPath);
22
- if (jsStat.mtime < tsStat.mtime) {
23
- outOfSync.push(tsFile);
24
- }
25
- }
26
- }
27
- if (outOfSync.length > 0 && !process.argv.includes('--force')) {
28
- console.error('\n' + styleText('red', '❌ Build check failed:'));
29
- console.error(styleText('red', ` ${outOfSync.length} file(s) need compilation in ${__dirname}:`));
30
- outOfSync.forEach(f => {
31
- const tsPath = path.join(__dirname, f);
32
- const jsPath = path.join(__dirname, f.replace('.ts', '.js'));
33
- const tsMtime = fs.statSync(tsPath).mtime;
34
- const jsMtime = fs.statSync(jsPath).mtime;
35
- const fmt = (d) => d.toLocaleString();
36
- console.error(styleText('red', ` - ${tsPath}`));
37
- console.error(styleText('red', ` .ts: ${fmt(tsMtime)} .js: ${fmt(jsMtime)}`));
38
- });
39
- console.error('\n' + styleText('yellow', ' Please run: tsc'));
40
- console.error(styleText('yellow', ' Or use --force to skip this check') + '\n');
41
- process.exit(1);
42
- }
10
+ // npmglobalize install directory (for --version)
11
+ const __dirname = import.meta.dirname;
43
12
  function printHelp() {
44
13
  console.log(`
45
14
  npmglobalize - Transform file: dependencies to npm versions for publishing
@@ -68,6 +37,7 @@ Install Options:
68
37
  --install, -i Global install after publish (from registry)
69
38
  --link Global install via symlink (npm install -g .)
70
39
  --wsl Also install globally in WSL
40
+ --once Don't persist flags to .globalize.json5
71
41
 
72
42
  Mode Options:
73
43
  --files Keep file: paths after publish (default)
@@ -300,6 +270,9 @@ function parseArgs(args) {
300
270
  case '-pkg':
301
271
  options.package = true;
302
272
  break;
273
+ case '--once':
274
+ options.once = true;
275
+ break;
303
276
  default:
304
277
  if (arg.startsWith('-')) {
305
278
  unrecognized.push(arg);
@@ -329,52 +302,11 @@ export async function main() {
329
302
  process.exit(0);
330
303
  }
331
304
  if (cliOptions.version) {
332
- const __filename = fileURLToPath(import.meta.url);
333
- const __dirname = path.dirname(__filename);
334
305
  const pkgPath = path.join(__dirname, 'package.json');
335
306
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
336
307
  console.log(pkg.version);
337
308
  process.exit(0);
338
309
  }
339
- // Check if TypeScript files are compiled (unless --force or --help/--version)
340
- if (!cliOptions.force && !cliOptions.help && !cliOptions.version) {
341
- const __filename = fileURLToPath(import.meta.url);
342
- const __dirname = path.dirname(__filename);
343
- // Check for noEmit in tsconfig.json
344
- let noEmit = false;
345
- try {
346
- const tsconfigPath = path.join(__dirname, 'tsconfig.json');
347
- const content = fs.readFileSync(tsconfigPath, 'utf-8');
348
- const tsconfig = JSON5.parse(content);
349
- noEmit = tsconfig.compilerOptions?.noEmit === true;
350
- }
351
- catch (error) {
352
- // If we can't read tsconfig, assume we need to check
353
- }
354
- if (!noEmit) {
355
- // Check if this .js file is older than its .ts source
356
- const jsFile = __filename;
357
- const tsFile = jsFile.replace(/\.js$/, '.ts');
358
- if (fs.existsSync(tsFile)) {
359
- if (!fs.existsSync(jsFile)) {
360
- console.error('\n❌ Error: TypeScript files not compiled');
361
- console.error(`Missing: ${path.basename(jsFile)}`);
362
- console.error('\nPlease run: npm run build');
363
- console.error('Or use --force to skip this check\n');
364
- process.exit(1);
365
- }
366
- const tsStat = fs.statSync(tsFile);
367
- const jsStat = fs.statSync(jsFile);
368
- if (jsStat.mtime < tsStat.mtime) {
369
- console.error('\n❌ Error: TypeScript files not compiled');
370
- console.error(`${path.basename(jsFile)} is older than ${path.basename(tsFile)}`);
371
- console.error('\nPlease run: npm run build');
372
- console.error('Or use --force to skip this check\n');
373
- process.exit(1);
374
- }
375
- }
376
- }
377
- }
378
310
  if (cliOptions.error) {
379
311
  console.error(`Error: ${cliOptions.error}`);
380
312
  console.error('Run with --help for usage.');
@@ -393,6 +325,50 @@ export async function main() {
393
325
  process.exit(1);
394
326
  }
395
327
  }
328
+ // Check if TypeScript files in target project are compiled
329
+ if (!cliOptions.force) {
330
+ let noEmit = false;
331
+ try {
332
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
333
+ const content = fs.readFileSync(tsconfigPath, 'utf-8');
334
+ const tsconfig = JSON5.parse(content);
335
+ noEmit = tsconfig.compilerOptions?.noEmit === true;
336
+ }
337
+ catch (error) {
338
+ // No tsconfig or can't parse — skip check
339
+ noEmit = true;
340
+ }
341
+ if (!noEmit) {
342
+ const outOfSync = [];
343
+ const jsFiles = fs.readdirSync(cwd).filter(f => f.endsWith('.js') && !f.endsWith('.min.js'));
344
+ for (const jsFile of jsFiles) {
345
+ const tsFile = jsFile.replace(/\.js$/, '.ts');
346
+ const tsPath = path.join(cwd, tsFile);
347
+ const jsPath = path.join(cwd, jsFile);
348
+ if (fs.existsSync(tsPath)) {
349
+ const tsStat = fs.statSync(tsPath);
350
+ const jsStat = fs.statSync(jsPath);
351
+ if (jsStat.mtime < tsStat.mtime) {
352
+ outOfSync.push(tsFile);
353
+ }
354
+ }
355
+ }
356
+ if (outOfSync.length > 0) {
357
+ console.error('\n' + styleText('red', '❌ Build check failed:'));
358
+ console.error(styleText('red', ` ${outOfSync.length} file(s) need compilation in ${cwd}:`));
359
+ outOfSync.forEach(f => {
360
+ const tsPath = path.join(cwd, f);
361
+ const jsPath = path.join(cwd, f.replace('.ts', '.js'));
362
+ const fmt = (d) => d.toLocaleString();
363
+ console.error(styleText('red', ` - ${f}`));
364
+ console.error(styleText('red', ` .ts: ${fmt(fs.statSync(tsPath).mtime)} .js: ${fmt(fs.statSync(jsPath).mtime)}`));
365
+ });
366
+ console.error('\n' + styleText('yellow', ' Please run: tsc'));
367
+ console.error(styleText('yellow', ' Or use --force to skip this check') + '\n');
368
+ process.exit(1);
369
+ }
370
+ }
371
+ }
396
372
  // Handle --package: update scripts in target package.json
397
373
  if (cliOptions.package) {
398
374
  const pkg = readPackageJson(cwd);
@@ -419,7 +395,7 @@ export async function main() {
419
395
  const configOptions = readConfig(cwd);
420
396
  const options = { ...configOptions, ...cliOptions };
421
397
  // Persist explicitly set CLI flags to .globalize.json5
422
- if (cliOptions.explicitKeys.size > 0) {
398
+ if (cliOptions.explicitKeys.size > 0 && !cliOptions.once) {
423
399
  const persistable = { ...configOptions };
424
400
  for (const key of cliOptions.explicitKeys) {
425
401
  persistable[key] = options[key];
package/lib.d.ts CHANGED
@@ -69,6 +69,8 @@ export interface GlobalizeOptions {
69
69
  continueOnError?: boolean;
70
70
  /** Update package.json scripts to use npmglobalize */
71
71
  package?: boolean;
72
+ /** Don't persist CLI flags to .globalize.json5 */
73
+ once?: boolean;
72
74
  /** Internal: signals this call is from workspace orchestrator */
73
75
  _fromWorkspace?: boolean;
74
76
  }
package/lib.js CHANGED
@@ -110,7 +110,7 @@ export function writeConfig(dir, config, explicitKeys) {
110
110
  const existing = readConfig(dir);
111
111
  // Filter out temporary flags and default values (unless explicitly set)
112
112
  const filtered = {};
113
- const omitKeys = new Set(['noPublish', 'cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'forcePublish']);
113
+ const omitKeys = new Set(['noPublish', 'cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'forcePublish', 'once']);
114
114
  for (const [key, value] of Object.entries(config)) {
115
115
  if (omitKeys.has(key))
116
116
  continue;
@@ -719,13 +719,33 @@ export function fixVersionTagMismatch(cwd, pkg, verbose = false) {
719
719
  }
720
720
  return deletedAny;
721
721
  }
722
+ /** Wait for a package version to appear on the npm registry */
723
+ function waitForNpmVersion(pkgName, version, maxWaitMs = 60000) {
724
+ const interval = 3000;
725
+ const maxAttempts = Math.ceil(maxWaitMs / interval);
726
+ process.stdout.write(`Waiting for ${pkgName}@${version} on npm registry`);
727
+ for (let i = 0; i < maxAttempts; i++) {
728
+ const result = spawnSync('npm', ['view', `${pkgName}@${version}`, 'version'], {
729
+ shell: process.platform === 'win32',
730
+ stdio: ['pipe', 'pipe', 'pipe'],
731
+ encoding: 'utf-8'
732
+ });
733
+ if (result.status === 0 && result.stdout?.trim() === version) {
734
+ process.stdout.write(' ready\n');
735
+ return true;
736
+ }
737
+ process.stdout.write('.');
738
+ spawnSync(process.platform === 'win32' ? 'timeout' : 'sleep', process.platform === 'win32' ? ['/t', '3', '/nobreak'] : ['3'], { stdio: 'pipe', shell: process.platform === 'win32' });
739
+ }
740
+ process.stdout.write(' timed out\n');
741
+ return false;
742
+ }
722
743
  /** Run a command and return success status */
723
744
  export function runCommand(cmd, args, options = {}) {
724
745
  const { silent = false, cwd } = options;
725
746
  try {
726
- // Only use shell:true for npm commands which need it on Windows
727
- // Git and other commands work fine without shell
728
- const needsShell = cmd === 'npm' || cmd === 'npm.cmd';
747
+ // Use shell:true for npm/gh commands which need it on Windows
748
+ const needsShell = cmd === 'npm' || cmd === 'npm.cmd' || cmd === 'gh';
729
749
  // Debug: log the command being executed
730
750
  if (!silent) {
731
751
  console.log(colors.dim(`[DEBUG] Running: ${cmd} ${args.join(' ')}`));
@@ -782,13 +802,13 @@ function getGitHubRepo(pkg) {
782
802
  }
783
803
  /** Run a command and throw on failure */
784
804
  export function runCommandOrThrow(cmd, args, options = {}) {
785
- // For commands that should throw, we need to capture output, so use silent=true
786
- const captureOptions = { ...options, silent: true };
805
+ const needsShell = cmd === 'npm' || cmd === 'npm.cmd' || cmd === 'gh';
787
806
  const result = spawnSync(cmd, args, {
788
807
  encoding: 'utf-8',
789
808
  stdio: 'pipe',
790
- cwd: captureOptions.cwd,
791
- shell: false
809
+ cwd: options.cwd,
810
+ env: process.env,
811
+ shell: needsShell
792
812
  });
793
813
  const stdout = result.stdout || '';
794
814
  const stderr = result.stderr || '';
@@ -2053,6 +2073,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
2053
2073
  }
2054
2074
  }
2055
2075
  else if (install) {
2076
+ waitForNpmVersion(pkgName, pkgVersion);
2056
2077
  console.log(`Installing ${pkgName}@${pkgVersion} globally from registry...`);
2057
2078
  const registryInstallResult = runCommand('npm', ['install', '-g', `${pkgName}@${pkgVersion}`], { cwd, silent: false });
2058
2079
  if (registryInstallResult.success) {
@@ -2065,6 +2086,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
2065
2086
  }
2066
2087
  if (wsl) {
2067
2088
  const wslArgs = link ? ['npm', 'install', '-g', '.'] : ['npm', 'install', '-g', `${pkgName}@${pkgVersion}`];
2089
+ if (!link && !install)
2090
+ waitForNpmVersion(pkgName, pkgVersion);
2068
2091
  console.log(`Installing ${pkgName} in WSL${link ? ' (link)' : ' from registry'}...`);
2069
2092
  const wslInstallResult = runCommand('wsl', wslArgs, { cwd, silent: false });
2070
2093
  if (wslInstallResult.success) {
@@ -2510,6 +2533,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
2510
2533
  else if (install) {
2511
2534
  console.log(`Installing globally from registry: ${pkgName}@${pkgVersion}...`);
2512
2535
  if (!dryRun) {
2536
+ waitForNpmVersion(pkgName, pkgVersion);
2513
2537
  const installResult = runCommand('npm', ['install', '-g', `${pkgName}@${pkgVersion}`], { cwd, silent: false });
2514
2538
  if (installResult.success) {
2515
2539
  console.log(colors.green(`✓ Installed globally: ${pkgName}@${pkgVersion}`));
@@ -2525,6 +2549,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
2525
2549
  }
2526
2550
  if (wsl) {
2527
2551
  const wslArgs = link ? ['npm', 'install', '-g', '.'] : ['npm', 'install', '-g', `${pkgName}@${pkgVersion}`];
2552
+ if (!link && !install)
2553
+ waitForNpmVersion(pkgName, pkgVersion);
2528
2554
  console.log(`Installing in WSL${link ? ' (link)' : ' from registry'}: ${pkgName}@${pkgVersion}...`);
2529
2555
  if (!dryRun) {
2530
2556
  const wslResult = runCommand('wsl', wslArgs, { cwd, silent: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.80",
3
+ "version": "1.0.82",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",