@bobfrankston/npmglobalize 1.0.208 → 1.0.210
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 +904 -910
- package/lib.d.ts +11 -0
- package/lib.js +268 -73
- package/package.json +1 -1
package/lib.d.ts
CHANGED
|
@@ -485,6 +485,14 @@ export declare function ensureWorkspaceDepModules(rootDir: string, members: Arra
|
|
|
485
485
|
* `npm error` lines minus stack frames and brace-only object-dump fragments,
|
|
486
486
|
* deduped. The full output belongs in a log file (saveNpmLog), not the terminal. */
|
|
487
487
|
export declare function distillNpmErrors(combined: string): string[];
|
|
488
|
+
/** True when a failed install is npm's "no such version" (ETARGET/E404) for the
|
|
489
|
+
* very spec we just published — registry propagation or a stale local
|
|
490
|
+
* packument, not a broken package. `pkgSpec` is `name@version`. */
|
|
491
|
+
export declare function isPropagationFailure(combined: string, pkgSpec: string): boolean;
|
|
492
|
+
/** What to tell the user when an install fails purely because npm cannot see a
|
|
493
|
+
* just-published version yet. Says what IS true (published, will install),
|
|
494
|
+
* what is NOT (nothing wrong with the package), and one command that works. */
|
|
495
|
+
export declare function propagationHelp(pkgSpec: string): string[];
|
|
488
496
|
/** Scan a puppeteer browser cache (layout <root>/<browser>/<platform-version>)
|
|
489
497
|
* and repair every version folder that has no browser executable in it — the
|
|
490
498
|
* residue of a truncated extraction (see extractBrowserArchive for the Node
|
|
@@ -570,6 +578,9 @@ export interface GitStatus {
|
|
|
570
578
|
currentBranch: string;
|
|
571
579
|
remoteBranch: string;
|
|
572
580
|
isBehindRemote: boolean;
|
|
581
|
+
inProgressOp: string; /** 'rebase' | 'cherry-pick' | 'revert' | 'merge' | '' — a sequencer operation git never finished */
|
|
582
|
+
detachedFromBranch: string; /** Branch the unfinished operation started from (read from the rebase state's head-name) */
|
|
583
|
+
unmergedPaths: string[]; /** Files with unresolved conflict markers */
|
|
573
584
|
}
|
|
574
585
|
export declare function getGitStatus(cwd: string): GitStatus;
|
|
575
586
|
/** Validate package.json for release */
|
package/lib.js
CHANGED
|
@@ -2128,13 +2128,30 @@ async function npmInstallWithCleanRetry(dir, verbose, context) {
|
|
|
2128
2128
|
* resolvable. We wait longer and re-probe `npm view` for the version
|
|
2129
2129
|
* string until it shows up (or we hit the cap). */
|
|
2130
2130
|
function waitForNpmVersion(pkgName, version, isNewPackage = false, maxWaitMs) {
|
|
2131
|
-
|
|
2132
|
-
|
|
2131
|
+
// 2026-09-01 10:15 EDT — Claude Code (Opus 5), at Bob's direction.
|
|
2132
|
+
// This loop was polling npm's LOCAL cache, not the registry. Registry
|
|
2133
|
+
// packuments carry `cache-control: public, max-age=300`, and npm's default
|
|
2134
|
+
// (prefer-online=false) serves a cached packument for that whole window
|
|
2135
|
+
// without revalidating — so once the first probe cached a packument that
|
|
2136
|
+
// predates the publish, every probe for the next 5 minutes returned the
|
|
2137
|
+
// same stale answer no matter what the registry actually held. Verified
|
|
2138
|
+
// 2026-09-01 on @bobfrankston/brother-label@1.1.22: the npm debug log for
|
|
2139
|
+
// the failing install shows `http cache … (cache hit)` — it never asked
|
|
2140
|
+
// the registry — while the cached entry's own resHeaders read
|
|
2141
|
+
// `cache-control: public, max-age=300`. `--prefer-online` forces the
|
|
2142
|
+
// staleness check (a conditional request), which is the only way this
|
|
2143
|
+
// poll can observe anything new.
|
|
2144
|
+
// Same run: publish at 13:52:48Z, packument `last-modified 13:57:59Z` —
|
|
2145
|
+
// real propagation took 5m11s, past the old 180s cap for an existing
|
|
2146
|
+
// package. Raised; with a probe that actually revalidates, an early
|
|
2147
|
+
// success still exits immediately, so the higher cap costs nothing.
|
|
2148
|
+
const effectiveMaxWait = maxWaitMs ?? (isNewPackage ? 600000 : 420000);
|
|
2149
|
+
const interval = 5000;
|
|
2133
2150
|
const maxAttempts = Math.ceil(effectiveMaxWait / interval);
|
|
2134
2151
|
const suffix = isNewPackage ? ' (new package, may take several minutes)' : '';
|
|
2135
2152
|
process.stdout.write(`${timestamp()} Waiting for ${pkgName}@${version} on npm registry${suffix}`);
|
|
2136
2153
|
for (let i = 0; i < maxAttempts; i++) {
|
|
2137
|
-
const result = spawnSafe('npm', ['view', `${pkgName}@${version}`, 'version'], {
|
|
2154
|
+
const result = spawnSafe('npm', ['view', '--prefer-online', `${pkgName}@${version}`, 'version'], {
|
|
2138
2155
|
shell: process.platform === 'win32',
|
|
2139
2156
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2140
2157
|
encoding: 'utf-8'
|
|
@@ -2147,6 +2164,10 @@ function waitForNpmVersion(pkgName, version, isNewPackage = false, maxWaitMs) {
|
|
|
2147
2164
|
sleepSync(interval);
|
|
2148
2165
|
}
|
|
2149
2166
|
process.stdout.write(' timed out\n');
|
|
2167
|
+
// Timing out here is not a publish failure — the version is on npm, this
|
|
2168
|
+
// client just cannot see it yet. Say so, or the install error that follows
|
|
2169
|
+
// reads as though the package itself were broken.
|
|
2170
|
+
console.log(colors.yellow(` ${pkgName}@${version} was published but is still not visible to npm after ${Math.round(effectiveMaxWait / 1000)}s — trying the install anyway.`));
|
|
2150
2171
|
return false;
|
|
2151
2172
|
}
|
|
2152
2173
|
function cleanNestedDepModules(pkg, cwd, verbose) {
|
|
@@ -3729,6 +3750,38 @@ export function distillNpmErrors(combined) {
|
|
|
3729
3750
|
}
|
|
3730
3751
|
return key.slice(0, 12);
|
|
3731
3752
|
}
|
|
3753
|
+
// 2026-09-01 10:40 EDT — Claude Code (Opus 5), at Bob's direction: "it installed
|
|
3754
|
+
// correctly on a retry - need better error messages". What the run printed was
|
|
3755
|
+
// npm's own text — "No matching version found ... In most cases you or one of
|
|
3756
|
+
// your dependencies are requesting a package version that doesn't exist" —
|
|
3757
|
+
// which is flatly wrong here: WE published that exact version 4 seconds
|
|
3758
|
+
// earlier, and a manual retry a minute later installed it. The two helpers
|
|
3759
|
+
// below name the real situation instead.
|
|
3760
|
+
/** True when a failed install is npm's "no such version" (ETARGET/E404) for the
|
|
3761
|
+
* very spec we just published — registry propagation or a stale local
|
|
3762
|
+
* packument, not a broken package. `pkgSpec` is `name@version`. */
|
|
3763
|
+
export function isPropagationFailure(combined, pkgSpec) {
|
|
3764
|
+
if (pkgSpec === '.')
|
|
3765
|
+
return false; // local-directory install can't be a registry-visibility problem
|
|
3766
|
+
if (!/npm (?:error|ERR!)\s+code E(?:TARGET|404)\b/.test(combined))
|
|
3767
|
+
return false;
|
|
3768
|
+
const name = pkgSpec.replace(/@[^@/]+$/, ''); // strip the trailing @version, keep any @scope
|
|
3769
|
+
return combined.includes(pkgSpec) || combined.includes(name);
|
|
3770
|
+
}
|
|
3771
|
+
/** What to tell the user when an install fails purely because npm cannot see a
|
|
3772
|
+
* just-published version yet. Says what IS true (published, will install),
|
|
3773
|
+
* what is NOT (nothing wrong with the package), and one command that works. */
|
|
3774
|
+
export function propagationHelp(pkgSpec) {
|
|
3775
|
+
return [
|
|
3776
|
+
`${pkgSpec} is published — npm just can't see it from this machine yet.`,
|
|
3777
|
+
` The registry takes up to several minutes to propagate a new version, and npm`,
|
|
3778
|
+
` caches the package's version list for 5 minutes (cache-control: max-age=300),`,
|
|
3779
|
+
` so a retry inside that window can still miss it unless the cache is bypassed.`,
|
|
3780
|
+
` Nothing is wrong with the package or the publish; the global copy is simply the`,
|
|
3781
|
+
` previous version until you re-run:`,
|
|
3782
|
+
` npm install -g --prefer-online ${pkgSpec}`,
|
|
3783
|
+
];
|
|
3784
|
+
}
|
|
3732
3785
|
/** Write the full output of a failed npm command to a log file under the temp
|
|
3733
3786
|
* dir and return its path (null if the write itself fails). */
|
|
3734
3787
|
function saveNpmLog(label, content) {
|
|
@@ -4054,8 +4107,16 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
|
|
|
4054
4107
|
// stays behind npm's allowScripts gate.
|
|
4055
4108
|
const allowArgs = allowOwnScriptsArgs(cwd);
|
|
4056
4109
|
while (true) {
|
|
4057
|
-
|
|
4058
|
-
|
|
4110
|
+
// 2026-09-01 10:15 EDT — Claude Code (Opus 5), at Bob's direction.
|
|
4111
|
+
// `--prefer-online`: this install always targets a version published
|
|
4112
|
+
// seconds ago, so a cached packument is exactly the hazard. Without it
|
|
4113
|
+
// all three ETARGET retries re-read the same stale local cache and are
|
|
4114
|
+
// guaranteed to fail identically — confirmed in npm's debug log for
|
|
4115
|
+
// brother-label@1.1.22 (`http cache … (cache hit)`, no registry
|
|
4116
|
+
// request at all). Costs one conditional request per packument.
|
|
4117
|
+
const online = pkgSpec === '.' ? [] : ['--prefer-online'];
|
|
4118
|
+
console.log(colors.cyan(`> npm install -g ${[...online, pkgSpec, ...allowArgs].join(' ')}`));
|
|
4119
|
+
result = await runCommandAsync('npm', ['install', '-g', ...online, pkgSpec, ...allowArgs], { cwd, silent: true });
|
|
4059
4120
|
const combined = `${result.output}\n${result.stderr}`;
|
|
4060
4121
|
fullLog += `===== attempt ${attempt + 1}: npm install -g ${pkgSpec} =====\n${combined}\n`;
|
|
4061
4122
|
if (result.success) {
|
|
@@ -4076,8 +4137,19 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
|
|
|
4076
4137
|
const cleanupFails = (combined.match(/npm warn cleanup\s+\[Error: EPERM/g) || []).length;
|
|
4077
4138
|
if (cleanupFails)
|
|
4078
4139
|
console.log(colors.dim(` (npm couldn't remove ${cleanupFails} leftover dir(s) — EPERM cleanup warnings, details in log)`));
|
|
4079
|
-
|
|
4140
|
+
const propagating = isPropagationFailure(combined, pkgSpec);
|
|
4141
|
+
for (const line of distillNpmErrors(combined)) {
|
|
4142
|
+
// npm's stock follow-up to ETARGET blames the request ("you or one of
|
|
4143
|
+
// your dependencies are requesting a version that doesn't exist").
|
|
4144
|
+
// For a version we published seconds ago that sends the reader
|
|
4145
|
+
// hunting a nonexistent dependency bug — drop it and say the real
|
|
4146
|
+
// thing below.
|
|
4147
|
+
if (propagating && /In most cases you or one of your dependencies/.test(line))
|
|
4148
|
+
continue;
|
|
4080
4149
|
console.error(colors.red(` ${line}`));
|
|
4150
|
+
}
|
|
4151
|
+
if (propagating)
|
|
4152
|
+
console.error(colors.yellow(` ↻ ${pkgSpec} not visible to npm yet — registry propagation, not a bad package.`));
|
|
4081
4153
|
// Known-fixable failure: repair and retry immediately — the cause is
|
|
4082
4154
|
// local, so waiting for registry propagation is pointless. Puppeteer
|
|
4083
4155
|
// reports one corrupted browser per run, so allow a few repair rounds
|
|
@@ -4288,7 +4360,10 @@ async function waitForNpmVersionInWsl(spec, maxWaitMs = 180000) {
|
|
|
4288
4360
|
const maxAttempts = Math.ceil(maxWaitMs / interval);
|
|
4289
4361
|
process.stdout.write(`${timestamp()} Waiting for ${spec} on npm registry (WSL view)`);
|
|
4290
4362
|
for (let i = 0; i < maxAttempts; i++) {
|
|
4291
|
-
|
|
4363
|
+
// 2026-09-01 10:15 EDT — Claude Code: --prefer-online for the same
|
|
4364
|
+
// reason as waitForNpmVersion — WSL's npm has its own cache with the
|
|
4365
|
+
// same 300s packument freshness window to poll past.
|
|
4366
|
+
const r = await runCommandAsync('wsl', ['npm', 'view', '--prefer-online', spec, 'version'], { silent: true });
|
|
4292
4367
|
if (r.success && (r.output || '').trim()) {
|
|
4293
4368
|
process.stdout.write(' ready\n');
|
|
4294
4369
|
return true;
|
|
@@ -4663,7 +4738,10 @@ export function getGitStatus(cwd) {
|
|
|
4663
4738
|
isDetachedHead: false,
|
|
4664
4739
|
currentBranch: '',
|
|
4665
4740
|
remoteBranch: '',
|
|
4666
|
-
isBehindRemote: false
|
|
4741
|
+
isBehindRemote: false,
|
|
4742
|
+
inProgressOp: '',
|
|
4743
|
+
detachedFromBranch: '',
|
|
4744
|
+
unmergedPaths: []
|
|
4667
4745
|
};
|
|
4668
4746
|
// Check if git repo — walk up the directory tree to find the enclosing
|
|
4669
4747
|
// .git, like git itself does. Previously this only looked for `.git/` at
|
|
@@ -4680,9 +4758,42 @@ export function getGitStatus(cwd) {
|
|
|
4680
4758
|
return status;
|
|
4681
4759
|
}
|
|
4682
4760
|
status.isRepo = true;
|
|
4683
|
-
//
|
|
4761
|
+
// 2026-09-01 14:5x EDT — Claude Code (Opus 5), at Bob's direction.
|
|
4762
|
+
// Detect a sequencer operation git never finished. This is the usual cause of a
|
|
4763
|
+
// detached HEAD in these trees: an npmglobalize run does `git pull --rebase`, the
|
|
4764
|
+
// run is interrupted, and the rebase stops after applying its patches but before
|
|
4765
|
+
// moving the branch ref — so HEAD sits on a real commit that belongs to no branch.
|
|
4766
|
+
// Previously getGitStatus saw only "detached" and the caller printed a lecture about
|
|
4767
|
+
// checking out a tag, which is not what happened and names the wrong branch.
|
|
4684
4768
|
const mergeHead = path.join(gitDir, 'MERGE_HEAD');
|
|
4685
|
-
|
|
4769
|
+
const rebaseMerge = path.join(gitDir, 'rebase-merge');
|
|
4770
|
+
const rebaseApply = path.join(gitDir, 'rebase-apply');
|
|
4771
|
+
if (fs.existsSync(rebaseMerge) || fs.existsSync(rebaseApply)) {
|
|
4772
|
+
status.inProgressOp = 'rebase';
|
|
4773
|
+
for (const dir of [rebaseMerge, rebaseApply]) {
|
|
4774
|
+
const headName = path.join(dir, 'head-name');
|
|
4775
|
+
if (!fs.existsSync(headName))
|
|
4776
|
+
continue;
|
|
4777
|
+
status.detachedFromBranch = fs.readFileSync(headName, 'utf-8').trim().replace(/^refs\/heads\//, '');
|
|
4778
|
+
break;
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
else if (fs.existsSync(path.join(gitDir, 'CHERRY_PICK_HEAD')))
|
|
4782
|
+
status.inProgressOp = 'cherry-pick';
|
|
4783
|
+
else if (fs.existsSync(path.join(gitDir, 'REVERT_HEAD')))
|
|
4784
|
+
status.inProgressOp = 'revert';
|
|
4785
|
+
else if (fs.existsSync(mergeHead))
|
|
4786
|
+
status.inProgressOp = 'merge';
|
|
4787
|
+
// Unresolved conflicts — the one state where the working tree is NOT authoritative,
|
|
4788
|
+
// because it still holds conflict markers rather than the content Bob wants published.
|
|
4789
|
+
try {
|
|
4790
|
+
const unmerged = execSync('git diff --name-only --diff-filter=U', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
4791
|
+
status.unmergedPaths = unmerged ? unmerged.split(/\r?\n/) : [];
|
|
4792
|
+
}
|
|
4793
|
+
catch (error) {
|
|
4794
|
+
// Ignore - repo may have no commits yet
|
|
4795
|
+
}
|
|
4796
|
+
status.hasMergeConflict = status.unmergedPaths.length > 0;
|
|
4686
4797
|
// Get branch info
|
|
4687
4798
|
try {
|
|
4688
4799
|
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
@@ -4736,6 +4847,97 @@ export function getGitStatus(cwd) {
|
|
|
4736
4847
|
}
|
|
4737
4848
|
return status;
|
|
4738
4849
|
}
|
|
4850
|
+
/** One-line `abbrev "subject"` description of a commit, for messages that must name what moved */
|
|
4851
|
+
function describeCommit(cwd, rev) {
|
|
4852
|
+
// Arg array, not a command string: the format contains a space and %s expands to a
|
|
4853
|
+
// commit subject that may contain quotes.
|
|
4854
|
+
const result = runCommand('git', ['log', '-1', '--format=%h %s', rev], { cwd, silent: true });
|
|
4855
|
+
if (!result.success)
|
|
4856
|
+
return rev;
|
|
4857
|
+
const line = result.output.trim().split(/\r?\n/)[0];
|
|
4858
|
+
const space = line.indexOf(' ');
|
|
4859
|
+
return space < 0 ? line : `${line.slice(0, space)} "${line.slice(space + 1)}"`;
|
|
4860
|
+
}
|
|
4861
|
+
/** Which branch a detached HEAD should be reattached to */
|
|
4862
|
+
function pickReattachBranch(cwd, status) {
|
|
4863
|
+
// The branch the unfinished rebase was rebuilding is the one that was meant to end up here.
|
|
4864
|
+
if (status.detachedFromBranch)
|
|
4865
|
+
return status.detachedFromBranch;
|
|
4866
|
+
// Otherwise the remote's own default branch, which is what a clone would have checked out.
|
|
4867
|
+
try {
|
|
4868
|
+
const originHead = execSync('git symbolic-ref --short refs/remotes/origin/HEAD', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
4869
|
+
if (originHead.startsWith('origin/'))
|
|
4870
|
+
return originHead.slice('origin/'.length);
|
|
4871
|
+
}
|
|
4872
|
+
catch (error) {
|
|
4873
|
+
// No origin/HEAD — fall through
|
|
4874
|
+
}
|
|
4875
|
+
// Otherwise whichever conventional branch this repo actually has.
|
|
4876
|
+
for (const name of ['main', 'master']) {
|
|
4877
|
+
const exists = runCommand('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${name}`], { cwd, silent: true }).success
|
|
4878
|
+
|| runCommand('git', ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${name}`], { cwd, silent: true }).success;
|
|
4879
|
+
if (exists)
|
|
4880
|
+
return name;
|
|
4881
|
+
}
|
|
4882
|
+
return 'master';
|
|
4883
|
+
}
|
|
4884
|
+
/**
|
|
4885
|
+
* 2026-09-01 14:5x EDT — Claude Code (Opus 5), at Bob's direction.
|
|
4886
|
+
* Reattach a detached HEAD to its branch instead of printing a menu of git commands.
|
|
4887
|
+
*
|
|
4888
|
+
* Bob: "The local directory is authoritative so I want legacy mismatches just cleaned up."
|
|
4889
|
+
* The working tree and the commit HEAD points at are what he wants published; a branch ref
|
|
4890
|
+
* left behind by an interrupted run is bookkeeping, not content. So: drop the unfinished
|
|
4891
|
+
* sequencer state, move the branch to this commit, and say exactly what moved and how to
|
|
4892
|
+
* get the old tip back. The commit the branch moved off of stays in the reflog, and the
|
|
4893
|
+
* message names it, so nothing is silently lost.
|
|
4894
|
+
*
|
|
4895
|
+
* Refuses only when the tree still has unresolved conflict markers — the one case where the
|
|
4896
|
+
* working tree is not what Bob means by authoritative.
|
|
4897
|
+
*/
|
|
4898
|
+
async function reattachDetachedHead(cwd, status, dryRun) {
|
|
4899
|
+
const branch = pickReattachBranch(cwd, status);
|
|
4900
|
+
const head = describeCommit(cwd, 'HEAD');
|
|
4901
|
+
const opLabel = status.inProgressOp ? `an unfinished ${status.inProgressOp}` : 'a checkout of a bare commit';
|
|
4902
|
+
console.log('');
|
|
4903
|
+
console.log(colors.yellow(`⚠ HEAD is not on a branch — left by ${opLabel}, at ${head}`));
|
|
4904
|
+
let oldTip = '';
|
|
4905
|
+
if (runCommand('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], { cwd, silent: true }).success) {
|
|
4906
|
+
const isAncestor = runCommand('git', ['merge-base', '--is-ancestor', branch, 'HEAD'], { cwd, silent: true }).success;
|
|
4907
|
+
if (!isAncestor)
|
|
4908
|
+
oldTip = describeCommit(cwd, branch);
|
|
4909
|
+
}
|
|
4910
|
+
if (dryRun) {
|
|
4911
|
+
console.log(colors.dim(` [dry-run] Would clear the ${status.inProgressOp || 'detached'} state and move branch ${branch} to ${head}`));
|
|
4912
|
+
if (oldTip)
|
|
4913
|
+
console.log(colors.dim(` [dry-run] Branch ${branch} currently points at ${oldTip}, which is not in this history`));
|
|
4914
|
+
return true;
|
|
4915
|
+
}
|
|
4916
|
+
if (status.inProgressOp && status.inProgressOp !== 'merge') {
|
|
4917
|
+
// --quit drops the sequencer bookkeeping and leaves HEAD and the working tree alone,
|
|
4918
|
+
// unlike --abort, which would throw away the commits the interrupted run produced.
|
|
4919
|
+
const quit = await runCommandAsync('git', [status.inProgressOp === 'rebase' ? 'rebase' : status.inProgressOp, '--quit'], { cwd, silent: true });
|
|
4920
|
+
if (!quit.success) {
|
|
4921
|
+
console.error(colors.red(`✗ Could not clear the unfinished ${status.inProgressOp}: ${quit.stderr.trim()}`));
|
|
4922
|
+
return false;
|
|
4923
|
+
}
|
|
4924
|
+
console.log(` Cleared the unfinished ${status.inProgressOp} (its commits are kept)`);
|
|
4925
|
+
}
|
|
4926
|
+
const checkout = await runCommandAsync('git', ['checkout', '-B', branch], { cwd, silent: true });
|
|
4927
|
+
if (!checkout.success) {
|
|
4928
|
+
console.error(colors.red(`✗ Could not move branch ${branch} to ${head}: ${checkout.stderr.trim()}`));
|
|
4929
|
+
return false;
|
|
4930
|
+
}
|
|
4931
|
+
console.log(colors.green(`✓ On branch ${branch} at ${head}`));
|
|
4932
|
+
if (oldTip) {
|
|
4933
|
+
console.log(` Branch ${branch} previously pointed at ${oldTip}, which is not in this history.`);
|
|
4934
|
+
console.log(colors.dim(` To put ${branch} back there: git reset --hard ${oldTip.split(' ')[0]}`));
|
|
4935
|
+
}
|
|
4936
|
+
if (runCommand('git', ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${branch}`], { cwd, silent: true }).success)
|
|
4937
|
+
runCommand('git', ['branch', `--set-upstream-to=origin/${branch}`, branch], { cwd, silent: true });
|
|
4938
|
+
console.log('');
|
|
4939
|
+
return true;
|
|
4940
|
+
}
|
|
4739
4941
|
/** Validate package.json for release */
|
|
4740
4942
|
export function validatePackageJson(pkg) {
|
|
4741
4943
|
const errors = [];
|
|
@@ -5816,7 +6018,9 @@ async function doLocalInstall(cwd, options) {
|
|
|
5816
6018
|
console.log(' [dry-run] Would run: wsl npm install -g .');
|
|
5817
6019
|
return true;
|
|
5818
6020
|
}
|
|
5819
|
-
|
|
6021
|
+
// 2026-09-01 10:55 EDT — Claude Code: see the note on the sibling local-install
|
|
6022
|
+
// path — captured, distilled output instead of raw npm chatter.
|
|
6023
|
+
const result = await installGlobalWithRetry('.', cwd, false, 1);
|
|
5820
6024
|
if (result.success) {
|
|
5821
6025
|
console.log(colors.green(`✓ Installed locally: ${pkgName}@${pkgVersion}`));
|
|
5822
6026
|
}
|
|
@@ -5932,7 +6136,16 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
5932
6136
|
console.log(' [dry-run] Would run: wsl npm install -g .');
|
|
5933
6137
|
return true;
|
|
5934
6138
|
}
|
|
5935
|
-
|
|
6139
|
+
// 2026-09-01 10:55 EDT — Claude Code (Opus 5), at Bob's direction. Was a
|
|
6140
|
+
// raw streamed `npm install -g .`, so the terminal got npm's chatter
|
|
6141
|
+
// verbatim — including `npm warn allow-scripts .npmrc allow-scripts
|
|
6142
|
+
// setting is being ignored because --allow-scripts was passed`, a
|
|
6143
|
+
// warning npmglobalize provokes on itself by passing that flag, and
|
|
6144
|
+
// which no reader can act on. installGlobalWithRetry captures the
|
|
6145
|
+
// output and prints only what matters (the added/changed/removed line,
|
|
6146
|
+
// or distilled errors plus a log file), which is what every other
|
|
6147
|
+
// install path here already does.
|
|
6148
|
+
const result = await installGlobalWithRetry('.', cwd, false, 1);
|
|
5936
6149
|
if (result.success) {
|
|
5937
6150
|
console.log(colors.green(`✓ Installed locally: ${pkgName}@${pkgVersion}`));
|
|
5938
6151
|
}
|
|
@@ -6285,70 +6498,37 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6285
6498
|
let currentGitStatus = getGitStatus(cwd);
|
|
6286
6499
|
// Validate git state
|
|
6287
6500
|
if (currentGitStatus.hasMergeConflict) {
|
|
6288
|
-
console.error(colors.red(
|
|
6501
|
+
console.error(colors.red(`ERROR: ${currentGitStatus.unmergedPaths.length} file(s) still have unresolved conflicts. Resolve before releasing:`));
|
|
6502
|
+
for (const file of currentGitStatus.unmergedPaths)
|
|
6503
|
+
console.error(` ${file}`);
|
|
6289
6504
|
return false;
|
|
6290
6505
|
}
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
console.log('');
|
|
6306
|
-
console.log(colors.yellow('✓ Good news: Your files are already fixed!'));
|
|
6307
|
-
console.log(colors.yellow(' You don\'t need to run --conform again.'));
|
|
6308
|
-
console.log('');
|
|
6309
|
-
console.log(colors.yellow('To publish, merge back to your branch:'));
|
|
6310
|
-
console.log(' git checkout -B master \x1b[2m# moves master to current commit\x1b[0m');
|
|
6311
|
-
console.log(' npmglobalize \x1b[2m# will now work normally\x1b[0m');
|
|
6312
|
-
console.log('');
|
|
6313
|
-
console.log(colors.yellow('Or if you haven\'t made commits yet:'));
|
|
6314
|
-
console.log(' git checkout master \x1b[2m# just switch back\x1b[0m');
|
|
6315
|
-
console.log('');
|
|
6316
|
-
return true; // Success - conform was completed
|
|
6506
|
+
// 2026-09-01 14:5x EDT — Claude Code (Opus 5), at Bob's direction.
|
|
6507
|
+
// A detached HEAD used to be a hard stop with a menu of git commands to type — and the
|
|
6508
|
+
// commands named "master" whether or not the repo had one. It is a bookkeeping mismatch,
|
|
6509
|
+
// not a content problem: the working tree is authoritative, so repair it and carry on.
|
|
6510
|
+
if (currentGitStatus.isDetachedHead) {
|
|
6511
|
+
if (!await reattachDetachedHead(cwd, currentGitStatus, dryRun))
|
|
6512
|
+
return false;
|
|
6513
|
+
currentGitStatus = getGitStatus(cwd);
|
|
6514
|
+
}
|
|
6515
|
+
// Reaching this means HEAD is still off a branch after the reattach above. A dry run
|
|
6516
|
+
// changes nothing by design, so it lands here and is expected; anything else is a real
|
|
6517
|
+
// failure and the reattach already printed why.
|
|
6518
|
+
if (currentGitStatus.isDetachedHead && !dryRun) {
|
|
6519
|
+
if (noPublish) {
|
|
6520
|
+
console.log(colors.yellow('Note: HEAD is not on a branch (this run does not publish)'));
|
|
6317
6521
|
}
|
|
6318
|
-
else if (
|
|
6319
|
-
|
|
6320
|
-
console.log(colors.yellow('
|
|
6522
|
+
else if (force) {
|
|
6523
|
+
console.log(colors.yellow('⚠ Publishing with HEAD off a branch (-force): the version tag and commits'));
|
|
6524
|
+
console.log(colors.yellow(' will not be reachable from any branch, so git push will not carry them.'));
|
|
6321
6525
|
}
|
|
6322
6526
|
else {
|
|
6323
|
-
console.
|
|
6324
|
-
console.error(
|
|
6325
|
-
console.log('');
|
|
6326
|
-
console.log(colors.yellow('ℹ What is "Detached HEAD"?'));
|
|
6327
|
-
console.log(' Your git repository is not on a branch (master, main, etc.)');
|
|
6328
|
-
console.log(' This happens after checking out a specific commit or tag.');
|
|
6329
|
-
console.log(' Publishing requires being on a branch so git can track changes.');
|
|
6330
|
-
console.log('');
|
|
6331
|
-
console.log(colors.yellow('To fix this (if you have commits to keep):'));
|
|
6332
|
-
console.log(' git checkout -B master \x1b[2m# moves master branch to current commit\x1b[0m');
|
|
6333
|
-
console.log(' npmglobalize \x1b[2m# will now work\x1b[0m');
|
|
6334
|
-
console.log('');
|
|
6335
|
-
console.log(colors.yellow('Or if you haven\'t made commits (safe to discard):'));
|
|
6336
|
-
console.log(' git checkout master \x1b[2m# just switch back to branch\x1b[0m');
|
|
6337
|
-
console.log(' npmglobalize \x1b[2m# will now work\x1b[0m');
|
|
6338
|
-
console.log('');
|
|
6339
|
-
console.log(colors.yellow('Or force publish anyway (risky):'));
|
|
6340
|
-
console.log(' npmglobalize --force \x1b[2m# proceeds despite detached HEAD\x1b[0m');
|
|
6341
|
-
console.log('');
|
|
6527
|
+
console.error(colors.red('ERROR: HEAD is still not on a branch, so there is nothing for git push to advance.'));
|
|
6528
|
+
console.error(` Put a branch on the current commit and rerun: git checkout -B ${pickReattachBranch(cwd, currentGitStatus)}`);
|
|
6342
6529
|
return false;
|
|
6343
6530
|
}
|
|
6344
6531
|
}
|
|
6345
|
-
if (currentGitStatus.isDetachedHead && force) {
|
|
6346
|
-
console.log('');
|
|
6347
|
-
console.log(colors.yellow('⚠ Warning: Publishing from Detached HEAD state (--force)'));
|
|
6348
|
-
console.log(' This means you\'re not on a branch. The published commits may be');
|
|
6349
|
-
console.log(' hard to track. Consider: git checkout master && git merge HEAD');
|
|
6350
|
-
console.log('');
|
|
6351
|
-
}
|
|
6352
6532
|
// Check if local branch is behind remote
|
|
6353
6533
|
if (currentGitStatus.isBehindRemote && !dryRun) {
|
|
6354
6534
|
console.log(colors.yellow('Local branch is behind remote.'));
|
|
@@ -7243,8 +7423,15 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
7243
7423
|
console.error(colors.yellow(` Run 'npm login' to fix, then: npm install -g ${pkgName}@${pkgVersion}`));
|
|
7244
7424
|
}
|
|
7245
7425
|
else {
|
|
7246
|
-
|
|
7247
|
-
|
|
7426
|
+
const spec = `${pkgName}@${pkgVersion}`;
|
|
7427
|
+
if (isPropagationFailure(`${registryInstallResult.output}\n${registryInstallResult.stderr}`, spec)) {
|
|
7428
|
+
for (const line of propagationHelp(spec))
|
|
7429
|
+
console.error(colors.yellow(` ${line}`));
|
|
7430
|
+
}
|
|
7431
|
+
else {
|
|
7432
|
+
console.error(colors.red(`✗ Global install failed`));
|
|
7433
|
+
console.error(colors.yellow(` Try running manually: npm install -g --prefer-online ${spec}`));
|
|
7434
|
+
}
|
|
7248
7435
|
}
|
|
7249
7436
|
}
|
|
7250
7437
|
}
|
|
@@ -8088,9 +8275,17 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
8088
8275
|
recordBuildIssue(pkgName, 'warning', `Global install failed (${auth.error || 'auth issue'}) — run 'npm login'`);
|
|
8089
8276
|
}
|
|
8090
8277
|
else {
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8278
|
+
const spec = `${pkgName}@${pkgVersion}`;
|
|
8279
|
+
if (isPropagationFailure(`${installResult.output}\n${installResult.stderr}`, spec)) {
|
|
8280
|
+
for (const line of propagationHelp(spec))
|
|
8281
|
+
console.error(colors.yellow(` ${line}`));
|
|
8282
|
+
recordBuildIssue(pkgName, 'warning', `Published, but npm could not see ${spec} yet — install with: npm install -g --prefer-online ${spec}`);
|
|
8283
|
+
}
|
|
8284
|
+
else {
|
|
8285
|
+
console.error(colors.red(`✗ Global install failed`));
|
|
8286
|
+
console.error(colors.yellow(` Try running manually: npm install -g --prefer-online ${spec}`));
|
|
8287
|
+
recordBuildIssue(pkgName, 'warning', 'Global install failed');
|
|
8288
|
+
}
|
|
8094
8289
|
}
|
|
8095
8290
|
}
|
|
8096
8291
|
}
|