@bobfrankston/npmglobalize 1.0.214 → 1.0.216

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 +17 -0
  2. package/cli.js +14 -1
  3. package/lib.d.ts +19 -0
  4. package/lib.js +74 -6
  5. package/package.json +1 -1
package/README.md CHANGED
@@ -562,6 +562,23 @@ Settings can be saved in `.globalize.json5`:
562
562
 
563
563
  Configuration persists across runs. CLI flags override config file.
564
564
 
565
+ ### `.stale` — retired directories
566
+
567
+ A directory that is no longer live or maintained can be retired in place by
568
+ dropping a `.stale` file into it — empty, or better holding one line saying why
569
+ and when. It is a marker file, not a setting in `.globalize.json5`, because
570
+ stale is a property of the directory (most retired directories never had a
571
+ config) and a file is findable without parsing.
572
+
573
+ npmglobalize refuses to build, publish or install a directory marked `.stale`,
574
+ and refuses to cascade into a `file:` dependency that is marked. Both are
575
+ reported as **errors**, not skips: a live package that depends on a retired one
576
+ has a reference that needs fixing. Remove the marker to make the directory live
577
+ again.
578
+
579
+ `cleanprev.ps1` (in `%OneDrive%\xfer\bin`) strips `node_modules`, lock files and
580
+ tsc output from every `.stale` directory the same way it does for `prev/`.
581
+
565
582
  ### `usePaths` — Standalone packages
566
583
 
567
584
  Default: `true`. Set to `false` (or pass `-no-use-paths` / `-nup`) to mark a
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,25 @@ 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;
300
+ /** A directory retired in place carries a `.stale` marker file (Bob's convention,
301
+ * 2026-09-06; `cleanprev.ps1` strips such directories like a `prev/`). The file
302
+ * is empty or holds a line saying why and when. Returns that line (or a fixed
303
+ * phrase when empty), or null when the directory is not marked.
304
+ *
305
+ * 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. A marker file rather
306
+ * than a flag in .globalize.json5: stale is a property of a directory, most stale
307
+ * directories never had a config, and a file is findable without parsing. */
308
+ export declare function staleMarker(dir: string): string;
290
309
  /** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
291
310
  * the spec in package.json and the junction npm created in node_modules.
292
311
  *
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,12 +1352,43 @@ 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
  }
1373
+ /** A directory retired in place carries a `.stale` marker file (Bob's convention,
1374
+ * 2026-09-06; `cleanprev.ps1` strips such directories like a `prev/`). The file
1375
+ * is empty or holds a line saying why and when. Returns that line (or a fixed
1376
+ * phrase when empty), or null when the directory is not marked.
1377
+ *
1378
+ * 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction. A marker file rather
1379
+ * than a flag in .globalize.json5: stale is a property of a directory, most stale
1380
+ * directories never had a config, and a file is findable without parsing. */
1381
+ export function staleMarker(dir) {
1382
+ const marker = path.join(dir, '.stale');
1383
+ if (!fs.existsSync(marker))
1384
+ return null;
1385
+ let note = '';
1386
+ try {
1387
+ note = fs.readFileSync(marker, 'utf8').split(/\r?\n/).map(l => l.trim()).find(l => l) || '';
1388
+ }
1389
+ catch { /* unreadable note: still stale */ }
1390
+ return note || 'no reason recorded in .stale';
1391
+ }
1356
1392
  /** Vet the casing of every `file:` dependency path, in BOTH places it is recorded:
1357
1393
  * the spec in package.json and the junction npm created in node_modules.
1358
1394
  *
@@ -2311,6 +2347,15 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
2311
2347
  const target = path.resolve(cwd, spec.slice('file:'.length));
2312
2348
  if (!fs.existsSync(path.join(target, 'package.json')))
2313
2349
  continue;
2350
+ // 2026-09-06 — Claude Code (Fable 5.1): never install into, or walk
2351
+ // through, a dep that is retired (`.stale`). Reported as an error so the
2352
+ // stale reference in package.json gets fixed rather than papered over.
2353
+ const targetStale = staleMarker(target);
2354
+ if (targetStale) {
2355
+ console.error(colors.red(`✗ file: dependency ${name} -> ${target} is marked .stale (${targetStale}); not installing into a retired directory.`));
2356
+ recordBuildIssue(pkg?.name || abs, 'error', `file: dependency ${name} (${target}) is marked .stale (${targetStale}).`);
2357
+ continue;
2358
+ }
2314
2359
  let targetPkg;
2315
2360
  try {
2316
2361
  targetPkg = readPackageJson(target);
@@ -3488,7 +3533,7 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
3488
3533
  const deps = pkg?.[key];
3489
3534
  if (!deps || typeof deps !== 'object')
3490
3535
  continue;
3491
- for (const [, spec] of Object.entries(deps)) {
3536
+ for (const [name, spec] of Object.entries(deps)) {
3492
3537
  if (typeof spec !== 'string' || !spec.startsWith('file:'))
3493
3538
  continue;
3494
3539
  const target = path.resolve(cwd, spec.slice('file:'.length));
@@ -3497,6 +3542,18 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
3497
3542
  const targetAbs = path.resolve(target);
3498
3543
  if (visited.has(targetAbs))
3499
3544
  continue;
3545
+ // 2026-09-06 — Claude Code (Fable 5.1): a retired (`.stale`) dep is not
3546
+ // built. Counts as a failed dep so the consumer's build stops here too,
3547
+ // unless -force says to carry on.
3548
+ const targetStale = staleMarker(target);
3549
+ if (targetStale) {
3550
+ console.error(colors.red(`✗ file: dependency ${name} -> ${target} is marked .stale (${targetStale}); not building a retired directory.`));
3551
+ recordBuildIssue(pkg?.name || abs, 'error', `file: dependency ${name} (${target}) is marked .stale (${targetStale}).`);
3552
+ allOk = false;
3553
+ if (!opts.force)
3554
+ return false;
3555
+ continue;
3556
+ }
3500
3557
  // Recurse first (deps before consumer)
3501
3558
  const childOk = await buildFileDepsTopologically(target, opts, visited);
3502
3559
  if (!childOk)
@@ -6124,6 +6181,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
6124
6181
  publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
6125
6182
  publicDeps = false, // -public-deps: cascade public visibility to all deps
6126
6183
  noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs, adopt = false, importCheck = false } = options;
6184
+ // 2026-09-06 — Claude Code (Fable 5.1), at Bob's direction: a directory marked
6185
+ // `.stale` is retired. Refuse to build, publish or install it — and because the
6186
+ // dep cascade reaches deps through this same function, refuse to cascade into
6187
+ // one too. The refusal is an error, not a skip: a live package depending on a
6188
+ // stale one is a fact the user has to act on.
6189
+ const staleNote = staleMarker(cwd);
6190
+ if (staleNote) {
6191
+ console.error(colors.red(`✗ ${cwd} is marked .stale (${staleNote}). Not publishing a retired directory.`));
6192
+ recordBuildIssue(path.basename(cwd), 'error', `Directory is marked .stale (${staleNote}); remove the marker to publish it again.`);
6193
+ return false;
6194
+ }
6127
6195
  // Show tool version only for recursive dep calls (CLI already prints it at startup)
6128
6196
  const toolVersion = getToolVersion();
6129
6197
  if (!options._fromWorkspace && !options._fromCli) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.214",
3
+ "version": "1.0.216",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",