@bobfrankston/npmglobalize 1.0.149 → 1.0.151

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/README.md CHANGED
@@ -94,6 +94,28 @@ Errors abort (unless `--force`); warnings prompt to continue.
94
94
 
95
95
  Skip the prescan with `-no-prescan` / `-nps`.
96
96
 
97
+ #### Workspace skip prescan
98
+
99
+ In workspace mode, before processing begins, each package is also checked to decide whether it actually needs work. A package is **skipped** (no rebuild, no version bump, no publish) only if **all** of these are true:
100
+
101
+ 1. **Working tree clean** — `git status --porcelain .` reports no uncommitted changes for that package's directory.
102
+ 2. **Version already on npm** — the version in its `package.json` is published to the registry, and no non-bookkeeping commit (anything outside "Pre-release commit" / "Restore file: dependencies" / "Pre-version cleanup" / "Untrack node_modules") is newer than that version's publish timestamp.
103
+ 3. **Build is fresh** — every `.ts` source file (excluding `.d.ts`) has a sibling `.js` whose mtime is ≥ the `.ts` mtime. A missing `.js` or an older `.js` counts as stale.
104
+ 4. **No sibling workspace `file:` dep flagged for work** — if another workspace package depends on this one via `file:`, and that dep is being updated in this run, this one is also processed (propagates through the graph in topological order).
105
+
106
+ If any condition fails, the package is processed. The prescan prints one line per package with `⟳` for "will process" (and the reason) or `✓` for "skip". Example:
107
+
108
+ ```
109
+ ⟳ mlproc — uncommitted changes
110
+ ⟳ pzip — stale build (index.ts newer than index.js)
111
+ ⟳ stage — dep pzip is being updated
112
+ ✓ puller — skip (clean, published, build fresh)
113
+ ```
114
+
115
+ The summary row for a skipped package shows `– name v1.0.0 (skipped — already up to date)`.
116
+
117
+ Use `--force` or `--force-publish` to bypass the skip prescan and process every package.
118
+
97
119
  **Force republish** all file: dependencies even if versions exist:
