@bobfrankston/npmglobalize 1.0.213 → 1.0.215

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 +4 -4
  2. package/cli.js +14 -1
  3. package/lib.d.ts +18 -2
  4. package/lib.js +86 -32
  5. package/package.json +1 -1
package/README.md CHANGED
@@ -298,10 +298,10 @@ reachable `repository.url` in `package.json`. If found, it offers two paths:
298
298
  ```
299
299
  How would you like to set up git?
300
300
  1) Adopt history from existing remote (recommended)
301
- 2) Initialize fresh git repository
302
- a) Adopt ALL (don't ask again for remaining deps)
303
- 3) Use local install only (skip git/publish)
304
- 4) Abort
301
+ 2) Adopt for ALL remaining deps (don't ask again)
302
+ 3) Initialize fresh git repository
303
+ 4) Use local install only (skip git/publish)
304
+ 5) Abort
305
305
  ```
306
306
 
307
307
  **Adopt** runs:
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, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations, fixTs7Deprecations } from './lib.js';
5
+ import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations, fixTs7Deprecations, canonicalCase } from './lib.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { colors } from './colors.js';
@@ -549,6 +549,19 @@ export async function main() {
549
549
  process.exit(1);
550
550
  }
551
551
  }
552
+ // 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction.
553
+ // `process.cwd()` carries whatever letter case the shell was started with
554
+ // (`y:\dev\...` from a lowercase prompt). Every `file:` junction npm writes
555
+ // inherits that case, and mixed `y:`/`Y:` junction targets are exactly what
556
+ // makes arborist crash with "Cannot read properties of null (reading
557
+ // 'package')" — see `caseDiffers` in lib.ts. Canonicalizing once up front
558
+ // means every npm the cascade spawns writes junctions with the on-disk
559
+ // spelling, so the repair in verifyDepPathCase stops being needed again.
560
+ const canonicalCwd = canonicalCase(cwd);
561
+ if (canonicalCwd && canonicalCwd !== cwd) {
562
+ cwd = canonicalCwd;
563
+ process.chdir(cwd);
564
+ }
552
565
  // Report-only TS7-readiness scan (no transform / build / publish).
