@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/gemini.ts
CHANGED
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
} from "./utils/terminal.js";
|
|
10
10
|
import { findCommand } from "./utils/command.js";
|
|
11
11
|
import { findLatestGeminiSessionId } from "./utils/session.js";
|
|
12
|
+
import {
|
|
13
|
+
runAgentWithPty,
|
|
14
|
+
shouldCaptureAgentOutput,
|
|
15
|
+
} from "./logging/agentOutput.js";
|
|
12
16
|
|
|
13
17
|
const GEMINI_CLI_PACKAGE = "@google/gemini-cli";
|
|
14
18
|
|
|
@@ -45,6 +49,7 @@ export async function launchGeminiCLI(
|
|
|
45
49
|
envOverrides?: Record<string, string>;
|
|
46
50
|
model?: string;
|
|
47
51
|
sessionId?: string | null;
|
|
52
|
+
branch?: string | null;
|
|
48
53
|
version?: string | null;
|
|
49
54
|
} = {},
|
|
50
55
|
): Promise<{ sessionId?: string | null }> {
|
|
@@ -154,8 +159,8 @@ export async function launchGeminiCLI(
|
|
|
154
159
|
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
155
160
|
),
|
|
156
161
|
);
|
|
157
|
-
|
|
158
|
-
const childStdio = createChildStdio();
|
|
162
|
+
const captureOutput = shouldCaptureAgentOutput(baseEnv);
|
|
163
|
+
const childStdio = captureOutput ? null : createChildStdio();
|
|
159
164
|
|
|
160
165
|
// Auto-detect locally installed gemini command
|
|
161
166
|
const geminiLookup = await findCommand("gemini");
|
|
@@ -166,7 +171,17 @@ export async function launchGeminiCLI(
|
|
|
166
171
|
|
|
167
172
|
// Determine execution strategy based on version selection
|
|
168
173
|
// FR-063b: "installed" option only appears when local command exists
|
|
169
|
-
const
|
|
174
|
+
const requestedVersion = options.version ?? "latest";
|
|
175
|
+
let selectedVersion = requestedVersion;
|
|
176
|
+
|
|
177
|
+
if (requestedVersion === "installed" && !geminiLookup.path) {
|
|
178
|
+
writeTerminalLine(
|
|
179
|
+
chalk.yellow(
|
|
180
|
+
" ⚠️ Installed gemini command not found. Falling back to latest.",
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
selectedVersion = "latest";
|
|
184
|
+
}
|
|
170
185
|
|
|
171
186
|
// Log version information (FR-072)
|
|
172
187
|
if (selectedVersion === "installed") {
|
|
@@ -188,8 +203,35 @@ export async function launchGeminiCLI(
|
|
|
188
203
|
throw execError;
|
|
189
204
|
}
|
|
190
205
|
};
|
|
206
|
+
// Treat SIGHUP (1), SIGINT (2), SIGTERM (15) as normal exit signals
|
|
207
|
+
// SIGHUP can occur when the PTY closes, SIGINT/SIGTERM are user interrupts
|
|
208
|
+
const isNormalExitSignal = (signal?: number | null) =>
|
|
209
|
+
signal === 1 || signal === 2 || signal === 15;
|
|
191
210
|
|
|
192
211
|
const run = async (cmd: string, args: string[]) => {
|
|
212
|
+
if (captureOutput) {
|
|
213
|
+
const result = await runAgentWithPty({
|
|
214
|
+
command: cmd,
|
|
215
|
+
args,
|
|
216
|
+
cwd: worktreePath,
|
|
217
|
+
env: baseEnv,
|
|
218
|
+
agentId: "gemini-cli",
|
|
219
|
+
});
|
|
220
|
+
if (isNormalExitSignal(result.signal)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (result.exitCode !== null && result.exitCode !== 0) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Gemini CLI exited with code ${result.exitCode ?? "unknown"}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!childStdio) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
193
235
|
const child = execa(cmd, args, {
|
|
194
236
|
cwd: worktreePath,
|
|
195
237
|
stdin: childStdio.stdin,
|
|
@@ -237,7 +279,7 @@ export async function launchGeminiCLI(
|
|
|
237
279
|
}
|
|
238
280
|
}
|
|
239
281
|
} finally {
|
|
240
|
-
childStdio
|
|
282
|
+
childStdio?.cleanup();
|
|
241
283
|
}
|
|
242
284
|
|
|
243
285
|
const explicitResumeSucceeded = usedExplicitSessionId && !fellBackToLatest;
|
|
@@ -253,6 +295,10 @@ export async function launchGeminiCLI(
|
|
|
253
295
|
capturedSessionId =
|
|
254
296
|
(await findLatestGeminiSessionId(worktreePath, {
|
|
255
297
|
cwd: worktreePath,
|
|
298
|
+
branch: options.branch ?? null,
|
|
299
|
+
worktrees: options.branch
|
|
300
|
+
? [{ path: worktreePath, branch: options.branch }]
|
|
301
|
+
: null,
|
|
256
302
|
})) ?? null;
|
|
257
303
|
} catch {
|
|
258
304
|
capturedSessionId = null;
|
package/src/index.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { getPackageVersion } from "./utils";
|
|
2
3
|
import * as utils from "./utils";
|
|
3
4
|
|
|
4
5
|
// showVersion関数のテスト(TDD Green phase)
|
|
@@ -28,7 +29,6 @@ describe("showVersion via CLI args", () => {
|
|
|
28
29
|
consoleLogSpy.mockRestore();
|
|
29
30
|
consoleErrorSpy.mockRestore();
|
|
30
31
|
processExitSpy.mockRestore();
|
|
31
|
-
|
|
32
32
|
// process.argvを復元
|
|
33
33
|
process.argv = originalArgv;
|
|
34
34
|
});
|
|
@@ -37,32 +37,34 @@ describe("showVersion via CLI args", () => {
|
|
|
37
37
|
// Arrange: CLIフラグを設定
|
|
38
38
|
process.argv = ["node", "index.js", "--version"];
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const expectedVersion = await getPackageVersion();
|
|
41
|
+
if (!expectedVersion) {
|
|
42
|
+
throw new Error("Failed to resolve package version for test.");
|
|
43
|
+
}
|
|
43
44
|
|
|
44
45
|
// Act: main()を呼び出す
|
|
45
46
|
const { main } = await import("./index");
|
|
46
47
|
await main();
|
|
47
48
|
|
|
48
49
|
// Assert: 標準出力にバージョンが表示されることを期待
|
|
49
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
50
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedVersion);
|
|
50
51
|
}, 30000);
|
|
51
52
|
|
|
52
53
|
it("正常系: -vフラグでバージョンを表示する", async () => {
|
|
53
54
|
// Arrange: CLIフラグを設定
|
|
54
55
|
process.argv = ["node", "index.js", "-v"];
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const expectedVersion = await getPackageVersion();
|
|
58
|
+
if (!expectedVersion) {
|
|
59
|
+
throw new Error("Failed to resolve package version for test.");
|
|
60
|
+
}
|
|
59
61
|
|
|
60
62
|
// Act: main()を呼び出す
|
|
61
63
|
const { main } = await import("./index");
|
|
62
64
|
await main();
|
|
63
65
|
|
|
64
66
|
// Assert: 標準出力にバージョンが表示されることを期待
|
|
65
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
67
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedVersion);
|
|
66
68
|
}, 30000);
|
|
67
69
|
|
|
68
70
|
// Note: This test is skipped due to module caching issues in CI environment
|
|
@@ -72,7 +74,10 @@ describe("showVersion via CLI args", () => {
|
|
|
72
74
|
process.argv = ["node", "index.js", "--version"];
|
|
73
75
|
|
|
74
76
|
// getPackageVersion()をモックしてnullを返す
|
|
75
|
-
spyOn(
|
|
77
|
+
const getPackageVersionSpy = spyOn(
|
|
78
|
+
utils,
|
|
79
|
+
"getPackageVersion",
|
|
80
|
+
).mockResolvedValue(null);
|
|
76
81
|
|
|
77
82
|
// Act: main()を呼び出す
|
|
78
83
|
const { main } = await import("./index");
|
|
@@ -83,5 +88,6 @@ describe("showVersion via CLI args", () => {
|
|
|
83
88
|
expect.stringContaining("Error"),
|
|
84
89
|
);
|
|
85
90
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
91
|
+
getPackageVersionSpy.mockRestore();
|
|
86
92
|
});
|
|
87
93
|
});
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
isProtectedBranchName,
|
|
28
28
|
switchToProtectedBranch,
|
|
29
29
|
resolveWorktreePathForBranch,
|
|
30
|
+
listAllWorktrees,
|
|
30
31
|
repairWorktreePath,
|
|
31
32
|
} from "./worktree.js";
|
|
32
33
|
import {
|
|
@@ -192,6 +193,11 @@ function performTerminalCleanup(): void {
|
|
|
192
193
|
* Returns SelectionResult if user made selections, undefined if user quit
|
|
193
194
|
*/
|
|
194
195
|
async function mainSolidUI(): Promise<SelectionResult | undefined> {
|
|
196
|
+
if (!("Bun" in globalThis)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"OpenTUI requires the Bun runtime. Run with Bun (e.g. bunx @akiojin/gwt@latest).",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
195
201
|
const { renderSolidApp } = await import("./opentui/index.solid.js");
|
|
196
202
|
const terminal = getTerminalStreams();
|
|
197
203
|
|
|
@@ -629,6 +635,7 @@ export async function handleAIToolWorkflow(
|
|
|
629
635
|
model?: string;
|
|
630
636
|
sessionId?: string | null;
|
|
631
637
|
chrome?: boolean;
|
|
638
|
+
branch?: string | null;
|
|
632
639
|
version?: string | null;
|
|
633
640
|
} = {
|
|
634
641
|
mode:
|
|
@@ -641,6 +648,7 @@ export async function handleAIToolWorkflow(
|
|
|
641
648
|
envOverrides: sharedEnv,
|
|
642
649
|
sessionId: resumeSessionId,
|
|
643
650
|
chrome: true,
|
|
651
|
+
branch,
|
|
644
652
|
version: toolVersion ?? null,
|
|
645
653
|
};
|
|
646
654
|
if (normalizedModel) {
|
|
@@ -655,6 +663,7 @@ export async function handleAIToolWorkflow(
|
|
|
655
663
|
model?: string;
|
|
656
664
|
reasoningEffort?: CodexReasoningEffort;
|
|
657
665
|
sessionId?: string | null;
|
|
666
|
+
branch?: string | null;
|
|
658
667
|
version?: string | null;
|
|
659
668
|
} = {
|
|
660
669
|
mode:
|
|
@@ -666,6 +675,7 @@ export async function handleAIToolWorkflow(
|
|
|
666
675
|
bypassApprovals: skipPermissions,
|
|
667
676
|
envOverrides: sharedEnv,
|
|
668
677
|
sessionId: resumeSessionId,
|
|
678
|
+
branch,
|
|
669
679
|
version: toolVersion ?? null,
|
|
670
680
|
};
|
|
671
681
|
if (normalizedModel) {
|
|
@@ -683,6 +693,7 @@ export async function handleAIToolWorkflow(
|
|
|
683
693
|
envOverrides?: Record<string, string>;
|
|
684
694
|
model?: string;
|
|
685
695
|
sessionId?: string | null;
|
|
696
|
+
branch?: string | null;
|
|
686
697
|
version?: string | null;
|
|
687
698
|
} = {
|
|
688
699
|
mode:
|
|
@@ -694,6 +705,7 @@ export async function handleAIToolWorkflow(
|
|
|
694
705
|
skipPermissions,
|
|
695
706
|
envOverrides: sharedEnv,
|
|
696
707
|
sessionId: resumeSessionId,
|
|
708
|
+
branch,
|
|
697
709
|
version: toolVersion ?? null,
|
|
698
710
|
};
|
|
699
711
|
if (normalizedModel) {
|
|
@@ -734,10 +746,29 @@ export async function handleAIToolWorkflow(
|
|
|
734
746
|
resumeSessionId ??
|
|
735
747
|
null;
|
|
736
748
|
|
|
749
|
+
let resolvedWorktrees: { path: string; branch: string }[] | null = null;
|
|
750
|
+
if (branch) {
|
|
751
|
+
try {
|
|
752
|
+
const allWorktrees = await listAllWorktrees();
|
|
753
|
+
resolvedWorktrees = allWorktrees
|
|
754
|
+
.filter((entry) => entry?.path && entry?.branch)
|
|
755
|
+
.map((entry) => ({ path: entry.path, branch: entry.branch }));
|
|
756
|
+
} catch {
|
|
757
|
+
resolvedWorktrees = null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const worktreeLookupOptions =
|
|
761
|
+
resolvedWorktrees && resolvedWorktrees.length > 0
|
|
762
|
+
? { worktrees: resolvedWorktrees }
|
|
763
|
+
: {};
|
|
764
|
+
|
|
737
765
|
if (!finalSessionId && tool === "claude-code") {
|
|
738
766
|
try {
|
|
739
767
|
finalSessionId =
|
|
740
|
-
(await findLatestClaudeSessionId(worktreePath
|
|
768
|
+
(await findLatestClaudeSessionId(worktreePath, {
|
|
769
|
+
branch,
|
|
770
|
+
...worktreeLookupOptions,
|
|
771
|
+
})) ?? null;
|
|
741
772
|
} catch {
|
|
742
773
|
finalSessionId = null;
|
|
743
774
|
}
|
|
@@ -752,6 +783,8 @@ export async function handleAIToolWorkflow(
|
|
|
752
783
|
preferClosestTo: finishedAt,
|
|
753
784
|
windowMs: 60 * 60 * 1000,
|
|
754
785
|
cwd: worktreePath,
|
|
786
|
+
branch,
|
|
787
|
+
...worktreeLookupOptions,
|
|
755
788
|
});
|
|
756
789
|
if (latest) {
|
|
757
790
|
finalSessionId = latest.id;
|
|
@@ -766,6 +799,8 @@ export async function handleAIToolWorkflow(
|
|
|
766
799
|
until: finishedAt + 60_000,
|
|
767
800
|
preferClosestTo: finishedAt,
|
|
768
801
|
windowMs: 60 * 60 * 1000,
|
|
802
|
+
branch,
|
|
803
|
+
...worktreeLookupOptions,
|
|
769
804
|
});
|
|
770
805
|
if (latestClaude) {
|
|
771
806
|
finalSessionId = latestClaude.id;
|
|
@@ -781,6 +816,8 @@ export async function handleAIToolWorkflow(
|
|
|
781
816
|
preferClosestTo: finishedAt,
|
|
782
817
|
windowMs: 60 * 60 * 1000,
|
|
783
818
|
cwd: worktreePath,
|
|
819
|
+
branch,
|
|
820
|
+
...worktreeLookupOptions,
|
|
784
821
|
});
|
|
785
822
|
if (latestGemini) {
|
|
786
823
|
finalSessionId = latestGemini.id;
|
package/src/launcher.ts
CHANGED
|
@@ -9,7 +9,14 @@ import { execa } from "execa";
|
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import type { CodingAgent, CodingAgentLaunchOptions } from "./types/tools.js";
|
|
11
11
|
import { createLogger } from "./logging/logger.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
runAgentWithPty,
|
|
14
|
+
shouldCaptureAgentOutput,
|
|
15
|
+
} from "./logging/agentOutput.js";
|
|
16
|
+
import {
|
|
17
|
+
parsePackageCommand,
|
|
18
|
+
resolveVersionSuffix,
|
|
19
|
+
} from "./utils/npmRegistry.js";
|
|
13
20
|
import { writeTerminalLine } from "./utils/terminal.js";
|
|
14
21
|
|
|
15
22
|
const logger = createLogger({ category: "launcher" });
|
|
@@ -125,14 +132,32 @@ export async function launchCodingAgent(
|
|
|
125
132
|
...(options.sharedEnv ?? {}),
|
|
126
133
|
...(agent.env ?? {}),
|
|
127
134
|
};
|
|
135
|
+
const workingDir = options.cwd ?? process.cwd();
|
|
136
|
+
const captureOutput = shouldCaptureAgentOutput(env);
|
|
128
137
|
|
|
129
138
|
// execa共通オプション(cwdがundefinedの場合は含めない)
|
|
130
139
|
const execaOptions = {
|
|
131
140
|
stdio: "inherit" as const,
|
|
132
|
-
...(
|
|
141
|
+
...(workingDir ? { cwd: workingDir } : {}),
|
|
133
142
|
env,
|
|
134
143
|
};
|
|
135
144
|
|
|
145
|
+
const runWithCapture = async (command: string, commandArgs: string[]) => {
|
|
146
|
+
const result = await runAgentWithPty({
|
|
147
|
+
command,
|
|
148
|
+
args: commandArgs,
|
|
149
|
+
cwd: workingDir,
|
|
150
|
+
env,
|
|
151
|
+
agentId: agent.id,
|
|
152
|
+
});
|
|
153
|
+
if (result.signal !== null && result.signal !== undefined) {
|
|
154
|
+
throw new Error(`Coding agent terminated by signal ${result.signal}`);
|
|
155
|
+
}
|
|
156
|
+
if (result.exitCode !== null && result.exitCode !== 0) {
|
|
157
|
+
throw new Error(`Coding agent exited with code ${result.exitCode}`);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
136
161
|
logger.info(
|
|
137
162
|
{
|
|
138
163
|
agentId: agent.id,
|
|
@@ -146,7 +171,11 @@ export async function launchCodingAgent(
|
|
|
146
171
|
switch (agent.type) {
|
|
147
172
|
case "path": {
|
|
148
173
|
// 絶対パスで直接実行
|
|
149
|
-
|
|
174
|
+
if (captureOutput) {
|
|
175
|
+
await runWithCapture(agent.command, args);
|
|
176
|
+
} else {
|
|
177
|
+
await execa(agent.command, args, execaOptions);
|
|
178
|
+
}
|
|
150
179
|
logger.info({ agentId: agent.id }, "Coding agent completed (path)");
|
|
151
180
|
break;
|
|
152
181
|
}
|
|
@@ -154,9 +183,12 @@ export async function launchCodingAgent(
|
|
|
154
183
|
case "bunx": {
|
|
155
184
|
// bunx経由でパッケージ実行
|
|
156
185
|
// バージョン指定がある場合はパッケージ名に付与
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
186
|
+
const { packageName, version: embeddedVersion } = parsePackageCommand(
|
|
187
|
+
agent.command,
|
|
188
|
+
);
|
|
189
|
+
const selectedVersion = options.version ?? embeddedVersion ?? "latest";
|
|
190
|
+
const versionSuffix = resolveVersionSuffix(selectedVersion);
|
|
191
|
+
const packageWithVersion = `${packageName}${versionSuffix}`;
|
|
160
192
|
|
|
161
193
|
// FR-072: Log version information
|
|
162
194
|
if (selectedVersion === "installed") {
|
|
@@ -167,7 +199,12 @@ export async function launchCodingAgent(
|
|
|
167
199
|
writeTerminalLine(chalk.cyan(` 🔄 Using bunx ${packageWithVersion}`));
|
|
168
200
|
|
|
169
201
|
// bunx [package@version] [args...]
|
|
170
|
-
await
|
|
202
|
+
const bunxCommand = captureOutput ? await resolveCommand("bunx") : "bunx";
|
|
203
|
+
if (captureOutput) {
|
|
204
|
+
await runWithCapture(bunxCommand, [packageWithVersion, ...args]);
|
|
205
|
+
} else {
|
|
206
|
+
await execa("bunx", [packageWithVersion, ...args], execaOptions);
|
|
207
|
+
}
|
|
171
208
|
logger.info(
|
|
172
209
|
{ agentId: agent.id, version: selectedVersion },
|
|
173
210
|
"Coding agent completed (bunx)",
|
|
@@ -178,7 +215,11 @@ export async function launchCodingAgent(
|
|
|
178
215
|
case "command": {
|
|
179
216
|
// PATH解決 → 実行
|
|
180
217
|
const resolvedPath = await resolveCommand(agent.command);
|
|
181
|
-
|
|
218
|
+
if (captureOutput) {
|
|
219
|
+
await runWithCapture(resolvedPath, args);
|
|
220
|
+
} else {
|
|
221
|
+
await execa(resolvedPath, args, execaOptions);
|
|
222
|
+
}
|
|
182
223
|
logger.info({ agentId: agent.id }, "Coding agent completed (command)");
|
|
183
224
|
break;
|
|
184
225
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as pty from "node-pty";
|
|
2
|
+
import type { IPty } from "node-pty";
|
|
3
|
+
import { createLogger } from "./logger.js";
|
|
4
|
+
import { resolveLogDir } from "./reader.js";
|
|
5
|
+
import { getTerminalStreams } from "../utils/terminal.js";
|
|
6
|
+
|
|
7
|
+
export const CAPTURE_AGENT_OUTPUT_ENV = "GWT_CAPTURE_AGENT_OUTPUT";
|
|
8
|
+
|
|
9
|
+
export function shouldCaptureAgentOutput(
|
|
10
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
11
|
+
): boolean {
|
|
12
|
+
const raw = env[CAPTURE_AGENT_OUTPUT_ENV];
|
|
13
|
+
if (raw === undefined) {
|
|
14
|
+
// Default to false to avoid PTY stdin/stdout conflicts with OpenTUI.
|
|
15
|
+
// Set GWT_CAPTURE_AGENT_OUTPUT=true to enable agent output logging.
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
19
|
+
if (!normalized) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return normalized === "true" || normalized === "1";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line no-control-regex
|
|
26
|
+
const ANSI_PATTERN = new RegExp("\\u001b\\[[0-9;]*[A-Za-z]", "g");
|
|
27
|
+
|
|
28
|
+
export function stripAnsi(value: string): string {
|
|
29
|
+
return value.replace(ANSI_PATTERN, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AgentOutputLineBuffer {
|
|
33
|
+
push: (chunk: string) => void;
|
|
34
|
+
flush: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createAgentOutputLineBuffer(
|
|
38
|
+
onLine: (line: string) => void,
|
|
39
|
+
): AgentOutputLineBuffer {
|
|
40
|
+
let buffer = "";
|
|
41
|
+
|
|
42
|
+
const push = (chunk: string) => {
|
|
43
|
+
const normalized = chunk.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
44
|
+
buffer += normalized;
|
|
45
|
+
while (true) {
|
|
46
|
+
const index = buffer.indexOf("\n");
|
|
47
|
+
if (index === -1) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const line = buffer.slice(0, index);
|
|
51
|
+
buffer = buffer.slice(index + 1);
|
|
52
|
+
onLine(line);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const flush = () => {
|
|
57
|
+
if (!buffer) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const line = buffer;
|
|
61
|
+
buffer = "";
|
|
62
|
+
onLine(line);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return { push, flush };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AgentOutputCaptureOptions {
|
|
69
|
+
command: string;
|
|
70
|
+
args: string[];
|
|
71
|
+
cwd: string;
|
|
72
|
+
env: NodeJS.ProcessEnv;
|
|
73
|
+
agentId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface AgentOutputCaptureResult {
|
|
77
|
+
exitCode: number | null;
|
|
78
|
+
signal?: number | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const normalizeEnv = (env: NodeJS.ProcessEnv): Record<string, string> =>
|
|
82
|
+
Object.fromEntries(
|
|
83
|
+
Object.entries(env).filter(
|
|
84
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const getTerminalSize = (terminal: ReturnType<typeof getTerminalStreams>) => {
|
|
89
|
+
const stdout = terminal.stdout as
|
|
90
|
+
| (NodeJS.WriteStream & { columns?: number; rows?: number })
|
|
91
|
+
| undefined;
|
|
92
|
+
const cols = stdout?.columns ?? process.stdout.columns ?? 80;
|
|
93
|
+
const rows = stdout?.rows ?? process.stdout.rows ?? 24;
|
|
94
|
+
return { cols, rows };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export async function runAgentWithPty(
|
|
98
|
+
options: AgentOutputCaptureOptions,
|
|
99
|
+
): Promise<AgentOutputCaptureResult> {
|
|
100
|
+
const terminal = getTerminalStreams();
|
|
101
|
+
const { cols, rows } = getTerminalSize(terminal);
|
|
102
|
+
const logDir = resolveLogDir(options.cwd);
|
|
103
|
+
const stdoutLogger = createLogger({
|
|
104
|
+
category: "agent.stdout",
|
|
105
|
+
logDir,
|
|
106
|
+
base: { agentId: options.agentId },
|
|
107
|
+
});
|
|
108
|
+
const stderrLogger = createLogger({
|
|
109
|
+
category: "agent.stderr",
|
|
110
|
+
logDir,
|
|
111
|
+
base: { agentId: options.agentId },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const normalizedEnv = normalizeEnv(options.env);
|
|
115
|
+
const ptyProcess: IPty = pty.spawn(options.command, options.args, {
|
|
116
|
+
name: process.env.TERM ?? "xterm-256color",
|
|
117
|
+
cols,
|
|
118
|
+
rows,
|
|
119
|
+
cwd: options.cwd,
|
|
120
|
+
env: normalizedEnv,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const lineBuffer = createAgentOutputLineBuffer((line) => {
|
|
124
|
+
const cleaned = stripAnsi(line).trimEnd();
|
|
125
|
+
if (!cleaned) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
stdoutLogger.info(cleaned);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const stdout = terminal.stdout;
|
|
132
|
+
const writeToTerminal =
|
|
133
|
+
stdout && typeof stdout.write === "function"
|
|
134
|
+
? stdout.write.bind(stdout)
|
|
135
|
+
: null;
|
|
136
|
+
|
|
137
|
+
ptyProcess.onData((data) => {
|
|
138
|
+
if (writeToTerminal) {
|
|
139
|
+
try {
|
|
140
|
+
writeToTerminal(data);
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore terminal write errors.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lineBuffer.push(data);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const stdin = terminal.stdin;
|
|
149
|
+
const handleInput = (chunk: Buffer | string) => {
|
|
150
|
+
const data = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
151
|
+
ptyProcess.write(data);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const stdinWasRaw =
|
|
155
|
+
stdin &&
|
|
156
|
+
typeof (stdin as NodeJS.ReadStream & { isRaw?: boolean }).isRaw ===
|
|
157
|
+
"boolean"
|
|
158
|
+
? (stdin as NodeJS.ReadStream & { isRaw?: boolean }).isRaw
|
|
159
|
+
: undefined;
|
|
160
|
+
|
|
161
|
+
if (stdin && typeof stdin.on === "function") {
|
|
162
|
+
if (stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
163
|
+
try {
|
|
164
|
+
stdin.setRawMode(true);
|
|
165
|
+
} catch {
|
|
166
|
+
// Ignore raw mode errors.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (typeof stdin.resume === "function") {
|
|
170
|
+
stdin.resume();
|
|
171
|
+
}
|
|
172
|
+
stdin.on("data", handleInput);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleResize = () => {
|
|
176
|
+
const next = getTerminalSize(terminal);
|
|
177
|
+
try {
|
|
178
|
+
ptyProcess.resize(next.cols, next.rows);
|
|
179
|
+
} catch {
|
|
180
|
+
// Ignore resize errors.
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
if (process.stdout && typeof process.stdout.on === "function") {
|
|
184
|
+
process.stdout.on("resize", handleResize);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return await new Promise<AgentOutputCaptureResult>((resolve) => {
|
|
188
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
189
|
+
lineBuffer.flush();
|
|
190
|
+
if (stdin && typeof stdin.off === "function") {
|
|
191
|
+
stdin.off("data", handleInput);
|
|
192
|
+
}
|
|
193
|
+
if (stdin && typeof stdin.pause === "function") {
|
|
194
|
+
stdin.pause();
|
|
195
|
+
}
|
|
196
|
+
if (stdin && stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
197
|
+
try {
|
|
198
|
+
stdin.setRawMode(Boolean(stdinWasRaw));
|
|
199
|
+
} catch {
|
|
200
|
+
// Ignore raw mode restore errors.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (process.stdout && typeof process.stdout.off === "function") {
|
|
204
|
+
process.stdout.off("resize", handleResize);
|
|
205
|
+
}
|
|
206
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
207
|
+
stderrLogger.error(
|
|
208
|
+
{ exitCode, signal },
|
|
209
|
+
"Agent exited with non-zero code",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const normalizedSignal = signal ?? null;
|
|
213
|
+
resolve({ exitCode, signal: normalizedSignal });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
package/src/logging/formatter.ts
CHANGED
|
@@ -19,17 +19,36 @@ const LEVEL_LABELS: Record<number, string> = {
|
|
|
19
19
|
60: "FATAL",
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
const LOCAL_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, {
|
|
23
|
+
hour: "2-digit",
|
|
24
|
+
minute: "2-digit",
|
|
25
|
+
second: "2-digit",
|
|
26
|
+
hour12: false,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const formatLocalTimeParts = (date: Date): string => {
|
|
30
|
+
const parts = LOCAL_TIME_FORMATTER.formatToParts(date);
|
|
31
|
+
const get = (type: Intl.DateTimeFormatPartTypes) =>
|
|
32
|
+
parts.find((part) => part.type === type)?.value;
|
|
33
|
+
const hour = get("hour");
|
|
34
|
+
const minute = get("minute");
|
|
35
|
+
const second = get("second");
|
|
36
|
+
|
|
37
|
+
if (!hour || !minute || !second) {
|
|
38
|
+
return LOCAL_TIME_FORMATTER.format(date);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return `${hour}:${minute}:${second}`;
|
|
42
|
+
};
|
|
43
|
+
|
|
22
44
|
const formatTimeLabel = (
|
|
23
45
|
value: unknown,
|
|
24
46
|
): { label: string; timestamp: number | null } => {
|
|
25
47
|
if (typeof value === "string" || typeof value === "number") {
|
|
26
48
|
const date = new Date(value);
|
|
27
49
|
if (!Number.isNaN(date.getTime())) {
|
|
28
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
29
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
30
|
-
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
31
50
|
return {
|
|
32
|
-
label:
|
|
51
|
+
label: formatLocalTimeParts(date),
|
|
33
52
|
timestamp: date.getTime(),
|
|
34
53
|
};
|
|
35
54
|
}
|
package/src/logging/logger.ts
CHANGED
|
@@ -59,6 +59,7 @@ export function createLogger(config: LoggerConfig = {}): Logger {
|
|
|
59
59
|
const destinationStream = pino.destination({
|
|
60
60
|
dest: destination,
|
|
61
61
|
sync: true,
|
|
62
|
+
append: true,
|
|
62
63
|
});
|
|
63
64
|
return pino(options, destinationStream);
|
|
64
65
|
}
|
|
@@ -89,6 +90,7 @@ export function createLogger(config: LoggerConfig = {}): Logger {
|
|
|
89
90
|
const destinationStream = pino.destination({
|
|
90
91
|
dest: destination,
|
|
91
92
|
sync: false,
|
|
93
|
+
append: true,
|
|
92
94
|
});
|
|
93
95
|
return pino(options, destinationStream);
|
|
94
96
|
}
|