98
120
  ```bash
99
121
  npmglobalize --force-publish
@@ -59,7 +59,7 @@
59
59
  "package-lock.json",
60
60
  "*.ts",
61
61
  "!*.d.ts",
62
- "*.map",
62
+ // "*.map", // keep source maps in published packages for debugging
63
63
  "tsconfig.json",
64
64
  ".vscode/",
65
65
  ".globalize.json5"
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Diagnose failures of npm commands run by npmglobalize.
3
+ *
4
+ * Pulls out common error shapes — especially ones whose root cause lives in
5
+ * a referenced (file:) module, not the current package — and turns them into
6
+ * a short summary + actionable hint instead of a raw arborist stack trace.
7
+ *
8
+ * Leaf module: no dependencies on lib.ts or other internal modules. Keeps
9
+ * the main publish flow in lib.ts readable.
10
+ */
11
+ export interface DiagnosedError {
12
+ /** Short one-liner suitable for the Issues Summary. */
13
+ summary: string;
14
+ /** Multi-line block to print to console.error (without color). */
15
+ details: string[];
16
+ /** If the root cause is a file: sibling, its package name. */
17
+ referencedModule?: string;
18
+ /** One concrete next step the user can take. */
19
+ hint?: string;
20
+ }
21
+ /** Diagnose an `npm pack` failure. */
22
+ export declare function diagnoseNpmPackFailure(cwd: string, output: string, stderr: string, pkg: any): DiagnosedError;
23
+ /** Given a nested-node_modules path like
24
+ * `node_modules/@scope/sibling/node_modules/transitive`
25
+ * or `../sibling/node_modules/transitive`,
26
+ * find the outermost dep name and resolve it to a file: sibling declared
27
+ * in pkg.dependencies / pkg['.dependencies'] / etc. */
28
+ declare function resolveSiblingFromNestedPath(nestedPath: string, cwd: string, pkg: any): {
29
+ name: string;
30
+ filePath: string;
31
+ } | undefined;
32
+ /** Scan pkg deps for a file: entry whose resolved absolute path equals absTarget. */
33
+ declare function findFileDepByPath(pkg: any, cwd: string, absTarget: string): {
34
+ name: string;
35
+ filePath: string;
36
+ } | undefined;
37
+ /** Exposed for ad-hoc testing — not used by the main flow. */
38
+ export declare const _test: {
39
+ resolveSiblingFromNestedPath: typeof resolveSiblingFromNestedPath;
40
+ findFileDepByPath: typeof findFileDepByPath;
41
+ };
42
+ export {};
43
+ //# sourceMappingURL=diagnose.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Diagnose failures of npm commands run by npmglobalize.
3
+ *
4
+ * Pulls out common error shapes — especially ones whose root cause lives in
5
+ * a referenced (file:) module, not the current package — and turns them into
6
+ * a short summary + actionable hint instead of a raw arborist stack trace.
7
+ *
8
+ * Leaf module: no dependencies on lib.ts or other internal modules. Keeps
9
+ * the main publish flow in lib.ts readable.
10
+ */
11
+ import path from 'path';
12
+ /** Diagnose an `npm pack` failure. */
13
+ export function diagnoseNpmPackFailure(cwd, output, stderr, pkg) {
14
+ const blob = `${output}\n${stderr}`;
15
+ const lower = blob.toLowerCase();
16
+ // Pattern 1: arborist "missing from lockfile: <path>" + null-`package` TypeError.
17
+ // Root cause: a file: sibling has its own populated node_modules/<transitive>
18
+ // that is not in the current package's lockfile. See notes.md §TODO:
19
+ // "Isolate npm pack from sibling file: dep junctions".
20
+ const missingMatch = blob.match(/missing from lockfile:\s*(\S+)/i);
21
+ const hasNullPackage = /cannot read propert(y|ies) of null \(reading 'package'\)/i.test(blob);
22
+ if (missingMatch && (hasNullPackage || /shrinkwrap failed to load/i.test(blob))) {
23
+ const nestedPath = missingMatch[1];
24
+ const sibling = resolveSiblingFromNestedPath(nestedPath, cwd, pkg);
25
+ const details = [
26
+ `npm arborist crashed walking a nested node_modules tree.`,
27
+ `Missing from lockfile: ${nestedPath}`,
28
+ ];
29
+ if (sibling) {
30
+ return {
31
+ summary: `npm pack failed — arborist crashed on sibling's node_modules`,
32
+ details,
33
+ referencedModule: sibling.name,
34
+ hint: `sibling ${sibling.name} has its own populated node_modules; see notes.md §TODO: Isolate npm pack from sibling file: dep junctions`,
35
+ };
36
+ }
37
+ return {
38
+ summary: `npm pack failed — arborist crashed on a nested node_modules`,
39
+ details,
40
+ hint: `likely a file: dep whose target has its own node_modules; see notes.md §TODO: Isolate npm pack from sibling file: dep junctions`,
41
+ };
42
+ }
43
+ // Pattern 2: arborist null-`package` without an obvious path — same class,
44
+ // no identifiable referenced module.
45
+ if (hasNullPackage) {
46
+ return {
47
+ summary: `npm pack failed — arborist internal error`,
48
+ details: [extractArboristFrame(blob) || 'Cannot read properties of null (reading \'package\')'],
49
+ hint: `likely a symlinked file: dep with its own node_modules; try renaming node_modules/ and running \`npm pack --dry-run\``,
50
+ };
51
+ }
52
+ // Pattern 3: shrinkwrap / lockfile parsing without the arborist crash.
53
+ if (/enolock|shrinkwrap failed to load|eresolve/i.test(lower)) {
54
+ return {
55
+ summary: `npm pack failed — lockfile problem`,
56
+ details: [extractFirstNpmError(blob) || blob.trim().slice(0, 400)],
57
+ hint: `run \`npm install\` in ${cwd}, then retry`,
58
+ };
59
+ }
60
+ // Pattern 4: file-locking on the tarball (AV, editor, explorer preview).
61
+ if (/\be(acces|busy|perm)\b/i.test(blob) && /\.tgz\b/i.test(blob)) {
62
+ return {
63
+ summary: `npm pack failed — tarball file locked`,
64
+ details: [extractFirstNpmError(blob) || blob.trim().slice(0, 400)],
65
+ hint: `close editors / antivirus holding the .tgz, then retry`,
66
+ };
67
+ }
68
+ // Fallthrough: raw output.
69
+ return {
70
+ summary: `npm pack failed`,
71
+ details: [
72
+ output.trim() ? `Output: ${output.trim()}` : '',
73
+ stderr.trim() ? `Error: ${stderr.trim()}` : '',
74
+ ].filter(Boolean),
75
+ };
76
+ }
77
+ /** Given a nested-node_modules path like
78
+ * `node_modules/@scope/sibling/node_modules/transitive`
79
+ * or `../sibling/node_modules/transitive`,
80
+ * find the outermost dep name and resolve it to a file: sibling declared
81
+ * in pkg.dependencies / pkg['.dependencies'] / etc. */
82
+ function resolveSiblingFromNestedPath(nestedPath, cwd, pkg) {
83
+ const normalized = nestedPath.replace(/\\/g, '/');
84
+ // Prefer the sibling-relative shape first: `../iflow-direct/node_modules/undici-types`.
85
+ // The *outermost* thing is the sibling dir, not the nested transitive.
86
+ const siblingMatch = normalized.match(/^(?:\.\.\/)+((?:@[^/]+\/)?[^/]+)\/node_modules\//);
87
+ if (siblingMatch) {
88
+ const absSibling = path.resolve(cwd, normalized.split('/node_modules/')[0]);
89
+ const resolved = findFileDepByPath(pkg, cwd, absSibling);
90
+ if (resolved)
91
+ return resolved;
92
+ return { name: siblingMatch[1], filePath: absSibling };
93
+ }
94
+ // Otherwise use the first `node_modules/<name>` segment.
95
+ const nmMatch = normalized.match(/(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)/);
96
+ const candidateName = nmMatch?.[1];
97
+ if (!candidateName)
98
+ return undefined;
99
+ // Try to confirm it's a file: dep by checking pkg.dependencies and .dependencies.
100
+ const allDeps = {};
101
+ for (const key of ['dependencies', '.dependencies', 'devDependencies', '.devDependencies']) {
102
+ if (pkg && pkg[key] && typeof pkg[key] === 'object')
103
+ Object.assign(allDeps, pkg[key]);
104
+ }
105
+ const spec = allDeps[candidateName];
106
+ if (spec && spec.startsWith('file:')) {
107
+ return { name: candidateName, filePath: path.resolve(cwd, spec.slice('file:'.length)) };
108
+ }
109
+ // Even if not explicitly file:, still useful to name it.
110
+ return { name: candidateName, filePath: path.resolve(cwd, 'node_modules', candidateName) };
111
+ }
112
+ /** Scan pkg deps for a file: entry whose resolved absolute path equals absTarget. */
113
+ function findFileDepByPath(pkg, cwd, absTarget) {
114
+ const target = path.resolve(absTarget).toLowerCase();
115
+ for (const key of ['dependencies', '.dependencies', 'devDependencies', '.devDependencies']) {
116
+ const deps = pkg && pkg[key];
117
+ if (!deps || typeof deps !== 'object')
118
+ continue;
119
+ for (const [name, spec] of Object.entries(deps)) {
120
+ if (typeof spec !== 'string' || !spec.startsWith('file:'))
121
+ continue;
122
+ const abs = path.resolve(cwd, spec.slice('file:'.length)).toLowerCase();
123
+ if (abs === target)
124
+ return { name, filePath: abs };
125
+ }
126
+ }
127
+ return undefined;
128
+ }
129
+ /** Pull out the first arborist stack frame for display. */
130
+ function extractArboristFrame(blob) {
131
+ const m = blob.match(/at [^\n]*arborist[^\n]*/i);
132
+ return m ? m[0].trim() : undefined;
133
+ }
134
+ /** Pull out the first `npm error <message>` line. */
135
+ function extractFirstNpmError(blob) {
136
+ const m = blob.match(/npm error [^\n]+/i);
137
+ return m ? m[0].trim() : undefined;
138
+ }
139
+ /** Exposed for ad-hoc testing — not used by the main flow. */
140
+ export const _test = { resolveSiblingFromNestedPath, findFileDepByPath };
141
+ //# sourceMappingURL=diagnose.js.map
package/lib.d.ts CHANGED
@@ -115,6 +115,7 @@ interface WorkspacePackageResult {
115
115
  success: boolean;
116
116
  version?: string;
117
117
  error?: string;
118
+ skipped?: boolean;
118
119
  }
119
120
  /** Aggregate result from workspace orchestration */
120
121
  export interface WorkspaceResult {
package/lib.js CHANGED
@@ -31,6 +31,7 @@ import libversion from 'libnpmversion';
31
31
  import JSON5 from 'json5';
32
32
  import { fileURLToPath } from 'url';
33
33
  import { themeColors } from '@bobfrankston/themecolors';
34
+ import { diagnoseNpmPackFailure } from './lib/diagnose.js';
34
35
  /** Semantic color functions — adapts to terminal light/dark theme */
35
36
  const colors = themeColors();
36
37
  const _buildIssues = [];
@@ -810,6 +811,49 @@ function hasUnpublishedTransitiveDeps(packageName, pkg, baseDir, verbose) {
810
811
  }
811
812
  return false;
812
813
  }
814
+ /** Walk a dir and return true if any `.ts` source has mtime newer than its sibling `.js`
815
+ * (or the `.js` is missing). Skips node_modules/prev/cruft/.git and declaration files. */
816
+ function hasStaleBuild(rootDir) {
817
+ const SKIP = new Set(['node_modules', 'prev', 'cruft', '.git']);
818
+ function walk(dir) {
819
+ let entries;
820
+ try {
821
+ entries = fs.readdirSync(dir, { withFileTypes: true });
822
+ }
823
+ catch {
824
+ return { stale: false };
825
+ }
826
+ for (const e of entries) {
827
+ const full = path.join(dir, e.name);
828
+ if (e.isDirectory()) {
829
+ if (SKIP.has(e.name.toLowerCase()))
830
+ continue;
831
+ const sub = walk(full);
832
+ if (sub.stale)
833
+ return sub;
834
+ }
835
+ else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
836
+ const jsPath = full.slice(0, -3) + '.js';
837
+ try {
838
+ const tsStat = fs.statSync(full);
839
+ let jsStat;
840
+ try {
841
+ jsStat = fs.statSync(jsPath);
842
+ }
843
+ catch {
844
+ return { stale: true, example: `${e.name} (no .js)` };
845
+ }
846
+ if (tsStat.mtimeMs > jsStat.mtimeMs) {
847
+ return { stale: true, example: `${e.name} newer than ${path.basename(jsPath)}` };
848
+ }
849
+ }
850
+ catch { /* unreadable — ignore */ }
851
+ }
852
+ }
853
+ return { stale: false };
854
+ }
855
+ return walk(rootDir);
856
+ }
813
857
  /** Check if local package directory has changes newer than the npm-published version.
814
858
  * Detects uncommitted changes or commits made after the version was published. */
