@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,204 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
4
+
5
+ export interface AppConfig {
6
+ defaultBaseBranch: string;
7
+ skipPermissions: boolean;
8
+ enableGitHubIntegration: boolean;
9
+ enableDebugMode: boolean;
10
+ worktreeNamingPattern: string;
11
+ }
12
+
13
+ export interface SessionData {
14
+ lastWorktreePath: string | null;
15
+ lastBranch: string | null;
16
+ lastUsedTool?: string;
17
+ timestamp: number;
18
+ repositoryRoot: string;
19
+ }
20
+
21
+ const DEFAULT_CONFIG: AppConfig = {
22
+ defaultBaseBranch: "main",
23
+ skipPermissions: false,
24
+ enableGitHubIntegration: true,
25
+ enableDebugMode: false,
26
+ worktreeNamingPattern: "{repo}-{branch}",
27
+ };
28
+
29
+ /**
30
+ * 設定ファイルを読み込む
31
+ */
32
+ export async function loadConfig(): Promise<AppConfig> {
33
+ const configPaths = [
34
+ path.join(process.cwd(), ".gwt.json"),
35
+ path.join(process.cwd(), ".claude-worktree.json"), // 後方互換性
36
+ path.join(homedir(), ".config", "gwt", "config.json"),
37
+ path.join(homedir(), ".config", "claude-worktree", "config.json"), // 後方互換性
38
+ path.join(homedir(), ".gwt.json"),
39
+ path.join(homedir(), ".claude-worktree.json"), // 後方互換性
40
+ ];
41
+
42
+ for (const configPath of configPaths) {
43
+ try {
44
+ const content = await readFile(configPath, "utf-8");
45
+ const userConfig = JSON.parse(content);
46
+ return { ...DEFAULT_CONFIG, ...userConfig };
47
+ } catch (error) {
48
+ // 設定ファイルが見つからない場合は次を試す
49
+ if (process.env.DEBUG_CONFIG) {
50
+ console.error(
51
+ `Failed to load config from ${configPath}:`,
52
+ error instanceof Error ? error.message : String(error),
53
+ );
54
+ }
55
+ }
56
+ }
57
+
58
+ // 環境変数からも読み込み
59
+ return {
60
+ ...DEFAULT_CONFIG,
61
+ defaultBaseBranch:
62
+ process.env.CLAUDE_WORKTREE_BASE_BRANCH ||
63
+ DEFAULT_CONFIG.defaultBaseBranch,
64
+ skipPermissions: process.env.CLAUDE_WORKTREE_SKIP_PERMISSIONS === "true",
65
+ enableGitHubIntegration: process.env.CLAUDE_WORKTREE_GITHUB !== "false",
66
+ enableDebugMode:
67
+ process.env.DEBUG_CLEANUP === "true" || process.env.DEBUG === "true",
68
+ };
69
+ }
70
+
71
+ /**
72
+ * 設定値を取得する
73
+ */
74
+ let cachedConfig: AppConfig | null = null;
75
+
76
+ export async function getConfig(): Promise<AppConfig> {
77
+ if (!cachedConfig) {
78
+ cachedConfig = await loadConfig();
79
+ }
80
+ return cachedConfig;
81
+ }
82
+
83
+ export function resetConfigCache(): void {
84
+ cachedConfig = null;
85
+ }
86
+
87
+ /**
88
+ * セッションデータの保存・読み込み
89
+ */
90
+ function getSessionFilePath(repositoryRoot: string): string {
91
+ const sessionDir = path.join(
92
+ homedir(),
93
+ ".config",
94
+ "gwt",
95
+ "sessions",
96
+ );
97
+ const repoName = path.basename(repositoryRoot);
98
+ const repoHash = Buffer.from(repositoryRoot)
99
+ .toString("base64")
100
+ .replace(/[/+=]/g, "_");
101
+ return path.join(sessionDir, `${repoName}_${repoHash}.json`);
102
+ }
103
+
104
+ export async function saveSession(sessionData: SessionData): Promise<void> {
105
+ try {
106
+ const sessionPath = getSessionFilePath(sessionData.repositoryRoot);
107
+ const sessionDir = path.dirname(sessionPath);
108
+
109
+ // ディレクトリを作成
110
+ await mkdir(sessionDir, { recursive: true });
111
+
112
+ await writeFile(sessionPath, JSON.stringify(sessionData, null, 2), "utf-8");
113
+ } catch (error) {
114
+ // セッション保存の失敗は致命的ではないため、エラーをログに出力するのみ
115
+ if (process.env.DEBUG_SESSION) {
116
+ console.error(
117
+ "Failed to save session:",
118
+ error instanceof Error ? error.message : String(error),
119
+ );
120
+ }
121
+ }
122
+ }
123
+
124
+ export async function loadSession(
125
+ repositoryRoot: string,
126
+ ): Promise<SessionData | null> {
127
+ try {
128
+ const sessionPath = getSessionFilePath(repositoryRoot);
129
+ const content = await readFile(sessionPath, "utf-8");
130
+ const sessionData = JSON.parse(content) as SessionData;
131
+
132
+ // セッションが24時間以内のもののみ有効とする
133
+ const now = Date.now();
134
+ const sessionAge = now - sessionData.timestamp;
135
+ const maxAge = 24 * 60 * 60 * 1000; // 24時間
136
+
137
+ if (sessionAge > maxAge) {
138
+ return null;
139
+ }
140
+
141
+ return sessionData;
142
+ } catch (error) {
143
+ if (process.env.DEBUG_SESSION) {
144
+ console.error(
145
+ "Failed to load session:",
146
+ error instanceof Error ? error.message : String(error),
147
+ );
148
+ }
149
+ return null;
150
+ }
151
+ }
152
+
153
+ export async function getAllSessions(): Promise<SessionData[]> {
154
+ try {
155
+ const sessionDir = path.join(
156
+ homedir(),
157
+ ".config",
158
+ "gwt",
159
+ "sessions",
160
+ );
161
+ const { readdir } = await import("node:fs/promises");
162
+
163
+ const files = await readdir(sessionDir);
164
+ const sessions: SessionData[] = [];
165
+ const now = Date.now();
166
+ const maxAge = 24 * 60 * 60 * 1000; // 24時間
167
+
168
+ for (const file of files) {
169
+ if (!file.endsWith(".json")) continue;
170
+
171
+ try {
172
+ const filePath = path.join(sessionDir, file);
173
+ const content = await readFile(filePath, "utf-8");
174
+ const sessionData = JSON.parse(content) as SessionData;
175
+
176
+ // 有効期限内のセッションのみ
177
+ const sessionAge = now - sessionData.timestamp;
178
+ if (sessionAge <= maxAge) {
179
+ sessions.push(sessionData);
180
+ }
181
+ } catch (error) {
182
+ if (process.env.DEBUG_SESSION) {
183
+ console.error(
184
+ `Failed to load session file ${file}:`,
185
+ error instanceof Error ? error.message : String(error),
186
+ );
187
+ }
188
+ }
189
+ }
190
+
191
+ // 最新のものから順にソート
192
+ sessions.sort((a, b) => b.timestamp - a.timestamp);
193
+
194
+ return sessions;
195
+ } catch (error) {
196
+ if (process.env.DEBUG_SESSION) {
197
+ console.error(
198
+ "Failed to get all sessions:",
199
+ error instanceof Error ? error.message : String(error),
200
+ );
201
+ }
202
+ return [];
203
+ }
204
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * カスタムツール設定管理
3
+ *
4
+ * ~/.gwt/tools.jsonから設定を読み込み、
5
+ * ビルトインツールと統合して管理します。
6
+ */
7
+
8
+ import { homedir } from "node:os";
9
+ import path from "node:path";
10
+ import { readFile, writeFile, mkdir, rename, access, cp } from "node:fs/promises";
11
+ import type {
12
+ ToolsConfig,
13
+ CustomAITool,
14
+ AIToolConfig,
15
+ } from "../types/tools.js";
16
+ import { BUILTIN_TOOLS } from "./builtin-tools.js";
17
+
18
+ /**
19
+ * ツール設定ファイルのパス
20
+ * 環境変数の優先順位: GWT_HOME > CLAUDE_WORKTREE_HOME (後方互換性) > ホームディレクトリ
21
+ */
22
+ export const WORKTREE_HOME =
23
+ (process.env.GWT_HOME && process.env.GWT_HOME.trim().length > 0)
24
+ ? process.env.GWT_HOME
25
+ : (process.env.CLAUDE_WORKTREE_HOME && process.env.CLAUDE_WORKTREE_HOME.trim().length > 0)
26
+ ? process.env.CLAUDE_WORKTREE_HOME
27
+ : homedir();
28
+
29
+ const LEGACY_CONFIG_DIR = path.join(homedir(), ".claude-worktree");
30
+ export const CONFIG_DIR = path.join(WORKTREE_HOME, ".gwt");
31
+ export const TOOLS_CONFIG_PATH = path.join(CONFIG_DIR, "tools.json");
32
+ const TEMP_CONFIG_PATH = `${TOOLS_CONFIG_PATH}.tmp`;
33
+
34
+ /**
35
+ * レガシー設定ディレクトリから新しいディレクトリへ移行
36
+ */
37
+ async function migrateLegacyConfig(): Promise<void> {
38
+ try {
39
+ // 新しいディレクトリが既に存在する場合は移行不要
40
+ try {
41
+ await access(CONFIG_DIR);
42
+ return;
43
+ } catch {
44
+ // 新しいディレクトリが存在しない場合は続行
45
+ }
46
+
47
+ // レガシーディレクトリの存在を確認
48
+ try {
49
+ await access(LEGACY_CONFIG_DIR);
50
+ } catch {
51
+ // レガシーディレクトリも存在しない場合は移行不要
52
+ return;
53
+ }
54
+
55
+ // レガシーディレクトリを新しいディレクトリにコピー
56
+ await mkdir(path.dirname(CONFIG_DIR), { recursive: true });
57
+ await cp(LEGACY_CONFIG_DIR, CONFIG_DIR, { recursive: true });
58
+ console.log(`✅ Migrated configuration from ${LEGACY_CONFIG_DIR} to ${CONFIG_DIR}`);
59
+ } catch (error) {
60
+ // 移行に失敗しても継続(エラーログのみ)
61
+ if (process.env.DEBUG) {
62
+ console.error("Failed to migrate legacy config:", error);
63
+ }
64
+ }
65
+ }
66
+
67
+ const DEFAULT_CONFIG: ToolsConfig = {
68
+ version: "1.0.0",
69
+ env: {},
70
+ customTools: [],
71
+ };
72
+
73
+ /**
74
+ * ツール設定を読み込む
75
+ *
76
+ * ~/.gwt/tools.jsonから設定を読み込みます。
77
+ * ファイルが存在しない場合は空配列を返します。
78
+ *
79
+ * @returns ToolsConfig
80
+ * @throws JSON構文エラー時
81
+ */
82
+ export async function loadToolsConfig(): Promise<ToolsConfig> {
83
+ // 最初の呼び出し時にレガシー設定の移行を試行
84
+ await migrateLegacyConfig();
85
+
86
+ try {
87
+ const content = await readFile(TOOLS_CONFIG_PATH, "utf-8");
88
+ const config = JSON.parse(content) as ToolsConfig;
89
+
90
+ // 検証
91
+ validateToolsConfig(config);
92
+
93
+ return {
94
+ ...config,
95
+ env: config.env ?? {},
96
+ };
97
+ } catch (error) {
98
+ // ファイルが存在しない場合は空配列を返す
99
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
100
+ return { ...DEFAULT_CONFIG };
101
+ }
102
+
103
+ // JSON構文エラーの場合
104
+ if (error instanceof SyntaxError) {
105
+ throw new Error(
106
+ `Failed to parse tools.json: ${error.message}\n` +
107
+ `Please check the JSON syntax in ${TOOLS_CONFIG_PATH}`,
108
+ );
109
+ }
110
+
111
+ // その他のエラー
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * ToolsConfig全体を検証
118
+ *
119
+ * @param config - 検証対象の設定
120
+ * @throws 検証エラー時
121
+ */
122
+ function validateToolsConfig(config: ToolsConfig): void {
123
+ // versionフィールドの検証
124
+ if (!config.version || typeof config.version !== "string") {
125
+ throw new Error("version field is required and must be a string");
126
+ }
127
+
128
+ // customToolsフィールドの検証
129
+ if (!Array.isArray(config.customTools)) {
130
+ throw new Error("customTools field must be an array");
131
+ }
132
+
133
+ if (config.env && typeof config.env !== "object") {
134
+ throw new Error("env field must be an object map of key/value pairs");
135
+ }
136
+
137
+ if (config.env) {
138
+ for (const [key, value] of Object.entries(config.env)) {
139
+ if (!key || typeof key !== "string") {
140
+ throw new Error("env keys must be non-empty strings");
141
+ }
142
+ if (typeof value !== "string") {
143
+ throw new Error(`env value for key "${key}" must be a string`);
144
+ }
145
+ }
146
+ }
147
+
148
+ // 各ツールの検証
149
+ const seenIds = new Set<string>();
150
+ for (const tool of config.customTools) {
151
+ validateCustomAITool(tool);
152
+
153
+ // ID重複チェック
154
+ if (seenIds.has(tool.id)) {
155
+ throw new Error(
156
+ `Duplicate tool ID found: "${tool.id}"\n` +
157
+ `Each tool must have a unique ID in ${TOOLS_CONFIG_PATH}`,
158
+ );
159
+ }
160
+ seenIds.add(tool.id);
161
+
162
+ // ビルトインツールとのID重複チェック
163
+ const builtinIds = BUILTIN_TOOLS.map((t) => t.id);
164
+ if (builtinIds.includes(tool.id)) {
165
+ throw new Error(
166
+ `Tool ID "${tool.id}" conflicts with builtin tool\n` +
167
+ `Builtin tool IDs: ${builtinIds.join(", ")}`,
168
+ );
169
+ }
170
+ }
171
+ }
172
+
173
+ export async function saveToolsConfig(config: ToolsConfig): Promise<void> {
174
+ const normalized: ToolsConfig = {
175
+ version: config.version,
176
+ updatedAt: config.updatedAt ?? new Date().toISOString(),
177
+ env: config.env ?? {},
178
+ customTools: config.customTools,
179
+ };
180
+
181
+ validateToolsConfig(normalized);
182
+
183
+ await mkdir(CONFIG_DIR, { recursive: true });
184
+ const payload = JSON.stringify(normalized, null, 2);
185
+ await writeFile(TEMP_CONFIG_PATH, payload, { mode: 0o600 });
186
+ await rename(TEMP_CONFIG_PATH, TOOLS_CONFIG_PATH);
187
+ }
188
+
189
+ export async function getSharedEnvironment(): Promise<Record<string, string>> {
190
+ const config = await loadToolsConfig();
191
+ return { ...(config.env ?? {}) };
192
+ }
193
+
194
+ /**
195
+ * CustomAITool単体を検証
196
+ *
197
+ * @param tool - 検証対象のツール
198
+ * @throws 検証エラー時
199
+ */
200
+ function validateCustomAITool(tool: unknown): asserts tool is CustomAITool {
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ const t = tool as any;
203
+
204
+ // 必須フィールドの存在チェック
205
+ const requiredFields = ["id", "displayName", "type", "command", "modeArgs"];
206
+ for (const field of requiredFields) {
207
+ if (!t[field]) {
208
+ throw new Error(
209
+ `Required field "${field}" is missing in tool configuration`,
210
+ );
211
+ }
212
+ }
213
+
214
+ // id形式の検証(小文字英数字とハイフンのみ)
215
+ if (!/^[a-z0-9-]+$/.test(t.id)) {
216
+ throw new Error(
217
+ `Invalid tool ID format: "${t.id}"\n` +
218
+ `Tool ID must contain only lowercase letters, numbers, and hyphens (pattern: ^[a-z0-9-]+$)`,
219
+ );
220
+ }
221
+
222
+ // typeフィールドの値検証
223
+ const validTypes = ["path", "bunx", "command"];
224
+ if (!validTypes.includes(t.type)) {
225
+ throw new Error(
226
+ `Invalid type: "${t.type}"\n` +
227
+ `Type must be one of: ${validTypes.join(", ")}`,
228
+ );
229
+ }
230
+
231
+ // type='path'の場合、commandが絶対パスであることを確認
232
+ if (t.type === "path" && !path.isAbsolute(t.command)) {
233
+ throw new Error(
234
+ `For type="path", command must be an absolute path: "${t.command}"`,
235
+ );
236
+ }
237
+
238
+ // modeArgsの検証(少なくとも1つのモードが定義されている)
239
+ if (!t.modeArgs.normal && !t.modeArgs.continue && !t.modeArgs.resume) {
240
+ throw new Error(
241
+ `modeArgs must define at least one mode (normal, continue, or resume) for tool "${t.id}"`,
242
+ );
243
+ }
244
+ }
245
+
246
+ /**
247
+ * IDでツールを検索
248
+ *
249
+ * @param id - ツールID
250
+ * @returns ツール設定(見つからない場合はundefined)
251
+ */
252
+ export async function getToolById(
253
+ id: string,
254
+ ): Promise<CustomAITool | undefined> {
255
+ // ビルトインツールから検索
256
+ const builtinTool = BUILTIN_TOOLS.find((t) => t.id === id);
257
+ if (builtinTool) {
258
+ return builtinTool;
259
+ }
260
+
261
+ // カスタムツールから検索
262
+ const config = await loadToolsConfig();
263
+ return config.customTools.find((t) => t.id === id);
264
+ }
265
+
266
+ /**
267
+ * すべてのツール(ビルトイン+カスタム)を取得
268
+ *
269
+ * @returns AIToolConfigの配列
270
+ */
271
+ export async function getAllTools(): Promise<AIToolConfig[]> {
272
+ const config = await loadToolsConfig();
273
+
274
+ // ビルトインツールをAIToolConfig形式に変換
275
+ const builtinConfigs: AIToolConfig[] = BUILTIN_TOOLS.map((tool) => ({
276
+ id: tool.id,
277
+ displayName: tool.displayName,
278
+ ...(tool.icon ? { icon: tool.icon } : {}),
279
+ isBuiltin: true,
280
+ }));
281
+
282
+ // カスタムツールをAIToolConfig形式に変換
283
+ const customConfigs: AIToolConfig[] = config.customTools.map((tool) => ({
284
+ id: tool.id,
285
+ displayName: tool.displayName,
286
+ ...(tool.icon ? { icon: tool.icon } : {}),
287
+ isBuiltin: false,
288
+ customConfig: tool,
289
+ }));
290
+
291
+ // ビルトイン + カスタム の順で統合
292
+ return [...builtinConfigs, ...customConfigs];
293
+ }