553
566
  if (cliOptions.ts7Report) {
554
567
  printTs7Report(cwd);
package/lib.d.ts CHANGED
@@ -287,6 +287,16 @@ export type UnpublishedDep = {
287
287
  path: string;
288
288
  reason: 'new' | 'update';
289
289
  };
290
+ /** The canonical on-disk spelling of `target`, or null if it does not exist.
291
+ * Windows answers path queries case-insensitively, so the only way to learn how
292
+ * a directory is really spelled is to ask its parent to list it — a segment at a
293
+ * time, from the root down. The drive letter is always uppercased.
294
+ *
295
+ * 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: exported so cli.ts can
296
+ * canonicalize the working directory at startup (see there for why). Deliberately
297
+ * NOT `fs.realpathSync.native`: that would also resolve a junction the user is
298
+ * standing inside, which is a behaviour change nobody asked for. */
299
+ export declare function canonicalCase(target: string): string;
290
300
  /** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
291
301
  * the spec in package.json and the junction npm created in node_modules.
292
302
  *
@@ -589,8 +599,14 @@ export declare function validatePackageJson(pkg: any): string[];
589
599
  export declare function confirm(message: string, defaultYes?: boolean): Promise<boolean>;
590
600
  /** Prompt user for free text input */
591
601
  export declare function promptText(message: string, defaultValue?: string): Promise<string>;
592
- /** Prompt user for multiple choice */
593
- export declare function promptChoice(message: string, choices: string[]): Promise<string | null>;
602
+ /**
603
+ * Prompt user for multiple choice. Re-asks until the answer is one of `choices`.
604
+ *
605
+ * 2026-09-01 — Claude Code (Fable 5.1), at Bob's direction. Previously an unrecognized
606
+ * answer resolved null and every caller fell through to its default action, so a typo
607
+ * silently did something. Now it says what it didn't understand and asks again.
608
+ */
609
+ export declare function promptChoice(message: string, choices: string[]): Promise<string>;
594
610
  /** Initialize a local git repo by adopting history from an existing remote.
595
611
  * Preserves the working tree — local files appear as uncommitted changes
596
612
  * on top of the remote's HEAD. No force-push is required. */
package/lib.js CHANGED
@@ -1323,8 +1323,13 @@ function hasLocalChanges(packageName, version, targetPath, verbose) {
1323
1323
  /** The canonical on-disk spelling of `target`, or null if it does not exist.
1324
1324
  * Windows answers path queries case-insensitively, so the only way to learn how
1325
1325
  * a directory is really spelled is to ask its parent to list it — a segment at a
1326
- * time, from the root down. */
1327
- function canonicalCase(target) {
1326
+ * time, from the root down. The drive letter is always uppercased.
1327
+ *
1328
+ * 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: exported so cli.ts can
1329
+ * canonicalize the working directory at startup (see there for why). Deliberately
1330
+ * NOT `fs.realpathSync.native`: that would also resolve a junction the user is
1331
+ * standing inside, which is a behaviour change nobody asked for. */
1332
+ export function canonicalCase(target) {
1328
1333
  const full = path.resolve(target);
1329
1334
  if (!fs.existsSync(full))
1330
1335
  return null;
@@ -1347,10 +1352,22 @@ function canonicalCase(target) {
1347
1352
  return cur;
1348
1353
  }
1349
1354
  /** True when two paths name the same place but are spelled with different case
1350
- * in a directory name. A bare drive-letter difference (`y:` vs `Y:`) doesn't
1351
- * count — it resolves identically everywhere, including WSL's /mnt/y. */
1355
+ * anywhere the drive letter included.
1356
+ *
1357
+ * 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. This used to strip the
1358
+ * root before comparing, on the theory that `y:` vs `Y:` "resolves identically
1359
+ * everywhere". It does on the filesystem; it does NOT inside npm's arborist.
1360
+ * Traced with an instrumented `loadActual` on dbitemx / pings / itemw: arborist
1361
+ * keeps the junction target's case in `realpath` (an exact-string index) but
1362
+ * computes `location` with `path.win32.relative`, which folds drive-letter case.
1363
+ * One directory reached as `Y:\...` and `y:\...` becomes two target nodes at one
1364
+ * location; the second clobbers the first, every Link into the first is left with
1365
+ * `target === null`, and the next same-case Link crashes on `target.package` —
1366
+ * the "Cannot read properties of null (reading 'package')" failure. npm writes
1367
+ * the junction target with whatever drive case the cwd had, so a lowercase shell
1368
+ * is all it takes. Full trace: `.llm/arborist-trace.md`. */
1352
1369
  function caseDiffers(a, b) {
1353
- const strip = (p) => p.replace(/[\\/]+$/, '').slice(path.parse(p).root.length);
1370
+ const strip = (p) => p.replace(/[\\/]+$/, '');
1354
1371
  return strip(a) !== strip(b);
1355
1372
  }
1356
1373
  /** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
@@ -4995,23 +5012,46 @@ export async function promptText(message, defaultValue) {
4995
5012
  });
4996
5013
  });
4997
5014
  }
4998
- /** Prompt user for multiple choice */
4999
- export async function promptChoice(message, choices) {
5015
+ /**
5016
+ * Prompt user for multiple choice. Re-asks until the answer is one of `choices`.
5017
+ *
5018
+ * 2026-09-01 — Claude Code (Fable 5.1), at Bob's direction. Previously an unrecognized
5019
+ * answer resolved null and every caller fell through to its default action, so a typo
5020
+ * silently did something. Now it says what it didn't understand and asks again.
5021
+ */
5022
+ export function promptChoice(message, choices) {
5000
5023
  const rl = readline.createInterface({
5001
5024
  input: process.stdin,
5002
5025
  output: process.stdout
5003
5026
  });
5027
+ const shown = choices.filter(c => c !== '').join(', ');
5028
+ // A 'line' listener rather than repeated rl.question(): when stdin is piped, several
5029
+ // lines can arrive in one chunk and a line landing between two question() calls is
5030
+ // dropped. The listener sees every line.
5004
5031
  return new Promise((resolve) => {
5005
- rl.question(`${message} `, (answer) => {
5032
+ let done = false;
5033
+ const finish = (value) => {
5034
+ done = true;
5006
5035
  rl.close();
5036
+ resolve(value);
5037
+ };
5038
+ rl.setPrompt(`${message} `);
5039
+ rl.prompt();
5040
+ rl.on('line', (answer) => {
5041
+ if (done)
5042
+ return;
5007
5043
  const a = answer.trim().toLowerCase();
5008
- if (choices.includes(a)) {
5009
- resolve(a);
5010
- }
5011
- else {
5012
- resolve(null);
5013
- }
5044
+ if (choices.includes(a))
5045
+ return finish(a);
5046
+ console.log(colors.yellow(`Unrecognized choice "${a}" — expected one of: ${shown}`));
5047
+ rl.prompt();
5014
5048
  });
5049
+ // EOF with no valid answer: behave as "just pressed Enter" ('' is the default where
5050
+ // a default exists; callers without one treat '' as abort).
5051
+ rl.on('close', () => { if (!done) {
5052
+ done = true;
5053
+ resolve('');
5054
+ } });
5015
5055
  });
5016
5056
  }
5017
5057
  /** Check npm authentication status */
@@ -6393,51 +6433,60 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6393
6433
  justInitialized = true;
6394
6434
  }
6395
6435
  else if (!init && !options.autoInit && !publishDepsYes) {
6436
+ // 2026-09-01 — Claude Code (Fable 5.1), at Bob's direction. These menus were
6437
+ // numbered "1 a 2 3": the "ALL" option was lettered and sat between 1 and 2, so
6438
+ // a reader who wanted the second line typed 2 and got something else. Bob:
6439
+ // "I type 2 when I mean a because that's what humans do." Options are now
6440
+ // numbered in display order; the ALL variant is 2, directly under the single.
6396
6441
  let choice;
6397
6442
  if (adoptable) {
6398
6443
  choice = await promptChoice('How would you like to set up git?\n'
6399
6444
  + ' 1) Adopt history from existing remote (recommended)\n'
6400
- + ' a) Adopt ALL (don\'t ask again for remaining deps)\n'
6401
- + ' 2) Initialize fresh git repository\n'
6402
- + ' 3) Use local install only (skip git/publish)\n'
6403
- + ' 4) Abort\nChoice:', ['1', 'a', '2', '3', '4', '']);
6404
- if (choice === '3') {
6445
+ + ' 2) Adopt for ALL remaining deps (don\'t ask again)\n'
6446
+ + ' 3) Initialize fresh git repository\n'
6447
+ + ' 4) Use local install only (skip git/publish)\n'
6448
+ + ' 5) Abort\nChoice [1]:', ['1', '2', '3', '4', '5', '']);
6449
+ if (choice === '4') {
6405
6450
  console.log(colors.dim('Switching to local-only mode...'));
6406
6451
  writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
6407
6452
  return doLocalInstall(cwd, options);
6408
6453
  }
6409
- if (choice === '4') {
6454
+ if (choice === '5') {
6410
6455
  console.log('Aborted. Run with --init to initialize.');
6411
6456
  return false;
6412
6457
  }
6413
- if (choice === 'a')
6458
+ if (choice === '2')
6414
6459
  options.autoInit = true;
6415
- if (choice === '2') {
6460
+ if (choice === '3') {
6416
6461
  const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
6417
6462
  if (!success)
6418
6463
  return false;
6419
6464
  }
6420
6465
  else {
6421
- // '1', 'a', '' default → adopt
6466
+ // '1', '2', '' default → adopt
6422
6467
  const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
6423
6468
  if (!success)
6424
6469
  return false;
6425
6470
  }
6426
6471
  }
6427
6472
  else {
6428
- choice = await promptChoice('No git repository found. What would you like to do?\n 1) Initialize git repository (default)\n a) Initialize ALL (don\'t ask again for remaining deps)\n 2) Use local install only (skip git/publish)\n 3) Abort\nChoice:', ['1', 'a', '2', '3', '']);
6429
- if (choice === '2') {
6473
+ choice = await promptChoice('No git repository found. What would you like to do?\n'
6474
+ + ' 1) Initialize git repository\n'
6475
+ + ' 2) Initialize for ALL remaining deps (don\'t ask again)\n'
6476
+ + ' 3) Use local install only (skip git/publish)\n'
6477
+ + ' 4) Abort\nChoice [1]:', ['1', '2', '3', '4', '']);
6478
+ if (choice === '3') {
6430
6479
  console.log(colors.dim('Switching to local-only mode...'));
6431
6480
  writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
6432
6481
  return doLocalInstall(cwd, options);
6433
6482
  }
6434
- if (choice === '3') {
6483
+ if (choice === '4') {
6435
6484
  console.log('Aborted. Run with --init to initialize.');
6436
6485
  return false;
6437
6486
  }
6438
- if (choice === 'a')
6487
+ if (choice === '2')
6439
6488
  options.autoInit = true;
6440
- // choice is '1', 'a', or '' (default)
6489
+ // choice is '1', '2', or '' (default) → init
6441
6490
  const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
6442
6491
  if (!success)
6443
6492
  return false;
@@ -6463,17 +6512,22 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6463
6512
  else if (!gitStatus.hasRemote) {
6464
6513
  // Git repo exists but no remote - need to create GitHub repo
6465
6514
  if (!init && !options.autoInit && !publishDepsYes) {
6466
- const choice = await promptChoice('No git remote configured. What would you like to do?\n 1) Create GitHub repository (default)\n a) Initialize ALL (don\'t ask again for remaining deps)\n 2) Use local install only (skip git/publish)\n 3) Abort\nChoice:', ['1', 'a', '2', '3', '']);
6467
- if (choice === '2') {
6515
+ // 2026-09-01 Claude Code (Fable 5.1): renumbered from "1 a 2 3" (see note above).
6516
+ const choice = await promptChoice('No git remote configured. What would you like to do?\n'
6517
+ + ' 1) Create GitHub repository\n'
6518
+ + ' 2) Create for ALL remaining deps (don\'t ask again)\n'
6519
+ + ' 3) Use local install only (skip git/publish)\n'
6520
+ + ' 4) Abort\nChoice [1]:', ['1', '2', '3', '4', '']);
6521
+ if (choice === '3') {
6468
6522
  console.log(colors.dim('Switching to local-only mode...'));
6469
6523
  writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
6470
6524
  return doLocalInstall(cwd, options);
6471
6525
  }
6472
- if (choice === '3') {
6526
+ if (choice === '4') {
6473
6527
  console.log('Aborted. Run with --init to set up GitHub repository.');
6474
6528
  return false;
6475
6529
  }
6476
- if (choice === 'a') {
6530
+ if (choice === '2') {
6477
6531
  options.autoInit = true;
6478
6532
  }
6479
6533
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.213",
3
+ "version": "1.0.215",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",