815
859
  function hasLocalChanges(packageName, version, targetPath, verbose) {
@@ -2933,7 +2977,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
2933
2977
  console.log(colors.yellow('Local branch is behind remote.'));
2934
2978
  if (rebase) {
2935
2979
  console.log('Rebasing local changes (--rebase)...');
2936
- const rebaseResult = runCommand('git', ['pull', '--rebase'], { cwd, silent: false });
2980
+ const rebaseResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd, silent: false });
2937
2981
  if (!rebaseResult.success) {
2938
2982
  console.error(colors.red('ERROR: Rebase failed.'));
2939
2983
  console.error('You may need to resolve conflicts manually.');
@@ -3143,9 +3187,6 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3143
3187
  }
3144
3188
  // Don't set "private": true in package.json - that blocks all publishing
3145
3189
  console.log(`Package '${pkg.name}' will publish as PRIVATE (restricted access).`);
3146
- if (!currentAccess) {
3147
- console.log(colors.dim(` First publish - requires paid npm account`));
3148
- }
3149
3190
  }
3150
3191
  else if (effectiveNpmVisibility === 'public') {
3151
3192
  // User explicitly wants public (or confirmed via prompt)
@@ -3832,7 +3873,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3832
3873
  }
3833
3874
  // Pull latest from remote before version bump to avoid push rejection
