@akiojin/gwt 4.5.1 → 4.6.1
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/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +33 -16
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +4 -1
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +7 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +19 -9
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/hooks/useToolStatus.d.ts +30 -0
- package/dist/cli/ui/hooks/useToolStatus.d.ts.map +1 -0
- package/dist/cli/ui/hooks/useToolStatus.js +49 -0
- package/dist/cli/ui/hooks/useToolStatus.js.map +1 -0
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +7 -5
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/command.d.ts +42 -1
- package/dist/utils/command.d.ts.map +1 -1
- package/dist/utils/command.js +130 -11
- package/dist/utils/command.js.map +1 -1
- package/package.json +1 -1
- package/src/claude.ts +45 -21
- package/src/cli/ui/components/App.tsx +5 -0
- package/src/cli/ui/components/screens/BranchListScreen.tsx +36 -10
- package/src/cli/ui/hooks/useToolStatus.ts +68 -0
- package/src/gemini.ts +8 -5
- package/src/index.ts +1 -0
- package/src/utils/command.ts +174 -12
|
@@ -10,6 +10,7 @@ import { useSpinnerFrame } from "../common/SpinnerIcon.js";
|
|
|
10
10
|
import { useAppInput } from "../../hooks/useAppInput.js";
|
|
11
11
|
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
12
12
|
import type { BranchItem, Statistics, BranchViewMode } from "../../types.js";
|
|
13
|
+
import type { ToolStatus } from "../../hooks/useToolStatus.js";
|
|
13
14
|
import stringWidth from "string-width";
|
|
14
15
|
import stripAnsi from "strip-ansi";
|
|
15
16
|
import chalk from "chalk";
|
|
@@ -109,13 +110,18 @@ export interface BranchListScreenProps {
|
|
|
109
110
|
testOnViewModeChange?: (mode: BranchViewMode) => void;
|
|
110
111
|
selectedBranches?: string[];
|
|
111
112
|
onToggleSelect?: (branchName: string) => void;
|
|
113
|
+
/**
|
|
114
|
+
* AIツールのインストール状態配列
|
|
115
|
+
* @see specs/SPEC-3b0ed29b/spec.md FR-019, FR-021
|
|
116
|
+
*/
|
|
117
|
+
toolStatuses?: ToolStatus[] | undefined;
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
/**
|
|
115
121
|
* BranchListScreen - Main screen for branch selection
|
|
116
122
|
* Layout: Header + Stats + Branch List + Footer
|
|
117
123
|
*/
|
|
118
|
-
export function BranchListScreen({
|
|
124
|
+
export const BranchListScreen = React.memo(function BranchListScreen({
|
|
119
125
|
branches,
|
|
120
126
|
stats,
|
|
121
127
|
onSelect,
|
|
@@ -138,6 +144,7 @@ export function BranchListScreen({
|
|
|
138
144
|
testOnViewModeChange,
|
|
139
145
|
selectedBranches = [],
|
|
140
146
|
onToggleSelect,
|
|
147
|
+
toolStatuses,
|
|
141
148
|
}: BranchListScreenProps) {
|
|
142
149
|
const { rows } = useTerminalSize();
|
|
143
150
|
const selectedSet = useMemo(
|
|
@@ -274,8 +281,14 @@ export function BranchListScreen({
|
|
|
274
281
|
let result = branches;
|
|
275
282
|
|
|
276
283
|
// Apply view mode filter
|
|
277
|
-
if (viewMode
|
|
278
|
-
result = result.filter((branch) => branch.type ===
|
|
284
|
+
if (viewMode === "local") {
|
|
285
|
+
result = result.filter((branch) => branch.type === "local");
|
|
286
|
+
} else if (viewMode === "remote") {
|
|
287
|
+
// リモート専用ブランチ OR ローカルだがリモートにも存在するブランチ
|
|
288
|
+
result = result.filter(
|
|
289
|
+
(branch) =>
|
|
290
|
+
branch.type === "remote" || branch.hasRemoteCounterpart === true,
|
|
291
|
+
);
|
|
279
292
|
}
|
|
280
293
|
|
|
281
294
|
// Apply search filter
|
|
@@ -446,11 +459,9 @@ export function BranchListScreen({
|
|
|
446
459
|
const indicatorInfo = cleanupUI?.indicators?.[item.name];
|
|
447
460
|
let leadingIndicator: string;
|
|
448
461
|
if (indicatorInfo) {
|
|
449
|
-
// Use spinner
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
? spinnerFrame
|
|
453
|
-
: indicatorInfo.icon;
|
|
462
|
+
// Use static spinner icon if isSpinning to avoid re-render dependency
|
|
463
|
+
// The static "⠋" provides visual feedback without causing flicker
|
|
464
|
+
let indicatorIcon = indicatorInfo.isSpinning ? "⠋" : indicatorInfo.icon;
|
|
454
465
|
if (indicatorIcon && indicatorInfo.color && !isSelected) {
|
|
455
466
|
switch (indicatorInfo.color) {
|
|
456
467
|
case "cyan":
|
|
@@ -588,7 +599,6 @@ export function BranchListScreen({
|
|
|
588
599
|
truncateToWidth,
|
|
589
600
|
selectedSet,
|
|
590
601
|
colorToolLabel,
|
|
591
|
-
spinnerFrame,
|
|
592
602
|
],
|
|
593
603
|
);
|
|
594
604
|
|
|
@@ -625,6 +635,22 @@ export function BranchListScreen({
|
|
|
625
635
|
)}
|
|
626
636
|
</Box>
|
|
627
637
|
|
|
638
|
+
{/* Tool Status - FR-019, FR-021 */}
|
|
639
|
+
{toolStatuses && toolStatuses.length > 0 && (
|
|
640
|
+
<Box>
|
|
641
|
+
<Text dimColor>Tools: </Text>
|
|
642
|
+
{toolStatuses.map((tool, index) => (
|
|
643
|
+
<React.Fragment key={tool.id}>
|
|
644
|
+
<Text>{tool.name}: </Text>
|
|
645
|
+
<Text color={tool.status === "installed" ? "green" : "yellow"}>
|
|
646
|
+
{tool.status}
|
|
647
|
+
</Text>
|
|
648
|
+
{index < toolStatuses.length - 1 && <Text dimColor> | </Text>}
|
|
649
|
+
</React.Fragment>
|
|
650
|
+
))}
|
|
651
|
+
</Box>
|
|
652
|
+
)}
|
|
653
|
+
|
|
628
654
|
{/* Stats */}
|
|
629
655
|
<Box>
|
|
630
656
|
<Stats stats={stats} lastUpdated={lastUpdated} viewMode={viewMode} />
|
|
@@ -708,4 +734,4 @@ export function BranchListScreen({
|
|
|
708
734
|
<Footer actions={footerActions} />
|
|
709
735
|
</Box>
|
|
710
736
|
);
|
|
711
|
-
}
|
|
737
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIツール状態管理フック
|
|
3
|
+
*
|
|
4
|
+
* 各AIツール(Claude Code、Codex、Gemini)のインストール状態を
|
|
5
|
+
* gwt起動時に検出し、キャッシュして提供します。
|
|
6
|
+
* @see specs/SPEC-3b0ed29b/spec.md FR-017〜FR-021
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect, useCallback } from "react";
|
|
10
|
+
import {
|
|
11
|
+
detectAllToolStatuses,
|
|
12
|
+
type ToolStatus,
|
|
13
|
+
} from "../../../utils/command.js";
|
|
14
|
+
|
|
15
|
+
export interface UseToolStatusResult {
|
|
16
|
+
/** ツール状態の配列(ロード中は空配列) */
|
|
17
|
+
tools: ToolStatus[];
|
|
18
|
+
/** ロード中フラグ */
|
|
19
|
+
loading: boolean;
|
|
20
|
+
/** エラー(なければnull) */
|
|
21
|
+
error: Error | null;
|
|
22
|
+
/** ツール状態を再検出(通常は不要、デバッグ用) */
|
|
23
|
+
refresh: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* AIツール状態管理フック
|
|
28
|
+
*
|
|
29
|
+
* コンポーネントでAIツールのインストール状態を取得するためのフック。
|
|
30
|
+
* 初回マウント時に自動的に全ツールの状態を検出してキャッシュします。
|
|
31
|
+
*
|
|
32
|
+
* キャッシュされた結果は、ブランチ選択時やツール起動時に再利用され、
|
|
33
|
+
* 毎回の検出オーバーヘッドを削減します(FR-020)。
|
|
34
|
+
*/
|
|
35
|
+
export function useToolStatus(): UseToolStatusResult {
|
|
36
|
+
const [tools, setTools] = useState<ToolStatus[]>([]);
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
const [error, setError] = useState<Error | null>(null);
|
|
39
|
+
|
|
40
|
+
// ツール状態を検出
|
|
41
|
+
const refresh = useCallback(async () => {
|
|
42
|
+
try {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
const statuses = await detectAllToolStatuses();
|
|
46
|
+
setTools(statuses);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
// 初回マウント時に検出(FR-017: 起動時に検出してキャッシュ)
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
refresh();
|
|
57
|
+
}, [refresh]);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
tools,
|
|
61
|
+
loading,
|
|
62
|
+
error,
|
|
63
|
+
refresh,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Re-export ToolStatus type for convenience
|
|
68
|
+
export type { ToolStatus };
|
package/src/gemini.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getTerminalStreams,
|
|
7
7
|
resetTerminalModes,
|
|
8
8
|
} from "./utils/terminal.js";
|
|
9
|
-
import {
|
|
9
|
+
import { findCommand } from "./utils/command.js";
|
|
10
10
|
import { findLatestGeminiSessionId } from "./utils/session.js";
|
|
11
11
|
|
|
12
12
|
const GEMINI_CLI_PACKAGE = "@google/gemini-cli@latest";
|
|
@@ -152,7 +152,7 @@ export async function launchGeminiCLI(
|
|
|
152
152
|
const childStdio = createChildStdio();
|
|
153
153
|
|
|
154
154
|
// Auto-detect locally installed gemini command
|
|
155
|
-
const
|
|
155
|
+
const geminiLookup = await findCommand("gemini");
|
|
156
156
|
|
|
157
157
|
// Preserve TTY for interactive UI (colors/width) by inheriting stdout/stderr.
|
|
158
158
|
// Session ID is determined via file-based detection after exit.
|
|
@@ -184,11 +184,12 @@ export async function launchGeminiCLI(
|
|
|
184
184
|
await execChild(child);
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
if (
|
|
187
|
+
if (geminiLookup.source === "installed" && geminiLookup.path) {
|
|
188
|
+
// Use the full path to avoid PATH issues in non-interactive shells
|
|
188
189
|
console.log(
|
|
189
190
|
chalk.green(" ✨ Using locally installed gemini command"),
|
|
190
191
|
);
|
|
191
|
-
return await run(
|
|
192
|
+
return await run(geminiLookup.path, runArgs);
|
|
192
193
|
}
|
|
193
194
|
console.log(
|
|
194
195
|
chalk.cyan(" 🔄 Falling back to bunx @google/gemini-cli@latest"),
|
|
@@ -263,7 +264,9 @@ export async function launchGeminiCLI(
|
|
|
263
264
|
|
|
264
265
|
return capturedSessionId ? { sessionId: capturedSessionId } : {};
|
|
265
266
|
} catch (error: unknown) {
|
|
266
|
-
const
|
|
267
|
+
const geminiCheck = await findCommand("gemini");
|
|
268
|
+
const hasLocalGemini =
|
|
269
|
+
geminiCheck.source === "installed" && geminiCheck.path !== null;
|
|
267
270
|
let errorMessage: string;
|
|
268
271
|
const err = error as NodeJS.ErrnoException;
|
|
269
272
|
|
package/src/index.ts
CHANGED
package/src/utils/command.ts
CHANGED
|
@@ -1,26 +1,188 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
+
* Known installation paths for common AI CLI tools.
|
|
8
|
+
* These are checked as fallback when `which`/`where` fails.
|
|
9
|
+
*/
|
|
10
|
+
const KNOWN_INSTALL_PATHS: Record<string, { unix: string[]; win32: string[] }> =
|
|
11
|
+
{
|
|
12
|
+
claude: {
|
|
13
|
+
unix: [
|
|
14
|
+
join(homedir(), ".bun", "bin", "claude"),
|
|
15
|
+
join(homedir(), ".local", "bin", "claude"),
|
|
16
|
+
"/usr/local/bin/claude",
|
|
17
|
+
],
|
|
18
|
+
win32: [
|
|
19
|
+
join(
|
|
20
|
+
process.env.LOCALAPPDATA ?? "",
|
|
21
|
+
"Programs",
|
|
22
|
+
"claude",
|
|
23
|
+
"claude.exe",
|
|
24
|
+
),
|
|
25
|
+
join(homedir(), ".bun", "bin", "claude.exe"),
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
codex: {
|
|
29
|
+
unix: [
|
|
30
|
+
join(homedir(), ".bun", "bin", "codex"),
|
|
31
|
+
join(homedir(), ".local", "bin", "codex"),
|
|
32
|
+
"/usr/local/bin/codex",
|
|
33
|
+
],
|
|
34
|
+
win32: [join(homedir(), ".bun", "bin", "codex.exe")],
|
|
35
|
+
},
|
|
36
|
+
gemini: {
|
|
37
|
+
unix: [
|
|
38
|
+
join(homedir(), ".bun", "bin", "gemini"),
|
|
39
|
+
join(homedir(), ".local", "bin", "gemini"),
|
|
40
|
+
"/usr/local/bin/gemini",
|
|
41
|
+
],
|
|
42
|
+
win32: [join(homedir(), ".bun", "bin", "gemini.exe")],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builtin AI tools with their command names and display names.
|
|
48
|
+
*/
|
|
49
|
+
const BUILTIN_TOOLS = [
|
|
50
|
+
{ id: "claude-code", commandName: "claude", displayName: "Claude" },
|
|
51
|
+
{ id: "codex-cli", commandName: "codex", displayName: "Codex" },
|
|
52
|
+
{ id: "gemini-cli", commandName: "gemini", displayName: "Gemini" },
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result of command lookup.
|
|
57
|
+
*/
|
|
58
|
+
export interface CommandLookupResult {
|
|
59
|
+
available: boolean;
|
|
60
|
+
path: string | null;
|
|
61
|
+
source: "installed" | "bunx";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tool status information for display.
|
|
66
|
+
*/
|
|
67
|
+
export interface ToolStatus {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
status: "installed" | "bunx";
|
|
71
|
+
path: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Module-level cache for command lookup results.
|
|
76
|
+
* This cache persists for the lifetime of the process (FR-020).
|
|
77
|
+
*/
|
|
78
|
+
const commandLookupCache = new Map<string, CommandLookupResult>();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clears the command lookup cache.
|
|
82
|
+
* Primarily for testing purposes.
|
|
83
|
+
*/
|
|
84
|
+
export function clearCommandLookupCache(): void {
|
|
85
|
+
commandLookupCache.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Finds a command by checking PATH first, then fallback paths.
|
|
90
|
+
* Results are cached for the lifetime of the process (FR-020).
|
|
7
91
|
*
|
|
8
|
-
* @param commandName - Command name to look up (e.g. `claude`, `
|
|
9
|
-
* @returns
|
|
92
|
+
* @param commandName - Command name to look up (e.g. `claude`, `codex`, `gemini`)
|
|
93
|
+
* @returns CommandLookupResult with availability, path, and source
|
|
10
94
|
*/
|
|
11
|
-
export async function
|
|
95
|
+
export async function findCommand(
|
|
12
96
|
commandName: string,
|
|
13
|
-
): Promise<
|
|
97
|
+
): Promise<CommandLookupResult> {
|
|
98
|
+
// Check cache first (FR-020: 再検出を行わない)
|
|
99
|
+
const cached = commandLookupCache.get(commandName);
|
|
100
|
+
if (cached) {
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let lookupResult: CommandLookupResult | null = null;
|
|
105
|
+
|
|
106
|
+
// Step 1: Try standard which/where lookup
|
|
14
107
|
try {
|
|
15
|
-
const
|
|
16
|
-
await execa(
|
|
108
|
+
const lookupCommand = process.platform === "win32" ? "where" : "which";
|
|
109
|
+
const execResult = await execa(lookupCommand, [commandName], {
|
|
17
110
|
shell: true,
|
|
18
111
|
stdin: "ignore",
|
|
19
|
-
stdout: "
|
|
112
|
+
stdout: "pipe",
|
|
20
113
|
stderr: "ignore",
|
|
21
114
|
});
|
|
22
|
-
|
|
115
|
+
const foundPath = execResult.stdout.trim().split("\n")[0];
|
|
116
|
+
if (foundPath) {
|
|
117
|
+
lookupResult = { available: true, path: foundPath, source: "installed" };
|
|
118
|
+
}
|
|
23
119
|
} catch {
|
|
24
|
-
|
|
120
|
+
// which/where failed, try fallback paths
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Step 2: Check known installation paths as fallback
|
|
124
|
+
if (!lookupResult) {
|
|
125
|
+
const knownPaths = KNOWN_INSTALL_PATHS[commandName];
|
|
126
|
+
if (knownPaths) {
|
|
127
|
+
const pathsToCheck =
|
|
128
|
+
process.platform === "win32" ? knownPaths.win32 : knownPaths.unix;
|
|
129
|
+
|
|
130
|
+
for (const p of pathsToCheck) {
|
|
131
|
+
if (p && existsSync(p)) {
|
|
132
|
+
lookupResult = { available: true, path: p, source: "installed" };
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Step 3: Fall back to bunx (always available for known tools)
|
|
140
|
+
if (!lookupResult) {
|
|
141
|
+
lookupResult = { available: true, path: null, source: "bunx" };
|
|
25
142
|
}
|
|
143
|
+
|
|
144
|
+
// Cache the result (FR-020)
|
|
145
|
+
commandLookupCache.set(commandName, lookupResult);
|
|
146
|
+
|
|
147
|
+
return lookupResult;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Checks whether a command is available in the current PATH.
|
|
152
|
+
*
|
|
153
|
+
* Uses `where` on Windows and `which` on other platforms.
|
|
154
|
+
* If the standard lookup fails, checks known installation paths
|
|
155
|
+
* as a fallback for common tools.
|
|
156
|
+
*
|
|
157
|
+
* @param commandName - Command name to look up (e.g. `claude`, `npx`, `gemini`)
|
|
158
|
+
* @returns true if the command exists in PATH or known paths
|
|
159
|
+
*/
|
|
160
|
+
export async function isCommandAvailable(
|
|
161
|
+
commandName: string,
|
|
162
|
+
): Promise<boolean> {
|
|
163
|
+
const result = await findCommand(commandName);
|
|
164
|
+
return result.available;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Detects installation status for all builtin AI tools.
|
|
169
|
+
*
|
|
170
|
+
* This function is designed to be called once at application startup
|
|
171
|
+
* and cached for the duration of the session.
|
|
172
|
+
*
|
|
173
|
+
* @returns Array of ToolStatus for all builtin tools
|
|
174
|
+
*/
|
|
175
|
+
export async function detectAllToolStatuses(): Promise<ToolStatus[]> {
|
|
176
|
+
const results = await Promise.all(
|
|
177
|
+
BUILTIN_TOOLS.map(async (tool) => {
|
|
178
|
+
const result = await findCommand(tool.commandName);
|
|
179
|
+
return {
|
|
180
|
+
id: tool.id,
|
|
181
|
+
name: tool.displayName,
|
|
182
|
+
status: result.source,
|
|
183
|
+
path: result.path,
|
|
184
|
+
};
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
return results;
|
|
26
188
|
}
|