@akiojin/gwt 2.0.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/README.ja.md +323 -0
- package/README.md +347 -0
- package/bin/gwt.js +5 -0
- package/package.json +125 -0
- package/src/claude-history.ts +717 -0
- package/src/claude.ts +292 -0
- package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
- package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
- package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
- package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
- package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
- package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
- package/src/cli/ui/components/App.tsx +793 -0
- package/src/cli/ui/components/common/Confirm.tsx +40 -0
- package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
- package/src/cli/ui/components/common/Input.tsx +36 -0
- package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
- package/src/cli/ui/components/common/Select.tsx +216 -0
- package/src/cli/ui/components/parts/Footer.tsx +41 -0
- package/src/cli/ui/components/parts/Header.test.tsx +85 -0
- package/src/cli/ui/components/parts/Header.tsx +63 -0
- package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
- package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
- package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
- package/src/cli/ui/components/parts/Stats.tsx +67 -0
- package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
- package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
- package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
- package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
- package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
- package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
- package/src/cli/ui/hooks/useGitData.ts +157 -0
- package/src/cli/ui/hooks/useScreenState.ts +44 -0
- package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
- package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
- package/src/cli/ui/types.ts +295 -0
- package/src/cli/ui/utils/baseBranch.ts +34 -0
- package/src/cli/ui/utils/branchFormatter.ts +222 -0
- package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
- package/src/codex.ts +139 -0
- package/src/config/builtin-tools.ts +44 -0
- package/src/config/constants.ts +100 -0
- package/src/config/env-history.ts +45 -0
- package/src/config/index.ts +204 -0
- package/src/config/tools.ts +293 -0
- package/src/git.ts +1102 -0
- package/src/github.ts +158 -0
- package/src/index.test.ts +87 -0
- package/src/index.ts +684 -0
- package/src/index.ts.backup +1543 -0
- package/src/launcher.ts +142 -0
- package/src/repositories/git.repository.ts +129 -0
- package/src/repositories/github.repository.ts +83 -0
- package/src/repositories/worktree.repository.ts +69 -0
- package/src/services/BatchMergeService.ts +251 -0
- package/src/services/WorktreeOrchestrator.ts +115 -0
- package/src/services/__tests__/BatchMergeService.test.ts +518 -0
- package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
- package/src/services/dependency-installer.ts +199 -0
- package/src/services/git.service.ts +113 -0
- package/src/services/github.service.ts +61 -0
- package/src/services/worktree.service.ts +66 -0
- package/src/types/api.ts +241 -0
- package/src/types/tools.ts +235 -0
- package/src/utils/spinner.ts +54 -0
- package/src/utils/terminal.ts +272 -0
- package/src/utils.test.ts +43 -0
- package/src/utils.ts +60 -0
- package/src/web/client/index.html +12 -0
- package/src/web/client/src/components/BranchGraph.tsx +231 -0
- package/src/web/client/src/components/EnvEditor.tsx +145 -0
- package/src/web/client/src/components/Terminal.tsx +137 -0
- package/src/web/client/src/hooks/useBranches.ts +41 -0
- package/src/web/client/src/hooks/useConfig.ts +31 -0
- package/src/web/client/src/hooks/useSessions.ts +59 -0
- package/src/web/client/src/hooks/useWorktrees.ts +47 -0
- package/src/web/client/src/index.css +834 -0
- package/src/web/client/src/lib/api.ts +184 -0
- package/src/web/client/src/lib/websocket.ts +174 -0
- package/src/web/client/src/main.tsx +29 -0
- package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
- package/src/web/client/src/pages/BranchListPage.tsx +264 -0
- package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
- package/src/web/client/src/router.tsx +27 -0
- package/src/web/client/vite.config.ts +21 -0
- package/src/web/server/env/importer.ts +54 -0
- package/src/web/server/index.ts +74 -0
- package/src/web/server/pty/manager.ts +189 -0
- package/src/web/server/routes/branches.ts +126 -0
- package/src/web/server/routes/config.ts +220 -0
- package/src/web/server/routes/index.ts +37 -0
- package/src/web/server/routes/sessions.ts +130 -0
- package/src/web/server/routes/worktrees.ts +108 -0
- package/src/web/server/services/branches.ts +368 -0
- package/src/web/server/services/worktrees.ts +85 -0
- package/src/web/server/websocket/handler.ts +180 -0
- package/src/worktree.ts +703 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { useBranches } from "../hooks/useBranches";
|
|
4
|
+
import { BranchGraph } from "../components/BranchGraph";
|
|
5
|
+
import type { Branch } from "../../../../types/api.js";
|
|
6
|
+
|
|
7
|
+
const numberFormatter = new Intl.NumberFormat("ja-JP");
|
|
8
|
+
|
|
9
|
+
const BRANCH_TYPE_LABEL: Record<Branch["type"], string> = {
|
|
10
|
+
local: "ローカル",
|
|
11
|
+
remote: "リモート",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const MERGE_STATUS_LABEL: Record<Branch["mergeStatus"], string> = {
|
|
15
|
+
merged: "マージ済み",
|
|
16
|
+
unmerged: "未マージ",
|
|
17
|
+
unknown: "状態不明",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MERGE_STATUS_TONE: Record<Branch["mergeStatus"], "success" | "warning" | "muted"> = {
|
|
21
|
+
merged: "success",
|
|
22
|
+
unmerged: "warning",
|
|
23
|
+
unknown: "muted",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface PageStateMessage {
|
|
27
|
+
title: string;
|
|
28
|
+
description: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SEARCH_PLACEHOLDER = "ブランチ名やタイプで検索...";
|
|
32
|
+
|
|
33
|
+
export function BranchListPage() {
|
|
34
|
+
const { data, isLoading, error } = useBranches();
|
|
35
|
+
const [query, setQuery] = useState("");
|
|
36
|
+
|
|
37
|
+
const branches = data ?? [];
|
|
38
|
+
|
|
39
|
+
const metrics = useMemo(() => {
|
|
40
|
+
const worktrees = branches.filter((branch) => Boolean(branch.worktreePath)).length;
|
|
41
|
+
const remote = branches.filter((branch) => branch.type === "remote").length;
|
|
42
|
+
const healthy = branches.filter((branch) => branch.divergence?.upToDate).length;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
total: branches.length,
|
|
46
|
+
worktrees,
|
|
47
|
+
remote,
|
|
48
|
+
healthy,
|
|
49
|
+
};
|
|
50
|
+
}, [branches]);
|
|
51
|
+
|
|
52
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
53
|
+
|
|
54
|
+
const filteredBranches = useMemo(() => {
|
|
55
|
+
if (!normalizedQuery) {
|
|
56
|
+
return branches;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return branches.filter((branch) => {
|
|
60
|
+
const haystack = [
|
|
61
|
+
branch.name,
|
|
62
|
+
branch.type,
|
|
63
|
+
branch.mergeStatus,
|
|
64
|
+
branch.commitMessage ?? "",
|
|
65
|
+
branch.worktreePath ?? "",
|
|
66
|
+
]
|
|
67
|
+
.join(" ")
|
|
68
|
+
.toLowerCase();
|
|
69
|
+
return haystack.includes(normalizedQuery);
|
|
70
|
+
});
|
|
71
|
+
}, [branches, normalizedQuery]);
|
|
72
|
+
|
|
73
|
+
const pageState: PageStateMessage | null = useMemo(() => {
|
|
74
|
+
if (isLoading) {
|
|
75
|
+
return {
|
|
76
|
+
title: "データを読み込み中",
|
|
77
|
+
description: "最新のブランチ一覧を取得しています...",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
return {
|
|
83
|
+
title: "ブランチの取得に失敗しました",
|
|
84
|
+
description:
|
|
85
|
+
error instanceof Error ? error.message : "未知のエラーが発生しました。",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!branches.length) {
|
|
90
|
+
return {
|
|
91
|
+
title: "ブランチが見つかりません",
|
|
92
|
+
description: "git fetch origin などで最新のブランチを取得してください。",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}, [branches.length, error, isLoading]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="app-shell">
|
|
101
|
+
<header className="page-hero">
|
|
102
|
+
<p className="page-hero__eyebrow">WORKTREE DASHBOARD</p>
|
|
103
|
+
<h1>gwt Control Center</h1>
|
|
104
|
+
<p>
|
|
105
|
+
ローカルのGitブランチとAIツールをブラウザ上で一元管理し、Worktree状態を瞬時に
|
|
106
|
+
可視化します。
|
|
107
|
+
</p>
|
|
108
|
+
<div className="page-hero__meta">リアルタイムで更新されるステータスビュー</div>
|
|
109
|
+
</header>
|
|
110
|
+
|
|
111
|
+
<main className="page-content">
|
|
112
|
+
{!pageState && filteredBranches.length > 0 && (
|
|
113
|
+
<BranchGraph branches={filteredBranches} />
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<section className="metrics-grid">
|
|
117
|
+
<article className="metric-card">
|
|
118
|
+
<p className="metric-card__label">総ブランチ数</p>
|
|
119
|
+
<p className="metric-card__value" data-testid="metric-total">
|
|
120
|
+
{numberFormatter.format(metrics.total)}
|
|
121
|
+
</p>
|
|
122
|
+
<p className="metric-card__hint">ローカル + リモート</p>
|
|
123
|
+
</article>
|
|
124
|
+
<article className="metric-card">
|
|
125
|
+
<p className="metric-card__label">作成済みWorktree</p>
|
|
126
|
+
<p className="metric-card__value" data-testid="metric-worktrees">
|
|
127
|
+
{numberFormatter.format(metrics.worktrees)}
|
|
128
|
+
</p>
|
|
129
|
+
<p className="metric-card__hint">即座にAIツールを起動可能</p>
|
|
130
|
+
</article>
|
|
131
|
+
<article className="metric-card">
|
|
132
|
+
<p className="metric-card__label">リモート追跡ブランチ</p>
|
|
133
|
+
<p className="metric-card__value">
|
|
134
|
+
{numberFormatter.format(metrics.remote)}
|
|
135
|
+
</p>
|
|
136
|
+
<p className="metric-card__hint">origin との同期ステータス</p>
|
|
137
|
+
</article>
|
|
138
|
+
<article className="metric-card">
|
|
139
|
+
<p className="metric-card__label">最新コミットが最新</p>
|
|
140
|
+
<p className="metric-card__value">
|
|
141
|
+
{numberFormatter.format(metrics.healthy)}
|
|
142
|
+
</p>
|
|
143
|
+
<p className="metric-card__hint">divergence 0 のブランチ</p>
|
|
144
|
+
</article>
|
|
145
|
+
</section>
|
|
146
|
+
|
|
147
|
+
<section className="toolbar">
|
|
148
|
+
<label className="toolbar__field">
|
|
149
|
+
<span className="toolbar__icon" aria-hidden="true">
|
|
150
|
+
🔍
|
|
151
|
+
</span>
|
|
152
|
+
<input
|
|
153
|
+
type="search"
|
|
154
|
+
className="search-input"
|
|
155
|
+
placeholder={SEARCH_PLACEHOLDER}
|
|
156
|
+
value={query}
|
|
157
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
158
|
+
/>
|
|
159
|
+
</label>
|
|
160
|
+
<span className="toolbar__count">
|
|
161
|
+
{numberFormatter.format(filteredBranches.length)} / {" "}
|
|
162
|
+
{numberFormatter.format(metrics.total)} branches
|
|
163
|
+
</span>
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
{pageState ? (
|
|
167
|
+
<div className="page-state page-state--card">
|
|
168
|
+
<h2>{pageState.title}</h2>
|
|
169
|
+
<p>{pageState.description}</p>
|
|
170
|
+
</div>
|
|
171
|
+
) : filteredBranches.length === 0 ? (
|
|
172
|
+
<div className="empty-state">
|
|
173
|
+
<h3>一致するブランチがありません</h3>
|
|
174
|
+
<p>
|
|
175
|
+
検索条件を見直すか、タグ・ブランチタイプ・コミットメッセージなど別のキーワードを
|
|
176
|
+
試してください。
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
) : (
|
|
180
|
+
<div className="branch-grid">
|
|
181
|
+
{filteredBranches.map((branch) => (
|
|
182
|
+
<article key={branch.name} className="branch-card">
|
|
183
|
+
<div className="branch-card__header">
|
|
184
|
+
<div>
|
|
185
|
+
<p className="branch-card__eyebrow">
|
|
186
|
+
{BRANCH_TYPE_LABEL[branch.type]}ブランチ
|
|
187
|
+
</p>
|
|
188
|
+
<h2>{branch.name}</h2>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="badge-group">
|
|
191
|
+
<span className={`status-badge status-badge--${branch.type}`}>
|
|
192
|
+
{BRANCH_TYPE_LABEL[branch.type]}
|
|
193
|
+
</span>
|
|
194
|
+
<span className={`status-badge status-badge--${MERGE_STATUS_TONE[branch.mergeStatus]}`}>
|
|
195
|
+
{MERGE_STATUS_LABEL[branch.mergeStatus]}
|
|
196
|
+
</span>
|
|
197
|
+
<span
|
|
198
|
+
className={`status-badge ${
|
|
199
|
+
branch.worktreePath
|
|
200
|
+
? "status-badge--success"
|
|
201
|
+
: "status-badge--muted"
|
|
202
|
+
}`}
|
|
203
|
+
>
|
|
204
|
+
{branch.worktreePath ? "Worktreeあり" : "Worktree未作成"}
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<p className="branch-card__commit">
|
|
210
|
+
{branch.commitMessage ?? "コミットメッセージがありません"}
|
|
211
|
+
</p>
|
|
212
|
+
|
|
213
|
+
<dl className="metadata-grid metadata-grid--compact">
|
|
214
|
+
<div>
|
|
215
|
+
<dt>最新コミット</dt>
|
|
216
|
+
<dd>{branch.commitHash.slice(0, 7)}</dd>
|
|
217
|
+
</div>
|
|
218
|
+
<div>
|
|
219
|
+
<dt>Author</dt>
|
|
220
|
+
<dd>{branch.author ?? "N/A"}</dd>
|
|
221
|
+
</div>
|
|
222
|
+
<div>
|
|
223
|
+
<dt>Worktree</dt>
|
|
224
|
+
<dd>{branch.worktreePath ?? "未作成"}</dd>
|
|
225
|
+
</div>
|
|
226
|
+
</dl>
|
|
227
|
+
|
|
228
|
+
{branch.divergence && (
|
|
229
|
+
<div className="pill-group">
|
|
230
|
+
<span className="pill">Ahead {branch.divergence.ahead}</span>
|
|
231
|
+
<span className="pill">Behind {branch.divergence.behind}</span>
|
|
232
|
+
<span
|
|
233
|
+
className={`pill ${
|
|
234
|
+
branch.divergence.upToDate ? "pill--success" : "pill--warning"
|
|
235
|
+
}`}
|
|
236
|
+
>
|
|
237
|
+
{branch.divergence.upToDate ? "最新" : "更新あり"}
|
|
238
|
+
</span>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<div className="branch-card__actions">
|
|
243
|
+
<Link
|
|
244
|
+
className="button button--ghost"
|
|
245
|
+
to={`/${encodeURIComponent(branch.name)}`}
|
|
246
|
+
>
|
|
247
|
+
詳細を見る
|
|
248
|
+
</Link>
|
|
249
|
+
<span
|
|
250
|
+
className={`info-pill ${
|
|
251
|
+
branch.worktreePath ? "info-pill--success" : "info-pill--warning"
|
|
252
|
+
}`}
|
|
253
|
+
>
|
|
254
|
+
{branch.worktreePath ?? "Worktree未作成"}
|
|
255
|
+
</span>
|
|
256
|
+
</div>
|
|
257
|
+
</article>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</main>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import type { ConfigPayload, CustomAITool, EnvironmentVariable } from "../../../../types/api.js";
|
|
4
|
+
import { useConfig, useUpdateConfig } from "../hooks/useConfig";
|
|
5
|
+
import { EnvEditor, createEnvRow, type EnvRow } from "../components/EnvEditor";
|
|
6
|
+
|
|
7
|
+
type ToolEnvState = Record<string, EnvRow[]>;
|
|
8
|
+
|
|
9
|
+
function rowsFromVariables(variables?: EnvironmentVariable[] | null): EnvRow[] {
|
|
10
|
+
if (!variables) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return variables.map((variable) => {
|
|
14
|
+
const partial: Partial<EnvRow> = {
|
|
15
|
+
key: variable.key,
|
|
16
|
+
value: variable.value,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (typeof variable.importedFromOs === "boolean") {
|
|
20
|
+
partial.importedFromOs = variable.importedFromOs;
|
|
21
|
+
}
|
|
22
|
+
if (variable.lastUpdated) {
|
|
23
|
+
partial.lastUpdated = variable.lastUpdated;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return createEnvRow(partial);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function serializeRows(rows: EnvRow[]): EnvironmentVariable[] {
|
|
31
|
+
return rows
|
|
32
|
+
.filter((row) => row.key.trim().length > 0)
|
|
33
|
+
.map((row) => ({
|
|
34
|
+
key: row.key.trim().toUpperCase(),
|
|
35
|
+
value: row.value,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildPayload(
|
|
40
|
+
base: ConfigPayload | undefined,
|
|
41
|
+
sharedEnv: EnvRow[],
|
|
42
|
+
toolState: ToolEnvState,
|
|
43
|
+
): ConfigPayload {
|
|
44
|
+
const tools: CustomAITool[] = (base?.tools ?? []).map((tool) => ({
|
|
45
|
+
...tool,
|
|
46
|
+
env: serializeRows(toolState[tool.id] ?? []),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
version: base?.version ?? "1.0.0",
|
|
51
|
+
env: serializeRows(sharedEnv),
|
|
52
|
+
tools,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ConfigManagementPage() {
|
|
57
|
+
const { data, isLoading, error } = useConfig();
|
|
58
|
+
const updateConfig = useUpdateConfig();
|
|
59
|
+
const [sharedEnv, setSharedEnv] = useState<EnvRow[]>([]);
|
|
60
|
+
const [toolEnv, setToolEnv] = useState<ToolEnvState>({});
|
|
61
|
+
const [banner, setBanner] = useState<{ type: "success" | "error"; message: string } | null>(
|
|
62
|
+
null,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!data) return;
|
|
67
|
+
setSharedEnv(rowsFromVariables(data.env));
|
|
68
|
+
const toolState: ToolEnvState = {};
|
|
69
|
+
data.tools?.forEach((tool) => {
|
|
70
|
+
toolState[tool.id] = rowsFromVariables(tool.env);
|
|
71
|
+
});
|
|
72
|
+
setToolEnv(toolState);
|
|
73
|
+
}, [data]);
|
|
74
|
+
|
|
75
|
+
const serializedOriginalShared = useMemo(
|
|
76
|
+
() => JSON.stringify(data?.env ?? []),
|
|
77
|
+
[data?.env],
|
|
78
|
+
);
|
|
79
|
+
const serializedCurrentShared = useMemo(
|
|
80
|
+
() => JSON.stringify(serializeRows(sharedEnv)),
|
|
81
|
+
[sharedEnv],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const hasInvalidRows = useMemo(() => {
|
|
85
|
+
const keyInvalid = sharedEnv.some((row) => !row.key || /[^A-Z0-9_]/.test(row.key));
|
|
86
|
+
const valueInvalid = sharedEnv.some((row) => row.key && row.value.trim().length === 0);
|
|
87
|
+
const toolInvalid = Object.values(toolEnv).some((rows) =>
|
|
88
|
+
rows.some((row) => !row.key || /[^A-Z0-9_]/.test(row.key) || row.value.trim().length === 0),
|
|
89
|
+
);
|
|
90
|
+
return keyInvalid || valueInvalid || toolInvalid;
|
|
91
|
+
}, [sharedEnv, toolEnv]);
|
|
92
|
+
|
|
93
|
+
const hasChanges = useMemo(() => {
|
|
94
|
+
if (serializedOriginalShared !== serializedCurrentShared) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (!data) return false;
|
|
98
|
+
const currentTool = data.tools?.map((tool) => serializeRows(toolEnv[tool.id] ?? [])) ?? [];
|
|
99
|
+
const originalTool = data.tools?.map((tool) => tool.env ?? []) ?? [];
|
|
100
|
+
return JSON.stringify(currentTool) !== JSON.stringify(originalTool);
|
|
101
|
+
}, [data, serializedOriginalShared, serializedCurrentShared, toolEnv]);
|
|
102
|
+
|
|
103
|
+
const handleSave = async () => {
|
|
104
|
+
if (!data) return;
|
|
105
|
+
try {
|
|
106
|
+
const payload = buildPayload(data, sharedEnv, toolEnv);
|
|
107
|
+
await updateConfig.mutateAsync(payload);
|
|
108
|
+
setBanner({ type: "success", message: "設定を保存しました" });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
setBanner({
|
|
111
|
+
type: "error",
|
|
112
|
+
message: err instanceof Error ? err.message : "保存に失敗しました",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (isLoading) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="app-shell">
|
|
120
|
+
<div className="page-state page-state--centered">
|
|
121
|
+
<h1>読み込み中</h1>
|
|
122
|
+
<p>設定を読み込んでいます...</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (error) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="app-shell">
|
|
131
|
+
<div className="page-state page-state--centered">
|
|
132
|
+
<h1>設定の取得に失敗しました</h1>
|
|
133
|
+
<p>{error instanceof Error ? error.message : "未知のエラーです"}</p>
|
|
134
|
+
<Link to="/" className="button button--ghost">
|
|
135
|
+
ブランチ一覧に戻る
|
|
136
|
+
</Link>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="app-shell">
|
|
144
|
+
<header className="page-hero">
|
|
145
|
+
<Link to="/" className="page-hero__back">
|
|
146
|
+
← ブランチ一覧へ
|
|
147
|
+
</Link>
|
|
148
|
+
<p className="page-hero__eyebrow">CONFIG</p>
|
|
149
|
+
<h1>環境変数の管理</h1>
|
|
150
|
+
<p className="page-hero__subtitle">
|
|
151
|
+
共通環境変数とツールごとの上書きをブラウザから編集できます。
|
|
152
|
+
</p>
|
|
153
|
+
<div className="page-hero__actions">
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
className="button button--primary"
|
|
157
|
+
onClick={handleSave}
|
|
158
|
+
disabled={updateConfig.isPending || hasInvalidRows || !hasChanges}
|
|
159
|
+
>
|
|
160
|
+
{updateConfig.isPending ? "保存中..." : "保存"}
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
{banner && <div className={`inline-banner inline-banner--${banner.type}`}>{banner.message}</div>}
|
|
164
|
+
</header>
|
|
165
|
+
|
|
166
|
+
<main className="page-content page-content--wide">
|
|
167
|
+
<section className="section-card">
|
|
168
|
+
<EnvEditor
|
|
169
|
+
title="共通環境変数"
|
|
170
|
+
description="全てのAIツールで共有される値。PAT やプロキシ設定などはこちらに入力してください。"
|
|
171
|
+
rows={sharedEnv}
|
|
172
|
+
onChange={setSharedEnv}
|
|
173
|
+
/>
|
|
174
|
+
</section>
|
|
175
|
+
|
|
176
|
+
<section className="section-card">
|
|
177
|
+
<h2>ツール固有の環境変数</h2>
|
|
178
|
+
<p className="section-card__body">
|
|
179
|
+
各ツール固有に上書きしたい値がある場合はこちらから設定します。共通設定との競合がある場合は
|
|
180
|
+
ツール設定が優先されます。
|
|
181
|
+
</p>
|
|
182
|
+
<div className="env-editor__tool-list">
|
|
183
|
+
{data?.tools?.map((tool) => (
|
|
184
|
+
<div key={tool.id} className="env-editor__tool">
|
|
185
|
+
<EnvEditor
|
|
186
|
+
title={tool.displayName}
|
|
187
|
+
description={`${tool.executionType} / ${tool.command}`}
|
|
188
|
+
rows={toolEnv[tool.id] ?? []}
|
|
189
|
+
onChange={(rows) =>
|
|
190
|
+
setToolEnv((prev) => ({
|
|
191
|
+
...prev,
|
|
192
|
+
[tool.id]: rows,
|
|
193
|
+
}))
|
|
194
|
+
}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</section>
|
|
200
|
+
</main>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createBrowserRouter } from "react-router-dom";
|
|
3
|
+
import { BranchListPage } from "./pages/BranchListPage";
|
|
4
|
+
import { BranchDetailPage } from "./pages/BranchDetailPage";
|
|
5
|
+
import { ConfigManagementPage } from "./pages/ConfigManagementPage";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* React Router設定
|
|
9
|
+
*
|
|
10
|
+
* URL構造:
|
|
11
|
+
* - / - ブランチ一覧(ホーム)
|
|
12
|
+
* - /:branchName - 個別ブランチ詳細(例: /feature-webui, /feature%2Fwebui)
|
|
13
|
+
*/
|
|
14
|
+
export const router = createBrowserRouter([
|
|
15
|
+
{
|
|
16
|
+
path: "/",
|
|
17
|
+
element: <BranchListPage />,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
path: "/:branchName",
|
|
21
|
+
element: <BranchDetailPage />,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: "/config",
|
|
25
|
+
element: <ConfigManagementPage />,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
root: path.resolve(__dirname),
|
|
8
|
+
build: {
|
|
9
|
+
outDir: path.resolve(__dirname, "../../../dist/client"),
|
|
10
|
+
emptyOutDir: true,
|
|
11
|
+
},
|
|
12
|
+
server: {
|
|
13
|
+
port: 5173,
|
|
14
|
+
proxy: {
|
|
15
|
+
"/api": {
|
|
16
|
+
target: "http://localhost:3000",
|
|
17
|
+
changeOrigin: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { loadToolsConfig, saveToolsConfig } from "../../../config/tools.js";
|
|
2
|
+
import { recordEnvHistory } from "../../../config/env-history.js";
|
|
3
|
+
import type { EnvironmentHistoryEntry } from "../../../types/api.js";
|
|
4
|
+
|
|
5
|
+
const IMPORTABLE_KEYS = [
|
|
6
|
+
"OPENAI_API_KEY",
|
|
7
|
+
"ANTHROPIC_API_KEY",
|
|
8
|
+
"GITHUB_TOKEN",
|
|
9
|
+
"GH_TOKEN",
|
|
10
|
+
"PERSONAL_ACCESS_TOKEN",
|
|
11
|
+
"HTTP_PROXY",
|
|
12
|
+
"HTTPS_PROXY",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const importedKeySet = new Set<string>();
|
|
16
|
+
|
|
17
|
+
export async function importOsEnvIntoSharedConfig(): Promise<string[]> {
|
|
18
|
+
const config = await loadToolsConfig();
|
|
19
|
+
const sharedEnv = { ...(config.env ?? {}) };
|
|
20
|
+
const importedKeys: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (const key of IMPORTABLE_KEYS) {
|
|
23
|
+
const value = process.env[key];
|
|
24
|
+
if (!value) continue;
|
|
25
|
+
if (sharedEnv[key]) continue;
|
|
26
|
+
sharedEnv[key] = value;
|
|
27
|
+
importedKeys.push(key);
|
|
28
|
+
importedKeySet.add(key);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!importedKeys.length) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await saveToolsConfig({
|
|
36
|
+
...config,
|
|
37
|
+
env: sharedEnv,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const timestamp = new Date().toISOString();
|
|
41
|
+
const historyEntries: EnvironmentHistoryEntry[] = importedKeys.map((key) => ({
|
|
42
|
+
key,
|
|
43
|
+
action: "import",
|
|
44
|
+
source: "os",
|
|
45
|
+
timestamp,
|
|
46
|
+
}));
|
|
47
|
+
await recordEnvHistory(historyEntries);
|
|
48
|
+
|
|
49
|
+
return importedKeys;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getImportedEnvKeys(): string[] {
|
|
53
|
+
return Array.from(importedKeySet);
|
|
54
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web UI Server Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Fastifyベースのウェブサーバーを起動し、REST APIとWebSocketを提供します。
|
|
5
|
+
* 仕様: specs/SPEC-d5e56259/contracts/rest-api.yaml
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Fastify from "fastify";
|
|
9
|
+
import fastifyStatic from "@fastify/static";
|
|
10
|
+
import fastifyWebsocket from "@fastify/websocket";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { PTYManager } from "./pty/manager.js";
|
|
14
|
+
import { WebSocketHandler } from "./websocket/handler.js";
|
|
15
|
+
import { registerRoutes } from "./routes/index.js";
|
|
16
|
+
import { importOsEnvIntoSharedConfig } from "./env/importer.js";
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Webサーバーを起動
|
|
23
|
+
*/
|
|
24
|
+
export async function startWebServer(): Promise<void> {
|
|
25
|
+
const fastify = Fastify({
|
|
26
|
+
logger: {
|
|
27
|
+
level: process.env.LOG_LEVEL || "info",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// PTYマネージャーとWebSocketハンドラーを初期化
|
|
32
|
+
const ptyManager = new PTYManager();
|
|
33
|
+
const wsHandler = new WebSocketHandler(ptyManager);
|
|
34
|
+
|
|
35
|
+
// WebSocketサポートを追加
|
|
36
|
+
await fastify.register(fastifyWebsocket);
|
|
37
|
+
|
|
38
|
+
// WebSocketエンドポイント
|
|
39
|
+
fastify.register(async (fastify) => {
|
|
40
|
+
fastify.get(
|
|
41
|
+
"/api/sessions/:sessionId/terminal",
|
|
42
|
+
{ websocket: true },
|
|
43
|
+
(connection, request) => {
|
|
44
|
+
wsHandler.handle(connection, request);
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// REST APIルートを登録
|
|
50
|
+
await importOsEnvIntoSharedConfig();
|
|
51
|
+
await registerRoutes(fastify, ptyManager);
|
|
52
|
+
|
|
53
|
+
// 静的ファイル配信(Viteビルド成果物)
|
|
54
|
+
const clientDistPath = join(__dirname, "../../../dist/client");
|
|
55
|
+
await fastify.register(fastifyStatic, {
|
|
56
|
+
root: clientDistPath,
|
|
57
|
+
prefix: "/",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// サーバー起動
|
|
61
|
+
try {
|
|
62
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
|
63
|
+
// Docker環境からホストOSでアクセスできるよう、0.0.0.0でリッスン
|
|
64
|
+
// IPv4/IPv6両方対応のため、listenOnStart: false も検討可能
|
|
65
|
+
const host = process.env.HOST || "0.0.0.0";
|
|
66
|
+
|
|
67
|
+
await fastify.listen({ port, host });
|
|
68
|
+
console.log(`Web UI server running at http://${host}:${port}`);
|
|
69
|
+
console.log(`Access from host: http://localhost:${port}`);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
fastify.log.error(err);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|