3834
3875
  if (currentGitStatus.hasRemote && !dryRun) {
3835
- const pullResult = runCommand('git', ['pull', '--rebase'], { cwd, silent: true });
3876
+ const pullResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd, silent: true });
3836
3877
  if (!pullResult.success) {
3837
3878
  console.error(colors.yellow('Warning: git pull --rebase failed before version bump'));
3838
3879
  if (verbose) {
@@ -3908,7 +3949,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3908
3949
  }
3909
3950
  catch (error) {
3910
3951
  let autoFixed = false;
3911
- console.error(colors.red('ERROR: Version bump failed:'), error.message);
3952
+ console.error(colors.red(`ERROR: Version bump failed in ${cwd} (${pkg.name || '<unnamed>'}):`), error.message);
3912
3953
  // Show additional error details if available
3913
3954
  if (error.stderr || error.stdout || error.code) {
3914
3955
  if (error.stderr)
@@ -3933,7 +3974,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3933
3974
  // Version bump + tag succeeded locally; only the push failed.
3934
3975
  // Auto-pull --rebase and retry the push.
3935
3976
  console.log(colors.yellow('\nLocal branch is behind remote — pulling with rebase...'));
3936
- const pullResult = runCommand('git', ['pull', '--rebase'], { cwd });
3977
+ const pullResult = runCommand('git', ['pull', '--rebase', 'origin', currentGitStatus.currentBranch], { cwd });
3937
3978
  if (pullResult.success) {
3938
3979
  console.log(colors.green(' ✓ Rebased onto remote'));
3939
3980
  const pushResult = runCommand('git', ['push'], { cwd });
@@ -4065,8 +4106,39 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4065
4106
  return false;
4066
4107
  }
4067
4108
  }
4109
+ else if (/paths are ignored by one of your \.gitignore files/i.test(combinedOutput)) {
4110
+ // git add refused to stage files (usually package.json) because a
4111
+ // .gitignore rule matches. Ask git which exact line is to blame.
4112
+ const ignoredMatch = combinedOutput.match(/paths are ignored by one of your \.gitignore files:\s*\n([\s\S]*?)(?:hint:|$)/i);
4113
+ const ignoredFiles = (ignoredMatch ? ignoredMatch[1] : '')
4114
+ .split('\n').map(s => s.trim()).filter(Boolean);
4115
+ console.error(colors.red(`\n.gitignore in ${cwd} is blocking required file(s):`));
4116
+ for (const f of ignoredFiles) {
4117
+ const chk = spawnSafe('git', ['-C', cwd, 'check-ignore', '-v', f], { encoding: 'utf-8', stdio: 'pipe' });
4118
+ if (chk.status === 0 && chk.stdout?.trim()) {
4119
+ // Output format: "<source>:<line>:<pattern>\t<file>"
4120
+ const m = chk.stdout.trim().match(/^(.+?):(\d+):(.+?)\t(.+)$/);
4121
+ if (m) {
4122
+ const [, src, line, pattern, file] = m;
4123
+ console.error(colors.red(` ${file} — matched by ${src}:${line} pattern: ${pattern}`));
4124
+ if (pattern.trim() === '*') {
4125
+ console.error(colors.yellow(` → Line ${line} is a bare "*" which ignores EVERY file. Delete it.`));
4126
+ }
4127
+ }
4128
+ else {
4129
+ console.error(colors.red(` ${f}`));
4130
+ console.error(colors.dim(` ${chk.stdout.trim()}`));
4131
+ }
4132
+ }
4133
+ else {
4134
+ console.error(colors.red(` ${f}`));
4135
+ }
4136
+ }
4137
+ console.error(colors.yellow(`\nEdit ${path.join(cwd, '.gitignore')} to remove/narrow the blocking pattern,`));
4138
+ console.error(colors.yellow(`or exempt with an explicit negation, e.g.: !package.json`));
4139
+ }
4068
4140
  else if (error.message?.includes('unknown git error')) {
4069
- console.error(colors.yellow('Unknown git error — check git hooks, signing, or permissions'));
4141
+ console.error(colors.yellow(`Unknown git error in ${cwd} — check git hooks, signing, or permissions`));
4070
4142
  }
4071
4143
  else if (combinedOutput.includes('gh013') || combinedOutput.includes('push protection') || combinedOutput.includes('push declined due to repository rule')) {
4072
4144
  // GitHub push protection blocked a push (from postversion script)
@@ -4168,10 +4240,15 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4168
4240
  // Create tarball first
4169
4241
  const packResult = runCommand('npm', ['pack'], { cwd, silent: true });
4170
4242
  if (!packResult.success) {
4171
- console.error(colors.red('ERROR: Failed to create package tarball'));
4172
- console.error(colors.yellow('Output:'), packResult.output);
4173
- console.error(colors.yellow('Error:'), packResult.stderr);
4174
- recordBuildIssue(pkg.name || path.basename(cwd), 'error', 'npm pack failed');
4243
+ const d = diagnoseNpmPackFailure(cwd, packResult.output, packResult.stderr, pkg);
4244
+ console.error(colors.red(`ERROR: ${d.summary}`));
4245
+ for (const line of d.details)
4246
+ console.error(colors.yellow(' ' + line));
4247
+ if (d.referencedModule)
4248
+ console.error(colors.yellow(` Caused by referenced module: ${d.referencedModule}`));
4249
+ if (d.hint)
4250
+ console.error(colors.yellow(` Hint: ${d.hint}`));
4251
+ recordBuildIssue(pkg.name || path.basename(cwd), 'error', d.referencedModule ? `${d.summary} (via ${d.referencedModule})` : d.summary);
4175
4252
  return false;
4176
4253
  }
4177
4254
  // Get the tarball filename from npm pack output
@@ -4637,9 +4714,84 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
4637
4714
  }
4638
4715
  }
4639
4716
  }
