@bobfrankston/npmglobalize 1.0.209 → 1.0.211
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 +3 -0
- package/lib.js +153 -59
- package/package.json +1 -1
package/lib.d.ts
CHANGED
|
@@ -578,6 +578,9 @@ export interface GitStatus {
|
|
|
578
578
|
currentBranch: string;
|
|
579
579
|
remoteBranch: string;
|
|
580
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 */
|
|
581
584
|
}
|
|
582
585
|
export declare function getGitStatus(cwd: string): GitStatus;
|
|
583
586
|
/** Validate package.json for release */
|
package/lib.js
CHANGED
|
@@ -4738,7 +4738,10 @@ export function getGitStatus(cwd) {
|
|
|
4738
4738
|
isDetachedHead: false,
|
|
4739
4739
|
currentBranch: '',
|
|
4740
4740
|
remoteBranch: '',
|
|
4741
|
-
isBehindRemote: false
|
|
4741
|
+
isBehindRemote: false,
|
|
4742
|
+
inProgressOp: '',
|
|
4743
|
+
detachedFromBranch: '',
|
|
4744
|
+
unmergedPaths: []
|
|
4742
4745
|
};
|
|
4743
4746
|
// Check if git repo — walk up the directory tree to find the enclosing
|
|
4744
4747
|
// .git, like git itself does. Previously this only looked for `.git/` at
|
|
@@ -4755,9 +4758,42 @@ export function getGitStatus(cwd) {
|
|
|
4755
4758
|
return status;
|
|
4756
4759
|
}
|
|
4757
4760
|
status.isRepo = true;
|
|
4758
|
-
//
|
|
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.
|
|
4759
4768
|
const mergeHead = path.join(gitDir, 'MERGE_HEAD');
|
|
4760
|
-
|
|
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;
|
|
4761
4797
|
// Get branch info
|
|
4762
4798
|
try {
|
|
4763
4799
|
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
@@ -4811,6 +4847,97 @@ export function getGitStatus(cwd) {
|
|
|
4811
4847
|
}
|
|
4812
4848
|
return status;
|
|
4813
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
|
+
}
|
|
4814
4941
|
/** Validate package.json for release */
|
|
4815
4942
|
export function validatePackageJson(pkg) {
|
|
4816
4943
|
const errors = [];
|
|
@@ -6371,70 +6498,37 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
6371
6498
|
let currentGitStatus = getGitStatus(cwd);
|
|
6372
6499
|
// Validate git state
|
|
6373
6500
|
if (currentGitStatus.hasMergeConflict) {
|
|
6374
|
-
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}`);
|
|
6375
6504
|
return false;
|
|
6376
6505
|
}
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
console.log('');
|
|
6392
|
-
console.log(colors.yellow('✓ Good news: Your files are already fixed!'));
|
|
6393
|
-
console.log(colors.yellow(' You don\'t need to run --conform again.'));
|
|
6394
|
-
console.log('');
|
|
6395
|
-
console.log(colors.yellow('To publish, merge back to your branch:'));
|
|
6396
|
-
console.log(' git checkout -B master \x1b[2m# moves master to current commit\x1b[0m');
|
|
6397
|
-
console.log(' npmglobalize \x1b[2m# will now work normally\x1b[0m');
|
|
6398
|
-
console.log('');
|
|
6399
|
-
console.log(colors.yellow('Or if you haven\'t made commits yet:'));
|
|
6400
|
-
console.log(' git checkout master \x1b[2m# just switch back\x1b[0m');
|
|
6401
|
-
console.log('');
|
|
6402
|
-
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)'));
|
|
6403
6521
|
}
|
|
6404
|
-
else if (
|
|
6405
|
-
|
|
6406
|
-
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.'));
|
|
6407
6525
|
}
|
|
6408
6526
|
else {
|
|
6409
|
-
console.
|
|
6410
|
-
console.error(
|
|
6411
|
-
console.log('');
|
|
6412
|
-
console.log(colors.yellow('ℹ What is "Detached HEAD"?'));
|
|
6413
|
-
console.log(' Your git repository is not on a branch (master, main, etc.)');
|
|
6414
|
-
console.log(' This happens after checking out a specific commit or tag.');
|
|
6415
|
-
console.log(' Publishing requires being on a branch so git can track changes.');
|
|
6416
|
-
console.log('');
|
|
6417
|
-
console.log(colors.yellow('To fix this (if you have commits to keep):'));
|
|
6418
|
-
console.log(' git checkout -B master \x1b[2m# moves master branch to current commit\x1b[0m');
|
|
6419
|
-
console.log(' npmglobalize \x1b[2m# will now work\x1b[0m');
|
|
6420
|
-
console.log('');
|
|
6421
|
-
console.log(colors.yellow('Or if you haven\'t made commits (safe to discard):'));
|
|
6422
|
-
console.log(' git checkout master \x1b[2m# just switch back to branch\x1b[0m');
|
|
6423
|
-
console.log(' npmglobalize \x1b[2m# will now work\x1b[0m');
|
|
6424
|
-
console.log('');
|
|
6425
|
-
console.log(colors.yellow('Or force publish anyway (risky):'));
|
|
6426
|
-
console.log(' npmglobalize --force \x1b[2m# proceeds despite detached HEAD\x1b[0m');
|
|
6427
|
-
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)}`);
|
|
6428
6529
|
return false;
|
|
6429
6530
|
}
|
|
6430
6531
|
}
|
|
6431
|
-
if (currentGitStatus.isDetachedHead && force) {
|
|
6432
|
-
console.log('');
|
|
6433
|
-
console.log(colors.yellow('⚠ Warning: Publishing from Detached HEAD state (--force)'));
|
|
6434
|
-
console.log(' This means you\'re not on a branch. The published commits may be');
|
|
6435
|
-
console.log(' hard to track. Consider: git checkout master && git merge HEAD');
|
|
6436
|
-
console.log('');
|
|
6437
|
-
}
|
|
6438
6532
|
// Check if local branch is behind remote
|
|
6439
6533
|
if (currentGitStatus.isBehindRemote && !dryRun) {
|
|
6440
6534
|
console.log(colors.yellow('Local branch is behind remote.'));
|