@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.
Files changed (132) hide show
  1. package/README.ja.md +323 -0
  2. package/README.md +347 -0
  3. package/bin/gwt.js +5 -0
  4. package/package.json +125 -0
  5. package/src/claude-history.ts +717 -0
  6. package/src/claude.ts +292 -0
  7. package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
  8. package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
  9. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
  10. package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
  11. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
  12. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
  13. package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
  14. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
  15. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
  16. package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
  17. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
  18. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
  19. package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
  20. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
  21. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
  22. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
  23. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
  24. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
  25. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
  26. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
  27. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
  28. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
  29. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
  30. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
  31. package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
  32. package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
  33. package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
  34. package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
  35. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
  36. package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
  37. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
  38. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
  39. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
  40. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
  41. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
  42. package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
  43. package/src/cli/ui/components/App.tsx +793 -0
  44. package/src/cli/ui/components/common/Confirm.tsx +40 -0
  45. package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
  46. package/src/cli/ui/components/common/Input.tsx +36 -0
  47. package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
  48. package/src/cli/ui/components/common/Select.tsx +216 -0
  49. package/src/cli/ui/components/parts/Footer.tsx +41 -0
  50. package/src/cli/ui/components/parts/Header.test.tsx +85 -0
  51. package/src/cli/ui/components/parts/Header.tsx +63 -0
  52. package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
  53. package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
  54. package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
  55. package/src/cli/ui/components/parts/Stats.tsx +67 -0
  56. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
  57. package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
  58. package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
  59. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
  60. package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
  61. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
  62. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
  63. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
  64. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
  65. package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
  66. package/src/cli/ui/hooks/useGitData.ts +157 -0
  67. package/src/cli/ui/hooks/useScreenState.ts +44 -0
  68. package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
  69. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
  70. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
  71. package/src/cli/ui/types.ts +295 -0
  72. package/src/cli/ui/utils/baseBranch.ts +34 -0
  73. package/src/cli/ui/utils/branchFormatter.ts +222 -0
  74. package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
  75. package/src/codex.ts +139 -0
  76. package/src/config/builtin-tools.ts +44 -0
  77. package/src/config/constants.ts +100 -0
  78. package/src/config/env-history.ts +45 -0
  79. package/src/config/index.ts +204 -0
  80. package/src/config/tools.ts +293 -0
  81. package/src/git.ts +1102 -0
  82. package/src/github.ts +158 -0
  83. package/src/index.test.ts +87 -0
  84. package/src/index.ts +684 -0
  85. package/src/index.ts.backup +1543 -0
  86. package/src/launcher.ts +142 -0
  87. package/src/repositories/git.repository.ts +129 -0
  88. package/src/repositories/github.repository.ts +83 -0
  89. package/src/repositories/worktree.repository.ts +69 -0
  90. package/src/services/BatchMergeService.ts +251 -0
  91. package/src/services/WorktreeOrchestrator.ts +115 -0
  92. package/src/services/__tests__/BatchMergeService.test.ts +518 -0
  93. package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
  94. package/src/services/dependency-installer.ts +199 -0
  95. package/src/services/git.service.ts +113 -0
  96. package/src/services/github.service.ts +61 -0
  97. package/src/services/worktree.service.ts +66 -0
  98. package/src/types/api.ts +241 -0
  99. package/src/types/tools.ts +235 -0
  100. package/src/utils/spinner.ts +54 -0
  101. package/src/utils/terminal.ts +272 -0
  102. package/src/utils.test.ts +43 -0
  103. package/src/utils.ts +60 -0
  104. package/src/web/client/index.html +12 -0
  105. package/src/web/client/src/components/BranchGraph.tsx +231 -0
  106. package/src/web/client/src/components/EnvEditor.tsx +145 -0
  107. package/src/web/client/src/components/Terminal.tsx +137 -0
  108. package/src/web/client/src/hooks/useBranches.ts +41 -0
  109. package/src/web/client/src/hooks/useConfig.ts +31 -0
  110. package/src/web/client/src/hooks/useSessions.ts +59 -0
  111. package/src/web/client/src/hooks/useWorktrees.ts +47 -0
  112. package/src/web/client/src/index.css +834 -0
  113. package/src/web/client/src/lib/api.ts +184 -0
  114. package/src/web/client/src/lib/websocket.ts +174 -0
  115. package/src/web/client/src/main.tsx +29 -0
  116. package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
  117. package/src/web/client/src/pages/BranchListPage.tsx +264 -0
  118. package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
  119. package/src/web/client/src/router.tsx +27 -0
  120. package/src/web/client/vite.config.ts +21 -0
  121. package/src/web/server/env/importer.ts +54 -0
  122. package/src/web/server/index.ts +74 -0
  123. package/src/web/server/pty/manager.ts +189 -0
  124. package/src/web/server/routes/branches.ts +126 -0
  125. package/src/web/server/routes/config.ts +220 -0
  126. package/src/web/server/routes/index.ts +37 -0
  127. package/src/web/server/routes/sessions.ts +130 -0
  128. package/src/web/server/routes/worktrees.ts +108 -0
  129. package/src/web/server/services/branches.ts +368 -0
  130. package/src/web/server/services/worktrees.ts +85 -0
  131. package/src/web/server/websocket/handler.ts +180 -0
  132. package/src/worktree.ts +703 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * PTY Manager