4717
+ // Prescan: decide which packages actually need processing so we don't waste
4718
+ // time rebuilding+republishing ones with no relevant changes.
4719
+ // A package is SKIPPED only if ALL of:
4720
+ // - working tree clean (no uncommitted changes)
4721
+ // - current version is on npm AND no commits newer than its publish time
4722
+ // - build not stale (every .ts source has a .js no older than it)
4723
+ // - no sibling workspace file: dep was itself flagged for processing
4724
+ const flaggedForWork = new Set();
4725
+ const skipReasons = new Map();
4726
+ if (!options.force && !options.forcePublish) {
4727
+ console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4728
+ console.log(` Prescan: checking ${filteredOrder.length} package(s)...`);
4729
+ console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4730
+ const wsDeps = new Map(); // pkg → workspace-sibling file: deps
4731
+ for (const pkgInfo of packages) {
4732
+ const sibs = new Set();
4733
+ const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
4734
+ for (const [dn, dv] of Object.entries(deps)) {
4735
+ if (typeof dv === 'string' && dv.startsWith('file:') && wsNameSet.has(dn)) {
4736
+ sibs.add(dn);
4737
+ }
4738
+ }
4739
+ wsDeps.set(pkgInfo.name, sibs);
4740
+ }
4741
+ for (const pkgName of filteredOrder) {
4742
+ const pkgInfo = packages.find(p => p.name === pkgName);
4743
+ if (!pkgInfo)
4744
+ continue;
4745
+ let reason;
4746
+ const status = spawnSafe('git', ['-C', pkgInfo.dir, 'status', '--porcelain', '.'], { encoding: 'utf-8', stdio: 'pipe', shell: true });
4747
+ if (status.status === 0 && status.stdout.trim()) {
4748
+ reason = 'uncommitted changes';
4749
+ }
4750
+ else {
4751
+ const version = pkgInfo.pkg.version;
4752
+ if (!checkVersionExists(pkgInfo.name, version)) {
4753
+ reason = `v${version} not on npm`;
4754
+ }
4755
+ else if (hasLocalChanges(pkgInfo.name, version, pkgInfo.dir, false)) {
4756
+ reason = 'commits since last publish';
4757
+ }
4758
+ else {
4759
+ const stale = hasStaleBuild(pkgInfo.dir);
4760
+ if (stale.stale) {
4761
+ reason = `stale build (${stale.example})`;
4762
+ }
4763
+ else {
4764
+ const flaggedSib = [...(wsDeps.get(pkgName) || [])].find(s => flaggedForWork.has(s));
4765
+ if (flaggedSib)
4766
+ reason = `dep ${flaggedSib} is being updated`;
4767
+ }
4768
+ }
4769
+ }
4770
+ if (reason) {
4771
+ flaggedForWork.add(pkgName);
4772
+ console.log(` ${colors.yellow('⟳')} ${pkgName} — ${reason}`);
4773
+ }
4774
+ else {
4775
+ skipReasons.set(pkgName, 'clean, published, build fresh');
4776
+ console.log(` ${colors.green('✓')} ${pkgName} — skip (clean, published, build fresh)`);
4777
+ }
4778
+ }
4779
+ console.log('');
4780
+ if (flaggedForWork.size === 0) {
4781
+ console.log(colors.green('Nothing to do — all packages up to date.'));
4782
+ console.log('');
4783
+ return { success: true, packages: [], publishOrder };
4784
+ }
4785
+ console.log(colors.dim(`Prescan: ${flaggedForWork.size} to process, ${skipReasons.size} skip. (Use --force to process all.)`));
4786
+ console.log('');
4787
+ }
4640
4788
  // Process each package in dependency order
