@akiojin/gwt 4.11.6 → 4.12.0
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/bin/gwt.js +1 -1
- package/dist/claude.d.ts +1 -0
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +50 -24
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/App.solid.d.ts.map +1 -1
- package/dist/cli/ui/App.solid.js +247 -49
- package/dist/cli/ui/App.solid.js.map +1 -1
- package/dist/cli/ui/components/solid/QuickStartStep.d.ts.map +1 -1
- package/dist/cli/ui/components/solid/QuickStartStep.js +35 -22
- package/dist/cli/ui/components/solid/QuickStartStep.js.map +1 -1
- package/dist/cli/ui/components/solid/SelectInput.d.ts.map +1 -1
- package/dist/cli/ui/components/solid/SelectInput.js +2 -1
- package/dist/cli/ui/components/solid/SelectInput.js.map +1 -1
- package/dist/cli/ui/components/solid/WizardController.d.ts.map +1 -1
- package/dist/cli/ui/components/solid/WizardController.js +19 -11
- package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
- package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
- package/dist/cli/ui/components/solid/WizardSteps.js +26 -69
- package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
- package/dist/cli/ui/core/theme.d.ts +9 -0
- package/dist/cli/ui/core/theme.d.ts.map +1 -1
- package/dist/cli/ui/core/theme.js +21 -0
- package/dist/cli/ui/core/theme.js.map +1 -1
- package/dist/cli/ui/screens/solid/BranchListScreen.d.ts +9 -2
- package/dist/cli/ui/screens/solid/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/screens/solid/BranchListScreen.js +101 -28
- package/dist/cli/ui/screens/solid/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts +2 -1
- package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts.map +1 -1
- package/dist/cli/ui/screens/solid/ConfirmScreen.js +11 -3
- package/dist/cli/ui/screens/solid/ConfirmScreen.js.map +1 -1
- package/dist/cli/ui/screens/solid/EnvironmentScreen.d.ts.map +1 -1
- package/dist/cli/ui/screens/solid/EnvironmentScreen.js +9 -10
- package/dist/cli/ui/screens/solid/EnvironmentScreen.js.map +1 -1
- package/dist/cli/ui/screens/solid/LogScreen.d.ts +7 -1
- package/dist/cli/ui/screens/solid/LogScreen.d.ts.map +1 -1
- package/dist/cli/ui/screens/solid/LogScreen.js +254 -16
- package/dist/cli/ui/screens/solid/LogScreen.js.map +1 -1
- package/dist/cli/ui/screens/solid/ProfileEnvScreen.d.ts.map +1 -1
- package/dist/cli/ui/screens/solid/ProfileEnvScreen.js +8 -5
- package/dist/cli/ui/screens/solid/ProfileEnvScreen.js.map +1 -1
- package/dist/cli/ui/screens/solid/SelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/screens/solid/SelectorScreen.js +12 -4
- package/dist/cli/ui/screens/solid/SelectorScreen.js.map +1 -1
- package/dist/cli/ui/types.d.ts +1 -0
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts +1 -0
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +29 -7
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/cli/ui/utils/continueSession.d.ts +14 -0
- package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
- package/dist/cli/ui/utils/continueSession.js +61 -3
- package/dist/cli/ui/utils/continueSession.js.map +1 -1
- package/dist/cli/ui/utils/versionCache.d.ts +37 -0
- package/dist/cli/ui/utils/versionCache.d.ts.map +1 -0
- package/dist/cli/ui/utils/versionCache.js +70 -0
- package/dist/cli/ui/utils/versionCache.js.map +1 -0
- package/dist/cli/ui/utils/versionFetcher.d.ts +41 -0
- package/dist/cli/ui/utils/versionFetcher.d.ts.map +1 -0
- package/dist/cli/ui/utils/versionFetcher.js +89 -0
- package/dist/cli/ui/utils/versionFetcher.js.map +1 -0
- package/dist/codex.d.ts +1 -0
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +48 -19
- package/dist/codex.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +10 -1
- package/dist/config/index.js.map +1 -1
- package/dist/gemini.d.ts +1 -0
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +36 -3
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -2
- package/dist/index.js.map +1 -1
- package/dist/launcher.d.ts.map +1 -1
- package/dist/launcher.js +43 -8
- package/dist/launcher.js.map +1 -1
- package/dist/logging/agentOutput.d.ts +21 -0
- package/dist/logging/agentOutput.d.ts.map +1 -0
- package/dist/logging/agentOutput.js +164 -0
- package/dist/logging/agentOutput.js.map +1 -0
- package/dist/logging/formatter.d.ts.map +1 -1
- package/dist/logging/formatter.js +18 -4
- package/dist/logging/formatter.js.map +1 -1
- package/dist/logging/logger.d.ts.map +1 -1
- package/dist/logging/logger.js +2 -0
- package/dist/logging/logger.js.map +1 -1
- package/dist/logging/reader.d.ts +21 -0
- package/dist/logging/reader.d.ts.map +1 -1
- package/dist/logging/reader.js +79 -0
- package/dist/logging/reader.js.map +1 -1
- package/dist/opentui/index.solid.js +2306 -653
- package/dist/services/dependency-installer.js +2 -2
- package/dist/services/dependency-installer.js.map +1 -1
- package/dist/utils/session/common.d.ts +8 -0
- package/dist/utils/session/common.d.ts.map +1 -1
- package/dist/utils/session/common.js +22 -0
- package/dist/utils/session/common.js.map +1 -1
- package/dist/utils/session/parsers/claude.d.ts +10 -4
- package/dist/utils/session/parsers/claude.d.ts.map +1 -1
- package/dist/utils/session/parsers/claude.js +64 -18
- package/dist/utils/session/parsers/claude.js.map +1 -1
- package/dist/utils/session/parsers/codex.d.ts.map +1 -1
- package/dist/utils/session/parsers/codex.js +48 -28
- package/dist/utils/session/parsers/codex.js.map +1 -1
- package/dist/utils/session/parsers/gemini.d.ts.map +1 -1
- package/dist/utils/session/parsers/gemini.js +43 -6
- package/dist/utils/session/parsers/gemini.js.map +1 -1
- package/dist/utils/session/parsers/opencode.d.ts.map +1 -1
- package/dist/utils/session/parsers/opencode.js +43 -6
- package/dist/utils/session/parsers/opencode.js.map +1 -1
- package/dist/utils/session/types.d.ts +7 -0
- package/dist/utils/session/types.d.ts.map +1 -1
- package/dist/web/client/src/components/ui/alert.d.ts +1 -1
- package/dist/worktree.d.ts +4 -1
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +21 -15
- package/dist/worktree.js.map +1 -1
- package/package.json +2 -1
- package/src/claude.ts +64 -28
- package/src/cli/ui/App.solid.tsx +324 -51
- package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +830 -1
- package/src/cli/ui/__tests__/solid/BranchListScreen.test.tsx +105 -5
- package/src/cli/ui/__tests__/solid/ConfirmScreen.test.tsx +77 -0
- package/src/cli/ui/__tests__/solid/LogScreen.test.tsx +351 -0
- package/src/cli/ui/__tests__/solid/components/QuickStartStep.test.tsx +73 -2
- package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +4 -1
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +72 -45
- package/src/cli/ui/components/solid/QuickStartStep.tsx +35 -23
- package/src/cli/ui/components/solid/SearchInput.tsx +1 -1
- package/src/cli/ui/components/solid/SelectInput.tsx +4 -0
- package/src/cli/ui/components/solid/WizardController.tsx +20 -11
- package/src/cli/ui/components/solid/WizardSteps.tsx +29 -86
- package/src/cli/ui/core/theme.ts +32 -0
- package/src/cli/ui/hooks/solid/useAsyncOperation.ts +8 -6
- package/src/cli/ui/hooks/solid/useGitOperations.ts +6 -5
- package/src/cli/ui/screens/solid/BranchListScreen.tsx +135 -32
- package/src/cli/ui/screens/solid/ConfirmScreen.tsx +20 -8
- package/src/cli/ui/screens/solid/EnvironmentScreen.tsx +22 -20
- package/src/cli/ui/screens/solid/LogScreen.tsx +364 -35
- package/src/cli/ui/screens/solid/ProfileEnvScreen.tsx +19 -15
- package/src/cli/ui/screens/solid/SelectorScreen.tsx +25 -14
- package/src/cli/ui/screens/solid/SettingsScreen.tsx +5 -3
- package/src/cli/ui/types.ts +1 -0
- package/src/cli/ui/utils/__tests__/branchFormatter.test.ts +53 -6
- package/src/cli/ui/utils/branchFormatter.ts +35 -7
- package/src/cli/ui/utils/continueSession.ts +90 -3
- package/src/cli/ui/utils/versionCache.ts +93 -0
- package/src/cli/ui/utils/versionFetcher.ts +120 -0
- package/src/codex.ts +62 -20
- package/src/config/__tests__/saveSession.test.ts +2 -2
- package/src/config/index.ts +11 -1
- package/src/gemini.ts +50 -4
- package/src/index.test.ts +16 -10
- package/src/index.ts +38 -1
- package/src/launcher.ts +49 -8
- package/src/logging/agentOutput.ts +216 -0
- package/src/logging/formatter.ts +23 -4
- package/src/logging/logger.ts +2 -0
- package/src/logging/reader.ts +117 -0
- package/src/services/__tests__/BatchMergeService.test.ts +34 -14
- package/src/services/dependency-installer.ts +2 -2
- package/src/utils/session/common.ts +28 -0
- package/src/utils/session/parsers/claude.ts +79 -29
- package/src/utils/session/parsers/codex.ts +50 -26
- package/src/utils/session/parsers/gemini.ts +46 -5
- package/src/utils/session/parsers/opencode.ts +46 -5
- package/src/utils/session/types.ts +4 -0
- package/src/worktree.ts +28 -15
package/src/logging/reader.ts
CHANGED
|
@@ -9,6 +9,25 @@ export interface LogFileInfo {
|
|
|
9
9
|
mtimeMs: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export type LogTargetReason =
|
|
13
|
+
| "worktree"
|
|
14
|
+
| "worktree-inaccessible"
|
|
15
|
+
| "current-working-directory"
|
|
16
|
+
| "working-directory"
|
|
17
|
+
| "no-worktree";
|
|
18
|
+
|
|
19
|
+
export interface LogTargetBranch {
|
|
20
|
+
name: string;
|
|
21
|
+
isCurrent?: boolean;
|
|
22
|
+
worktree?: { path: string; isAccessible?: boolean } | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LogTargetResolution {
|
|
26
|
+
logDir: string | null;
|
|
27
|
+
sourcePath: string | null;
|
|
28
|
+
reason: LogTargetReason;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
const LOG_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
13
32
|
|
|
14
33
|
export function resolveLogDir(cwd: string = process.cwd()): string {
|
|
@@ -16,6 +35,46 @@ export function resolveLogDir(cwd: string = process.cwd()): string {
|
|
|
16
35
|
return path.join(os.homedir(), ".gwt", "logs", cwdBase);
|
|
17
36
|
}
|
|
18
37
|
|
|
38
|
+
export function resolveLogTarget(
|
|
39
|
+
branch: LogTargetBranch | null,
|
|
40
|
+
workingDirectory: string = process.cwd(),
|
|
41
|
+
): LogTargetResolution {
|
|
42
|
+
if (!branch) {
|
|
43
|
+
return {
|
|
44
|
+
logDir: resolveLogDir(workingDirectory),
|
|
45
|
+
sourcePath: workingDirectory,
|
|
46
|
+
reason: "working-directory",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const worktreePath = branch.worktree?.path;
|
|
51
|
+
if (worktreePath) {
|
|
52
|
+
const accessible = branch.worktree?.isAccessible !== false;
|
|
53
|
+
if (accessible) {
|
|
54
|
+
return {
|
|
55
|
+
logDir: resolveLogDir(worktreePath),
|
|
56
|
+
sourcePath: worktreePath,
|
|
57
|
+
reason: "worktree",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
logDir: null,
|
|
62
|
+
sourcePath: worktreePath,
|
|
63
|
+
reason: "worktree-inaccessible",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (branch.isCurrent) {
|
|
68
|
+
return {
|
|
69
|
+
logDir: resolveLogDir(workingDirectory),
|
|
70
|
+
sourcePath: workingDirectory,
|
|
71
|
+
reason: "current-working-directory",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { logDir: null, sourcePath: null, reason: "no-worktree" };
|
|
76
|
+
}
|
|
77
|
+
|
|
19
78
|
export function buildLogFilePath(logDir: string, date: string): string {
|
|
20
79
|
return path.join(logDir, `${date}.jsonl`);
|
|
21
80
|
}
|
|
@@ -66,6 +125,23 @@ export async function listLogFiles(logDir: string): Promise<LogFileInfo[]> {
|
|
|
66
125
|
}
|
|
67
126
|
}
|
|
68
127
|
|
|
128
|
+
export async function clearLogFiles(logDir: string): Promise<number> {
|
|
129
|
+
const files = await listLogFiles(logDir);
|
|
130
|
+
let cleared = 0;
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
try {
|
|
133
|
+
await fs.truncate(file.path, 0);
|
|
134
|
+
cleared += 1;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const err = error as NodeJS.ErrnoException;
|
|
137
|
+
if (err.code !== "ENOENT") {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return cleared;
|
|
143
|
+
}
|
|
144
|
+
|
|
69
145
|
export async function listRecentLogFiles(
|
|
70
146
|
logDir: string,
|
|
71
147
|
days = 7,
|
|
@@ -74,3 +150,44 @@ export async function listRecentLogFiles(
|
|
|
74
150
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
75
151
|
return files.filter((file) => file.mtimeMs >= cutoff);
|
|
76
152
|
}
|
|
153
|
+
|
|
154
|
+
export interface LogReadResult {
|
|
155
|
+
date: string;
|
|
156
|
+
lines: string[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function readLogLinesForDate(
|
|
160
|
+
logDir: string,
|
|
161
|
+
preferredDate: string,
|
|
162
|
+
): Promise<LogReadResult | null> {
|
|
163
|
+
const files = await listLogFiles(logDir);
|
|
164
|
+
if (files.length === 0) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ordered: LogFileInfo[] = [];
|
|
169
|
+
const preferred = files.find((file) => file.date === preferredDate);
|
|
170
|
+
if (preferred) {
|
|
171
|
+
ordered.push(preferred);
|
|
172
|
+
}
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
if (preferred && file.date === preferred.date) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
ordered.push(file);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const file of ordered) {
|
|
181
|
+
const lines = await readLogFileLines(file.path);
|
|
182
|
+
if (lines.length > 0) {
|
|
183
|
+
return { date: file.date, lines };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const fallback = files[0];
|
|
188
|
+
if (!fallback) {
|
|
189
|
+
return { date: preferredDate, lines: [] };
|
|
190
|
+
}
|
|
191
|
+
const fallbackDate = preferred?.date ?? fallback.date;
|
|
192
|
+
return { date: fallbackDate, lines: [] };
|
|
193
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
2
2
|
import { BatchMergeService } from "../BatchMergeService";
|
|
3
|
-
import type { BatchMergeConfig } from "../../ui/types";
|
|
3
|
+
import type { BatchMergeConfig, BatchMergeProgress } from "../../cli/ui/types";
|
|
4
4
|
|
|
5
5
|
// Mock git module
|
|
6
6
|
mock.module("../../git", () => ({
|
|
@@ -235,7 +235,9 @@ describe("BatchMergeService", () => {
|
|
|
235
235
|
(
|
|
236
236
|
worktree.generateWorktreePath as ReturnType<typeof mock>
|
|
237
237
|
).mockResolvedValue("/repo/.worktrees/feature-b");
|
|
238
|
-
(worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue(
|
|
238
|
+
(worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue(
|
|
239
|
+
undefined,
|
|
240
|
+
);
|
|
239
241
|
|
|
240
242
|
const worktreePath = await service.ensureWorktree("feature/b");
|
|
241
243
|
|
|
@@ -271,7 +273,9 @@ describe("BatchMergeService", () => {
|
|
|
271
273
|
});
|
|
272
274
|
|
|
273
275
|
it("should successfully merge without conflicts", async () => {
|
|
274
|
-
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
276
|
+
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
277
|
+
undefined,
|
|
278
|
+
);
|
|
275
279
|
(git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
|
|
276
280
|
false,
|
|
277
281
|
);
|
|
@@ -293,7 +297,7 @@ describe("BatchMergeService", () => {
|
|
|
293
297
|
new Error("Merge conflict"),
|
|
294
298
|
);
|
|
295
299
|
(git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(true);
|
|
296
|
-
(git.abortMerge as ReturnType<typeof mock>).mockResolvedValue();
|
|
300
|
+
(git.abortMerge as ReturnType<typeof mock>).mockResolvedValue(undefined);
|
|
297
301
|
|
|
298
302
|
const status = await service.mergeBranch("feature/a", "main", config);
|
|
299
303
|
|
|
@@ -339,11 +343,13 @@ describe("BatchMergeService", () => {
|
|
|
339
343
|
});
|
|
340
344
|
|
|
341
345
|
it("should rollback with resetToHead after successful dry-run merge", async () => {
|
|
342
|
-
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
346
|
+
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
347
|
+
undefined,
|
|
348
|
+
);
|
|
343
349
|
(git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
|
|
344
350
|
false,
|
|
345
351
|
);
|
|
346
|
-
(git.resetToHead as ReturnType<typeof mock>).mockResolvedValue();
|
|
352
|
+
(git.resetToHead as ReturnType<typeof mock>).mockResolvedValue(undefined);
|
|
347
353
|
|
|
348
354
|
const status = await service.mergeBranch(
|
|
349
355
|
"feature/a",
|
|
@@ -368,7 +374,7 @@ describe("BatchMergeService", () => {
|
|
|
368
374
|
new Error("CONFLICT (content)"),
|
|
369
375
|
);
|
|
370
376
|
(git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(true);
|
|
371
|
-
(git.abortMerge as ReturnType<typeof mock>).mockResolvedValue();
|
|
377
|
+
(git.abortMerge as ReturnType<typeof mock>).mockResolvedValue(undefined);
|
|
372
378
|
|
|
373
379
|
const status = await service.mergeBranch(
|
|
374
380
|
"feature/a",
|
|
@@ -405,14 +411,18 @@ describe("BatchMergeService", () => {
|
|
|
405
411
|
});
|
|
406
412
|
|
|
407
413
|
it("should push successfully after merge when autoPush is enabled", async () => {
|
|
408
|
-
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
414
|
+
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
415
|
+
undefined,
|
|
416
|
+
);
|
|
409
417
|
(git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
|
|
410
418
|
false,
|
|
411
419
|
);
|
|
412
420
|
(git.getCurrentBranchName as ReturnType<typeof mock>).mockResolvedValue(
|
|
413
421
|
"feature/a",
|
|
414
422
|
);
|
|
415
|
-
(git.pushBranchToRemote as ReturnType<typeof mock>).mockResolvedValue(
|
|
423
|
+
(git.pushBranchToRemote as ReturnType<typeof mock>).mockResolvedValue(
|
|
424
|
+
undefined,
|
|
425
|
+
);
|
|
416
426
|
|
|
417
427
|
const status = await service.mergeBranch(
|
|
418
428
|
"feature/a",
|
|
@@ -431,7 +441,9 @@ describe("BatchMergeService", () => {
|
|
|
431
441
|
});
|
|
432
442
|
|
|
433
443
|
it("should handle push failure without failing merge", async () => {
|
|
434
|
-
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
444
|
+
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
445
|
+
undefined,
|
|
446
|
+
);
|
|
435
447
|
(git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
|
|
436
448
|
false,
|
|
437
449
|
);
|
|
@@ -455,7 +467,9 @@ describe("BatchMergeService", () => {
|
|
|
455
467
|
|
|
456
468
|
it("should not push when autoPush is false", async () => {
|
|
457
469
|
const noPushConfig = { ...autoPushConfig, autoPush: false };
|
|
458
|
-
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
470
|
+
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
471
|
+
undefined,
|
|
472
|
+
);
|
|
459
473
|
(git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
|
|
460
474
|
false,
|
|
461
475
|
);
|
|
@@ -482,7 +496,9 @@ describe("BatchMergeService", () => {
|
|
|
482
496
|
autoPush: false,
|
|
483
497
|
};
|
|
484
498
|
|
|
485
|
-
(git.fetchAllRemotes as ReturnType<typeof mock>).mockResolvedValue(
|
|
499
|
+
(git.fetchAllRemotes as ReturnType<typeof mock>).mockResolvedValue(
|
|
500
|
+
undefined,
|
|
501
|
+
);
|
|
486
502
|
(git.getRepositoryRoot as ReturnType<typeof mock>).mockResolvedValue(
|
|
487
503
|
"/repo",
|
|
488
504
|
);
|
|
@@ -492,8 +508,12 @@ describe("BatchMergeService", () => {
|
|
|
492
508
|
(worktree.generateWorktreePath as ReturnType<typeof mock>)
|
|
493
509
|
.mockResolvedValueOnce("/repo/.worktrees/feature-a")
|
|
494
510
|
.mockResolvedValueOnce("/repo/.worktrees/feature-b");
|
|
495
|
-
(worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue(
|
|
496
|
-
|
|
511
|
+
(worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue(
|
|
512
|
+
undefined,
|
|
513
|
+
);
|
|
514
|
+
(git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
|
|
515
|
+
undefined,
|
|
516
|
+
);
|
|
497
517
|
(git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
|
|
498
518
|
false,
|
|
499
519
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
3
|
import { execa } from "execa";
|
|
4
4
|
|
|
5
5
|
export type PackageManager = "bun" | "pnpm" | "npm";
|
|
@@ -72,7 +72,7 @@ const INSTALL_CANDIDATES: PackageManagerCandidate[] = [
|
|
|
72
72
|
|
|
73
73
|
async function fileExists(targetPath: string): Promise<boolean> {
|
|
74
74
|
try {
|
|
75
|
-
await
|
|
75
|
+
await access(targetPath);
|
|
76
76
|
return true;
|
|
77
77
|
} catch (error) {
|
|
78
78
|
const code = (error as NodeJS.ErrnoException)?.code;
|
|
@@ -444,3 +444,31 @@ export function matchesCwd(
|
|
|
444
444
|
isPathPrefix(normalizedSession, normalizedTarget)
|
|
445
445
|
);
|
|
446
446
|
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Resolves a branch name for a session cwd using worktree paths.
|
|
450
|
+
* Picks the worktree with the longest matching path prefix.
|
|
451
|
+
*/
|
|
452
|
+
export function resolveBranchFromCwd(
|
|
453
|
+
sessionCwd: string | null,
|
|
454
|
+
worktrees: { path: string; branch: string }[],
|
|
455
|
+
): string | null {
|
|
456
|
+
if (!sessionCwd) return null;
|
|
457
|
+
const normalizedSession = normalizePath(sessionCwd);
|
|
458
|
+
let bestMatch: { branch: string; path: string } | null = null;
|
|
459
|
+
|
|
460
|
+
for (const worktree of worktrees) {
|
|
461
|
+
if (!worktree?.path || !worktree.branch) continue;
|
|
462
|
+
const normalizedPath = normalizePath(worktree.path);
|
|
463
|
+
if (
|
|
464
|
+
normalizedSession === normalizedPath ||
|
|
465
|
+
isPathPrefix(normalizedPath, normalizedSession)
|
|
466
|
+
) {
|
|
467
|
+
if (!bestMatch || normalizedPath.length > bestMatch.path.length) {
|
|
468
|
+
bestMatch = { branch: worktree.branch, path: normalizedPath };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return bestMatch?.branch ?? null;
|
|
474
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
readFileContent,
|
|
17
17
|
checkFileStat,
|
|
18
18
|
} from "../common.js";
|
|
19
|
+
import { listAllWorktrees } from "../../../worktree.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Encodes a project path for Claude's directory structure.
|
|
@@ -75,36 +76,72 @@ function getClaudeRootCandidates(): string[] {
|
|
|
75
76
|
* 3. ~/.claude/history.jsonl (global history fallback)
|
|
76
77
|
*
|
|
77
78
|
* @param cwd - The working directory to find sessions for
|
|
78
|
-
* @param options - Search options (since, until, preferClosestTo, windowMs)
|
|
79
|
+
* @param options - Search options (since, until, preferClosestTo, windowMs, branch/worktrees)
|
|
79
80
|
* @returns Session info with ID and modification time, or null if not found
|
|
80
81
|
*/
|
|
81
82
|
export async function findLatestClaudeSession(
|
|
82
83
|
cwd: string,
|
|
83
|
-
options:
|
|
84
|
+
options: SessionSearchOptions = {},
|
|
84
85
|
): Promise<ClaudeSessionInfo | null> {
|
|
85
86
|
const rootCandidates = getClaudeRootCandidates();
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const projectDir = path.join(claudeRoot, "projects", encoded);
|
|
91
|
-
const sessionsDir = path.join(projectDir, "sessions");
|
|
92
|
-
|
|
93
|
-
// 1) Look under sessions/ (official location)
|
|
94
|
-
const session = await findNewestSessionIdFromDir(
|
|
95
|
-
sessionsDir,
|
|
96
|
-
false,
|
|
97
|
-
options,
|
|
98
|
-
);
|
|
99
|
-
if (session) return session;
|
|
87
|
+
const branchFilter =
|
|
88
|
+
typeof options.branch === "string" && options.branch.trim().length > 0
|
|
89
|
+
? options.branch.trim()
|
|
90
|
+
: null;
|
|
100
91
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
92
|
+
let cwdCandidates: string[] = [];
|
|
93
|
+
if (branchFilter) {
|
|
94
|
+
let worktrees: { path: string; branch: string }[] = [];
|
|
95
|
+
if (Array.isArray(options.worktrees) && options.worktrees.length > 0) {
|
|
96
|
+
worktrees = options.worktrees
|
|
97
|
+
.filter((entry) => entry?.path && entry?.branch)
|
|
98
|
+
.map((entry) => ({ path: entry.path, branch: entry.branch }));
|
|
99
|
+
} else {
|
|
100
|
+
try {
|
|
101
|
+
const allWorktrees = await listAllWorktrees();
|
|
102
|
+
worktrees = allWorktrees
|
|
103
|
+
.filter((entry) => entry?.path && entry?.branch)
|
|
104
|
+
.map((entry) => ({ path: entry.path, branch: entry.branch }));
|
|
105
|
+
} catch {
|
|
106
|
+
worktrees = [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const matches = worktrees
|
|
110
|
+
.filter((entry) => entry.branch === branchFilter)
|
|
111
|
+
.map((entry) => entry.path);
|
|
112
|
+
if (!matches.length) return null;
|
|
113
|
+
cwdCandidates = matches;
|
|
114
|
+
} else {
|
|
115
|
+
const baseCwd = options.cwd ?? cwd;
|
|
116
|
+
if (!baseCwd) return null;
|
|
117
|
+
cwdCandidates = [baseCwd];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const uniqueCwds = Array.from(new Set(cwdCandidates));
|
|
121
|
+
|
|
122
|
+
for (const candidateCwd of uniqueCwds) {
|
|
123
|
+
const encodedPaths = generateClaudeProjectPathCandidates(candidateCwd);
|
|
124
|
+
for (const claudeRoot of rootCandidates) {
|
|
125
|
+
for (const encoded of encodedPaths) {
|
|
126
|
+
const projectDir = path.join(claudeRoot, "projects", encoded);
|
|
127
|
+
const sessionsDir = path.join(projectDir, "sessions");
|
|
128
|
+
|
|
129
|
+
// 1) Look under sessions/ (official location)
|
|
130
|
+
const session = await findNewestSessionIdFromDir(
|
|
131
|
+
sessionsDir,
|
|
132
|
+
false,
|
|
133
|
+
options,
|
|
134
|
+
);
|
|
135
|
+
if (session) return session;
|
|
136
|
+
|
|
137
|
+
// 2) Look directly under project dir and subdirs
|
|
138
|
+
const rootSession = await findNewestSessionIdFromDir(
|
|
139
|
+
projectDir,
|
|
140
|
+
true,
|
|
141
|
+
options,
|
|
142
|
+
);
|
|
143
|
+
if (rootSession) return rootSession;
|
|
144
|
+
}
|
|
108
145
|
}
|
|
109
146
|
}
|
|
110
147
|
|
|
@@ -124,7 +161,10 @@ export async function findLatestClaudeSession(
|
|
|
124
161
|
typeof parsed.project === "string" ? parsed.project : null;
|
|
125
162
|
const sessionId =
|
|
126
163
|
typeof parsed.sessionId === "string" ? parsed.sessionId : null;
|
|
127
|
-
|
|
164
|
+
const matchesProject = uniqueCwds.some((candidate) =>
|
|
165
|
+
matchesCwd(project, candidate),
|
|
166
|
+
);
|
|
167
|
+
if (project && sessionId && matchesProject) {
|
|
128
168
|
return { id: sessionId, mtime: historyStat.mtimeMs };
|
|
129
169
|
}
|
|
130
170
|
} catch {
|
|
@@ -141,14 +181,14 @@ export async function findLatestClaudeSession(
|
|
|
141
181
|
/**
|
|
142
182
|
* Finds the latest Claude session ID for a given working directory.
|
|
143
183
|
* @param cwd - The working directory to find sessions for
|
|
144
|
-
* @param options - Search options (since, until, preferClosestTo, windowMs)
|
|
184
|
+
* @param options - Search options (since, until, preferClosestTo, windowMs, branch/worktrees)
|
|
145
185
|
* @returns Session ID string or null if not found
|
|
146
186
|
*/
|
|
147
187
|
export async function findLatestClaudeSessionId(
|
|
148
188
|
cwd: string,
|
|
149
|
-
options:
|
|
189
|
+
options: SessionSearchOptions = {},
|
|
150
190
|
): Promise<string | null> {
|
|
151
|
-
const found = await findLatestClaudeSession(cwd, options);
|
|
191
|
+
const found = await findLatestClaudeSession(options.cwd ?? cwd, options);
|
|
152
192
|
return found?.id ?? null;
|
|
153
193
|
}
|
|
154
194
|
|
|
@@ -167,6 +207,9 @@ export async function waitForClaudeSessionId(
|
|
|
167
207
|
until?: number;
|
|
168
208
|
preferClosestTo?: number;
|
|
169
209
|
windowMs?: number;
|
|
210
|
+
branch?: string | null;
|
|
211
|
+
worktrees?: { path: string; branch: string }[] | null;
|
|
212
|
+
cwd?: string | null;
|
|
170
213
|
} = {},
|
|
171
214
|
): Promise<string | null> {
|
|
172
215
|
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
@@ -174,15 +217,22 @@ export async function waitForClaudeSessionId(
|
|
|
174
217
|
const deadline = Date.now() + timeoutMs;
|
|
175
218
|
|
|
176
219
|
// Build search options once outside the loop
|
|
177
|
-
const searchOptions:
|
|
220
|
+
const searchOptions: SessionSearchOptions = {};
|
|
178
221
|
if (options.since !== undefined) searchOptions.since = options.since;
|
|
179
222
|
if (options.until !== undefined) searchOptions.until = options.until;
|
|
180
223
|
if (options.preferClosestTo !== undefined)
|
|
181
224
|
searchOptions.preferClosestTo = options.preferClosestTo;
|
|
182
225
|
if (options.windowMs !== undefined) searchOptions.windowMs = options.windowMs;
|
|
226
|
+
if (options.branch !== undefined) searchOptions.branch = options.branch;
|
|
227
|
+
if (options.worktrees !== undefined)
|
|
228
|
+
searchOptions.worktrees = options.worktrees;
|
|
229
|
+
if (options.cwd !== undefined) searchOptions.cwd = options.cwd;
|
|
183
230
|
|
|
184
231
|
while (Date.now() < deadline) {
|
|
185
|
-
const found = await findLatestClaudeSession(
|
|
232
|
+
const found = await findLatestClaudeSession(
|
|
233
|
+
options.cwd ?? cwd,
|
|
234
|
+
searchOptions,
|
|
235
|
+
);
|
|
186
236
|
if (found?.id) return found.id;
|
|
187
237
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
188
238
|
}
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
UUID_REGEX,
|
|
15
15
|
collectFilesIterative,
|
|
16
16
|
matchesCwd,
|
|
17
|
+
resolveBranchFromCwd,
|
|
17
18
|
readSessionInfoFromFile,
|
|
18
19
|
} from "../common.js";
|
|
20
|
+
import { listAllWorktrees } from "../../../worktree.js";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Finds the latest Codex session with optional time filtering and cwd matching.
|
|
@@ -63,43 +65,65 @@ export async function findLatestCodexSession(
|
|
|
63
65
|
return b.mtime - a.mtime;
|
|
64
66
|
});
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
const branchFilter =
|
|
69
|
+
typeof options.branch === "string" && options.branch.trim().length > 0
|
|
70
|
+
? options.branch.trim()
|
|
71
|
+
: null;
|
|
72
|
+
const shouldCheckBranch = Boolean(branchFilter);
|
|
73
|
+
const shouldCheckCwd = Boolean(options.cwd) && !shouldCheckBranch;
|
|
74
|
+
|
|
75
|
+
let worktrees: { path: string; branch: string }[] = [];
|
|
76
|
+
if (shouldCheckBranch) {
|
|
77
|
+
if (Array.isArray(options.worktrees) && options.worktrees.length > 0) {
|
|
78
|
+
worktrees = options.worktrees
|
|
79
|
+
.filter((entry) => entry?.path && entry?.branch)
|
|
80
|
+
.map((entry) => ({ path: entry.path, branch: entry.branch }));
|
|
81
|
+
} else {
|
|
82
|
+
try {
|
|
83
|
+
const allWorktrees = await listAllWorktrees();
|
|
84
|
+
worktrees = allWorktrees
|
|
85
|
+
.filter((entry) => entry?.path && entry?.branch)
|
|
86
|
+
.map((entry) => ({ path: entry.path, branch: entry.branch }));
|
|
87
|
+
} catch {
|
|
88
|
+
worktrees = [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!worktrees.length) return null;
|
|
92
|
+
}
|
|
67
93
|
|
|
68
94
|
for (const file of ordered) {
|
|
69
95
|
// Priority 1: Extract session ID from filename (most reliable for Codex)
|
|
70
96
|
const filenameMatch = path.basename(file.fullPath).match(UUID_REGEX);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
continue; // cwd doesn't match, try next file
|
|
97
|
+
const idFromName = filenameMatch?.[0] ?? null;
|
|
98
|
+
const needsInfo = shouldCheckBranch || shouldCheckCwd || !idFromName;
|
|
99
|
+
const info = needsInfo
|
|
100
|
+
? await readSessionInfoFromFile(file.fullPath)
|
|
101
|
+
: null;
|
|
102
|
+
const sessionCwd = info?.cwd ?? null;
|
|
103
|
+
|
|
104
|
+
if (shouldCheckBranch) {
|
|
105
|
+
const resolvedBranch = resolveBranchFromCwd(sessionCwd, worktrees);
|
|
106
|
+
if (resolvedBranch !== branchFilter) {
|
|
107
|
+
continue;
|
|
83
108
|
}
|
|
84
|
-
return { id: sessionId, mtime: file.mtime };
|
|
85
109
|
}
|
|
86
110
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (options.cwd) {
|
|
91
|
-
if (matchesCwd(info.cwd, options.cwd)) {
|
|
92
|
-
return { id: info.id, mtime: file.mtime };
|
|
111
|
+
if (shouldCheckCwd && options.cwd) {
|
|
112
|
+
if (!matchesCwd(sessionCwd, options.cwd)) {
|
|
113
|
+
continue;
|
|
93
114
|
}
|
|
94
|
-
if (!info.cwd && !fallbackMissingCwd) {
|
|
95
|
-
fallbackMissingCwd = { id: info.id, mtime: file.mtime };
|
|
96
|
-
}
|
|
97
|
-
continue;
|
|
98
115
|
}
|
|
99
|
-
|
|
116
|
+
|
|
117
|
+
const sessionId = idFromName ?? info?.id ?? null;
|
|
118
|
+
if (sessionId) {
|
|
119
|
+
return { id: sessionId, mtime: file.mtime };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Priority 2: Fallback to reading file content if filename lacks UUID
|
|
123
|
+
// (already handled via info above)
|
|
100
124
|
}
|
|
101
125
|
|
|
102
|
-
return
|
|
126
|
+
return null;
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
/**
|
|
@@ -12,8 +12,10 @@ import type { GeminiSessionInfo, SessionSearchOptions } from "../types.js";
|
|
|
12
12
|
import {
|
|
13
13
|
collectFilesIterative,
|
|
14
14
|
matchesCwd,
|
|
15
|
+
resolveBranchFromCwd,
|
|
15
16
|
readSessionInfoFromFile,
|
|
16
17
|
} from "../common.js";
|
|
18
|
+
import { listAllWorktrees } from "../../../worktree.js";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Finds the latest Gemini session with optional time filtering and cwd matching.
|
|
@@ -57,15 +59,50 @@ export async function findLatestGeminiSession(
|
|
|
57
59
|
return b.mtime - a.mtime;
|
|
58
60
|
});
|
|
59
61
|
|
|
62
|
+
const branchFilter =
|
|
63
|
+
typeof options.branch === "string" && options.branch.trim().length > 0
|
|
64
|
+
? options.branch.trim()
|
|
65
|
+
: null;
|
|
66
|
+
const shouldCheckBranch = Boolean(branchFilter);
|
|
67
|
+
const shouldCheckCwd = Boolean(options.cwd) && !shouldCheckBranch;
|
|
68
|
+
|
|
69
|
+
let worktrees: { path: string; branch: string }[] = [];
|
|
70
|
+
if (shouldCheckBranch) {
|
|
71
|
+
if (Array.isArray(options.worktrees) && options.worktrees.length > 0) {
|
|
72
|
+
worktrees = options.worktrees
|
|
73
|
+
.filter((entry) => entry?.path && entry?.branch)
|
|
74
|
+
.map((entry) => ({ path: entry.path, branch: entry.branch }));
|
|
75
|
+
} else {
|
|
76
|
+
try {
|
|
77
|
+
const allWorktrees = await listAllWorktrees();
|
|
78
|
+
worktrees = allWorktrees
|
|
79
|
+
.filter((entry) => entry?.path && entry?.branch)
|
|
80
|
+
.map((entry) => ({ path: entry.path, branch: entry.branch }));
|
|
81
|
+
} catch {
|
|
82
|
+
worktrees = [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!worktrees.length) return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
for (const file of pool) {
|
|
61
89
|
const info = await readSessionInfoFromFile(file.fullPath);
|
|
62
90
|
if (!info.id) continue;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
const sessionCwd = info.cwd ?? null;
|
|
92
|
+
|
|
93
|
+
if (shouldCheckBranch) {
|
|
94
|
+
const resolvedBranch = resolveBranchFromCwd(sessionCwd, worktrees);
|
|
95
|
+
if (resolvedBranch !== branchFilter) {
|
|
96
|
+
continue;
|
|
66
97
|
}
|
|
67
|
-
continue;
|
|
68
98
|
}
|
|
99
|
+
|
|
100
|
+
if (shouldCheckCwd && options.cwd) {
|
|
101
|
+
if (!matchesCwd(sessionCwd, options.cwd)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
69
106
|
return { id: info.id, mtime: file.mtime };
|
|
70
107
|
}
|
|
71
108
|
|
|
@@ -82,7 +119,11 @@ export async function findLatestGeminiSessionId(
|
|
|
82
119
|
cwd: string,
|
|
83
120
|
options: SessionSearchOptions = {},
|
|
84
121
|
): Promise<string | null> {
|
|
85
|
-
const searchOptions: SessionSearchOptions = {
|
|
122
|
+
const searchOptions: SessionSearchOptions = {
|
|
123
|
+
cwd: options.cwd ?? cwd,
|
|
124
|
+
branch: options.branch ?? null,
|
|
125
|
+
worktrees: options.worktrees ?? null,
|
|
126
|
+
};
|
|
86
127
|
if (options.since !== undefined) searchOptions.since = options.since;
|
|
87
128
|
if (options.until !== undefined) searchOptions.until = options.until;
|
|
88
129
|
if (options.preferClosestTo !== undefined)
|