3
+ *
4
+ * AI Toolセッションの疑似端末(PTY)を管理します。
5
+ * node-ptyを使用してプロセスをスポーンし、WebSocketを通じて入出力を中継します。
6
+ */
7
+
8
+ import * as pty from "node-pty";
9
+ import type { IPty } from "node-pty";
10
+ import { randomUUID } from "node:crypto";
11
+ import type { AIToolSession } from "../../../types/api.js";
12
+
13
+ export interface PTYInstance {
14
+ ptyProcess: IPty;
15
+ session: AIToolSession;
16
+ }
17
+
18
+ /**
19
+ * PTYマネージャー - セッションとPTYプロセスのライフサイクル管理
20
+ */
21
+ export class PTYManager {
22
+ private instances: Map<string, PTYInstance> = new Map();
23
+
24
+ /**
25
+ * 新しいPTYセッションを作成
26
+ */
27
+ public spawn(
28
+ toolType: "claude-code" | "codex-cli" | "custom",
29
+ worktreePath: string,
30
+ mode: "normal" | "continue" | "resume",
31
+ toolName?: string | null,
32
+ cols = 80,
33
+ rows = 24,
34
+ ): { sessionId: string; session: AIToolSession } {
35
+ const sessionId = randomUUID();
36
+
37
+ // AI Toolコマンドを構築
38
+ const command = this.buildCommand(toolType, mode, toolName);
39
+ const args = this.buildArgs(toolType, mode);
40
+
41
+ // PTYプロセスをスポーン
42
+ const ptyProcess = pty.spawn(command, args, {
43
+ name: "xterm-256color",
44
+ cols,
45
+ rows,
46
+ cwd: worktreePath,
47
+ env: {
48
+ ...process.env,
49
+ TERM: "xterm-256color",
50
+ COLORTERM: "truecolor",
51
+ },
52
+ });
53
+
54
+ const session: AIToolSession = {
55
+ sessionId,
56
+ toolType,
57
+ toolName: toolName || null,
58
+ mode,
59
+ worktreePath,
60
+ ptyPid: ptyProcess.pid,
61
+ websocketId: null,
62
+ status: "pending",
63
+ startedAt: new Date().toISOString(),
64
+ endedAt: null,
65
+ exitCode: null,
66
+ errorMessage: null,
67
+ };
68
+
69
+ this.instances.set(sessionId, { ptyProcess, session });
70
+
71
+ return { sessionId, session };
72
+ }
73
+
74
+ /**
75
+ * セッションIDからPTYインスタンスを取得
76
+ */
77
+ public get(sessionId: string): PTYInstance | undefined {
78
+ return this.instances.get(sessionId);
79
+ }
80
+
81
+ /**
82
+ * セッションを削除
83
+ */
84
+ public delete(sessionId: string): boolean {
85
+ const instance = this.instances.get(sessionId);
86
+ if (!instance) {
87
+ return false;
88
+ }
89
+
90
+ // PTYプロセスを終了
91
+ try {
92
+ instance.ptyProcess.kill();
93
+ } catch {
94
+ // プロセスが既に終了している場合は無視
95
+ }
96
+
97
+ this.instances.delete(sessionId);
98
+ return true;
99
+ }
100
+
101
+ /**
102
+ * セッションのステータスを更新
103
+ */
104
+ public updateStatus(
105
+ sessionId: string,
106
+ status: AIToolSession["status"],
107
+ exitCode?: number,
108
+ errorMessage?: string,
109
+ ): boolean {
110
+ const instance = this.instances.get(sessionId);
111
+ if (!instance) {
112
+ return false;
113
+ }
114
+
115
+ instance.session.status = status;
116
+ if (exitCode !== undefined) {
117
+ instance.session.exitCode = exitCode;
118
+ }
119
+ if (errorMessage !== undefined) {
120
+ instance.session.errorMessage = errorMessage;
121
+ }
122
+ if (status === "completed" || status === "failed") {
123
+ instance.session.endedAt = new Date().toISOString();
124
+ }
125
+
126
+ return true;
127
+ }
128
+
129
+ /**
130
+ * すべてのセッション一覧を取得
131
+ */
132
+ public list(): AIToolSession[] {
133
+ return Array.from(this.instances.values()).map((inst) => inst.session);
134
+ }
135
+
136
+ /**
137
+ * AI Toolのコマンドを構築
138
+ */
139
+ private buildCommand(
140
+ toolType: "claude-code" | "codex-cli" | "custom",
141
+ mode: "normal" | "continue" | "resume",
142
+ toolName?: string | null,
143
+ ): string {
144
+ if (toolType === "custom" && toolName) {
145
+ // カスタムツールは別途config.jsonから取得する必要があるが、
146
+ // ここでは簡易実装としてtoolNameをそのままコマンドとして使用
147
+ return toolName;
148
+ }
149
+
150
+ if (toolType === "codex-cli") {
151
+ return "codex";
152
+ }
153
+
154
+ // claude-code
155
+ return "claude";
156
+ }
157
+
158
+ /**
159
+ * AI Toolの引数を構築
160
+ */
161
+ private buildArgs(
162
+ toolType: "claude-code" | "codex-cli" | "custom",
163
+ mode: "normal" | "continue" | "resume",
164
+ ): string[] {
165
+ if (toolType === "custom") {
166
+ // カスタムツールの引数は別途config.jsonから取得
167
+ return [];
168
+ }
169
+
170
+ if (toolType === "codex-cli") {
171
+ if (mode === "continue") {
172
+ return ["--continue"];
173
+ }
174
+ if (mode === "resume") {
175
+ return ["--resume"];
176
+ }
177
+ return [];
178
+ }
179
+
180
+ // claude-code
181
+ if (mode === "continue") {
182
+ return ["--continue"];
183
+ }
184
+ if (mode === "resume") {
185
+ return ["--resume"];
186
+ }
187
+ return [];
188
+ }
189
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Branch Routes
3
+ *
4
+ * ブランチ関連のREST APIエンドポイント。
5
+ */
6
+
7
+ import type { FastifyInstance } from "fastify";
8
+ import {
9
+ listBranches,
10
+ getBranchByName,
11
+ syncBranchState,
12
+ } from "../services/branches.js";
13
+ import type {
14
+ ApiResponse,
15
+ Branch,
16
+ BranchSyncRequest,
17
+ BranchSyncResult,
18
+ } from "../../../types/api.js";
19
+
20
+ /**
21
+ * ブランチ関連のルートを登録
22
+ */
23
+ export async function registerBranchRoutes(
24
+ fastify: FastifyInstance,
25
+ ): Promise<void> {
26
+ // GET /api/branches - すべてのブランチ一覧を取得
27
+ fastify.get<{ Reply: ApiResponse<Branch[]> }>(
28
+ "/api/branches",
29
+ async (request, reply) => {
30
+ try {
31
+ const branches = await listBranches();
32
+ return { success: true, data: branches };
33
+ } catch (error) {
34
+ const errorMsg = error instanceof Error ? error.message : String(error);
35
+ reply.code(500);
36
+ return {
37
+ success: false,
38
+ error: "Failed to fetch branches",
39
+ details: errorMsg,
40
+ };
41
+ }
42
+ },
43
+ );
44
+
45
+ // GET /api/branches/:branchName - 特定のブランチ情報を取得
46
+ fastify.get<{
47
+ Params: { branchName: string };
48
+ Reply: ApiResponse<Branch>;
49
+ }>("/api/branches/:branchName", async (request, reply) => {
50
+ try {
51
+ const { branchName } = request.params;
52
+ const decodedBranchName = decodeURIComponent(branchName);
53
+
54
+ const branch = await getBranchByName(decodedBranchName);
55
+ if (!branch) {
56
+ reply.code(404);
57
+ return {
58
+ success: false,
59
+ error: "Branch not found",
60
+ details: `Branch ${decodedBranchName} does not exist`,
61
+ };
62
+ }
63
+
64
+ return { success: true, data: branch };
65
+ } catch (error) {
66
+ const errorMsg = error instanceof Error ? error.message : String(error);
67
+ reply.code(500);
68
+ return {
69
+ success: false,
70
+ error: "Failed to fetch branch",
71
+ details: errorMsg,
72
+ };
73
+ }
74
+ });
75
+
76
+ // POST /api/branches/:branchName/sync - Fetch & fast-forward pull
77
+ fastify.post<{
78
+ Params: { branchName: string };
79
+ Body: BranchSyncRequest;
80
+ Reply: ApiResponse<BranchSyncResult>;
81
+ }>("/api/branches/:branchName/sync", async (request, reply) => {
82
+ const { branchName } = request.params;
83
+ const decodedBranchName = decodeURIComponent(branchName);
84
+ const { worktreePath } = request.body;
85
+
86
+ if (!worktreePath) {
87
+ reply.code(400);
88
+ return {
89
+ success: false,
90
+ error: "worktreePath is required",
91
+ details: null,
92
+ };
93
+ }
94
+
95
+ try {
96
+ const result = await syncBranchState(decodedBranchName, worktreePath);
97
+ return { success: true, data: result };
98
+ } catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ if (message.includes("Branch not found")) {
101
+ reply.code(404);
102
+ return {
103
+ success: false,
104
+ error: "Branch not found",
105
+ details: message,
106
+ };
107
+ }
108
+
109
+ if (message.includes("Worktree path is required")) {
110
+ reply.code(400);
111
+ return {
112
+ success: false,
113
+ error: "Invalid request",
114
+ details: message,
115
+ };
116
+ }
117
+
118
+ reply.code(500);
119
+ return {
120
+ success: false,
121
+ error: "Failed to sync branch",
122
+ details: message,
123
+ };
124
+ }
125
+ });
126
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Config Routes
3
+ */
4
+
5
+ import type { FastifyInstance } from "fastify";
6
+ import { loadToolsConfig, saveToolsConfig } from "../../../config/tools.js";
7
+ import {
8
+ loadEnvHistory,
9
+ recordEnvHistory,
10
+ } from "../../../config/env-history.js";
11
+ import type {
12
+ ApiResponse,
13
+ ConfigPayload,
14
+ CustomAITool as ApiCustomAITool,
15
+ EnvironmentHistoryEntry,
16
+ EnvironmentVariable,
17
+ } from "../../../types/api.js";
18
+ import type { CustomAITool as FileCustomAITool } from "../../../types/tools.js";
19
+ import { getImportedEnvKeys } from "../env/importer.js";
20
+
21
+ function normalizeEnv(
22
+ env: Record<string, string> | undefined,
23
+ importedKeys: Set<string>,
24
+ history: EnvironmentHistoryEntry[],
25
+ ): EnvironmentVariable[] {
26
+ if (!env) {
27
+ return [];
28
+ }
29
+
30
+ const lastUpdated = new Map<string, string | null>();
31
+ for (const entry of history) {
32
+ lastUpdated.set(entry.key, entry.timestamp ?? null);
33
+ }
34
+
35
+ return Object.entries(env).map(([key, value]) => {
36
+ const variable: EnvironmentVariable = {
37
+ key,
38
+ value,
39
+ lastUpdated: lastUpdated.get(key) ?? null,
40
+ };
41
+
42
+ if (importedKeys.has(key)) {
43
+ variable.importedFromOs = true;
44
+ }
45
+
46
+ return variable;
47
+ });
48
+ }
49
+
50
+ function envArrayToRecord(
51
+ env?: EnvironmentVariable[] | null,
52
+ ): Record<string, string> {
53
+ if (!env) {
54
+ return {};
55
+ }
56
+
57
+ const record: Record<string, string> = {};
58
+ for (const variable of env) {
59
+ if (!variable.key) continue;
60
+ record[variable.key] = variable.value;
61
+ }
62
+ return record;
63
+ }
64
+
65
+ function toApiTool(
66
+ tool: FileCustomAITool,
67
+ history: EnvironmentHistoryEntry[],
68
+ importedKeys: Set<string>,
69
+ ): ApiCustomAITool {
70
+ return {
71
+ id: tool.id,
72
+ displayName: tool.displayName,
73
+ icon: tool.icon ?? null,
74
+ command: tool.command,
75
+ executionType: tool.type,
76
+ defaultArgs: tool.defaultArgs ?? null,
77
+ modeArgs: tool.modeArgs,
78
+ permissionSkipArgs: tool.permissionSkipArgs ?? null,
79
+ env: normalizeEnv(tool.env, importedKeys, history),
80
+ description: null,
81
+ createdAt: null,
82
+ updatedAt: null,
83
+ };
84
+ }
85
+
86
+ function toFileTool(tool: ApiCustomAITool): FileCustomAITool {
87
+ const envRecord = envArrayToRecord(tool.env);
88
+ const fileTool: FileCustomAITool = {
89
+ id: tool.id,
90
+ displayName: tool.displayName,
91
+ type: tool.executionType,
92
+ command: tool.command,
93
+ modeArgs: tool.modeArgs,
94
+ };
95
+
96
+ if (tool.icon) {
97
+ fileTool.icon = tool.icon;
98
+ }
99
+ if (tool.defaultArgs && tool.defaultArgs.length > 0) {
100
+ fileTool.defaultArgs = tool.defaultArgs;
101
+ }
102
+ if (tool.permissionSkipArgs && tool.permissionSkipArgs.length > 0) {
103
+ fileTool.permissionSkipArgs = tool.permissionSkipArgs;
104
+ }
105
+ if (Object.keys(envRecord).length > 0) {
106
+ fileTool.env = envRecord;
107
+ }
108
+
109
+ return fileTool;
110
+ }
111
+
112
+ function diffEnvHistory(
113
+ previous: Record<string, string>,
114
+ next: Record<string, string>,
115
+ source: EnvironmentHistoryEntry["source"],
116
+ ): EnvironmentHistoryEntry[] {
117
+ const entries: EnvironmentHistoryEntry[] = [];
118
+ const timestamp = new Date().toISOString();
119
+
120
+ for (const [key, value] of Object.entries(next)) {
121
+ if (!(key in previous)) {
122
+ entries.push({ key, action: "add", source, timestamp });
123
+ } else if (previous[key] !== value) {
124
+ entries.push({ key, action: "update", source, timestamp });
125
+ }
126
+ }
127
+
128
+ for (const key of Object.keys(previous)) {
129
+ if (!(key in next)) {
130
+ entries.push({ key, action: "delete", source, timestamp });
131
+ }
132
+ }
133
+
134
+ return entries;
135
+ }
136
+
137
+ export async function registerConfigRoutes(
138
+ fastify: FastifyInstance,
139
+ ): Promise<void> {
140
+ fastify.get<{ Reply: ApiResponse<ConfigPayload> }>(
141
+ "/api/config",
142
+ async (request, reply) => {
143
+ try {
144
+ const config = await loadToolsConfig();
145
+ const history = await loadEnvHistory();
146
+ const importedSet = new Set(getImportedEnvKeys());
147
+
148
+ return {
149
+ success: true,
150
+ data: {
151
+ version: config.version,
152
+ updatedAt: config.updatedAt ?? null,
153
+ env: normalizeEnv(config.env, importedSet, history),
154
+ history,
155
+ tools: config.customTools.map((tool) =>
156
+ toApiTool(tool, history, importedSet),
157
+ ),
158
+ },
159
+ } satisfies ApiResponse<ConfigPayload>;
160
+ } catch (error) {
161
+ request.log.error({ err: error }, "Failed to load custom tool config");
162
+ reply.code(500);
163
+ return {
164
+ success: false,
165
+ error: "Failed to load config",
166
+ details: error instanceof Error ? error.message : String(error),
167
+ };
168
+ }
169
+ },
170
+ );
171
+
172
+ fastify.put<{
173
+ Body: ConfigPayload;
174
+ Reply: ApiResponse<ConfigPayload>;
175
+ }>("/api/config", async (request, reply) => {
176
+ try {
177
+ const payload = request.body;
178
+ const existing = await loadToolsConfig();
179
+ const nextEnvRecord = envArrayToRecord(payload.env);
180
+ const envHistory = diffEnvHistory(
181
+ existing.env ?? {},
182
+ nextEnvRecord,
183
+ "ui",
184
+ );
185
+
186
+ await saveToolsConfig({
187
+ version: payload.version || existing.version,
188
+ env: nextEnvRecord,
189
+ customTools: payload.tools.map(toFileTool),
190
+ });
191
+
192
+ if (envHistory.length) {
193
+ await recordEnvHistory(envHistory);
194
+ }
195
+
196
+ const history = await loadEnvHistory();
197
+ const importedSet = new Set(getImportedEnvKeys());
198
+
199
+ return {
200
+ success: true,
201
+ data: {
202
+ version: payload.version || existing.version,
203
+ updatedAt: new Date().toISOString(),
204
+ env: normalizeEnv(nextEnvRecord, importedSet, history),
205
+ history,
206
+ tools: payload.tools,
207
+ },
208
+ } satisfies ApiResponse<ConfigPayload>;
209
+ } catch (error) {
210
+ const errorMsg = error instanceof Error ? error.message : String(error);
211
+ request.log.error({ err: error }, "Failed to update config");
212
+ reply.code(500);
213
+ return {
214
+ success: false,
215
+ error: "Failed to update config",
216
+ details: errorMsg,
217
+ };
218
+ }
219
+ });
220
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * REST API Routes
3
+ *
4
+ * すべてのREST APIエンドポイントを登録します。
5
+ * 仕様: specs/SPEC-d5e56259/contracts/rest-api.yaml
6
+ */
7
+
8
+ import type { FastifyInstance } from "fastify";
9
+ import type { PTYManager } from "../pty/manager.js";
10
+ import { registerBranchRoutes } from "./branches.js";
11
+ import { registerWorktreeRoutes } from "./worktrees.js";
12
+ import { registerSessionRoutes } from "./sessions.js";
13
+ import { registerConfigRoutes } from "./config.js";
14
+ import type { HealthResponse } from "../../../types/api.js";
15
+
16
+ /**
17
+ * すべてのルートを登録
18
+ */
19
+ export async function registerRoutes(
20
+ fastify: FastifyInstance,
21
+ ptyManager: PTYManager,
22
+ ): Promise<void> {
23
+ // ヘルスチェック
24
+ fastify.get<{ Reply: HealthResponse }>("/api/health", async () => {
25
+ return {
26
+ success: true,
27
+ status: "ok",
28
+ timestamp: new Date().toISOString(),
29
+ };
30
+ });
31
+
32
+ // 各エンドポイントグループを登録
33
+ await registerBranchRoutes(fastify);
34
+ await registerWorktreeRoutes(fastify);
35
+ await registerSessionRoutes(fastify, ptyManager);
36
+ await registerConfigRoutes(fastify);
37
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Session Routes
3
+ *
4
+ * AI Toolセッション関連のREST APIエンドポイント。
5
+ */
6
+
7
+ import type { FastifyInstance } from "fastify";
8
+ import type { PTYManager } from "../pty/manager.js";
9
+ import type {
10
+ ApiResponse,
11
+ AIToolSession,
12
+ StartSessionRequest,
13
+ } from "../../../types/api.js";
14
+
15
+ /**
16
+ * セッション関連のルートを登録
17
+ */
18
+ export async function registerSessionRoutes(
19
+ fastify: FastifyInstance,
20
+ ptyManager: PTYManager,
21
+ ): Promise<void> {
22
+ // GET /api/sessions - すべてのセッション一覧を取得
23
+ fastify.get<{ Reply: ApiResponse<AIToolSession[]> }>(
24
+ "/api/sessions",
25
+ async (request, reply) => {
26
+ try {
27
+ const sessions = ptyManager.list();
28
+ return { success: true, data: sessions };
29
+ } catch (error) {
30
+ const errorMsg = error instanceof Error ? error.message : String(error);
31
+ reply.code(500);
32
+ return {
33
+ success: false,
34
+ error: "Failed to fetch sessions",
35
+ details: errorMsg,
36
+ };
37
+ }
38
+ },
39
+ );
40
+
41
+ // POST /api/sessions - 新しいセッションを開始
42
+ fastify.post<{
43
+ Body: StartSessionRequest;
44
+ Reply: ApiResponse<AIToolSession>;
45
+ }>("/api/sessions", async (request, reply) => {
46
+ try {
47
+ const { toolType, toolName, mode, worktreePath } = request.body;
48
+
49
+ const { session } = ptyManager.spawn(
50
+ toolType,
51
+ worktreePath,
52
+ mode,
53
+ toolName,
54
+ );
55
+
56
+ reply.code(201);
57
+ return { success: true, data: session };
58
+ } catch (error) {
59
+ const errorMsg = error instanceof Error ? error.message : String(error);
60
+ reply.code(500);
61
+ return {
62
+ success: false,
63
+ error: "Failed to start session",
64
+ details: errorMsg,
65
+ };
66
+ }
67
+ });
68
+
69
+ // GET /api/sessions/:sessionId - 特定のセッション情報を取得
70
+ fastify.get<{
71
+ Params: { sessionId: string };
72
+ Reply: ApiResponse<AIToolSession>;
73
+ }>("/api/sessions/:sessionId", async (request, reply) => {
74
+ try {
75
+ const { sessionId } = request.params;
76
+
77
+ const instance = ptyManager.get(sessionId);
78
+ if (!instance) {
79
+ reply.code(404);
80
+ return {
81
+ success: false,
82
+ error: "Session not found",
83
+ details: `Session ${sessionId} does not exist`,
84
+ };
85
+ }
86
+
87
+ return { success: true, data: instance.session };
88
+ } catch (error) {
89
+ const errorMsg = error instanceof Error ? error.message : String(error);
90
+ reply.code(500);
91
+ return {
92
+ success: false,
93
+ error: "Failed to fetch session",
94
+ details: errorMsg,
95
+ };
96
+ }
97
+ });
98
+
99
+ // DELETE /api/sessions/:sessionId - セッションを終了
100
+ fastify.delete<{
101
+ Params: { sessionId: string };
102
+ Reply:
103
+ | { success: true }
104
+ | { success: false; error: string; details?: string | null };
105
+ }>("/api/sessions/:sessionId", async (request, reply) => {
106
+ try {
107
+ const { sessionId } = request.params;
108
+
109
+ const deleted = ptyManager.delete(sessionId);
110
+ if (!deleted) {
111
+ reply.code(404);
112
+ return {
113
+ success: false,
114
+ error: "Session not found",
115
+ details: `Session ${sessionId} does not exist`,
116
+ };
117
+ }
118
+
119
+ return { success: true };
120
+ } catch (error) {
121
+ const errorMsg = error instanceof Error ? error.message : String(error);
122
+ reply.code(500);
123
+ return {
124
+ success: false,
125
+ error: "Failed to delete session",
126
+ details: errorMsg,
127
+ };
128
+ }
129
+ });
130
+ }