@akiojin/gwt 3.0.0 → 3.1.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/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +19 -4
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/parts/Header.d.ts +8 -0
- package/dist/cli/ui/components/parts/Header.d.ts.map +1 -1
- package/dist/cli/ui/components/parts/Header.js +7 -2
- package/dist/cli/ui/components/parts/Header.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +16 -8
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts +16 -0
- package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js +576 -0
- package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js.map +1 -0
- package/dist/cli/ui/hooks/useProfiles.d.ts +41 -0
- package/dist/cli/ui/hooks/useProfiles.d.ts.map +1 -0
- package/dist/cli/ui/hooks/useProfiles.js +136 -0
- package/dist/cli/ui/hooks/useProfiles.js.map +1 -0
- package/dist/cli/ui/types.d.ts +1 -1
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +2 -2
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/config/profiles.d.ts +94 -0
- package/dist/config/profiles.d.ts.map +1 -0
- package/dist/config/profiles.js +287 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/tools.d.ts +10 -0
- package/dist/config/tools.d.ts.map +1 -1
- package/dist/config/tools.js +19 -2
- package/dist/config/tools.js.map +1 -1
- package/dist/types/profiles.d.ts +54 -0
- package/dist/types/profiles.d.ts.map +1 -0
- package/dist/types/profiles.js +33 -0
- package/dist/types/profiles.js.map +1 -0
- package/dist/web/client/src/components/ui/alert.d.ts +1 -1
- package/dist/web/client/src/components/ui/badge.d.ts +1 -1
- package/package.json +4 -3
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +40 -1
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +1 -1
- package/src/cli/ui/components/App.tsx +49 -36
- package/src/cli/ui/components/parts/Header.tsx +16 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +17 -7
- package/src/cli/ui/components/screens/EnvironmentProfileScreen.tsx +924 -0
- package/src/cli/ui/hooks/useProfiles.ts +211 -0
- package/src/cli/ui/types.ts +2 -1
- package/src/cli/ui/utils/branchFormatter.ts +2 -3
- package/src/config/profiles.ts +362 -0
- package/src/config/tools.ts +20 -2
- package/src/types/profiles.ts +64 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* プロファイル管理フック
|
|
3
|
+
*
|
|
4
|
+
* 環境変数プロファイルの読み込み・操作を提供します。
|
|
5
|
+
* @see specs/SPEC-dafff079/spec.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback } from "react";
|
|
9
|
+
import {
|
|
10
|
+
loadProfiles,
|
|
11
|
+
setActiveProfile as setActiveProfileApi,
|
|
12
|
+
createProfile as createProfileApi,
|
|
13
|
+
updateProfile as updateProfileApi,
|
|
14
|
+
deleteProfile as deleteProfileApi,
|
|
15
|
+
} from "../../../config/profiles.js";
|
|
16
|
+
import type {
|
|
17
|
+
ProfilesConfig,
|
|
18
|
+
EnvironmentProfile,
|
|
19
|
+
} from "../../../types/profiles.js";
|
|
20
|
+
|
|
21
|
+
export interface UseProfilesResult {
|
|
22
|
+
/** プロファイル設定(ロード中はnull) */
|
|
23
|
+
profiles: ProfilesConfig | null;
|
|
24
|
+
/** ロード中フラグ */
|
|
25
|
+
loading: boolean;
|
|
26
|
+
/** エラー(なければnull) */
|
|
27
|
+
error: Error | null;
|
|
28
|
+
/** アクティブなプロファイル名 */
|
|
29
|
+
activeProfileName: string | null;
|
|
30
|
+
/** アクティブなプロファイル設定 */
|
|
31
|
+
activeProfile: EnvironmentProfile | null;
|
|
32
|
+
|
|
33
|
+
/** プロファイル設定を再読み込み */
|
|
34
|
+
refresh: () => Promise<void>;
|
|
35
|
+
/** アクティブなプロファイルを設定 */
|
|
36
|
+
setActiveProfile: (name: string | null) => Promise<void>;
|
|
37
|
+
/** 新しいプロファイルを作成 */
|
|
38
|
+
createProfile: (name: string, profile: EnvironmentProfile) => Promise<void>;
|
|
39
|
+
/** プロファイルを更新 */
|
|
40
|
+
updateProfile: (
|
|
41
|
+
name: string,
|
|
42
|
+
updates: Partial<EnvironmentProfile>,
|
|
43
|
+
) => Promise<void>;
|
|
44
|
+
/** プロファイルを削除 */
|
|
45
|
+
deleteProfile: (name: string) => Promise<void>;
|
|
46
|
+
/** 環境変数を更新 */
|
|
47
|
+
updateEnvVar: (
|
|
48
|
+
profileName: string,
|
|
49
|
+
key: string,
|
|
50
|
+
value: string,
|
|
51
|
+
) => Promise<void>;
|
|
52
|
+
/** 環境変数を削除 */
|
|
53
|
+
deleteEnvVar: (profileName: string, key: string) => Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* プロファイル管理フック
|
|
58
|
+
*
|
|
59
|
+
* コンポーネントでプロファイルの読み込みと操作を行うためのフック。
|
|
60
|
+
* 初回マウント時に自動的にプロファイル設定を読み込みます。
|
|
61
|
+
*/
|
|
62
|
+
export function useProfiles(): UseProfilesResult {
|
|
63
|
+
const [profiles, setProfiles] = useState<ProfilesConfig | null>(null);
|
|
64
|
+
const [loading, setLoading] = useState(true);
|
|
65
|
+
const [error, setError] = useState<Error | null>(null);
|
|
66
|
+
|
|
67
|
+
// プロファイル設定を読み込む
|
|
68
|
+
const refresh = useCallback(async () => {
|
|
69
|
+
try {
|
|
70
|
+
setLoading(true);
|
|
71
|
+
setError(null);
|
|
72
|
+
const config = await loadProfiles();
|
|
73
|
+
setProfiles(config);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
76
|
+
} finally {
|
|
77
|
+
setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
// 初回マウント時に読み込み
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
refresh();
|
|
84
|
+
}, [refresh]);
|
|
85
|
+
|
|
86
|
+
// アクティブなプロファイル名
|
|
87
|
+
const activeProfileName = profiles?.activeProfile ?? null;
|
|
88
|
+
|
|
89
|
+
// アクティブなプロファイル設定
|
|
90
|
+
const activeProfile = activeProfileName
|
|
91
|
+
? (profiles?.profiles[activeProfileName] ?? null)
|
|
92
|
+
: null;
|
|
93
|
+
|
|
94
|
+
// アクティブなプロファイルを設定
|
|
95
|
+
const setActiveProfile = useCallback(
|
|
96
|
+
async (name: string | null) => {
|
|
97
|
+
try {
|
|
98
|
+
await setActiveProfileApi(name);
|
|
99
|
+
await refresh();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[refresh],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// プロファイルを作成
|
|
109
|
+
const createProfile = useCallback(
|
|
110
|
+
async (name: string, profile: EnvironmentProfile) => {
|
|
111
|
+
try {
|
|
112
|
+
await createProfileApi(name, profile);
|
|
113
|
+
await refresh();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
[refresh],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// プロファイルを更新
|
|
123
|
+
const updateProfile = useCallback(
|
|
124
|
+
async (name: string, updates: Partial<EnvironmentProfile>) => {
|
|
125
|
+
try {
|
|
126
|
+
await updateProfileApi(name, updates);
|
|
127
|
+
await refresh();
|
|
128
|
+
} catch (err) {
|
|
129
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
[refresh],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// プロファイルを削除
|
|
137
|
+
const deleteProfile = useCallback(
|
|
138
|
+
async (name: string) => {
|
|
139
|
+
try {
|
|
140
|
+
await deleteProfileApi(name);
|
|
141
|
+
await refresh();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
[refresh],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// 環境変数を更新
|
|
151
|
+
const updateEnvVar = useCallback(
|
|
152
|
+
async (profileName: string, key: string, value: string) => {
|
|
153
|
+
if (!profiles) {
|
|
154
|
+
const err = new Error("Profiles not loaded");
|
|
155
|
+
setError(err);
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!profiles.profiles[profileName]) {
|
|
160
|
+
const err = new Error(`Profile "${profileName}" does not exist`);
|
|
161
|
+
setError(err);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const existingProfile = profiles.profiles[profileName];
|
|
166
|
+
const newEnv = { ...existingProfile.env, [key]: value };
|
|
167
|
+
|
|
168
|
+
await updateProfile(profileName, { env: newEnv });
|
|
169
|
+
},
|
|
170
|
+
[profiles, updateProfile],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// 環境変数を削除
|
|
174
|
+
const deleteEnvVar = useCallback(
|
|
175
|
+
async (profileName: string, key: string) => {
|
|
176
|
+
if (!profiles) {
|
|
177
|
+
const err = new Error("Profiles not loaded");
|
|
178
|
+
setError(err);
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!profiles.profiles[profileName]) {
|
|
183
|
+
const err = new Error(`Profile "${profileName}" does not exist`);
|
|
184
|
+
setError(err);
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const existingProfile = profiles.profiles[profileName];
|
|
189
|
+
const newEnv = { ...existingProfile.env };
|
|
190
|
+
delete newEnv[key];
|
|
191
|
+
|
|
192
|
+
await updateProfile(profileName, { env: newEnv });
|
|
193
|
+
},
|
|
194
|
+
[profiles, updateProfile],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
profiles,
|
|
199
|
+
loading,
|
|
200
|
+
error,
|
|
201
|
+
activeProfileName,
|
|
202
|
+
activeProfile,
|
|
203
|
+
refresh,
|
|
204
|
+
setActiveProfile,
|
|
205
|
+
createProfile,
|
|
206
|
+
updateProfile,
|
|
207
|
+
deleteProfile,
|
|
208
|
+
updateEnvVar,
|
|
209
|
+
deleteEnvVar,
|
|
210
|
+
};
|
|
211
|
+
}
|
package/src/cli/ui/types.ts
CHANGED
|
@@ -191,7 +191,8 @@ export type ScreenType =
|
|
|
191
191
|
| "session-selector"
|
|
192
192
|
| "execution-mode-selector"
|
|
193
193
|
| "batch-merge-progress"
|
|
194
|
-
| "batch-merge-result"
|
|
194
|
+
| "batch-merge-result"
|
|
195
|
+
| "environment-profile";
|
|
195
196
|
|
|
196
197
|
/**
|
|
197
198
|
* Branch action types for action selector screen
|
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
WorktreeInfo,
|
|
7
7
|
} from "../types.js";
|
|
8
8
|
import stringWidth from "string-width";
|
|
9
|
-
import chalk from "chalk";
|
|
10
9
|
|
|
11
10
|
// Icon mappings
|
|
12
11
|
const branchIcons: Record<BranchType, string> = {
|
|
@@ -21,7 +20,7 @@ const branchIcons: Record<BranchType, string> = {
|
|
|
21
20
|
|
|
22
21
|
const worktreeIcons: Record<Exclude<WorktreeStatus, undefined>, string> = {
|
|
23
22
|
active: "🟢",
|
|
24
|
-
inaccessible: "
|
|
23
|
+
inaccessible: "🔴",
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
const changeIcons = {
|
|
@@ -67,7 +66,7 @@ const iconWidthOverrides: Record<string, number> = {
|
|
|
67
66
|
// Worktree status icons
|
|
68
67
|
"🟢": 2,
|
|
69
68
|
"⚪": 2,
|
|
70
|
-
"
|
|
69
|
+
"🔴": 2,
|
|
71
70
|
// Change status icons
|
|
72
71
|
"👉": 1,
|
|
73
72
|
"💾": 1,
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 環境変数プロファイル設定管理
|
|
3
|
+
*
|
|
4
|
+
* ~/.gwt/profiles.yamlからプロファイル設定を読み込み、
|
|
5
|
+
* AIツール起動時の環境変数を管理します。
|
|
6
|
+
*
|
|
7
|
+
* @see specs/SPEC-dafff079/spec.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
mkdir,
|
|
12
|
+
open,
|
|
13
|
+
readFile,
|
|
14
|
+
rename,
|
|
15
|
+
stat,
|
|
16
|
+
unlink,
|
|
17
|
+
writeFile,
|
|
18
|
+
} from "node:fs/promises";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_PROFILES_CONFIG,
|
|
24
|
+
isValidProfileName,
|
|
25
|
+
type EnvironmentProfile,
|
|
26
|
+
type ProfilesConfig,
|
|
27
|
+
} from "../types/profiles.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 設定ディレクトリのパスを取得
|
|
31
|
+
*
|
|
32
|
+
* 環境変数の優先順位: GWT_HOME > CLAUDE_WORKTREE_HOME (後方互換性) > ホームディレクトリ
|
|
33
|
+
*/
|
|
34
|
+
function getConfigDir(): string {
|
|
35
|
+
const worktreeHome =
|
|
36
|
+
process.env.GWT_HOME && process.env.GWT_HOME.trim().length > 0
|
|
37
|
+
? process.env.GWT_HOME
|
|
38
|
+
: process.env.CLAUDE_WORKTREE_HOME &&
|
|
39
|
+
process.env.CLAUDE_WORKTREE_HOME.trim().length > 0
|
|
40
|
+
? process.env.CLAUDE_WORKTREE_HOME
|
|
41
|
+
: homedir();
|
|
42
|
+
return path.join(worktreeHome, ".gwt");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* プロファイル設定ファイルのパスを取得
|
|
47
|
+
*/
|
|
48
|
+
function getProfilesConfigPath(): string {
|
|
49
|
+
return path.join(getConfigDir(), "profiles.yaml");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const LOCK_RETRY_INTERVAL_MS = 50;
|
|
53
|
+
const LOCK_TIMEOUT_MS = 3000;
|
|
54
|
+
const LOCK_STALE_MS = 30_000;
|
|
55
|
+
|
|
56
|
+
function getProfilesLockPath(): string {
|
|
57
|
+
return `${getProfilesConfigPath()}.lock`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function withProfilesLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
61
|
+
const lockPath = getProfilesLockPath();
|
|
62
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
63
|
+
|
|
64
|
+
const startedAt = Date.now();
|
|
65
|
+
|
|
66
|
+
while (true) {
|
|
67
|
+
try {
|
|
68
|
+
const handle = await open(lockPath, "wx");
|
|
69
|
+
try {
|
|
70
|
+
await handle.writeFile(
|
|
71
|
+
JSON.stringify(
|
|
72
|
+
{ pid: process.pid, createdAt: new Date().toISOString() },
|
|
73
|
+
null,
|
|
74
|
+
2,
|
|
75
|
+
),
|
|
76
|
+
"utf-8",
|
|
77
|
+
);
|
|
78
|
+
} finally {
|
|
79
|
+
await handle.close();
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (!(error instanceof Error) || !("code" in error)) {
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (error.code !== "EEXIST") {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const lockStat = await stat(lockPath);
|
|
93
|
+
const isStale = Date.now() - lockStat.mtimeMs > LOCK_STALE_MS;
|
|
94
|
+
if (isStale) {
|
|
95
|
+
await unlink(lockPath).catch(() => {
|
|
96
|
+
// 他プロセスが先に削除した可能性があるため無視する
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// lock の stat/unlink が失敗してもリトライする
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (Date.now() - startedAt > LOCK_TIMEOUT_MS) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Timeout acquiring profiles lock: ${lockPath}. Another gwt instance may be running.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await new Promise((resolve) =>
|
|
111
|
+
setTimeout(resolve, LOCK_RETRY_INTERVAL_MS),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return await fn();
|
|
118
|
+
} finally {
|
|
119
|
+
await unlink(lockPath).catch(() => {
|
|
120
|
+
// lock が既に削除されている/削除できない場合は無視する
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function mutateProfiles(
|
|
126
|
+
mutator: (config: ProfilesConfig) => void | Promise<void>,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
await withProfilesLock(async () => {
|
|
129
|
+
const config = await loadProfiles();
|
|
130
|
+
await mutator(config);
|
|
131
|
+
await saveProfiles(config);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* プロファイル設定ファイルのパス(後方互換性のためエクスポート)
|
|
137
|
+
*
|
|
138
|
+
* @deprecated 内部では getProfilesConfigPath() を使用してください。
|
|
139
|
+
* この定数はモジュールロード時に評価されるため、
|
|
140
|
+
* 実行中に環境変数(GWT_HOME/CLAUDE_WORKTREE_HOME)を変更しても反映されません。
|
|
141
|
+
*/
|
|
142
|
+
export const PROFILES_CONFIG_PATH = getProfilesConfigPath();
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* プロファイル設定を読み込む
|
|
146
|
+
*
|
|
147
|
+
* ~/.gwt/profiles.yamlから設定を読み込みます。
|
|
148
|
+
* ファイルが存在しない場合はデフォルト設定を返します。
|
|
149
|
+
*
|
|
150
|
+
* @returns ProfilesConfig
|
|
151
|
+
* @throws YAML構文エラー時
|
|
152
|
+
*/
|
|
153
|
+
export async function loadProfiles(): Promise<ProfilesConfig> {
|
|
154
|
+
try {
|
|
155
|
+
const configPath = getProfilesConfigPath();
|
|
156
|
+
const content = await readFile(configPath, "utf-8");
|
|
157
|
+
const config = parseYaml(content) as ProfilesConfig;
|
|
158
|
+
|
|
159
|
+
// 基本的な検証
|
|
160
|
+
if (!config.version || typeof config.version !== "string") {
|
|
161
|
+
throw new Error("version field is required and must be a string");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (config.profiles && typeof config.profiles !== "object") {
|
|
165
|
+
throw new Error("profiles field must be an object");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
config.activeProfile !== null &&
|
|
170
|
+
config.activeProfile !== undefined &&
|
|
171
|
+
typeof config.activeProfile !== "string"
|
|
172
|
+
) {
|
|
173
|
+
throw new Error("activeProfile field must be a string or null");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
version: config.version,
|
|
178
|
+
activeProfile: config.activeProfile ?? null,
|
|
179
|
+
profiles: config.profiles ?? {},
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// ファイルが存在しない場合はデフォルト設定を返す
|
|
183
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
184
|
+
// DEFAULT_PROFILES_CONFIG は不変オブジェクトのため、
|
|
185
|
+
// 呼び出し側が編集できるように新しい参照を返す
|
|
186
|
+
return {
|
|
187
|
+
version: DEFAULT_PROFILES_CONFIG.version,
|
|
188
|
+
activeProfile: null,
|
|
189
|
+
profiles: {},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// YAML構文エラーの場合
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* プロファイル設定を保存する
|
|
200
|
+
*
|
|
201
|
+
* ~/.gwt/profiles.yamlに設定を保存します。
|
|
202
|
+
* ディレクトリが存在しない場合は自動的に作成します。
|
|
203
|
+
*
|
|
204
|
+
* @param config - 保存するプロファイル設定
|
|
205
|
+
*/
|
|
206
|
+
export async function saveProfiles(config: ProfilesConfig): Promise<void> {
|
|
207
|
+
const configDir = getConfigDir();
|
|
208
|
+
const configPath = getProfilesConfigPath();
|
|
209
|
+
const tempPath = `${configPath}.tmp`;
|
|
210
|
+
|
|
211
|
+
await mkdir(configDir, { recursive: true });
|
|
212
|
+
|
|
213
|
+
const yaml = stringifyYaml(config);
|
|
214
|
+
await writeFile(tempPath, yaml, { mode: 0o600 });
|
|
215
|
+
try {
|
|
216
|
+
await rename(tempPath, configPath);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
await unlink(tempPath).catch(() => {
|
|
219
|
+
// 一時ファイルが存在しない/削除できない場合は無視する
|
|
220
|
+
});
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* アクティブなプロファイルを取得
|
|
227
|
+
*
|
|
228
|
+
* 現在選択されているプロファイルの設定を返します。
|
|
229
|
+
* プロファイルが選択されていない場合、または選択されたプロファイルが
|
|
230
|
+
* 存在しない場合はnullを返します。
|
|
231
|
+
*
|
|
232
|
+
* @returns アクティブなプロファイル、またはnull
|
|
233
|
+
*/
|
|
234
|
+
export async function getActiveProfile(): Promise<EnvironmentProfile | null> {
|
|
235
|
+
const config = await loadProfiles();
|
|
236
|
+
|
|
237
|
+
if (!config.activeProfile) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const profile = config.profiles[config.activeProfile];
|
|
242
|
+
return profile ?? null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* アクティブなプロファイル名を取得
|
|
247
|
+
*
|
|
248
|
+
* @returns アクティブなプロファイル名、またはnull
|
|
249
|
+
*/
|
|
250
|
+
export async function getActiveProfileName(): Promise<string | null> {
|
|
251
|
+
const config = await loadProfiles();
|
|
252
|
+
return config.activeProfile;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* アクティブなプロファイルを設定
|
|
257
|
+
*
|
|
258
|
+
* @param profileName - 設定するプロファイル名(nullで無効化)
|
|
259
|
+
* @throws プロファイルが存在しない場合
|
|
260
|
+
*/
|
|
261
|
+
export async function setActiveProfile(
|
|
262
|
+
profileName: string | null,
|
|
263
|
+
): Promise<void> {
|
|
264
|
+
await mutateProfiles(async (config) => {
|
|
265
|
+
if (profileName !== null && !config.profiles[profileName]) {
|
|
266
|
+
throw new Error(`Profile "${profileName}" does not exist`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
config.activeProfile = profileName;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 新しいプロファイルを作成
|
|
275
|
+
*
|
|
276
|
+
* @param name - プロファイル名
|
|
277
|
+
* @param profile - プロファイル設定
|
|
278
|
+
* @throws 既存のプロファイル名の場合
|
|
279
|
+
* @throws 無効なプロファイル名の場合
|
|
280
|
+
*/
|
|
281
|
+
export async function createProfile(
|
|
282
|
+
name: string,
|
|
283
|
+
profile: EnvironmentProfile,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
if (!isValidProfileName(name)) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Invalid profile name: "${name}". Use lowercase letters, numbers, and hyphens (must start and end with a letter or number).`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await mutateProfiles(async (config) => {
|
|
292
|
+
if (config.profiles[name]) {
|
|
293
|
+
throw new Error(`Profile "${name}" already exists`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
config.profiles[name] = profile;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* プロファイルを更新
|
|
302
|
+
*
|
|
303
|
+
* @param name - プロファイル名
|
|
304
|
+
* @param updates - 更新するフィールド(envが指定された場合は完全に置き換えられます)
|
|
305
|
+
* @throws プロファイルが存在しない場合
|
|
306
|
+
*/
|
|
307
|
+
export async function updateProfile(
|
|
308
|
+
name: string,
|
|
309
|
+
updates: Partial<EnvironmentProfile>,
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
await mutateProfiles(async (config) => {
|
|
312
|
+
if (!config.profiles[name]) {
|
|
313
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
config.profiles[name] = {
|
|
317
|
+
...config.profiles[name],
|
|
318
|
+
...updates,
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* プロファイルを削除
|
|
325
|
+
*
|
|
326
|
+
* @param name - プロファイル名
|
|
327
|
+
* @throws アクティブなプロファイルの場合
|
|
328
|
+
* @throws プロファイルが存在しない場合
|
|
329
|
+
*/
|
|
330
|
+
export async function deleteProfile(name: string): Promise<void> {
|
|
331
|
+
await mutateProfiles(async (config) => {
|
|
332
|
+
if (!config.profiles[name]) {
|
|
333
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (config.activeProfile === name) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Cannot delete active profile "${name}". Please switch to another profile first.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
delete config.profiles[name];
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* アクティブなプロファイルの環境変数を解決
|
|
348
|
+
*
|
|
349
|
+
* AIツール起動時に使用する環境変数を返します。
|
|
350
|
+
* プロファイルが選択されていない場合は空オブジェクトを返します。
|
|
351
|
+
*
|
|
352
|
+
* @returns 環境変数のRecord
|
|
353
|
+
*/
|
|
354
|
+
export async function resolveProfileEnv(): Promise<Record<string, string>> {
|
|
355
|
+
const profile = await getActiveProfile();
|
|
356
|
+
|
|
357
|
+
if (!profile) {
|
|
358
|
+
return {};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return { ...profile.env };
|
|
362
|
+
}
|
package/src/config/tools.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
AIToolConfig,
|
|
22
22
|
} from "../types/tools.js";
|
|
23
23
|
import { BUILTIN_TOOLS } from "./builtin-tools.js";
|
|
24
|
+
import { resolveProfileEnv } from "./profiles.js";
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* ツール設定ファイルのパス
|
|
@@ -196,9 +197,26 @@ export async function saveToolsConfig(config: ToolsConfig): Promise<void> {
|
|
|
196
197
|
await rename(TEMP_CONFIG_PATH, TOOLS_CONFIG_PATH);
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
/**
|
|
201
|
+
* 共有環境変数を取得
|
|
202
|
+
*
|
|
203
|
+
* AIツール起動時に適用される環境変数を返します。
|
|
204
|
+
* マージ優先順位(後勝ち):
|
|
205
|
+
* 1. tools.json の env フィールド
|
|
206
|
+
* 2. profiles.yaml のアクティブプロファイル
|
|
207
|
+
*
|
|
208
|
+
* @returns 環境変数のRecord
|
|
209
|
+
*/
|
|
199
210
|
export async function getSharedEnvironment(): Promise<Record<string, string>> {
|
|
200
|
-
const config = await
|
|
201
|
-
|
|
211
|
+
const [config, profileEnv] = await Promise.all([
|
|
212
|
+
loadToolsConfig(),
|
|
213
|
+
resolveProfileEnv(),
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
...(config.env ?? {}),
|
|
218
|
+
...profileEnv, // プロファイルが後勝ち
|
|
219
|
+
};
|
|
202
220
|
}
|
|
203
221
|
|
|
204
222
|
/**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 環境変数プロファイル型定義
|
|
3
|
+
*
|
|
4
|
+
* プロファイル機能で使用する型を定義します。
|
|
5
|
+
* @see specs/SPEC-dafff079/spec.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 環境変数プロファイル
|
|
10
|
+
*
|
|
11
|
+
* 環境変数のセットを表します。
|
|
12
|
+
*/
|
|
13
|
+
export interface EnvironmentProfile {
|
|
14
|
+
/** プロファイルの表示名 */
|
|
15
|
+
displayName: string;
|
|
16
|
+
/** プロファイルの説明(オプション) */
|
|
17
|
+
description?: string;
|
|
18
|
+
/** 環境変数のキーバリューペア */
|
|
19
|
+
env: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* プロファイル設定
|
|
24
|
+
*
|
|
25
|
+
* 全プロファイルを管理する設定です。
|
|
26
|
+
* ~/.gwt/profiles.yaml に保存されます。
|
|
27
|
+
*/
|
|
28
|
+
export interface ProfilesConfig {
|
|
29
|
+
/** 設定ファイルのバージョン */
|
|
30
|
+
version: string;
|
|
31
|
+
/** 現在アクティブなプロファイル名(nullの場合はプロファイルなし) */
|
|
32
|
+
activeProfile: string | null;
|
|
33
|
+
/** プロファイルの辞書(キー: プロファイル名、値: プロファイル設定) */
|
|
34
|
+
profiles: Record<string, EnvironmentProfile>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* デフォルトのプロファイル設定
|
|
39
|
+
*
|
|
40
|
+
* profiles.yamlが存在しない場合に使用されます。
|
|
41
|
+
* 不変オブジェクトとして扱われるため、変更は禁止されています。
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_PROFILES_CONFIG: Readonly<ProfilesConfig> = Object.freeze({
|
|
44
|
+
version: "1.0",
|
|
45
|
+
activeProfile: null,
|
|
46
|
+
profiles: Object.freeze({}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* プロファイル名のバリデーションパターン
|
|
51
|
+
*
|
|
52
|
+
* 小文字英数字とハイフンのみを許可し、先頭と末尾は英数字でなければなりません。
|
|
53
|
+
*/
|
|
54
|
+
export const PROFILE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* プロファイル名をバリデート
|
|
58
|
+
*
|
|
59
|
+
* @param name - 検証するプロファイル名
|
|
60
|
+
* @returns バリデーション結果
|
|
61
|
+
*/
|
|
62
|
+
export function isValidProfileName(name: string): boolean {
|
|
63
|
+
return name.length > 0 && PROFILE_NAME_PATTERN.test(name);
|
|
64
|
+
}
|