@imdeadpool/guardex 7.0.18 → 7.0.20
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 +34 -19
- package/bin/multiagent-safety.js +2 -7784
- package/package.json +2 -1
- package/src/cli/args.js +7 -0
- package/src/cli/dispatch.js +86 -0
- package/src/cli/main.js +7719 -0
- package/src/context.js +503 -0
- package/src/core/runtime.js +119 -0
- package/src/finish/index.js +425 -0
- package/src/git/index.js +112 -0
- package/src/hooks/index.js +74 -0
- package/src/output/index.js +398 -0
- package/src/sandbox/index.js +68 -0
- package/src/scaffold/index.js +169 -0
- package/src/toolchain/index.js +223 -0
- package/templates/AGENTS.multiagent-safety.md +3 -0
- package/templates/codex/skills/gitguardex/SKILL.md +1 -1
- package/templates/codex/skills/guardex-merge-skills-to-dev/SKILL.md +3 -3
- package/templates/githooks/pre-commit +21 -2
- package/templates/scripts/agent-branch-finish.sh +32 -19
- package/templates/scripts/agent-branch-merge.sh +24 -5
- package/templates/scripts/agent-branch-start.sh +74 -36
- package/templates/scripts/agent-file-locks.py +11 -11
- package/templates/scripts/codex-agent.sh +179 -61
- package/templates/scripts/review-bot-watch.sh +30 -7
- package/templates/vscode/guardex-active-agents/README.md +16 -10
- package/templates/vscode/guardex-active-agents/extension.js +901 -49
- package/templates/vscode/guardex-active-agents/package.json +61 -1
- package/templates/vscode/guardex-active-agents/session-schema.js +211 -17
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
function createFinishApi(deps) {
|
|
2
|
+
const {
|
|
3
|
+
TOOL_NAME,
|
|
4
|
+
LOCK_FILE_RELATIVE,
|
|
5
|
+
path,
|
|
6
|
+
fs,
|
|
7
|
+
run,
|
|
8
|
+
runPackageAsset,
|
|
9
|
+
resolveRepoRoot,
|
|
10
|
+
parseCleanupArgs,
|
|
11
|
+
parseMergeArgs,
|
|
12
|
+
parseFinishArgs,
|
|
13
|
+
parseSyncArgs,
|
|
14
|
+
listAgentWorktrees,
|
|
15
|
+
listLocalAgentBranchesForFinish,
|
|
16
|
+
uniquePreserveOrder,
|
|
17
|
+
branchExists,
|
|
18
|
+
resolveFinishBaseBranch,
|
|
19
|
+
worktreeHasLocalChanges,
|
|
20
|
+
branchMergedIntoBase,
|
|
21
|
+
autoCommitWorktreeForFinish,
|
|
22
|
+
resolveBaseBranch,
|
|
23
|
+
resolveSyncStrategy,
|
|
24
|
+
ensureOriginBaseRef,
|
|
25
|
+
gitRun,
|
|
26
|
+
currentBranchName,
|
|
27
|
+
workingTreeIsDirty,
|
|
28
|
+
aheadBehind,
|
|
29
|
+
lockRegistryStatus,
|
|
30
|
+
syncOperation,
|
|
31
|
+
} = deps;
|
|
32
|
+
|
|
33
|
+
function cleanup(rawArgs) {
|
|
34
|
+
const options = parseCleanupArgs(rawArgs);
|
|
35
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
36
|
+
|
|
37
|
+
const args = [];
|
|
38
|
+
if (options.base) {
|
|
39
|
+
args.push('--base', options.base);
|
|
40
|
+
}
|
|
41
|
+
if (options.branch) {
|
|
42
|
+
args.push('--branch', options.branch);
|
|
43
|
+
}
|
|
44
|
+
if (options.forceDirty) {
|
|
45
|
+
args.push('--force-dirty');
|
|
46
|
+
}
|
|
47
|
+
if (options.dryRun) {
|
|
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');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const runCleanupCycle = () => {
|
|
68
|
+
const runResult = runPackageAsset('worktreePrune', args, { cwd: repoRoot, stdio: 'inherit' });
|
|
69
|
+
if (runResult.status !== 0) {
|
|
70
|
+
throw new Error('Cleanup command failed');
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (options.watch) {
|
|
75
|
+
let cycle = 0;
|
|
76
|
+
while (true) {
|
|
77
|
+
cycle += 1;
|
|
78
|
+
console.log(
|
|
79
|
+
`[${TOOL_NAME}] Cleanup watch cycle=${cycle} (interval=${options.intervalSeconds}s, idleMinutes=${options.idleMinutes}, maxBranches=${options.maxBranches > 0 ? options.maxBranches : 'unbounded'}).`,
|
|
80
|
+
);
|
|
81
|
+
runCleanupCycle();
|
|
82
|
+
if (options.once) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
const sleepResult = run('sleep', [String(options.intervalSeconds)], { cwd: repoRoot });
|
|
86
|
+
if (sleepResult.status !== 0) {
|
|
87
|
+
throw new Error(`Cleanup watch sleep failed (interval=${options.intervalSeconds}s)`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
process.exitCode = 0;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
runCleanupCycle();
|
|
95
|
+
process.exitCode = 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function merge(rawArgs) {
|
|
99
|
+
const options = parseMergeArgs(rawArgs);
|
|
100
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
101
|
+
|
|
102
|
+
const args = [];
|
|
103
|
+
if (options.base) {
|
|
104
|
+
args.push('--base', options.base);
|
|
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
|
+
}
|
|
118
|
+
|
|
119
|
+
const mergeResult = runPackageAsset('branchMerge', args, { cwd: repoRoot, stdio: 'pipe' });
|
|
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
|
+
}
|
|
129
|
+
|
|
130
|
+
process.exitCode = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function finish(rawArgs, defaults = {}) {
|
|
134
|
+
const options = parseFinishArgs(rawArgs, defaults);
|
|
135
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
136
|
+
|
|
137
|
+
const worktreeEntries = listAgentWorktrees(repoRoot);
|
|
138
|
+
const worktreeByBranch = new Map(worktreeEntries.map((entry) => [entry.branch, entry.worktreePath]));
|
|
139
|
+
|
|
140
|
+
let candidateBranches = [];
|
|
141
|
+
if (options.branch) {
|
|
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
|
+
}
|
|
152
|
+
|
|
153
|
+
const candidates = [];
|
|
154
|
+
for (const branch of candidateBranches) {
|
|
155
|
+
const worktreePath = worktreeByBranch.get(branch) || '';
|
|
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
|
+
}
|
|
169
|
+
|
|
170
|
+
if (candidates.length === 0) {
|
|
171
|
+
console.log(`[${TOOL_NAME}] No pending agent branches to finish.`);
|
|
172
|
+
process.exitCode = 0;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let succeeded = 0;
|
|
177
|
+
let failed = 0;
|
|
178
|
+
let autoCommitted = 0;
|
|
179
|
+
|
|
180
|
+
for (const candidate of candidates) {
|
|
181
|
+
const { branch, baseBranch, worktreePath } = candidate;
|
|
182
|
+
console.log(
|
|
183
|
+
`[${TOOL_NAME}] Finishing '${branch}' -> '${baseBranch}'${worktreePath ? ` (${worktreePath})` : ''}...`,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
let commitState = { changed: false, committed: false };
|
|
188
|
+
if (worktreePath) {
|
|
189
|
+
commitState = autoCommitWorktreeForFinish(repoRoot, worktreePath, branch, options);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (commitState.committed) {
|
|
193
|
+
autoCommitted += 1;
|
|
194
|
+
console.log(`[${TOOL_NAME}] Auto-committed '${branch}' before finish.`);
|
|
195
|
+
} else if (commitState.changed && commitState.dryRun) {
|
|
196
|
+
console.log(`[${TOOL_NAME}] [dry-run] Would auto-commit pending changes on '${branch}'.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const finishArgs = [
|
|
200
|
+
'--branch',
|
|
201
|
+
branch,
|
|
202
|
+
'--base',
|
|
203
|
+
baseBranch,
|
|
204
|
+
options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
205
|
+
options.cleanup ? '--cleanup' : '--no-cleanup',
|
|
206
|
+
];
|
|
207
|
+
if (options.mergeMode === 'pr') {
|
|
208
|
+
finishArgs.push('--via-pr');
|
|
209
|
+
} else if (options.mergeMode === 'direct') {
|
|
210
|
+
finishArgs.push('--direct-only');
|
|
211
|
+
} else {
|
|
212
|
+
finishArgs.push('--mode', 'auto');
|
|
213
|
+
}
|
|
214
|
+
if (options.keepRemote) {
|
|
215
|
+
finishArgs.push('--keep-remote-branch');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (options.dryRun) {
|
|
219
|
+
console.log(`[${TOOL_NAME}] [dry-run] Would run: gx branch finish ${finishArgs.join(' ')}`);
|
|
220
|
+
succeeded += 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot, stdio: 'pipe' });
|
|
225
|
+
if (finishResult.stdout) {
|
|
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
|
+
}
|
|
234
|
+
|
|
235
|
+
succeeded += 1;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
failed += 1;
|
|
238
|
+
console.error(`[${TOOL_NAME}] Finish failed for '${branch}': ${error.message}`);
|
|
239
|
+
if (options.failFast) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(
|
|
246
|
+
`[${TOOL_NAME}] Finish summary: total=${candidates.length}, success=${succeeded}, failed=${failed}, autoCommitted=${autoCommitted}`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (failed > 0) {
|
|
250
|
+
throw new Error('finish command failed for one or more agent branches');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
process.exitCode = 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sync(rawArgs) {
|
|
257
|
+
const options = parseSyncArgs(rawArgs);
|
|
258
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
259
|
+
const baseBranch = resolveBaseBranch(repoRoot, options.base);
|
|
260
|
+
const strategy = resolveSyncStrategy(repoRoot, options.strategy);
|
|
261
|
+
const baseRef = `origin/${baseBranch}`;
|
|
262
|
+
|
|
263
|
+
ensureOriginBaseRef(repoRoot, baseBranch);
|
|
264
|
+
|
|
265
|
+
if (options.allAgentBranches) {
|
|
266
|
+
const refs = gitRun(repoRoot, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/agent/*'], { allowFailure: true });
|
|
267
|
+
if (refs.status !== 0) {
|
|
268
|
+
throw new Error('Unable to list local agent branches');
|
|
269
|
+
}
|
|
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
|
+
|
|
282
|
+
if (options.json) {
|
|
283
|
+
process.stdout.write(`${JSON.stringify({
|
|
284
|
+
repoRoot,
|
|
285
|
+
base: baseRef,
|
|
286
|
+
branchCount: rows.length,
|
|
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
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const hasBehind = rows.some((row) => row.behind > 0);
|
|
302
|
+
process.exitCode = options.check && hasBehind ? 1 : 0;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const branch = currentBranchName(repoRoot);
|
|
307
|
+
if (!options.allowNonAgent && !branch.startsWith('agent/')) {
|
|
308
|
+
throw new Error(`sync is limited to agent/* branches by default (current: ${branch}). Use --allow-non-agent to override.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const dirty = workingTreeIsDirty(repoRoot);
|
|
312
|
+
if (!options.check && !options.allowDirty && dirty) {
|
|
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`);
|
|
333
|
+
} else {
|
|
334
|
+
console.log(`[${TOOL_NAME}] Sync check target: ${repoRoot}`);
|
|
335
|
+
console.log(`[${TOOL_NAME}] Branch: ${branch}`);
|
|
336
|
+
console.log(`[${TOOL_NAME}] Base: ${baseRef}`);
|
|
337
|
+
console.log(`[${TOOL_NAME}] Ahead: ${before.ahead}`);
|
|
338
|
+
console.log(`[${TOOL_NAME}] Behind: ${before.behind}`);
|
|
339
|
+
console.log(`[${TOOL_NAME}] Sync required: ${before.behind > 0 ? 'yes' : 'no'}`);
|
|
340
|
+
}
|
|
341
|
+
process.exitCode = before.behind > 0 ? 1 : 0;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (before.behind === 0) {
|
|
346
|
+
const result = { ...payload, status: 'no-op', aheadAfter: before.ahead, behindAfter: before.behind };
|
|
347
|
+
if (options.json) {
|
|
348
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
349
|
+
} else {
|
|
350
|
+
console.log(`[${TOOL_NAME}] Branch '${branch}' is already up to date with ${baseRef}.`);
|
|
351
|
+
}
|
|
352
|
+
process.exitCode = 0;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (options.dryRun) {
|
|
357
|
+
const result = { ...payload, status: 'dry-run' };
|
|
358
|
+
if (options.json) {
|
|
359
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
360
|
+
} else {
|
|
361
|
+
console.log(`[${TOOL_NAME}] Dry run: would sync '${branch}' onto ${baseRef} via ${strategy}.`);
|
|
362
|
+
}
|
|
363
|
+
process.exitCode = 0;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const lockPath = path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
368
|
+
const lockState = lockRegistryStatus(repoRoot);
|
|
369
|
+
let lockBackup = null;
|
|
370
|
+
if (lockState.dirty && fs.existsSync(lockPath)) {
|
|
371
|
+
lockBackup = fs.readFileSync(lockPath, 'utf8');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (lockState.dirty) {
|
|
375
|
+
if (lockState.untracked) {
|
|
376
|
+
fs.rmSync(lockPath, { force: true });
|
|
377
|
+
} else {
|
|
378
|
+
const resetLock = gitRun(repoRoot, ['checkout', '--', LOCK_FILE_RELATIVE], { allowFailure: true });
|
|
379
|
+
if (resetLock.status !== 0) {
|
|
380
|
+
throw new Error(`Unable to temporarily reset ${LOCK_FILE_RELATIVE} before sync`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
syncOperation(repoRoot, strategy, baseRef, options.ffOnly);
|
|
387
|
+
} finally {
|
|
388
|
+
if (lockBackup !== null) {
|
|
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
|
+
};
|
|
400
|
+
|
|
401
|
+
if (options.json) {
|
|
402
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
403
|
+
} else {
|
|
404
|
+
console.log(`[${TOOL_NAME}] Sync target: ${repoRoot}`);
|
|
405
|
+
console.log(`[${TOOL_NAME}] Branch: ${branch}`);
|
|
406
|
+
console.log(`[${TOOL_NAME}] Base: ${baseRef}`);
|
|
407
|
+
console.log(`[${TOOL_NAME}] Strategy: ${strategy}`);
|
|
408
|
+
console.log(`[${TOOL_NAME}] Behind before sync: ${before.behind}`);
|
|
409
|
+
console.log(`[${TOOL_NAME}] Result: success (behind now: ${after.behind})`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
process.exitCode = 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
cleanup,
|
|
417
|
+
merge,
|
|
418
|
+
finish,
|
|
419
|
+
sync,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
module.exports = {
|
|
424
|
+
createFinishApi,
|
|
425
|
+
};
|
package/src/git/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const { path } = require('../context');
|
|
2
|
+
const { run } = require('../core/runtime');
|
|
3
|
+
|
|
4
|
+
function gitRun(repoRoot, args, { allowFailure = false } = {}) {
|
|
5
|
+
const result = run('git', ['-C', repoRoot, ...args]);
|
|
6
|
+
if (!allowFailure && result.status !== 0) {
|
|
7
|
+
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || '').trim()}`);
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveRepoRoot(targetPath) {
|
|
13
|
+
const resolvedTarget = path.resolve(targetPath || process.cwd());
|
|
14
|
+
const result = run('git', ['-C', resolvedTarget, 'rev-parse', '--show-toplevel']);
|
|
15
|
+
if (result.status !== 0) {
|
|
16
|
+
const stderr = (result.stderr || '').trim();
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Target is not inside a git repository: ${resolvedTarget}${stderr ? `\n${stderr}` : ''}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return result.stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isGitRepo(targetPath) {
|
|
25
|
+
const resolvedTarget = path.resolve(targetPath || process.cwd());
|
|
26
|
+
const result = run('git', ['-C', resolvedTarget, 'rev-parse', '--show-toplevel']);
|
|
27
|
+
return result.status === 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NESTED_REPO_DEFAULT_MAX_DEPTH = 6;
|
|
31
|
+
const NESTED_REPO_DEFAULT_SKIP_DIRS = new Set([
|
|
32
|
+
'node_modules',
|
|
33
|
+
'.git',
|
|
34
|
+
'dist',
|
|
35
|
+
'build',
|
|
36
|
+
'.next',
|
|
37
|
+
'.cache',
|
|
38
|
+
'target',
|
|
39
|
+
'vendor',
|
|
40
|
+
'.venv',
|
|
41
|
+
'.pnpm-store',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function discoverNestedGitRepos(rootPath, opts = {}) {
|
|
45
|
+
const maxDepth = Number.isFinite(opts.maxDepth)
|
|
46
|
+
? Math.max(1, opts.maxDepth)
|
|
47
|
+
: NESTED_REPO_DEFAULT_MAX_DEPTH;
|
|
48
|
+
const extraSkip = new Set(Array.isArray(opts.extraSkip) ? opts.extraSkip : []);
|
|
49
|
+
const includeSubmodules = Boolean(opts.includeSubmodules);
|
|
50
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
51
|
+
|
|
52
|
+
if (!isGitRepo(resolvedRoot)) {
|
|
53
|
+
throw new Error(`Target is not inside a git repository: ${resolvedRoot}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const results = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
|
|
59
|
+
function visit(directoryPath, depth) {
|
|
60
|
+
const repoRoot = resolveRepoRoot(directoryPath);
|
|
61
|
+
if (!seen.has(repoRoot)) {
|
|
62
|
+
seen.add(repoRoot);
|
|
63
|
+
results.push(repoRoot);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (depth >= maxDepth) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let entries = [];
|
|
71
|
+
try {
|
|
72
|
+
entries = require('node:fs').readdirSync(directoryPath, { withFileTypes: true });
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!entry.isDirectory()) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (NESTED_REPO_DEFAULT_SKIP_DIRS.has(entry.name) || extraSkip.has(entry.name)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const childPath = path.join(directoryPath, entry.name);
|
|
86
|
+
const gitDir = path.join(childPath, '.git');
|
|
87
|
+
if (require('node:fs').existsSync(gitDir)) {
|
|
88
|
+
if (!includeSubmodules) {
|
|
89
|
+
const gitInfo = require('node:fs').lstatSync(gitDir);
|
|
90
|
+
if (gitInfo.isFile()) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
visit(childPath, depth + 1);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
visit(childPath, depth + 1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
visit(resolvedRoot, 0);
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
DEFAULT_NESTED_REPO_MAX_DEPTH: NESTED_REPO_DEFAULT_MAX_DEPTH,
|
|
108
|
+
gitRun,
|
|
109
|
+
resolveRepoRoot,
|
|
110
|
+
isGitRepo,
|
|
111
|
+
discoverNestedGitRepos,
|
|
112
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
function hook(rawArgs, deps) {
|
|
4
|
+
const {
|
|
5
|
+
extractTargetedArgs,
|
|
6
|
+
run,
|
|
7
|
+
resolveRepoRoot,
|
|
8
|
+
packageAssetEnv,
|
|
9
|
+
configureHooks,
|
|
10
|
+
TEMPLATE_ROOT,
|
|
11
|
+
HOOK_NAMES,
|
|
12
|
+
TOOL_NAME,
|
|
13
|
+
SHORT_TOOL_NAME,
|
|
14
|
+
} = deps;
|
|
15
|
+
|
|
16
|
+
const [subcommand, ...rest] = rawArgs;
|
|
17
|
+
if (subcommand === 'run') {
|
|
18
|
+
const [hookName, ...hookArgs] = rest;
|
|
19
|
+
if (!HOOK_NAMES.includes(hookName)) {
|
|
20
|
+
throw new Error(`Unknown hook name: ${hookName || '(missing)'}`);
|
|
21
|
+
}
|
|
22
|
+
const { target, passthrough } = extractTargetedArgs(hookArgs);
|
|
23
|
+
const hookAssetPath = path.join(TEMPLATE_ROOT, 'githooks', hookName);
|
|
24
|
+
const result = run('bash', [hookAssetPath, ...passthrough], {
|
|
25
|
+
cwd: resolveRepoRoot(target),
|
|
26
|
+
stdio: hookName === 'pre-push' ? 'inherit' : 'pipe',
|
|
27
|
+
env: packageAssetEnv(),
|
|
28
|
+
});
|
|
29
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
30
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
31
|
+
process.exitCode = result.status;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (subcommand === 'install') {
|
|
35
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
36
|
+
if (passthrough.length > 0) {
|
|
37
|
+
throw new Error(`Unknown hook install option: ${passthrough[0]}`);
|
|
38
|
+
}
|
|
39
|
+
const repoRoot = resolveRepoRoot(target);
|
|
40
|
+
const hookResult = configureHooks(repoRoot, false);
|
|
41
|
+
console.log(`[${TOOL_NAME}] Hook install target: ${repoRoot}`);
|
|
42
|
+
console.log(` - hooksPath ${hookResult.status} ${hookResult.key}=${hookResult.value}`);
|
|
43
|
+
process.exitCode = 0;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Usage: ${SHORT_TOOL_NAME} hook <run|install> ...`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function internal(rawArgs, deps) {
|
|
50
|
+
const {
|
|
51
|
+
extractTargetedArgs,
|
|
52
|
+
resolveRepoRoot,
|
|
53
|
+
runReviewBotCommand,
|
|
54
|
+
runPackageAsset,
|
|
55
|
+
} = deps;
|
|
56
|
+
|
|
57
|
+
const [subcommand, assetKey, ...rest] = rawArgs;
|
|
58
|
+
if (subcommand !== 'run-shell') {
|
|
59
|
+
throw new Error(`Unknown internal command: ${subcommand || '(missing)'}`);
|
|
60
|
+
}
|
|
61
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
62
|
+
const repoRoot = resolveRepoRoot(target);
|
|
63
|
+
const result = assetKey === 'reviewBot'
|
|
64
|
+
? runReviewBotCommand(repoRoot, passthrough)
|
|
65
|
+
: runPackageAsset(assetKey, passthrough, { cwd: repoRoot });
|
|
66
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
67
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
68
|
+
process.exitCode = result.status;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
hook,
|
|
73
|
+
internal,
|
|
74
|
+
};
|