4641
4789
  const results = [];
4642
4790
  for (const pkgName of filteredOrder) {
4791
+ if (skipReasons.has(pkgName)) {
4792
+ results.push({ name: pkgName, dir: packages.find(p => p.name === pkgName).dir, success: true, version: packages.find(p => p.name === pkgName).pkg.version, skipped: true });
4793
+ continue;
4794
+ }
4643
4795
  const pkgInfo = packages.find(p => p.name === pkgName);
4644
4796
  if (!pkgInfo)
4645
4797
  continue;
@@ -4692,10 +4844,11 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
4692
4844
  console.log(allSuccess ? colors.green('✓ Workspace Summary') : colors.red('✗ Workspace Summary'));
4693
4845
  console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4694
4846
  for (const r of results) {
4695
- const status = r.success ? colors.green('✓') : colors.red('✗');
4847
+ const status = r.skipped ? colors.dim('–') : (r.success ? colors.green('✓') : colors.red('✗'));
4696
4848
  const ver = r.version ? ` v${r.version}` : '';
4697
4849
  const err = r.error ? colors.red(` (${r.error})`) : '';
4698
- console.log(` ${status} ${r.name}${ver}${err}`);
4850
+ const tag = r.skipped ? colors.dim(' (skipped — already up to date)') : '';
4851
+ console.log(` ${status} ${r.name}${ver}${err}${tag}`);
4699
4852
  }
4700
4853
  console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
4701
4854
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.149",
3
+ "version": "1.0.151",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",