@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.
Files changed (53) hide show
  1. package/README.ja.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/cli/ui/components/App.d.ts.map +1 -1
  4. package/dist/cli/ui/components/App.js +19 -4
  5. package/dist/cli/ui/components/App.js.map +1 -1
  6. package/dist/cli/ui/components/parts/Header.d.ts +8 -0
  7. package/dist/cli/ui/components/parts/Header.d.ts.map +1 -1
  8. package/dist/cli/ui/components/parts/Header.js +7 -2
  9. package/dist/cli/ui/components/parts/Header.js.map +1 -1
  10. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -1
  11. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  12. package/dist/cli/ui/components/screens/BranchListScreen.js +16 -8
  13. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  14. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts +16 -0
  15. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts.map +1 -0
  16. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js +576 -0
  17. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js.map +1 -0
  18. package/dist/cli/ui/hooks/useProfiles.d.ts +41 -0
  19. package/dist/cli/ui/hooks/useProfiles.d.ts.map +1 -0
  20. package/dist/cli/ui/hooks/useProfiles.js +136 -0
  21. package/dist/cli/ui/hooks/useProfiles.js.map +1 -0
  22. package/dist/cli/ui/types.d.ts +1 -1
  23. package/dist/cli/ui/types.d.ts.map +1 -1
  24. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  25. package/dist/cli/ui/utils/branchFormatter.js +2 -2
  26. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  27. package/dist/config/profiles.d.ts +94 -0
  28. package/dist/config/profiles.d.ts.map +1 -0
  29. package/dist/config/profiles.js +287 -0
  30. package/dist/config/profiles.js.map +1 -0
  31. package/dist/config/tools.d.ts +10 -0
  32. package/dist/config/tools.d.ts.map +1 -1
  33. package/dist/config/tools.js +19 -2
  34. package/dist/config/tools.js.map +1 -1
  35. package/dist/types/profiles.d.ts +54 -0
  36. package/dist/types/profiles.d.ts.map +1 -0
  37. package/dist/types/profiles.js +33 -0
  38. package/dist/types/profiles.js.map +1 -0
  39. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  40. package/dist/web/client/src/components/ui/badge.d.ts +1 -1
  41. package/package.json +4 -3
  42. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +40 -1
  43. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +1 -1
  44. package/src/cli/ui/components/App.tsx +49 -36
  45. package/src/cli/ui/components/parts/Header.tsx +16 -1
  46. package/src/cli/ui/components/screens/BranchListScreen.tsx +17 -7
  47. package/src/cli/ui/components/screens/EnvironmentProfileScreen.tsx +924 -0
  48. package/src/cli/ui/hooks/useProfiles.ts +211 -0
  49. package/src/cli/ui/types.ts +2 -1
  50. package/src/cli/ui/utils/branchFormatter.ts +2 -3
  51. package/src/config/profiles.ts +362 -0
  52. package/src/config/tools.ts +20 -2
  53. package/src/types/profiles.ts +64 -0
