@link-assistant/hive-mind 2.1.8 → 2.1.9
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.1.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5559c5e: Check all replacement repository branch tips before deleting a non-fork fork replacement, and report concrete branch-only commits when deletion is unsafe.
|
|
8
|
+
|
|
3
9
|
## 2.1.8
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getTrackedToolCommentIds, postTrackedComment, SOLUTION_DRAFT_FAILED_MARKER } from './tool-comments.lib.mjs';
|
|
2
|
-
import { extractForkReplacementBlockedDetails, isForkReplacementBlockedReason } from './solve.repository-recovery-message.lib.mjs';
|
|
2
|
+
import { extractForkReplacementBlockedDetails, isForkReplacementBlockedReason, GITHUB_FORK_SUPPORT_URL } from './solve.repository-recovery-message.lib.mjs';
|
|
3
3
|
import { FORK_DIVERGENCE_RESOLUTION_OPTION, buildForkDivergenceFailureActionSection } from './solve.branch-divergence.lib.mjs';
|
|
4
4
|
|
|
5
5
|
export { FORK_DIVERGENCE_RESOLUTION_OPTION };
|
|
@@ -41,6 +41,7 @@ export function buildPrePullRequestFailureActionSection(reason = '') {
|
|
|
41
41
|
- It could not prove whether the existing repository has unique commits, so it did not delete it automatically.
|
|
42
42
|
|
|
43
43
|
### What you can do
|
|
44
|
+
- Recover the fork without deleting: if \`${repository}\` was a fork of \`${upstream}\` that GitHub detached (for example after a private/public visibility change), ask GitHub Support at ${GITHUB_FORK_SUPPORT_URL} ("Attach, detach or reroute forks") to re-attach it. Detachment from a visibility change is documented as permanent, so this is not guaranteed, and while detached the repository cannot open a cross-repository pull request to \`${upstream}\`.
|
|
44
45
|
- Repository owner path: back up any needed work, then delete, rename, archive, or repair \`${repository}\` in GitHub and rerun the solver.
|
|
45
46
|
- External-owner path: ask the repository owner or a Hive Mind administrator to clean it up and rerun the solver.
|
|
46
47
|
- Use \`--allow-force-non-fork-repository-deletion\` only after confirming that deleting \`${repository}\` is acceptable and any unique commits can be lost.`;
|
|
@@ -26,27 +26,47 @@ export function buildForkReplacementSafetyCheckDescription({ aheadBy = null, com
|
|
|
26
26
|
return 'Hive Mind could not prove the repository has no unique commits.';
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// GitHub Support's self-service fork request workflow. See
|
|
30
|
+
// https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/detaching-a-fork
|
|
31
|
+
export const GITHUB_FORK_SUPPORT_URL = 'https://support.github.com/request/fork';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Explain how to recover the GitHub fork relationship without deleting the
|
|
35
|
+
* existing repository. GitHub detaches a fork from its upstream network when the
|
|
36
|
+
* repository's visibility is toggled (for example private -> public), and that
|
|
37
|
+
* detachment is documented as permanent with no API/self-service reattachment.
|
|
38
|
+
* The only non-deletion path is a GitHub Support request. See issue #2019.
|
|
39
|
+
*/
|
|
40
|
+
export function buildDetachedForkRecoveryGuidance({ existingRepository, expectedUpstream } = {}) {
|
|
41
|
+
const repository = fallback(existingRepository, 'the existing repository');
|
|
42
|
+
const upstream = fallback(expectedUpstream, 'the expected upstream repository');
|
|
43
|
+
|
|
44
|
+
return `Recover the fork link WITHOUT deleting ${repository}: if ${repository} was once a GitHub fork of ${upstream} that got detached (for example after its visibility was switched to private and then back to public), the only path that keeps the repository is a GitHub Support request. Open ${GITHUB_FORK_SUPPORT_URL}, choose the "Attach, detach or reroute forks" flow, and ask to re-attach ${repository} to ${upstream}. GitHub documents visibility-change detachment as permanent, so reattachment is not guaranteed. While detached, ${repository} cannot open a cross-repository pull request to ${upstream}, so the solver needs either a reattached fork or a fresh fork to continue.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildForkReplacementBlockedReason({ existingRepository, expectedUpstream, relationshipDescription, safetyCheckDescription, likelyDetachedFork = false } = {}) {
|
|
30
48
|
const repository = fallback(existingRepository, 'the existing repository');
|
|
31
49
|
const upstream = fallback(expectedUpstream, 'the expected upstream repository');
|
|
32
50
|
const relationship = fallback(relationshipDescription, `${repository} is not a confirmed GitHub fork of ${upstream}.`);
|
|
33
51
|
const safetyCheck = fallback(safetyCheckDescription, buildForkReplacementSafetyCheckDescription());
|
|
52
|
+
const detachedForkNote = likelyDetachedFork ? `\n- Likely cause: ${repository} shares history with ${upstream} but GitHub reports it as a non-fork, which matches a fork that was detached from its network (commonly after a private/public visibility change).` : '';
|
|
34
53
|
|
|
35
54
|
return `${FORK_REPLACEMENT_BLOCKED_REASON_PREFIX}
|
|
36
55
|
|
|
37
56
|
What happened:
|
|
38
57
|
- Expected fork or replacement repository: ${repository}
|
|
39
58
|
- Expected upstream: ${upstream}
|
|
40
|
-
- Actual state: ${relationship}
|
|
59
|
+
- Actual state: ${relationship}${detachedForkNote}
|
|
41
60
|
- Safety check: ${safetyCheck}
|
|
42
61
|
|
|
43
62
|
Why it stopped:
|
|
44
63
|
Hive Mind did not delete ${repository} because the repository may contain commits that would be lost.
|
|
45
64
|
|
|
46
65
|
Options:
|
|
47
|
-
1.
|
|
48
|
-
2.
|
|
49
|
-
3.
|
|
66
|
+
1. ${buildDetachedForkRecoveryGuidance({ existingRepository: repository, expectedUpstream: upstream })}
|
|
67
|
+
2. Back up any needed work in ${repository}, then delete, rename, archive, or repair that repository in GitHub and rerun the solver.
|
|
68
|
+
3. If you do not control ${repository}, ask the repository owner or a Hive Mind administrator to clean it up and rerun the solver.
|
|
69
|
+
4. Rerun with --allow-force-non-fork-repository-deletion only after confirming that deleting ${repository} is acceptable.`;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
export function isForkReplacementBlockedReason(reason = '') {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { summarizeGitHubCompareFailure } from './solve.repository-recovery-message.lib.mjs';
|
|
6
|
+
|
|
7
|
+
const resultText = result => `${result?.stderr?.toString?.() || ''}${result?.stdout?.toString?.() || ''}`.trim();
|
|
8
|
+
const resultStdout = result => result?.stdout?.toString?.().trim() || '';
|
|
9
|
+
const shortSha = sha => String(sha || '').slice(0, 12);
|
|
10
|
+
|
|
11
|
+
const branchLabel = branch => {
|
|
12
|
+
const subject = branch.subject ? ` ${branch.subject}` : '';
|
|
13
|
+
return `${branch.ref} (${branch.uniqueCommitCount} commit(s), ${shortSha(branch.sha)}${subject})`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function buildReplacementBranchSafetyDescription({ uniqueBranches = [], branchCount = 0, failureOutput = '' } = {}) {
|
|
17
|
+
if (failureOutput) {
|
|
18
|
+
return `Local Git branch reachability check failed (${summarizeGitHubCompareFailure(failureOutput)}), so Hive Mind could not prove all replacement repository branches are preserved upstream.`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (uniqueBranches.length > 0) {
|
|
22
|
+
const examples = uniqueBranches.slice(0, 3).map(branchLabel).join('; ');
|
|
23
|
+
const remaining = uniqueBranches.length > 3 ? `; and ${uniqueBranches.length - 3} more` : '';
|
|
24
|
+
return `Local Git branch reachability found ${uniqueBranches.length} replacement branch tip(s) with commits not reachable from upstream branches or PR refs: ${examples}${remaining}.`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `Local Git branch reachability found all ${branchCount} replacement branch tip(s) reachable from upstream branches or PR refs.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function runStep(commandPromise, failurePrefix) {
|
|
31
|
+
const result = await commandPromise;
|
|
32
|
+
if (result.code !== 0) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
output: `${failurePrefix}: ${resultText(result) || `exit ${result.code}`}`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { ok: true, result };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const parseReplacementRefs = output =>
|
|
42
|
+
String(output || '')
|
|
43
|
+
.split('\n')
|
|
44
|
+
.map(line => line.trim())
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.map(line => {
|
|
47
|
+
const [ref, sha] = line.split(/\s+/, 2);
|
|
48
|
+
return { ref, sha };
|
|
49
|
+
})
|
|
50
|
+
.filter(item => item.ref && item.sha);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Verify that deleting a non-fork replacement repository will not remove branch
|
|
54
|
+
* tips that are absent from the upstream repository. This is intentionally used
|
|
55
|
+
* only in the rare fork-replacement recovery path.
|
|
56
|
+
*/
|
|
57
|
+
export async function checkReplacementRepositoryBranchSafety({ $, owner, repo, existingRepository, tempRoot = os.tmpdir() }) {
|
|
58
|
+
const tempDir = await fs.mkdtemp(path.join(tempRoot, 'hive-mind-fork-replacement-'));
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const init = await runStep($({ cwd: tempDir })`git init -q 2>&1`, 'git init failed');
|
|
62
|
+
if (!init.ok) {
|
|
63
|
+
return {
|
|
64
|
+
safeToDelete: false,
|
|
65
|
+
branchCount: 0,
|
|
66
|
+
uniqueBranches: [],
|
|
67
|
+
safetyCheckDescription: buildReplacementBranchSafetyDescription({ failureOutput: init.output }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const upstreamUrl = `https://github.com/${owner}/${repo}.git`;
|
|
72
|
+
const replacementUrl = `https://github.com/${existingRepository}.git`;
|
|
73
|
+
|
|
74
|
+
const upstreamFetch = await runStep($({ cwd: tempDir })`git fetch --filter=blob:none ${upstreamUrl} '+refs/heads/*:refs/remotes/upstream/*' '+refs/pull/*/head:refs/remotes/upstream-pr/*' 2>&1`, 'upstream fetch failed');
|
|
75
|
+
if (!upstreamFetch.ok) {
|
|
76
|
+
return {
|
|
77
|
+
safeToDelete: false,
|
|
78
|
+
branchCount: 0,
|
|
79
|
+
uniqueBranches: [],
|
|
80
|
+
safetyCheckDescription: buildReplacementBranchSafetyDescription({ failureOutput: upstreamFetch.output }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const replacementFetch = await runStep($({ cwd: tempDir })`git fetch --filter=blob:none ${replacementUrl} '+refs/heads/*:refs/remotes/replacement/*' 2>&1`, 'replacement fetch failed');
|
|
85
|
+
if (!replacementFetch.ok) {
|
|
86
|
+
return {
|
|
87
|
+
safeToDelete: false,
|
|
88
|
+
branchCount: 0,
|
|
89
|
+
uniqueBranches: [],
|
|
90
|
+
safetyCheckDescription: buildReplacementBranchSafetyDescription({ failureOutput: replacementFetch.output }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const refsResult = await runStep($({ cwd: tempDir })`git for-each-ref --format='%(refname:short) %(objectname)' refs/remotes/replacement 2>&1`, 'replacement ref listing failed');
|
|
95
|
+
if (!refsResult.ok) {
|
|
96
|
+
return {
|
|
97
|
+
safeToDelete: false,
|
|
98
|
+
branchCount: 0,
|
|
99
|
+
uniqueBranches: [],
|
|
100
|
+
safetyCheckDescription: buildReplacementBranchSafetyDescription({ failureOutput: refsResult.output }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const replacementRefs = parseReplacementRefs(resultStdout(refsResult.result));
|
|
105
|
+
const uniqueBranches = [];
|
|
106
|
+
|
|
107
|
+
for (const { ref, sha } of replacementRefs) {
|
|
108
|
+
const countResult = await runStep($({ cwd: tempDir })`git rev-list --count ${ref} --not --remotes=upstream --remotes=upstream-pr 2>&1`, `unique commit count failed for ${ref}`);
|
|
109
|
+
if (!countResult.ok) {
|
|
110
|
+
return {
|
|
111
|
+
safeToDelete: false,
|
|
112
|
+
branchCount: replacementRefs.length,
|
|
113
|
+
uniqueBranches,
|
|
114
|
+
safetyCheckDescription: buildReplacementBranchSafetyDescription({ failureOutput: countResult.output }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const uniqueCommitCount = Number.parseInt(resultStdout(countResult.result), 10);
|
|
119
|
+
if (!Number.isFinite(uniqueCommitCount)) {
|
|
120
|
+
return {
|
|
121
|
+
safeToDelete: false,
|
|
122
|
+
branchCount: replacementRefs.length,
|
|
123
|
+
uniqueBranches,
|
|
124
|
+
safetyCheckDescription: buildReplacementBranchSafetyDescription({ failureOutput: `invalid unique commit count for ${ref}: ${resultStdout(countResult.result)}` }),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (uniqueCommitCount > 0) {
|
|
129
|
+
const subjectResult = await $({ cwd: tempDir })`git log -1 --format=%s ${ref} 2>&1`;
|
|
130
|
+
uniqueBranches.push({
|
|
131
|
+
ref: ref.replace(/^replacement\//, ''),
|
|
132
|
+
sha,
|
|
133
|
+
uniqueCommitCount,
|
|
134
|
+
subject: subjectResult.code === 0 ? resultStdout(subjectResult) : '',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const reachableBranchCount = replacementRefs.length - uniqueBranches.length;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
safeToDelete: uniqueBranches.length === 0,
|
|
143
|
+
branchCount: replacementRefs.length,
|
|
144
|
+
reachableBranchCount,
|
|
145
|
+
// At least one branch tip is fully reachable from upstream refs, so the
|
|
146
|
+
// replacement shares history with upstream. Combined with GitHub reporting
|
|
147
|
+
// it as a non-fork, that matches a fork detached from its network (see
|
|
148
|
+
// issue #2019), typically after a private/public visibility change.
|
|
149
|
+
likelyDetachedFork: replacementRefs.length > 0 && reachableBranchCount > 0,
|
|
150
|
+
uniqueBranches,
|
|
151
|
+
safetyCheckDescription: buildReplacementBranchSafetyDescription({ uniqueBranches, branchCount: replacementRefs.length }),
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return {
|
|
155
|
+
safeToDelete: false,
|
|
156
|
+
branchCount: 0,
|
|
157
|
+
uniqueBranches: [],
|
|
158
|
+
safetyCheckDescription: buildReplacementBranchSafetyDescription({ failureOutput: error.message }),
|
|
159
|
+
};
|
|
160
|
+
} finally {
|
|
161
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -30,6 +30,7 @@ const { log, formatAligned } = lib;
|
|
|
30
30
|
// Import exit handler
|
|
31
31
|
import { safeExit } from './exit-handler.lib.mjs';
|
|
32
32
|
import { parseForkFullNameFromGhOutput } from './github-repository-names.lib.mjs';
|
|
33
|
+
import { checkReplacementRepositoryBranchSafety } from './solve.repository-safety.lib.mjs';
|
|
33
34
|
import { buildForkReplacementBlockedReason, buildForkReplacementSafetyCheckDescription } from './solve.repository-recovery-message.lib.mjs';
|
|
34
35
|
|
|
35
36
|
// Import GitHub utilities for permission checks
|
|
@@ -525,34 +526,75 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
525
526
|
const relationshipDescription = !forkValidation.isFork ? `${existingForkName} is not a GitHub fork of ${owner}/${repo}.` : `${existingForkName} is a GitHub fork of ${forkValidation.parent || 'unknown parent'}, not ${owner}/${repo}.`;
|
|
526
527
|
await log(`${formatAligned('', '', detail)}`);
|
|
527
528
|
await log(`${formatAligned('', '', `Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}, source: ${forkValidation.source || 'N/A'}, expected: ${owner}/${repo}`)}`);
|
|
528
|
-
// Safety check: compare commits before deleting to avoid data loss
|
|
529
|
-
|
|
529
|
+
// Safety check: compare commits before deleting to avoid data loss.
|
|
530
|
+
// GitHub compare only answers for fork-network refs, so a local Git
|
|
531
|
+
// fallback checks every replacement branch tip before deletion.
|
|
532
|
+
await log(`${formatAligned('🔍', 'Safety check:', 'Comparing default branch against upstream...')}`);
|
|
530
533
|
let safeToDelete = false;
|
|
531
534
|
let safetyCheckDescription = buildForkReplacementSafetyCheckDescription();
|
|
535
|
+
let shouldRunBranchReachabilityCheck = false;
|
|
536
|
+
let likelyDetachedFork = false;
|
|
532
537
|
try {
|
|
533
538
|
const cmp = await $`gh api repos/${owner}/${repo}/compare/${owner}:HEAD...${existingForkName.split('/')[0]}:HEAD --paginate --jq '.ahead_by' 2>&1`;
|
|
534
539
|
const aheadBy = parseInt(cmp.stdout.toString().trim(), 10);
|
|
535
540
|
if (cmp.code === 0 && aheadBy === 0) {
|
|
536
|
-
await log(`${formatAligned('✅', '
|
|
537
|
-
|
|
541
|
+
await log(`${formatAligned('✅', 'Default branch:', 'No additional commits in replacement repository')}`);
|
|
542
|
+
shouldRunBranchReachabilityCheck = true;
|
|
538
543
|
} else if (cmp.code === 0) {
|
|
539
544
|
safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ aheadBy });
|
|
540
|
-
|
|
541
|
-
|
|
545
|
+
if (Number.isFinite(aheadBy)) {
|
|
546
|
+
await log(`${formatAligned('⚠️', 'UNSAFE:', `Default branch has ${aheadBy} commit(s) ahead of upstream that would be lost`)}`, { level: 'warning' });
|
|
547
|
+
} else {
|
|
548
|
+
await log(`${formatAligned('⚠️', 'Compare unclear:', 'GitHub compare did not return a numeric ahead_by value')}`, { level: 'warning' });
|
|
549
|
+
shouldRunBranchReachabilityCheck = true;
|
|
550
|
+
}
|
|
542
551
|
} else {
|
|
543
552
|
const compareOutput = (cmp.stderr?.toString() || '') + (cmp.stdout?.toString() || '');
|
|
544
553
|
safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ compareFailureOutput: compareOutput });
|
|
545
554
|
await log(`${formatAligned('⚠️', 'Compare failed:', compareOutput.split('\n')[0])}`, { level: 'warning' });
|
|
555
|
+
shouldRunBranchReachabilityCheck = true;
|
|
546
556
|
}
|
|
547
557
|
} catch (e) {
|
|
548
558
|
safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ compareFailureOutput: e.message });
|
|
549
559
|
await log(`${formatAligned('⚠️', 'Compare error:', e.message)}`, { level: 'warning' });
|
|
560
|
+
shouldRunBranchReachabilityCheck = true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (shouldRunBranchReachabilityCheck) {
|
|
564
|
+
await log(`${formatAligned('🔍', 'Safety check:', 'Checking all replacement branches against upstream refs...')}`);
|
|
565
|
+
const branchSafety = await checkReplacementRepositoryBranchSafety({
|
|
566
|
+
$,
|
|
567
|
+
owner,
|
|
568
|
+
repo,
|
|
569
|
+
existingRepository: existingForkName,
|
|
570
|
+
});
|
|
571
|
+
safetyCheckDescription = branchSafety.safetyCheckDescription;
|
|
572
|
+
likelyDetachedFork = Boolean(branchSafety.likelyDetachedFork);
|
|
573
|
+
|
|
574
|
+
if (likelyDetachedFork) {
|
|
575
|
+
await log(`${formatAligned('🔗', 'Detached fork:', `${existingForkName} shares history with ${owner}/${repo} but is not a GitHub fork — this matches a fork detached by a private/public visibility change`)}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (branchSafety.safeToDelete) {
|
|
579
|
+
await log(`${formatAligned('✅', 'Safe to delete:', `All ${branchSafety.branchCount} replacement branch tip(s) are reachable from upstream`)}`);
|
|
580
|
+
safeToDelete = true;
|
|
581
|
+
} else if (branchSafety.uniqueBranches.length > 0) {
|
|
582
|
+
await log(`${formatAligned('⚠️', 'UNSAFE:', `${branchSafety.uniqueBranches.length} replacement branch tip(s) have commits not reachable from upstream`)}`, { level: 'warning' });
|
|
583
|
+
for (const branch of branchSafety.uniqueBranches.slice(0, 3)) {
|
|
584
|
+
await log(`${formatAligned('', '', `${branch.ref}: ${branch.uniqueCommitCount} commit(s), ${branch.sha.slice(0, 12)} ${branch.subject || ''}`)}`, { level: 'warning' });
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
await log(`${formatAligned('⚠️', 'Branch check failed:', safetyCheckDescription)}`, { level: 'warning' });
|
|
588
|
+
}
|
|
550
589
|
}
|
|
551
590
|
if (!safeToDelete) {
|
|
552
591
|
if (argv.allowForceNonForkRepositoryDeletion) {
|
|
553
592
|
// Force flag set — proceed with deletion despite the failed safety check.
|
|
554
593
|
await log(`${formatAligned('⚠️', 'Force deletion ENABLED:', '--allow-force-non-fork-repository-deletion — proceeding despite potential data loss')}`, { level: 'warning' });
|
|
555
594
|
} else {
|
|
595
|
+
if (likelyDetachedFork) {
|
|
596
|
+
await log(` 🔗 Recover without deletion: ask GitHub Support to re-attach ${existingForkName} to ${owner}/${repo} at https://support.github.com/request/fork ("Attach, detach or reroute forks")`);
|
|
597
|
+
}
|
|
556
598
|
await log(` 💡 Manual fix required: back up work, then: gh repo delete ${existingForkName} --yes`);
|
|
557
599
|
await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
|
|
558
600
|
await log(` 🔧 Or force deletion (DANGEROUS): solve ${argv.url || argv['issue-url'] || argv._[0] || '<issue-url>'} --allow-force-non-fork-repository-deletion`);
|
|
@@ -563,6 +605,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
563
605
|
expectedUpstream: `${owner}/${repo}`,
|
|
564
606
|
relationshipDescription,
|
|
565
607
|
safetyCheckDescription,
|
|
608
|
+
likelyDetachedFork,
|
|
566
609
|
})
|
|
567
610
|
);
|
|
568
611
|
}
|