@imdeadpool/guardex 7.0.20 → 7.0.22
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 +66 -30
- package/package.json +1 -1
- package/src/cli/args.js +804 -2
- package/src/cli/main.js +744 -5101
- package/src/context.js +197 -33
- package/src/doctor/index.js +1071 -0
- package/src/finish/index.js +456 -358
- package/src/git/index.js +645 -32
- package/src/output/index.js +8 -1
- package/src/sandbox/index.js +301 -52
- package/src/scaffold/index.js +681 -22
- package/src/toolchain/index.js +622 -178
- package/templates/scripts/agent-branch-finish.sh +56 -5
- package/templates/scripts/agent-worktree-prune.sh +15 -1
- package/templates/scripts/codex-agent.sh +14 -2
- package/templates/vscode/guardex-active-agents/README.md +3 -1
- package/templates/vscode/guardex-active-agents/extension.js +321 -12
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +5 -1
- package/templates/vscode/guardex-active-agents/session-schema.js +233 -29
package/src/finish/index.js
CHANGED
|
@@ -1,425 +1,523 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
args.push('--dry-run');
|
|
49
|
-
}
|
|
50
|
-
if (!options.keepCleanWorktrees) {
|
|
51
|
-
args.push('--only-dirty-worktrees');
|
|
52
|
-
}
|
|
53
|
-
if (options.includePrMerged) {
|
|
54
|
-
args.push('--include-pr-merged');
|
|
55
|
-
}
|
|
56
|
-
if (options.idleMinutes > 0) {
|
|
57
|
-
args.push('--idle-minutes', String(options.idleMinutes));
|
|
58
|
-
}
|
|
59
|
-
if (options.maxBranches > 0) {
|
|
60
|
-
args.push('--max-branches', String(options.maxBranches));
|
|
61
|
-
}
|
|
62
|
-
args.push('--delete-branches');
|
|
63
|
-
if (!options.keepRemote) {
|
|
64
|
-
args.push('--delete-remote-branches');
|
|
1
|
+
const { TOOL_NAME, LOCK_FILE_RELATIVE, path, fs } = require('../context');
|
|
2
|
+
const { run, runPackageAsset } = require('../core/runtime');
|
|
3
|
+
const {
|
|
4
|
+
resolveRepoRoot,
|
|
5
|
+
uniquePreserveOrder,
|
|
6
|
+
listAgentWorktrees,
|
|
7
|
+
listLocalAgentBranchesForFinish,
|
|
8
|
+
branchExists,
|
|
9
|
+
resolveFinishBaseBranch,
|
|
10
|
+
worktreeHasLocalChanges,
|
|
11
|
+
branchMergedIntoBase,
|
|
12
|
+
resolveBaseBranch,
|
|
13
|
+
resolveSyncStrategy,
|
|
14
|
+
ensureOriginBaseRef,
|
|
15
|
+
gitRun,
|
|
16
|
+
currentBranchName,
|
|
17
|
+
workingTreeIsDirty,
|
|
18
|
+
aheadBehind,
|
|
19
|
+
lockRegistryStatus,
|
|
20
|
+
syncOperation,
|
|
21
|
+
gitOutputLines,
|
|
22
|
+
} = require('../git');
|
|
23
|
+
const {
|
|
24
|
+
parseCleanupArgs,
|
|
25
|
+
parseMergeArgs,
|
|
26
|
+
parseFinishArgs,
|
|
27
|
+
parseSyncArgs,
|
|
28
|
+
} = require('../cli/args');
|
|
29
|
+
|
|
30
|
+
function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
|
|
31
|
+
const changedFiles = uniquePreserveOrder([
|
|
32
|
+
...gitOutputLines(worktreePath, ['diff', '--name-only', '--', '.', ':(exclude).omx/state/agent-file-locks.json']),
|
|
33
|
+
...gitOutputLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', ':(exclude).omx/state/agent-file-locks.json']),
|
|
34
|
+
...gitOutputLines(worktreePath, ['ls-files', '--others', '--exclude-standard']),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
if (changedFiles.length > 0) {
|
|
38
|
+
const claim = runPackageAsset('lockTool', ['claim', '--branch', branch, ...changedFiles], {
|
|
39
|
+
cwd: repoRoot,
|
|
40
|
+
stdio: 'pipe',
|
|
41
|
+
});
|
|
42
|
+
if (claim.status !== 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Lock claim failed for ${branch}: ${(
|
|
45
|
+
claim.stderr || claim.stdout || ''
|
|
46
|
+
).trim()}`,
|
|
47
|
+
);
|
|
65
48
|
}
|
|
49
|
+
}
|
|
66
50
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
51
|
+
const deletedFiles = uniquePreserveOrder([
|
|
52
|
+
...gitOutputLines(worktreePath, [
|
|
53
|
+
'diff',
|
|
54
|
+
'--name-only',
|
|
55
|
+
'--diff-filter=D',
|
|
56
|
+
'--',
|
|
57
|
+
'.',
|
|
58
|
+
':(exclude).omx/state/agent-file-locks.json',
|
|
59
|
+
]),
|
|
60
|
+
...gitOutputLines(worktreePath, [
|
|
61
|
+
'diff',
|
|
62
|
+
'--cached',
|
|
63
|
+
'--name-only',
|
|
64
|
+
'--diff-filter=D',
|
|
65
|
+
'--',
|
|
66
|
+
'.',
|
|
67
|
+
':(exclude).omx/state/agent-file-locks.json',
|
|
68
|
+
]),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
if (deletedFiles.length > 0) {
|
|
72
|
+
const allowDelete = runPackageAsset('lockTool', ['allow-delete', '--branch', branch, ...deletedFiles], {
|
|
73
|
+
cwd: repoRoot,
|
|
74
|
+
stdio: 'pipe',
|
|
75
|
+
});
|
|
76
|
+
if (allowDelete.status !== 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Delete-lock grant failed for ${branch}: ${(
|
|
79
|
+
allowDelete.stderr || allowDelete.stdout || ''
|
|
80
|
+
).trim()}`,
|
|
81
|
+
);
|
|
92
82
|
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
93
85
|
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
function autoCommitWorktreeForFinish(repoRoot, worktreePath, branch, options) {
|
|
87
|
+
const hasChanges = worktreeHasLocalChanges(worktreePath);
|
|
88
|
+
if (!hasChanges) {
|
|
89
|
+
return { changed: false, committed: false };
|
|
96
90
|
}
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
if (options.noAutoCommit) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Branch '${branch}' has local changes in ${worktreePath}. Re-run without --no-auto-commit or commit manually first.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
101
97
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
if (options.into) {
|
|
107
|
-
args.push('--into', options.into);
|
|
108
|
-
}
|
|
109
|
-
if (options.task) {
|
|
110
|
-
args.push('--task', options.task);
|
|
111
|
-
}
|
|
112
|
-
if (options.agent) {
|
|
113
|
-
args.push('--agent', options.agent);
|
|
114
|
-
}
|
|
115
|
-
for (const branch of options.branches) {
|
|
116
|
-
args.push('--branch', branch);
|
|
117
|
-
}
|
|
98
|
+
if (options.dryRun) {
|
|
99
|
+
return { changed: true, committed: false, dryRun: true };
|
|
100
|
+
}
|
|
118
101
|
|
|
119
|
-
|
|
120
|
-
if (mergeResult.stdout) {
|
|
121
|
-
process.stdout.write(mergeResult.stdout);
|
|
122
|
-
}
|
|
123
|
-
if (mergeResult.stderr) {
|
|
124
|
-
process.stderr.write(mergeResult.stderr);
|
|
125
|
-
}
|
|
126
|
-
if (mergeResult.status !== 0) {
|
|
127
|
-
throw new Error(`merge command failed with status ${mergeResult.status}`);
|
|
128
|
-
}
|
|
102
|
+
claimLocksForAutoCommit(repoRoot, worktreePath, branch);
|
|
129
103
|
|
|
130
|
-
|
|
104
|
+
const addResult = run('git', ['-C', worktreePath, 'add', '-A'], { stdio: 'pipe' });
|
|
105
|
+
if (addResult.status !== 0) {
|
|
106
|
+
throw new Error(`git add failed in ${worktreePath}: ${(addResult.stderr || addResult.stdout || '').trim()}`);
|
|
131
107
|
}
|
|
132
108
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
109
|
+
const stagedHasChanges = run('git', [
|
|
110
|
+
'-C',
|
|
111
|
+
worktreePath,
|
|
112
|
+
'diff',
|
|
113
|
+
'--cached',
|
|
114
|
+
'--quiet',
|
|
115
|
+
'--',
|
|
116
|
+
'.',
|
|
117
|
+
':(exclude).omx/state/agent-file-locks.json',
|
|
118
|
+
], { stdio: 'pipe' }).status === 1;
|
|
119
|
+
if (!stagedHasChanges) {
|
|
120
|
+
return { changed: true, committed: false };
|
|
121
|
+
}
|
|
136
122
|
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
const commitMessage = options.commitMessage || `Auto-finish: ${branch}`;
|
|
124
|
+
const commitResult = run('git', ['-C', worktreePath, 'commit', '-m', commitMessage], { stdio: 'pipe' });
|
|
125
|
+
if (commitResult.status !== 0) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Auto-commit failed on '${branch}': ${(
|
|
128
|
+
commitResult.stderr || commitResult.stdout || ''
|
|
129
|
+
).trim()}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
139
132
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!branchExists(repoRoot, options.branch)) {
|
|
143
|
-
throw new Error(`Local branch not found: ${options.branch}`);
|
|
144
|
-
}
|
|
145
|
-
candidateBranches = [options.branch];
|
|
146
|
-
} else {
|
|
147
|
-
candidateBranches = uniquePreserveOrder([
|
|
148
|
-
...listLocalAgentBranchesForFinish(repoRoot),
|
|
149
|
-
...worktreeEntries.map((entry) => entry.branch),
|
|
150
|
-
]);
|
|
151
|
-
}
|
|
133
|
+
return { changed: true, committed: true, message: commitMessage };
|
|
134
|
+
}
|
|
152
135
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const baseBranch = resolveFinishBaseBranch(repoRoot, branch, options.base);
|
|
157
|
-
const hasChanges = worktreePath ? worktreeHasLocalChanges(worktreePath) : false;
|
|
158
|
-
const alreadyMerged = branchMergedIntoBase(repoRoot, branch, baseBranch);
|
|
159
|
-
if (options.all || options.branch || hasChanges || !alreadyMerged) {
|
|
160
|
-
candidates.push({
|
|
161
|
-
branch,
|
|
162
|
-
baseBranch,
|
|
163
|
-
worktreePath,
|
|
164
|
-
hasChanges,
|
|
165
|
-
alreadyMerged,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
136
|
+
function cleanup(rawArgs) {
|
|
137
|
+
const options = parseCleanupArgs(rawArgs);
|
|
138
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
169
139
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
140
|
+
const args = [];
|
|
141
|
+
if (options.base) {
|
|
142
|
+
args.push('--base', options.base);
|
|
143
|
+
}
|
|
144
|
+
if (options.branch) {
|
|
145
|
+
args.push('--branch', options.branch);
|
|
146
|
+
}
|
|
147
|
+
if (options.forceDirty) {
|
|
148
|
+
args.push('--force-dirty');
|
|
149
|
+
}
|
|
150
|
+
if (options.dryRun) {
|
|
151
|
+
args.push('--dry-run');
|
|
152
|
+
}
|
|
153
|
+
if (!options.keepCleanWorktrees) {
|
|
154
|
+
args.push('--only-dirty-worktrees');
|
|
155
|
+
}
|
|
156
|
+
if (options.includePrMerged) {
|
|
157
|
+
args.push('--include-pr-merged');
|
|
158
|
+
}
|
|
159
|
+
if (options.idleMinutes > 0) {
|
|
160
|
+
args.push('--idle-minutes', String(options.idleMinutes));
|
|
161
|
+
}
|
|
162
|
+
if (options.maxBranches > 0) {
|
|
163
|
+
args.push('--max-branches', String(options.maxBranches));
|
|
164
|
+
}
|
|
165
|
+
args.push('--delete-branches');
|
|
166
|
+
if (!options.keepRemote) {
|
|
167
|
+
args.push('--delete-remote-branches');
|
|
168
|
+
}
|
|
175
169
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
const runCleanupCycle = () => {
|
|
171
|
+
const runResult = runPackageAsset('worktreePrune', args, { cwd: repoRoot, stdio: 'inherit' });
|
|
172
|
+
if (runResult.status !== 0) {
|
|
173
|
+
throw new Error('Cleanup command failed');
|
|
174
|
+
}
|
|
175
|
+
};
|
|
179
176
|
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
if (options.watch) {
|
|
178
|
+
let cycle = 0;
|
|
179
|
+
while (true) {
|
|
180
|
+
cycle += 1;
|
|
182
181
|
console.log(
|
|
183
|
-
`[${TOOL_NAME}]
|
|
182
|
+
`[${TOOL_NAME}] Cleanup watch cycle=${cycle} (interval=${options.intervalSeconds}s, idleMinutes=${options.idleMinutes}, maxBranches=${options.maxBranches > 0 ? options.maxBranches : 'unbounded'}).`,
|
|
184
183
|
);
|
|
184
|
+
runCleanupCycle();
|
|
185
|
+
if (options.once) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const sleepResult = run('sleep', [String(options.intervalSeconds)], { cwd: repoRoot });
|
|
189
|
+
if (sleepResult.status !== 0) {
|
|
190
|
+
throw new Error(`Cleanup watch sleep failed (interval=${options.intervalSeconds}s)`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
process.exitCode = 0;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
185
196
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
commitState = autoCommitWorktreeForFinish(repoRoot, worktreePath, branch, options);
|
|
190
|
-
}
|
|
197
|
+
runCleanupCycle();
|
|
198
|
+
process.exitCode = 0;
|
|
199
|
+
}
|
|
191
200
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
} else if (commitState.changed && commitState.dryRun) {
|
|
196
|
-
console.log(`[${TOOL_NAME}] [dry-run] Would auto-commit pending changes on '${branch}'.`);
|
|
197
|
-
}
|
|
201
|
+
function merge(rawArgs) {
|
|
202
|
+
const options = parseMergeArgs(rawArgs);
|
|
203
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
198
204
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
finishArgs.push('--keep-remote-branch');
|
|
216
|
-
}
|
|
205
|
+
const args = [];
|
|
206
|
+
if (options.base) {
|
|
207
|
+
args.push('--base', options.base);
|
|
208
|
+
}
|
|
209
|
+
if (options.into) {
|
|
210
|
+
args.push('--into', options.into);
|
|
211
|
+
}
|
|
212
|
+
if (options.task) {
|
|
213
|
+
args.push('--task', options.task);
|
|
214
|
+
}
|
|
215
|
+
if (options.agent) {
|
|
216
|
+
args.push('--agent', options.agent);
|
|
217
|
+
}
|
|
218
|
+
for (const branch of options.branches) {
|
|
219
|
+
args.push('--branch', branch);
|
|
220
|
+
}
|
|
217
221
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
const mergeResult = runPackageAsset('branchMerge', args, { cwd: repoRoot, stdio: 'pipe' });
|
|
223
|
+
if (mergeResult.stdout) {
|
|
224
|
+
process.stdout.write(mergeResult.stdout);
|
|
225
|
+
}
|
|
226
|
+
if (mergeResult.stderr) {
|
|
227
|
+
process.stderr.write(mergeResult.stderr);
|
|
228
|
+
}
|
|
229
|
+
if (mergeResult.status !== 0) {
|
|
230
|
+
throw new Error(`merge command failed with status ${mergeResult.status}`);
|
|
231
|
+
}
|
|
223
232
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
process.stdout.write(finishResult.stdout);
|
|
227
|
-
}
|
|
228
|
-
if (finishResult.stderr) {
|
|
229
|
-
process.stderr.write(finishResult.stderr);
|
|
230
|
-
}
|
|
231
|
-
if (finishResult.status !== 0) {
|
|
232
|
-
throw new Error(`agent-branch-finish exited with status ${finishResult.status}`);
|
|
233
|
-
}
|
|
233
|
+
process.exitCode = 0;
|
|
234
|
+
}
|
|
234
235
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
console.error(`[${TOOL_NAME}] Finish failed for '${branch}': ${error.message}`);
|
|
239
|
-
if (options.failFast) {
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
236
|
+
function finish(rawArgs, defaults = {}) {
|
|
237
|
+
const options = parseFinishArgs(rawArgs, defaults);
|
|
238
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
244
239
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
const worktreeEntries = listAgentWorktrees(repoRoot);
|
|
241
|
+
const worktreeByBranch = new Map(worktreeEntries.map((entry) => [entry.branch, entry.worktreePath]));
|
|
242
|
+
|
|
243
|
+
let candidateBranches = [];
|
|
244
|
+
if (options.branch) {
|
|
245
|
+
if (!branchExists(repoRoot, options.branch)) {
|
|
246
|
+
throw new Error(`Local branch not found: ${options.branch}`);
|
|
247
|
+
}
|
|
248
|
+
candidateBranches = [options.branch];
|
|
249
|
+
} else {
|
|
250
|
+
candidateBranches = uniquePreserveOrder([
|
|
251
|
+
...listLocalAgentBranchesForFinish(repoRoot),
|
|
252
|
+
...worktreeEntries.map((entry) => entry.branch),
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
248
255
|
|
|
249
|
-
|
|
250
|
-
|
|
256
|
+
const candidates = [];
|
|
257
|
+
for (const branch of candidateBranches) {
|
|
258
|
+
const worktreePath = worktreeByBranch.get(branch) || '';
|
|
259
|
+
const baseBranch = resolveFinishBaseBranch(repoRoot, branch, options.base);
|
|
260
|
+
const hasChanges = worktreePath ? worktreeHasLocalChanges(worktreePath) : false;
|
|
261
|
+
const alreadyMerged = branchMergedIntoBase(repoRoot, branch, baseBranch);
|
|
262
|
+
if (options.all || options.branch || hasChanges || !alreadyMerged) {
|
|
263
|
+
candidates.push({
|
|
264
|
+
branch,
|
|
265
|
+
baseBranch,
|
|
266
|
+
worktreePath,
|
|
267
|
+
hasChanges,
|
|
268
|
+
alreadyMerged,
|
|
269
|
+
});
|
|
251
270
|
}
|
|
271
|
+
}
|
|
252
272
|
|
|
273
|
+
if (candidates.length === 0) {
|
|
274
|
+
console.log(`[${TOOL_NAME}] No pending agent branches to finish.`);
|
|
253
275
|
process.exitCode = 0;
|
|
276
|
+
return;
|
|
254
277
|
}
|
|
255
278
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const baseBranch = resolveBaseBranch(repoRoot, options.base);
|
|
260
|
-
const strategy = resolveSyncStrategy(repoRoot, options.strategy);
|
|
261
|
-
const baseRef = `origin/${baseBranch}`;
|
|
279
|
+
let succeeded = 0;
|
|
280
|
+
let failed = 0;
|
|
281
|
+
let autoCommitted = 0;
|
|
262
282
|
|
|
263
|
-
|
|
283
|
+
for (const candidate of candidates) {
|
|
284
|
+
const { branch, baseBranch, worktreePath } = candidate;
|
|
285
|
+
console.log(
|
|
286
|
+
`[${TOOL_NAME}] Finishing '${branch}' -> '${baseBranch}'${worktreePath ? ` (${worktreePath})` : ''}...`,
|
|
287
|
+
);
|
|
264
288
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
|
|
289
|
+
try {
|
|
290
|
+
let commitState = { changed: false, committed: false };
|
|
291
|
+
if (worktreePath) {
|
|
292
|
+
commitState = autoCommitWorktreeForFinish(repoRoot, worktreePath, branch, options);
|
|
269
293
|
}
|
|
270
|
-
const branches = (refs.stdout || '').split('\n').map((item) => item.trim()).filter(Boolean);
|
|
271
|
-
const rows = branches.map((branch) => {
|
|
272
|
-
const counts = aheadBehind(repoRoot, branch, baseRef);
|
|
273
|
-
return {
|
|
274
|
-
branch,
|
|
275
|
-
base: baseRef,
|
|
276
|
-
ahead: counts.ahead,
|
|
277
|
-
behind: counts.behind,
|
|
278
|
-
syncRequired: counts.behind > 0,
|
|
279
|
-
};
|
|
280
|
-
});
|
|
281
294
|
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
rows,
|
|
288
|
-
}, null, 2)}\n`);
|
|
289
|
-
} else {
|
|
290
|
-
console.log(`[${TOOL_NAME}] Sync report target: ${repoRoot}`);
|
|
291
|
-
console.log(`[${TOOL_NAME}] Base: ${baseRef}`);
|
|
292
|
-
if (rows.length === 0) {
|
|
293
|
-
console.log(`[${TOOL_NAME}] No local agent branches found.`);
|
|
294
|
-
} else {
|
|
295
|
-
for (const row of rows) {
|
|
296
|
-
console.log(` - ${row.branch} | ahead ${row.ahead} | behind ${row.behind} | syncRequired=${row.syncRequired}`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
295
|
+
if (commitState.committed) {
|
|
296
|
+
autoCommitted += 1;
|
|
297
|
+
console.log(`[${TOOL_NAME}] Auto-committed '${branch}' before finish.`);
|
|
298
|
+
} else if (commitState.changed && commitState.dryRun) {
|
|
299
|
+
console.log(`[${TOOL_NAME}] [dry-run] Would auto-commit pending changes on '${branch}'.`);
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
throw new Error('Sync blocked: working tree is not clean. Commit or stash changes first, or pass --allow-dirty.');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const before = aheadBehind(repoRoot, branch, baseRef);
|
|
317
|
-
|
|
318
|
-
const payload = {
|
|
319
|
-
repoRoot,
|
|
320
|
-
branch,
|
|
321
|
-
base: baseRef,
|
|
322
|
-
strategy,
|
|
323
|
-
dirty,
|
|
324
|
-
aheadBefore: before.ahead,
|
|
325
|
-
behindBefore: before.behind,
|
|
326
|
-
syncRequired: before.behind > 0,
|
|
327
|
-
status: 'checked',
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
if (options.check) {
|
|
331
|
-
if (options.json) {
|
|
332
|
-
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
302
|
+
const finishArgs = [
|
|
303
|
+
'--branch',
|
|
304
|
+
branch,
|
|
305
|
+
'--base',
|
|
306
|
+
baseBranch,
|
|
307
|
+
options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
308
|
+
options.cleanup ? '--cleanup' : '--no-cleanup',
|
|
309
|
+
];
|
|
310
|
+
if (options.mergeMode === 'pr') {
|
|
311
|
+
finishArgs.push('--via-pr');
|
|
312
|
+
} else if (options.mergeMode === 'direct') {
|
|
313
|
+
finishArgs.push('--direct-only');
|
|
333
314
|
} else {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
console.log(`[${TOOL_NAME}] Behind: ${before.behind}`);
|
|
339
|
-
console.log(`[${TOOL_NAME}] Sync required: ${before.behind > 0 ? 'yes' : 'no'}`);
|
|
315
|
+
finishArgs.push('--mode', 'auto');
|
|
316
|
+
}
|
|
317
|
+
if (options.keepRemote) {
|
|
318
|
+
finishArgs.push('--keep-remote-branch');
|
|
340
319
|
}
|
|
341
|
-
process.exitCode = before.behind > 0 ? 1 : 0;
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
320
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
} else {
|
|
350
|
-
console.log(`[${TOOL_NAME}] Branch '${branch}' is already up to date with ${baseRef}.`);
|
|
321
|
+
if (options.dryRun) {
|
|
322
|
+
console.log(`[${TOOL_NAME}] [dry-run] Would run: gx branch finish ${finishArgs.join(' ')}`);
|
|
323
|
+
succeeded += 1;
|
|
324
|
+
continue;
|
|
351
325
|
}
|
|
352
|
-
process.exitCode = 0;
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
326
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
327
|
+
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot, stdio: 'pipe' });
|
|
328
|
+
if (finishResult.stdout) {
|
|
329
|
+
process.stdout.write(finishResult.stdout);
|
|
330
|
+
}
|
|
331
|
+
if (finishResult.stderr) {
|
|
332
|
+
process.stderr.write(finishResult.stderr);
|
|
333
|
+
}
|
|
334
|
+
if (finishResult.status !== 0) {
|
|
335
|
+
throw new Error(`agent-branch-finish exited with status ${finishResult.status}`);
|
|
362
336
|
}
|
|
363
|
-
process.exitCode = 0;
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
337
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
338
|
+
succeeded += 1;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
failed += 1;
|
|
341
|
+
console.error(`[${TOOL_NAME}] Finish failed for '${branch}': ${error.message}`);
|
|
342
|
+
if (options.failFast) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
372
345
|
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(
|
|
349
|
+
`[${TOOL_NAME}] Finish summary: total=${candidates.length}, success=${succeeded}, failed=${failed}, autoCommitted=${autoCommitted}`,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
if (failed > 0) {
|
|
353
|
+
throw new Error('finish command failed for one or more agent branches');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
process.exitCode = 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function sync(rawArgs) {
|
|
360
|
+
const options = parseSyncArgs(rawArgs);
|
|
361
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
362
|
+
const baseBranch = resolveBaseBranch(repoRoot, options.base);
|
|
363
|
+
const strategy = resolveSyncStrategy(repoRoot, options.strategy);
|
|
364
|
+
const baseRef = `origin/${baseBranch}`;
|
|
365
|
+
|
|
366
|
+
ensureOriginBaseRef(repoRoot, baseBranch);
|
|
367
|
+
|
|
368
|
+
if (options.allAgentBranches) {
|
|
369
|
+
const refs = gitRun(repoRoot, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/agent/*'], { allowFailure: true });
|
|
370
|
+
if (refs.status !== 0) {
|
|
371
|
+
throw new Error('Unable to list local agent branches');
|
|
372
|
+
}
|
|
373
|
+
const branches = (refs.stdout || '').split('\n').map((item) => item.trim()).filter(Boolean);
|
|
374
|
+
const rows = branches.map((branch) => {
|
|
375
|
+
const counts = aheadBehind(repoRoot, branch, baseRef);
|
|
376
|
+
return {
|
|
377
|
+
branch,
|
|
378
|
+
base: baseRef,
|
|
379
|
+
ahead: counts.ahead,
|
|
380
|
+
behind: counts.behind,
|
|
381
|
+
syncRequired: counts.behind > 0,
|
|
382
|
+
};
|
|
383
|
+
});
|
|
373
384
|
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
385
|
+
if (options.json) {
|
|
386
|
+
process.stdout.write(`${JSON.stringify({
|
|
387
|
+
repoRoot,
|
|
388
|
+
base: baseRef,
|
|
389
|
+
branchCount: rows.length,
|
|
390
|
+
rows,
|
|
391
|
+
}, null, 2)}\n`);
|
|
392
|
+
} else {
|
|
393
|
+
console.log(`[${TOOL_NAME}] Sync report target: ${repoRoot}`);
|
|
394
|
+
console.log(`[${TOOL_NAME}] Base: ${baseRef}`);
|
|
395
|
+
if (rows.length === 0) {
|
|
396
|
+
console.log(`[${TOOL_NAME}] No local agent branches found.`);
|
|
377
397
|
} else {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
throw new Error(`Unable to temporarily reset ${LOCK_FILE_RELATIVE} before sync`);
|
|
398
|
+
for (const row of rows) {
|
|
399
|
+
console.log(` - ${row.branch} | ahead ${row.ahead} | behind ${row.behind} | syncRequired=${row.syncRequired}`);
|
|
381
400
|
}
|
|
382
401
|
}
|
|
383
402
|
}
|
|
384
403
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
390
|
-
fs.writeFileSync(lockPath, lockBackup, 'utf8');
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const after = aheadBehind(repoRoot, branch, baseRef);
|
|
394
|
-
const result = {
|
|
395
|
-
...payload,
|
|
396
|
-
status: 'success',
|
|
397
|
-
aheadAfter: after.ahead,
|
|
398
|
-
behindAfter: after.behind,
|
|
399
|
-
};
|
|
404
|
+
const hasBehind = rows.some((row) => row.behind > 0);
|
|
405
|
+
process.exitCode = options.check && hasBehind ? 1 : 0;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
400
408
|
|
|
409
|
+
const branch = currentBranchName(repoRoot);
|
|
410
|
+
if (!options.allowNonAgent && !branch.startsWith('agent/')) {
|
|
411
|
+
throw new Error(`sync is limited to agent/* branches by default (current: ${branch}). Use --allow-non-agent to override.`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const dirty = workingTreeIsDirty(repoRoot);
|
|
415
|
+
if (!options.check && !options.allowDirty && dirty) {
|
|
416
|
+
throw new Error('Sync blocked: working tree is not clean. Commit or stash changes first, or pass --allow-dirty.');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const before = aheadBehind(repoRoot, branch, baseRef);
|
|
420
|
+
|
|
421
|
+
const payload = {
|
|
422
|
+
repoRoot,
|
|
423
|
+
branch,
|
|
424
|
+
base: baseRef,
|
|
425
|
+
strategy,
|
|
426
|
+
dirty,
|
|
427
|
+
aheadBefore: before.ahead,
|
|
428
|
+
behindBefore: before.behind,
|
|
429
|
+
syncRequired: before.behind > 0,
|
|
430
|
+
status: 'checked',
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
if (options.check) {
|
|
401
434
|
if (options.json) {
|
|
402
|
-
process.stdout.write(`${JSON.stringify(
|
|
435
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
403
436
|
} else {
|
|
404
|
-
console.log(`[${TOOL_NAME}] Sync target: ${repoRoot}`);
|
|
437
|
+
console.log(`[${TOOL_NAME}] Sync check target: ${repoRoot}`);
|
|
405
438
|
console.log(`[${TOOL_NAME}] Branch: ${branch}`);
|
|
406
439
|
console.log(`[${TOOL_NAME}] Base: ${baseRef}`);
|
|
407
|
-
console.log(`[${TOOL_NAME}]
|
|
408
|
-
console.log(`[${TOOL_NAME}] Behind
|
|
409
|
-
console.log(`[${TOOL_NAME}]
|
|
440
|
+
console.log(`[${TOOL_NAME}] Ahead: ${before.ahead}`);
|
|
441
|
+
console.log(`[${TOOL_NAME}] Behind: ${before.behind}`);
|
|
442
|
+
console.log(`[${TOOL_NAME}] Sync required: ${before.behind > 0 ? 'yes' : 'no'}`);
|
|
443
|
+
}
|
|
444
|
+
process.exitCode = before.behind > 0 ? 1 : 0;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (before.behind === 0) {
|
|
449
|
+
const result = { ...payload, status: 'no-op', aheadAfter: before.ahead, behindAfter: before.behind };
|
|
450
|
+
if (options.json) {
|
|
451
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
452
|
+
} else {
|
|
453
|
+
console.log(`[${TOOL_NAME}] Branch '${branch}' is already up to date with ${baseRef}.`);
|
|
410
454
|
}
|
|
455
|
+
process.exitCode = 0;
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
411
458
|
|
|
459
|
+
if (options.dryRun) {
|
|
460
|
+
const result = { ...payload, status: 'dry-run' };
|
|
461
|
+
if (options.json) {
|
|
462
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
463
|
+
} else {
|
|
464
|
+
console.log(`[${TOOL_NAME}] Dry run: would sync '${branch}' onto ${baseRef} via ${strategy}.`);
|
|
465
|
+
}
|
|
412
466
|
process.exitCode = 0;
|
|
467
|
+
return;
|
|
413
468
|
}
|
|
414
469
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
470
|
+
const lockPath = path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
471
|
+
const lockState = lockRegistryStatus(repoRoot);
|
|
472
|
+
let lockBackup = null;
|
|
473
|
+
if (lockState.dirty && fs.existsSync(lockPath)) {
|
|
474
|
+
lockBackup = fs.readFileSync(lockPath, 'utf8');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (lockState.dirty) {
|
|
478
|
+
if (lockState.untracked) {
|
|
479
|
+
fs.rmSync(lockPath, { force: true });
|
|
480
|
+
} else {
|
|
481
|
+
const resetLock = gitRun(repoRoot, ['checkout', '--', LOCK_FILE_RELATIVE], { allowFailure: true });
|
|
482
|
+
if (resetLock.status !== 0) {
|
|
483
|
+
throw new Error(`Unable to temporarily reset ${LOCK_FILE_RELATIVE} before sync`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
syncOperation(repoRoot, strategy, baseRef, options.ffOnly);
|
|
490
|
+
} finally {
|
|
491
|
+
if (lockBackup !== null) {
|
|
492
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
493
|
+
fs.writeFileSync(lockPath, lockBackup, 'utf8');
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const after = aheadBehind(repoRoot, branch, baseRef);
|
|
497
|
+
const result = {
|
|
498
|
+
...payload,
|
|
499
|
+
status: 'success',
|
|
500
|
+
aheadAfter: after.ahead,
|
|
501
|
+
behindAfter: after.behind,
|
|
420
502
|
};
|
|
503
|
+
|
|
504
|
+
if (options.json) {
|
|
505
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
506
|
+
} else {
|
|
507
|
+
console.log(`[${TOOL_NAME}] Sync target: ${repoRoot}`);
|
|
508
|
+
console.log(`[${TOOL_NAME}] Branch: ${branch}`);
|
|
509
|
+
console.log(`[${TOOL_NAME}] Base: ${baseRef}`);
|
|
510
|
+
console.log(`[${TOOL_NAME}] Strategy: ${strategy}`);
|
|
511
|
+
console.log(`[${TOOL_NAME}] Behind before sync: ${before.behind}`);
|
|
512
|
+
console.log(`[${TOOL_NAME}] Result: success (behind now: ${after.behind})`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
process.exitCode = 0;
|
|
421
516
|
}
|
|
422
517
|
|
|
423
518
|
module.exports = {
|
|
424
|
-
|
|
519
|
+
cleanup,
|
|
520
|
+
merge,
|
|
521
|
+
finish,
|
|
522
|
+
sync,
|
|
425
523
|
};
|