@@ -0,0 +1,924 @@
1
+ /**
2
+ * 環境変数プロファイルエディター画面
3
+ *
4
+ * プロファイルの選択・作成・削除・環境変数の編集を行います。
5
+ * @see specs/SPEC-dafff079/spec.md
6
+ */
7
+
8
+ import React, { useState, useCallback, useMemo, useEffect } from "react";
9
+ import { Box, Text, useInput } from "ink";
10
+ import { Header } from "../parts/Header.js";
11
+ import { Footer } from "../parts/Footer.js";
12
+ import { Select } from "../common/Select.js";
13
+ import { Input } from "../common/Input.js";
14
+ import { Confirm } from "../common/Confirm.js";
15
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
16
+ import { useProfiles } from "../../hooks/useProfiles.js";
17
+ import { isValidProfileName } from "../../../../types/profiles.js";
18
+
19
+ export interface EnvironmentProfileScreenProps {
20
+ onBack: () => void;
21
+ version?: string | null;
22
+ }
23
+
24
+ type ScreenMode =
25
+ | "list" // プロファイル一覧
26
+ | "view" // プロファイル詳細
27
+ | "create-name" // 新規作成:名前入力
28
+ | "create-display" // 新規作成:表示名入力
29
+ | "add-env-key" // 環境変数追加:キー入力
30
+ | "add-env-value" // 環境変数追加:値入力
31
+ | "edit-env-value" // 環境変数編集:値入力
32
+ | "confirm-delete-profile" // プロファイル削除確認
33
+ | "confirm-delete-env"; // 環境変数削除確認
34
+
35
+ interface ProfileItem {
36
+ label: string;
37
+ value: string;
38
+ profileName: string | null;
39
+ isActive: boolean;
40
+ }
41
+
42
+ interface EnvVarItem {
43
+ label: string;
44
+ value: string;
45
+ key: string;
46
+ envValue: string;
47
+ }
48
+
49
+ const UI_CHROME_HEIGHT = 20; // ヘッダー/フッター/余白などの固定行数
50
+ const ENV_VAR_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
51
+ const NO_PROFILE_VALUE = "__gwt_no_profile__";
52
+
53
+ type FocusTarget = "profiles" | "env" | "osenv";
54
+
55
+ interface SelectionState {
56
+ profileIndex: number;
57
+ envIndex: number;
58
+ osEnvIndex: number;
59
+ focus: FocusTarget;
60
+ selectedProfileName: string | null;
61
+ }
62
+
63
+ interface ProfileCreationState {
64
+ name: string;
65
+ displayName: string;
66
+ }
67
+
68
+ interface EnvEditState {
69
+ key: string;
70
+ value: string;
71
+ selectedKey: string | null;
72
+ }
73
+
74
+ type NumberUpdater = number | ((prev: number) => number);
75
+ type FocusUpdater = FocusTarget | ((prev: FocusTarget) => FocusTarget);
76
+
77
+ /**
78
+ * 環境変数プロファイルエディター画面
79
+ */
80
+ export function EnvironmentProfileScreen({
81
+ onBack,
82
+ version,
83
+ }: EnvironmentProfileScreenProps) {
84
+ const { rows } = useTerminalSize();
85
+ const {
86
+ profiles,
87
+ loading,
88
+ error,
89
+ activeProfileName,
90
+ setActiveProfile,
91
+ createProfile,
92
+ deleteProfile,
93
+ updateEnvVar,
94
+ deleteEnvVar,
95
+ } = useProfiles();
96
+
97
+ // 画面モード
98
+ const [mode, setMode] = useState<ScreenMode>("list");
99
+
100
+ const [selection, setSelection] = useState<SelectionState>({
101
+ profileIndex: 0,
102
+ envIndex: 0,
103
+ osEnvIndex: 0,
104
+ focus: "profiles",
105
+ selectedProfileName: null,
106
+ });
107
+
108
+ const [profileCreation, setProfileCreation] = useState<ProfileCreationState>({
109
+ name: "",
110
+ displayName: "",
111
+ });
112
+
113
+ const [envEdit, setEnvEdit] = useState<EnvEditState>({
114
+ key: "",
115
+ value: "",
116
+ selectedKey: null,
117
+ });
118
+
119
+ // バリデーションエラー
120
+ const [validationError, setValidationError] = useState<string | null>(null);
121
+
122
+ const profileIndex = selection.profileIndex;
123
+ const envIndex = selection.envIndex;
124
+ const osEnvIndex = selection.osEnvIndex;
125
+ const focus = selection.focus;
126
+ const selectedProfileName = selection.selectedProfileName;
127
+
128
+ const newProfileName = profileCreation.name;
129
+ const newProfileDisplayName = profileCreation.displayName;
130
+
131
+ const newEnvKey = envEdit.key;
132
+ const newEnvValue = envEdit.value;
133
+ const selectedEnvKey = envEdit.selectedKey;
134
+
135
+ const setProfileIndex = useCallback((index: number) => {
136
+ setSelection((prev) => ({ ...prev, profileIndex: index }));
137
+ }, []);
138
+
139
+ const setEnvIndex = useCallback((updater: NumberUpdater) => {
140
+ setSelection((prev) => ({
141
+ ...prev,
142
+ envIndex:
143
+ typeof updater === "function" ? updater(prev.envIndex) : updater,
144
+ }));
145
+ }, []);
146
+
147
+ const setOsEnvIndex = useCallback((updater: NumberUpdater) => {
148
+ setSelection((prev) => ({
149
+ ...prev,
150
+ osEnvIndex:
151
+ typeof updater === "function" ? updater(prev.osEnvIndex) : updater,
152
+ }));
153
+ }, []);
154
+
155
+ const setFocus = useCallback((updater: FocusUpdater) => {
156
+ setSelection((prev) => ({
157
+ ...prev,
158
+ focus: typeof updater === "function" ? updater(prev.focus) : updater,
159
+ }));
160
+ }, []);
161
+
162
+ const setSelectedProfileName = useCallback((name: string | null) => {
163
+ setSelection((prev) => ({ ...prev, selectedProfileName: name }));
164
+ }, []);
165
+
166
+ const setNewProfileName = useCallback((value: string) => {
167
+ setProfileCreation((prev) => ({ ...prev, name: value }));
168
+ }, []);
169
+
170
+ const setNewProfileDisplayName = useCallback((value: string) => {
171
+ setProfileCreation((prev) => ({ ...prev, displayName: value }));
172
+ }, []);
173
+
174
+ const setNewEnvKey = useCallback((value: string) => {
175
+ setEnvEdit((prev) => ({ ...prev, key: value }));
176
+ }, []);
177
+
178
+ const setNewEnvValue = useCallback((value: string) => {
179
+ setEnvEdit((prev) => ({ ...prev, value }));
180
+ }, []);
181
+
182
+ const setSelectedEnvKey = useCallback((key: string | null) => {
183
+ setEnvEdit((prev) => ({ ...prev, selectedKey: key }));
184
+ }, []);
185
+
186
+ // プロファイル一覧アイテム
187
+ const profileItems: ProfileItem[] = useMemo(() => {
188
+ if (!profiles) return [];
189
+ const items = Object.entries(profiles.profiles).map(([name, profile]) => ({
190
+ label: `${profile.displayName}${name === activeProfileName ? " (active)" : ""}`,
191
+ value: name,
192
+ profileName: name,
193
+ isActive: name === activeProfileName,
194
+ }));
195
+ return [
196
+ {
197
+ label: `(none)${activeProfileName === null ? " (active)" : ""}`,
198
+ value: NO_PROFILE_VALUE,
199
+ profileName: null,
200
+ isActive: activeProfileName === null,
201
+ },
202
+ ...items,
203
+ ];
204
+ }, [profiles, activeProfileName]);
205
+
206
+ // 現在選択中のプロファイル
207
+ const currentProfile =
208
+ selectedProfileName && profiles
209
+ ? profiles.profiles[selectedProfileName]
210
+ : null;
211
+
212
+ // 環境変数一覧アイテム
213
+ const envItems: EnvVarItem[] = useMemo(() => {
214
+ if (!currentProfile) return [];
215
+ return Object.entries(currentProfile.env).map(([key, value]) => ({
216
+ label: `${key}=${value}`,
217
+ value: key,
218
+ key,
219
+ envValue: value,
220
+ }));
221
+ }, [currentProfile]);
222
+
223
+ // OS環境変数(プロファイルで上書きされるものをハイライト)
224
+ const osEnvItems = useMemo(() => {
225
+ const profileEnvKeys = new Set(
226
+ currentProfile ? Object.keys(currentProfile.env) : [],
227
+ );
228
+ return Object.entries(process.env)
229
+ .filter(([, value]) => value !== undefined)
230
+ .map(([key, value]) => ({
231
+ key,
232
+ value: value ?? "",
233
+ isOverwritten: profileEnvKeys.has(key),
234
+ }))
235
+ .sort((a, b) => a.key.localeCompare(b.key));
236
+ }, [currentProfile]);
237
+
238
+ // 配列サイズ変更に追従してインデックスをクランプ(削除後の範囲外アクセス防止)
239
+ useEffect(() => {
240
+ if (profileItems.length === 0) {
241
+ if (profileIndex !== 0) {
242
+ setProfileIndex(0);
243
+ }
244
+ return;
245
+ }
246
+ if (profileIndex >= profileItems.length) {
247
+ setProfileIndex(profileItems.length - 1);
248
+ }
249
+ }, [profileItems.length, profileIndex, setProfileIndex]);
250
+
251
+ useEffect(() => {
252
+ if (envItems.length === 0) {
253
+ if (envIndex !== 0) {
254
+ setEnvIndex(0);
255
+ }
256
+ return;
257
+ }
258
+ if (envIndex >= envItems.length) {
259
+ setEnvIndex(envItems.length - 1);
260
+ }
261
+ }, [envItems.length, envIndex, setEnvIndex]);
262
+
263
+ useEffect(() => {
264
+ if (osEnvItems.length === 0) {
265
+ if (osEnvIndex !== 0) {
266
+ setOsEnvIndex(0);
267
+ }
268
+ return;
269
+ }
270
+ if (osEnvIndex >= osEnvItems.length) {
271
+ setOsEnvIndex(osEnvItems.length - 1);
272
+ }
273
+ }, [osEnvItems.length, osEnvIndex, setOsEnvIndex]);
274
+
275
+ // プロファイルを選択してアクティブ化
276
+ const handleActivateProfile = useCallback(
277
+ async (item: ProfileItem) => {
278
+ try {
279
+ await setActiveProfile(item.profileName);
280
+ if (item.profileName === null) {
281
+ setSelectedProfileName(null);
282
+ setFocus("profiles");
283
+ setProfileIndex(0);
284
+ setMode("list");
285
+ return;
286
+ }
287
+
288
+ setSelectedProfileName(item.profileName);
289
+ setFocus("env"); // viewモードでは環境変数にフォーカス
290
+ setMode("view");
291
+ } catch {
292
+ // エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
293
+ }
294
+ },
295
+ [setActiveProfile, setSelectedProfileName, setFocus, setProfileIndex],
296
+ );
297
+
298
+ // 新規プロファイル作成開始
299
+ const handleStartCreateProfile = useCallback(() => {
300
+ setNewProfileName("");
301
+ setNewProfileDisplayName("");
302
+ setValidationError(null);
303
+ setMode("create-name");
304
+ }, []);
305
+
306
+ // 新規プロファイル名入力完了
307
+ const handleCreateNameSubmit = useCallback((name: string) => {
308
+ if (!isValidProfileName(name)) {
309
+ setValidationError(
310
+ "Invalid profile name. Use lowercase letters, numbers, and hyphens (must start and end with a letter or number).",
311
+ );
312
+ return;
313
+ }
314
+ setValidationError(null);
315
+ setNewProfileName(name);
316
+ setNewProfileDisplayName(name);
317
+ setMode("create-display");
318
+ }, []);
319
+
320
+ // 新規プロファイル作成完了
321
+ const handleCreateProfileSubmit = useCallback(
322
+ async (displayName: string) => {
323
+ try {
324
+ await createProfile(newProfileName, {
325
+ displayName,
326
+ env: {},
327
+ });
328
+ setSelectedProfileName(newProfileName);
329
+ setFocus("env"); // viewモードでは環境変数にフォーカス
330
+ setMode("view");
331
+ } catch {
332
+ // エラー状態は useProfiles フック側で管理するため、ここでは一覧に戻す
333
+ setMode("list");
334
+ }
335
+ },
336
+ [createProfile, newProfileName],
337
+ );
338
+
339
+ // プロファイル削除確認
340
+ const handleConfirmDeleteProfile = useCallback(
341
+ async (confirmed: boolean) => {
342
+ if (confirmed && selectedProfileName) {
343
+ try {
344
+ await deleteProfile(selectedProfileName);
345
+ setSelectedProfileName(null);
346
+ } catch {
347
+ // エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
348
+ }
349
+ }
350
+ setMode("list");
351
+ },
352
+ [deleteProfile, selectedProfileName],
353
+ );
354
+
355
+ // 環境変数追加開始
356
+ const handleStartAddEnv = useCallback(() => {
357
+ setNewEnvKey("");
358
+ setNewEnvValue("");
359
+ setValidationError(null);
360
+ setMode("add-env-key");
361
+ }, []);
362
+
363
+ // 環境変数キー入力完了
364
+ const handleEnvKeySubmit = useCallback((key: string) => {
365
+ const trimmedKey = key.trim();
366
+ if (!trimmedKey || !ENV_VAR_KEY_PATTERN.test(trimmedKey)) {
367
+ setValidationError(
368
+ "Invalid variable name. Use letters, numbers, and underscores (must start with a letter or underscore).",
369
+ );
370
+ setNewEnvKey(trimmedKey);
371
+ return;
372
+ }
373
+
374
+ setValidationError(null);
375
+ setNewEnvKey(trimmedKey);
376
+ setMode("add-env-value");
377
+ }, []);
378
+
379
+ // 環境変数追加完了
380
+ const handleEnvValueSubmit = useCallback(
381
+ async (value: string) => {
382
+ if (selectedProfileName) {
383
+ try {
384
+ await updateEnvVar(selectedProfileName, newEnvKey, value);
385
+ } catch {
386
+ // エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
387
+ }
388
+ }
389
+ setMode("view");
390
+ },
391
+ [updateEnvVar, selectedProfileName, newEnvKey],
392
+ );
393
+
394
+ // 環境変数編集開始
395
+ const handleStartEditEnv = useCallback(
396
+ (key: string, currentValue: string) => {
397
+ setSelectedEnvKey(key);
398
+ setNewEnvValue(currentValue);
399
+ setMode("edit-env-value");
400
+ },
401
+ [],
402
+ );
403
+
404
+ // 環境変数編集完了
405
+ const handleEditEnvSubmit = useCallback(
406
+ async (value: string) => {
407
+ if (selectedProfileName && selectedEnvKey) {
408
+ try {
409
+ await updateEnvVar(selectedProfileName, selectedEnvKey, value);
410
+ } catch {
411
+ // エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
412
+ }
413
+ }
414
+ setMode("view");
415
+ },
416
+ [updateEnvVar, selectedProfileName, selectedEnvKey],
417
+ );
418
+
419
+ // 環境変数削除確認
420
+ const handleConfirmDeleteEnv = useCallback(
421
+ async (confirmed: boolean) => {
422
+ if (confirmed && selectedProfileName && selectedEnvKey) {
423
+ try {
424
+ await deleteEnvVar(selectedProfileName, selectedEnvKey);
425
+ } catch {
426
+ // エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
427
+ }
428
+ }
429
+ setMode("view");
430
+ },
431
+ [deleteEnvVar, selectedProfileName, selectedEnvKey],
432
+ );
433
+
434
+ // キーボード入力ハンドリング
435
+ useInput(
436
+ (input, key) => {
437
+ // 入力モード時は他のキーハンドリングをスキップ
438
+ if (
439
+ mode === "create-name" ||
440
+ mode === "create-display" ||
441
+ mode === "add-env-key" ||
442
+ mode === "add-env-value" ||
443
+ mode === "edit-env-value"
444
+ ) {
445
+ if (key.escape) {
446
+ setMode(mode.startsWith("create") ? "list" : "view");
447
+ }
448
+ return;
449
+ }
450
+
451
+ // 確認ダイアログ時は Confirm コンポーネントがハンドリング
452
+ if (mode === "confirm-delete-profile" || mode === "confirm-delete-env") {
453
+ return;
454
+ }
455
+
456
+ // Escape で戻る
457
+ if (key.escape) {
458
+ if (mode === "view") {
459
+ setMode("list");
460
+ setSelectedProfileName(null);
461
+ } else {
462
+ onBack();
463
+ }
464
+ return;
465
+ }
466
+
467
+ // プロファイル一覧モード
468
+ if (mode === "list") {
469
+ if (input === "n") {
470
+ handleStartCreateProfile();
471
+ return;
472
+ }
473
+ if (input === "d" && profileItems.length > 0) {
474
+ const item = profileItems[profileIndex];
475
+ if (item?.profileName && item.profileName !== activeProfileName) {
476
+ setSelectedProfileName(item.profileName);
477
+ setMode("confirm-delete-profile");
478
+ }
479
+ return;
480
+ }
481
+ return;
482
+ }
483
+
484
+ // プロファイル詳細モード
485
+ if (mode === "view") {
486
+ // Tab でフォーカス切り替え (env ↔ osenv)
487
+ if (key.tab) {
488
+ setFocus((prev) => (prev === "env" ? "osenv" : "env"));
489
+ return;
490
+ }
491
+
492
+ // j/k でスクロール
493
+ if (input === "j" || key.downArrow) {
494
+ if (focus === "env") {
495
+ setEnvIndex((prev) =>
496
+ Math.min(prev + 1, Math.max(0, envItems.length - 1)),
497
+ );
498
+ } else if (focus === "osenv") {
499
+ setOsEnvIndex((prev) =>
500
+ Math.min(prev + 1, Math.max(0, osEnvItems.length - 1)),
501
+ );
502
+ }
503
+ return;
504
+ }
505
+ if (input === "k" || key.upArrow) {
506
+ if (focus === "env") {
507
+ setEnvIndex((prev) => Math.max(prev - 1, 0));
508
+ } else if (focus === "osenv") {
509
+ setOsEnvIndex((prev) => Math.max(prev - 1, 0));
510
+ }
511
+ return;
512
+ }
513
+
514
+ // 環境変数操作
515
+ if (focus === "env") {
516
+ if (input === "a") {
517
+ handleStartAddEnv();
518
+ return;
519
+ }
520
+ if (input === "e" && envItems.length > 0) {
521
+ const item = envItems[envIndex];
522
+ if (item) {
523
+ handleStartEditEnv(item.key, item.envValue);
524
+ }
525
+ return;
526
+ }
527
+ if (input === "d" && envItems.length > 0) {
528
+ const item = envItems[envIndex];
529
+ if (item) {
530
+ setSelectedEnvKey(item.key);
531
+ setMode("confirm-delete-env");
532
+ }
533
+ return;
534
+ }
535
+ }
536
+ }
537
+ },
538
+ { isActive: true },
539
+ );
540
+
541
+ // フッターアクション
542
+ const getFooterActions = () => {
543
+ if (mode === "list") {
544
+ return [
545
+ { key: "enter", description: "Select" },
546
+ { key: "n", description: "New" },
547
+ { key: "d", description: "Delete" },
548
+ { key: "esc", description: "Back" },
549
+ ];
550
+ }
551
+ if (mode === "view") {
552
+ return [
553
+ { key: "tab", description: "Switch focus" },
554
+ { key: "j/k", description: "Scroll" },
555
+ { key: "a", description: "Add env" },
556
+ { key: "e", description: "Edit env" },
557
+ { key: "d", description: "Delete env" },
558
+ { key: "esc", description: "Back" },
559
+ ];
560
+ }
561
+ return [{ key: "esc", description: "Cancel" }];
562
+ };
563
+
564
+ // ローディング表示
565
+ if (loading) {
566
+ return (
567
+ <Box flexDirection="column" height={rows}>
568
+ <Header
569
+ title="Environment Profiles"
570
+ titleColor="magenta"
571
+ version={version}
572
+ />
573
+ <Box flexGrow={1} justifyContent="center" alignItems="center">
574
+ <Text>Loading profiles...</Text>
575
+ </Box>
576
+ </Box>
577
+ );
578
+ }
579
+
580
+ // エラー表示
581
+ if (error) {
582
+ return (
583
+ <Box flexDirection="column" height={rows}>
584
+ <Header
585
+ title="Environment Profiles"
586
+ titleColor="magenta"
587
+ version={version}
588
+ />
589
+ <Box flexGrow={1} marginTop={1}>
590
+ <Text color="red">Error: {error.message}</Text>
591
+ </Box>
592
+ <Footer actions={[{ key: "esc", description: "Back" }]} />
593
+ </Box>
594
+ );
595
+ }
596
+
597
+ // 新規プロファイル作成:名前入力
598
+ if (mode === "create-name") {
599
+ return (
600
+ <Box flexDirection="column" height={rows}>
601
+ <Header
602
+ title="Environment Profiles"
603
+ titleColor="magenta"
604
+ version={version}
605
+ />
606
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
607
+ <Text>Create new profile</Text>
608
+ <Box marginTop={1}>
609
+ <Input
610
+ value={newProfileName}
611
+ onChange={setNewProfileName}
612
+ onSubmit={handleCreateNameSubmit}
613
+ label="Profile name (lowercase, a-z0-9-):"
614
+ placeholder="development"
615
+ />
616
+ </Box>
617
+ {validationError && (
618
+ <Box marginTop={1}>
619
+ <Text color="red">{validationError}</Text>
620
+ </Box>
621
+ )}
622
+ </Box>
623
+ <Footer actions={getFooterActions()} />
624
+ </Box>
625
+ );
626
+ }
627
+
628
+ // 新規プロファイル作成:表示名入力
629
+ if (mode === "create-display") {
630
+ return (
631
+ <Box flexDirection="column" height={rows}>
632
+ <Header
633
+ title="Environment Profiles"
634
+ titleColor="magenta"
635
+ version={version}
636
+ />
637
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
638
+ <Text>Create new profile: {newProfileName}</Text>
639
+ <Box marginTop={1}>
640
+ <Input
641
+ value={newProfileDisplayName}
642
+ onChange={setNewProfileDisplayName}
643
+ onSubmit={handleCreateProfileSubmit}
644
+ label="Display name:"
645
+ placeholder="Development"
646
+ />
647
+ </Box>
648
+ </Box>
649
+ <Footer actions={getFooterActions()} />
650
+ </Box>
651
+ );
652
+ }
653
+
654
+ // 環境変数追加:キー入力
655
+ if (mode === "add-env-key") {
656
+ return (
657
+ <Box flexDirection="column" height={rows}>
658
+ <Header
659
+ title="Environment Profiles"
660
+ titleColor="magenta"
661
+ version={version}
662
+ />
663
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
664
+ <Text>Add environment variable</Text>
665
+ <Box marginTop={1}>
666
+ <Input
667
+ value={newEnvKey}
668
+ onChange={setNewEnvKey}
669
+ onSubmit={handleEnvKeySubmit}
670
+ label="Variable name:"
671
+ placeholder="API_KEY"
672
+ />
673
+ </Box>
674
+ {validationError && (
675
+ <Box marginTop={1}>
676
+ <Text color="red">{validationError}</Text>
677
+ </Box>
678
+ )}
679
+ </Box>
680
+ <Footer actions={getFooterActions()} />
681
+ </Box>
682
+ );
683
+ }
684
+
685
+ // 環境変数追加:値入力
686
+ if (mode === "add-env-value") {
687
+ return (
688
+ <Box flexDirection="column" height={rows}>
689
+ <Header
690
+ title="Environment Profiles"
691
+ titleColor="magenta"
692
+ version={version}
693
+ />
694
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
695
+ <Text>Add environment variable: {newEnvKey}</Text>
696
+ <Box marginTop={1}>
697
+ <Input
698
+ value={newEnvValue}
699
+ onChange={setNewEnvValue}
700
+ onSubmit={handleEnvValueSubmit}
701
+ label="Value:"
702
+ placeholder="your-value"
703
+ />
704
+ </Box>
705
+ </Box>
706
+ <Footer actions={getFooterActions()} />
707
+ </Box>
708
+ );
709
+ }
710
+
711
+ // 環境変数編集:値入力
712
+ if (mode === "edit-env-value") {
713
+ return (
714
+ <Box flexDirection="column" height={rows}>
715
+ <Header
716
+ title="Environment Profiles"
717
+ titleColor="magenta"
718
+ version={version}
719
+ />
720
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
721
+ <Text>Edit environment variable: {selectedEnvKey}</Text>
722
+ <Box marginTop={1}>
723
+ <Input
724
+ value={newEnvValue}
725
+ onChange={setNewEnvValue}
726
+ onSubmit={handleEditEnvSubmit}
727
+ label="New value:"
728
+ placeholder="new-value"
729
+ />
730
+ </Box>
731
+ </Box>
732
+ <Footer actions={getFooterActions()} />
733
+ </Box>
734
+ );
735
+ }
736
+
737
+ // プロファイル削除確認
738
+ if (mode === "confirm-delete-profile") {
739
+ const profileToDelete = selectedProfileName
740
+ ? profiles?.profiles[selectedProfileName]
741
+ : null;
742
+ return (
743
+ <Box flexDirection="column" height={rows}>
744
+ <Header
745
+ title="Environment Profiles"
746
+ titleColor="magenta"
747
+ version={version}
748
+ />
749
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
750
+ <Confirm
751
+ message={`Delete profile "${profileToDelete?.displayName ?? selectedProfileName}"?`}
752
+ onConfirm={handleConfirmDeleteProfile}
753
+ />
754
+ </Box>
755
+ </Box>
756
+ );
757
+ }
758
+
759
+ // 環境変数削除確認
760
+ if (mode === "confirm-delete-env") {
761
+ return (
762
+ <Box flexDirection="column" height={rows}>
763
+ <Header
764
+ title="Environment Profiles"
765
+ titleColor="magenta"
766
+ version={version}
767
+ />
768
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
769
+ <Confirm
770
+ message={`Delete environment variable "${selectedEnvKey}"?`}
771
+ onConfirm={handleConfirmDeleteEnv}
772
+ />
773
+ </Box>
774
+ </Box>
775
+ );
776
+ }
777
+
778
+ // プロファイル一覧
779
+ if (mode === "list") {
780
+ const hasProfiles = Boolean(
781
+ profiles && Object.keys(profiles.profiles).length > 0,
782
+ );
783
+ return (
784
+ <Box flexDirection="column" height={rows}>
785
+ <Header
786
+ title="Environment Profiles"
787
+ titleColor="magenta"
788
+ version={version}
789
+ activeProfile={activeProfileName}
790
+ />
791
+
792
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
793
+ <Box marginBottom={1}>
794
+ <Text>Select a profile to activate (or choose "(none)"):</Text>
795
+ </Box>
796
+
797
+ <Select
798
+ items={profileItems}
799
+ onSelect={handleActivateProfile}
800
+ selectedIndex={profileIndex}
801
+ onSelectedIndexChange={setProfileIndex}
802
+ />
803
+ {!hasProfiles && (
804
+ <Box marginTop={1}>
805
+ <Text dimColor>No profiles. Press [n] to create one.</Text>
806
+ </Box>
807
+ )}
808
+ </Box>
809
+
810
+ <Footer actions={getFooterActions()} />
811
+ </Box>
812
+ );
813
+ }
814
+
815
+ // プロファイル詳細
816
+ const maxOsEnvVisible = Math.max(
817
+ 5,
818
+ Math.floor((rows - UI_CHROME_HEIGHT) / 2),
819
+ );
820
+
821
+ return (
822
+ <Box flexDirection="column" height={rows}>
823
+ <Header
824
+ title="Environment Profiles"
825
+ titleColor="magenta"
826
+ version={version}
827
+ activeProfile={activeProfileName}
828
+ />
829
+
830
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
831
+ {/* プロファイル情報 */}
832
+ <Box marginBottom={1}>
833
+ <Text bold>Profile: </Text>
834
+ <Text color="cyan">
835
+ {currentProfile?.displayName ?? selectedProfileName}
836
+ </Text>
837
+ {selectedProfileName === activeProfileName && (
838
+ <Text color="green"> (active)</Text>
839
+ )}
840
+ </Box>
841
+
842
+ {currentProfile?.description && (
843
+ <Box marginBottom={1}>
844
+ <Text dimColor>{currentProfile.description}</Text>
845
+ </Box>
846
+ )}
847
+
848
+ {/* プロファイル環境変数 */}
849
+ <Box marginBottom={1}>
850
+ <Text bold {...(focus === "env" ? { color: "cyan" as const } : {})}>
851
+ Profile Environment Variables:
852
+ </Text>
853
+ </Box>
854
+
855
+ <Box flexDirection="column" marginLeft={2} marginBottom={1}>
856
+ {envItems.length === 0 ? (
857
+ <Text dimColor>No environment variables. Press [a] to add.</Text>
858
+ ) : (
859
+ envItems.map((item, idx) => {
860
+ const isEnvSelected = focus === "env" && idx === envIndex;
861
+ return (
862
+ <Box key={item.key}>
863
+ <Text
864
+ {...(isEnvSelected ? { color: "cyan" as const } : {})}
865
+ inverse={isEnvSelected}
866
+ >
867
+ {item.key}
868
+ </Text>
869
+ <Text>=</Text>
870
+ <Text>{item.envValue}</Text>
871
+ </Box>
872
+ );
873
+ })
874
+ )}
875
+ </Box>
876
+
877
+ {/* OS環境変数(上書きされるものをハイライト) */}
878
+ <Box marginBottom={1}>
879
+ <Text bold {...(focus === "osenv" ? { color: "cyan" as const } : {})}>
880
+ OS Environment (overwritten keys in yellow):
881
+ </Text>
882
+ </Box>
883
+
884
+ <Box flexDirection="column" marginLeft={2} overflow="hidden">
885
+ {osEnvItems
886
+ .slice(osEnvIndex, osEnvIndex + maxOsEnvVisible)
887
+ .map((item, idx) => {
888
+ // osEnvIndex は「選択中のOS環境変数のインデックス」であり、同時にスクロールの先頭位置でもある
889
+ // そのため、表示上は slice した先頭要素が選択状態になる
890
+ const actualIndex = osEnvIndex + idx;
891
+ const isOsEnvSelected =
892
+ focus === "osenv" && actualIndex === osEnvIndex;
893
+ return (
894
+ <Box key={item.key}>
895
+ <Text
896
+ {...(item.isOverwritten
897
+ ? { color: "yellow" as const }
898
+ : {})}
899
+ inverse={isOsEnvSelected}
900
+ >
901
+ {item.key}
902
+ </Text>
903
+ <Text>=</Text>
904
+ <Text dimColor>
905
+ {item.value.slice(0, 50)}
906
+ {item.value.length > 50 ? "..." : ""}
907
+ </Text>
908
+ </Box>
909
+ );
910
+ })}
911
+ {osEnvItems.length > maxOsEnvVisible && (
912
+ <Text dimColor>
913
+ ... ({osEnvIndex + 1}-
914
+ {Math.min(osEnvIndex + maxOsEnvVisible, osEnvItems.length)} of{" "}
915
+ {osEnvItems.length})
916
+ </Text>
917
+ )}
918
+ </Box>
919
+ </Box>
920
+
921
+ <Footer actions={getFooterActions()} />
922
+ </Box>
923
+ );
924
+ }