@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,241 @@
1
+ /**
2
+ * Web UI API型定義
3
+ *
4
+ * データモデル仕様: specs/SPEC-d5e56259/data-model.md
5
+ * REST API仕様: specs/SPEC-d5e56259/contracts/rest-api.yaml
6
+ */
7
+
8
+ /**
9
+ * Branch - Gitブランチ
10
+ */
11
+ export interface Branch {
12
+ name: string;
13
+ type: "local" | "remote";
14
+ commitHash: string;
15
+ commitMessage?: string | null;
16
+ author?: string | null;
17
+ commitDate?: string | null; // ISO8601
18
+ mergeStatus: "unmerged" | "merged" | "unknown";
19
+ hasUnpushedCommits: boolean;
20
+ worktreePath?: string | null;
21
+ baseBranch?: string | null;
22
+ divergence?: {
23
+ ahead: number;
24
+ behind: number;
25
+ upToDate: boolean;
26
+ } | null;
27
+ prInfo?: {
28
+ number: number;
29
+ title: string;
30
+ state: "open" | "merged" | "closed";
31
+ mergedAt?: string | null;
32
+ } | null;
33
+ }
34
+
35
+ /**
36
+ * Worktree - Gitワークツリー
37
+ */
38
+ export interface Worktree {
39
+ path: string;
40
+ branchName: string;
41
+ head: string;
42
+ isLocked: boolean;
43
+ isPrunable: boolean;
44
+ isProtected: boolean;
45
+ createdAt?: string | null; // ISO8601
46
+ lastAccessedAt?: string | null; // ISO8601
47
+ divergence?: Branch["divergence"];
48
+ prInfo?: Branch["prInfo"];
49
+ }
50
+
51
+ /**
52
+ * AIToolSession - AI Tool実行セッション
53
+ */
54
+ export interface AIToolSession {
55
+ sessionId: string; // UUID v4
56
+ toolType: "claude-code" | "codex-cli" | "custom";
57
+ toolName?: string | null;
58
+ mode: "normal" | "continue" | "resume";
59
+ worktreePath: string;
60
+ ptyPid?: number | null;
61
+ websocketId?: string | null;
62
+ status: "pending" | "running" | "completed" | "failed";
63
+ startedAt: string; // ISO8601
64
+ endedAt?: string | null; // ISO8601
65
+ exitCode?: number | null;
66
+ errorMessage?: string | null;
67
+ }
68
+
69
+ /**
70
+ * CustomAITool - カスタムAI Tool設定
71
+ */
72
+ export interface EnvironmentVariable {
73
+ key: string;
74
+ value: string;
75
+ lastUpdated?: string | null;
76
+ importedFromOs?: boolean;
77
+ }
78
+
79
+ export interface EnvironmentHistoryEntry {
80
+ key: string;
81
+ action: "add" | "update" | "delete" | "import";
82
+ timestamp: string;
83
+ source: "ui" | "os" | "cli";
84
+ }
85
+
86
+ export interface CustomAITool {
87
+ id: string; // UUID v4 or slug
88
+ displayName: string;
89
+ icon?: string | null;
90
+ command: string;
91
+ executionType: "path" | "bunx" | "command";
92
+ defaultArgs?: string[] | null;
93
+ modeArgs: {
94
+ normal?: string[];
95
+ continue?: string[];
96
+ resume?: string[];
97
+ };
98
+ permissionSkipArgs?: string[] | null;
99
+ env?: EnvironmentVariable[] | null;
100
+ description?: string | null;
101
+ createdAt?: string | null; // ISO8601
102
+ updatedAt?: string | null; // ISO8601
103
+ }
104
+
105
+ /**
106
+ * REST API Response wrappers
107
+ */
108
+ export interface SuccessResponse<T = unknown> {
109
+ success: true;
110
+ data: T;
111
+ }
112
+
113
+ export interface ErrorResponse {
114
+ success: false;
115
+ error: string;
116
+ details?: string | null;
117
+ }
118
+
119
+ export type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
120
+
121
+ /**
122
+ * API Endpoints
123
+ */
124
+ export interface HealthResponse {
125
+ success: true;
126
+ status: string;
127
+ timestamp: string; // ISO8601
128
+ }
129
+
130
+ export type BranchListResponse = SuccessResponse<Branch[]>;
131
+ export type BranchResponse = SuccessResponse<Branch>;
132
+ export type BranchSyncResponse = SuccessResponse<BranchSyncResult>;
133
+ export type WorktreeListResponse = SuccessResponse<Worktree[]>;
134
+ export type WorktreeResponse = SuccessResponse<Worktree>;
135
+ export type SessionListResponse = SuccessResponse<AIToolSession[]>;
136
+ export type SessionResponse = SuccessResponse<AIToolSession>;
137
+ export interface ConfigPayload {
138
+ version: string;
139
+ updatedAt?: string | null;
140
+ env?: EnvironmentVariable[] | null;
141
+ history?: EnvironmentHistoryEntry[] | null;
142
+ tools: CustomAITool[];
143
+ }
144
+
145
+ export type ConfigResponse = SuccessResponse<ConfigPayload>;
146
+
147
+ /**
148
+ * API Request bodies
149
+ */
150
+ export interface CreateWorktreeRequest {
151
+ branchName: string;
152
+ createBranch?: boolean;
153
+ }
154
+
155
+ export interface BranchSyncRequest {
156
+ worktreePath: string;
157
+ }
158
+
159
+ export interface StartSessionRequest {
160
+ toolType: "claude-code" | "codex-cli" | "custom";
161
+ toolName?: string | null;
162
+ mode: "normal" | "continue" | "resume";
163
+ worktreePath: string;
164
+ skipPermissions?: boolean;
165
+ bypassApprovals?: boolean;
166
+ extraArgs?: string[];
167
+ customToolId?: string | null;
168
+ }
169
+
170
+ export type UpdateConfigRequest = ConfigPayload;
171
+
172
+ export interface CleanupResponse {
173
+ success: true;
174
+ deleted: string[];
175
+ }
176
+
177
+ export interface BranchSyncResult {
178
+ branch: Branch;
179
+ divergence?: Branch["divergence"];
180
+ fetchStatus: "success";
181
+ pullStatus: "success" | "failed";
182
+ warnings?: string[];
183
+ }
184
+
185
+ /**
186
+ * WebSocket Messages
187
+ */
188
+ export interface WebSocketMessage {
189
+ type: string;
190
+ data?: unknown;
191
+ timestamp?: string; // ISO8601
192
+ }
193
+
194
+ export interface InputMessage extends WebSocketMessage {
195
+ type: "input";
196
+ data: string;
197
+ }
198
+
199
+ export interface ResizeMessage extends WebSocketMessage {
200
+ type: "resize";
201
+ data: {
202
+ cols: number;
203
+ rows: number;
204
+ };
205
+ }
206
+
207
+ export interface PingMessage extends WebSocketMessage {
208
+ type: "ping";
209
+ }
210
+
211
+ export interface OutputMessage extends WebSocketMessage {
212
+ type: "output";
213
+ data: string;
214
+ }
215
+
216
+ export interface ExitMessage extends WebSocketMessage {
217
+ type: "exit";
218
+ data: {
219
+ code: number;
220
+ signal?: string;
221
+ };
222
+ }
223
+
224
+ export interface ErrorMessage extends WebSocketMessage {
225
+ type: "error";
226
+ data: {
227
+ message: string;
228
+ code?: string;
229
+ };
230
+ }
231
+
232
+ export interface PongMessage extends WebSocketMessage {
233
+ type: "pong";
234
+ }
235
+
236
+ export type ClientMessage = InputMessage | ResizeMessage | PingMessage;
237
+ export type ServerMessage =
238
+ | OutputMessage
239
+ | ExitMessage
240
+ | ErrorMessage
241
+ | PongMessage;
@@ -0,0 +1,235 @@
1
+ /**
2
+ * カスタムAIツール対応機能の型定義
3
+ *
4
+ * この型定義ファイルは、設定ファイル(tools.json)のスキーマと
5
+ * 内部で使用するデータ構造を定義します。
6
+ */
7
+
8
+ // ============================================================================
9
+ // 設定ファイルのスキーマ
10
+ // ============================================================================
11
+
12
+ /**
13
+ * ツール実行方式
14
+ *
15
+ * - path: 絶対パスで直接実行(例: /usr/local/bin/my-tool)
16
+ * - bunx: bunx経由でパッケージを実行(例: @org/package@latest)
17
+ * - command: PATH環境変数から探して実行(例: aider)
18
+ */
19
+ export type ToolExecutionType = "path" | "bunx" | "command";
20
+
21
+ /**
22
+ * 実行モード別引数
23
+ *
24
+ * 各モードで使用する引数の配列を定義します。
25
+ * 少なくとも1つのモードを定義する必要があります。
26
+ */
27
+ export interface ModeArgs {
28
+ /**
29
+ * 通常モード時の引数
30
+ */
31
+ normal?: string[];
32
+
33
+ /**
34
+ * 継続モード時の引数
35
+ */
36
+ continue?: string[];
37
+
38
+ /**
39
+ * 再開モード時の引数
40
+ */
41
+ resume?: string[];
42
+ }
43
+
44
+ /**
45
+ * カスタムAIツール定義
46
+ *
47
+ * tools.jsonファイルで定義される個別のツール設定。
48
+ */
49
+ export interface CustomAITool {
50
+ /**
51
+ * ツールの一意識別子
52
+ *
53
+ * 小文字英数字とハイフンのみ使用可能(パターン: ^[a-z0-9-]+$)
54
+ * ビルトインツール(claude-code, codex-cli)との重複は不可。
55
+ */
56
+ id: string;
57
+
58
+ /**
59
+ * UI表示名
60
+ *
61
+ * ツール選択画面で表示される名前。日本語も使用可能。
62
+ */
63
+ displayName: string;
64
+
65
+ /**
66
+ * アイコン文字(オプション)
67
+ *
68
+ * ツール選択画面で表示されるUnicode文字。
69
+ */
70
+ icon?: string;
71
+
72
+ /**
73
+ * 実行方式
74
+ *
75
+ * - "path": 絶対パスで直接実行
76
+ * - "bunx": bunx経由でパッケージを実行
77
+ * - "command": PATH環境変数から探して実行
78
+ */
79
+ type: ToolExecutionType;
80
+
81
+ /**
82
+ * 実行パス/パッケージ名/コマンド名
83
+ *
84
+ * typeに応じた値を設定:
85
+ * - type="path": 絶対パス(例: /usr/local/bin/my-tool)
86
+ * - type="bunx": パッケージ名(例: @org/package@latest)
87
+ * - type="command": コマンド名(例: aider)
88
+ */
89
+ command: string;
90
+
91
+ /**
92
+ * デフォルト引数(オプション)
93
+ *
94
+ * ツール実行時に常に付与される引数。
95
+ * 最終的な引数は: defaultArgs + modeArgs[mode] + permissionSkipArgs + extraArgs
96
+ */
97
+ defaultArgs?: string[];
98
+
99
+ /**
100
+ * モード別引数
101
+ *
102
+ * normal/continue/resumeの各モードで使用する引数。
103
+ * 少なくとも1つのモードを定義する必要があります。
104
+ */
105
+ modeArgs: ModeArgs;
106
+
107
+ /**
108
+ * 権限スキップ時の引数(オプション)
109
+ *
110
+ * ユーザーが権限スキップを有効にした場合に追加される引数。
111
+ */
112
+ permissionSkipArgs?: string[];
113
+
114
+ /**
115
+ * 環境変数(オプション)
116
+ *
117
+ * ツール起動時に設定される環境変数。
118
+ * APIキーや設定ファイルパスなどを指定。
119
+ */
120
+ env?: Record<string, string>;
121
+ }
122
+
123
+ /**
124
+ * ツール設定ファイル全体
125
+ *
126
+ * ~/.gwt/tools.json のスキーマ。
127
+ */
128
+ export interface ToolsConfig {
129
+ /**
130
+ * 設定フォーマットのバージョン
131
+ *
132
+ * セマンティックバージョニング形式。
133
+ */
134
+ version: string;
135
+
136
+ /**
137
+ * 設定ファイルの最終更新日時(ISO8601)
138
+ */
139
+ updatedAt?: string;
140
+
141
+ /**
142
+ * すべてのツールで共有する環境変数
143
+ */
144
+ env?: Record<string, string>;
145
+
146
+ /**
147
+ * カスタムツール定義の配列
148
+ *
149
+ * 空配列も許可(ビルトインツールのみ使用)。
150
+ */
151
+ customTools: CustomAITool[];
152
+ }
153
+
154
+ // ============================================================================
155
+ // 内部使用の型定義
156
+ // ============================================================================
157
+
158
+ /**
159
+ * 統合ツール設定
160
+ *
161
+ * ビルトインツールとカスタムツールを統合して扱うための内部型。
162
+ * getAllTools() 関数がこの型の配列を返します。
163
+ */
164
+ export interface AIToolConfig {
165
+ /**
166
+ * ツールID
167
+ *
168
+ * ビルトイン: "claude-code" | "codex-cli"
169
+ * カスタム: CustomAITool.id
170
+ */
171
+ id: string;
172
+
173
+ /**
174
+ * UI表示名
175
+ */
176
+ displayName: string;
177
+
178
+ /**
179
+ * アイコン文字(オプション)
180
+ */
181
+ icon?: string;
182
+
183
+ /**
184
+ * ビルトインツールかどうか
185
+ *
186
+ * true: Claude Code または Codex CLI
187
+ * false: カスタムツール
188
+ */
189
+ isBuiltin: boolean;
190
+
191
+ /**
192
+ * カスタムツールの場合、元の設定
193
+ *
194
+ * isBuiltin=false の場合のみ存在。
195
+ */
196
+ customConfig?: CustomAITool;
197
+ }
198
+
199
+ /**
200
+ * ツール起動オプション
201
+ *
202
+ * launchCustomAITool() 関数の引数として使用。
203
+ */
204
+ export interface LaunchOptions {
205
+ /**
206
+ * 実行モード
207
+ */
208
+ mode?: "normal" | "continue" | "resume";
209
+
210
+ /**
211
+ * 権限スキップを有効にするか
212
+ *
213
+ * true の場合、permissionSkipArgs が追加されます。
214
+ */
215
+ skipPermissions?: boolean;
216
+
217
+ /**
218
+ * 追加引数
219
+ *
220
+ * コマンドラインから -- 以降に渡された引数。
221
+ */
222
+ extraArgs?: string[];
223
+
224
+ /**
225
+ * 作業ディレクトリ(ワークツリーパス)
226
+ *
227
+ * ツール起動時のcwdとして使用されます。
228
+ */
229
+ cwd?: string;
230
+
231
+ /**
232
+ * 共有環境変数(共通env + ローカル取り込み)
233
+ */
234
+ sharedEnv?: Record<string, string>;
235
+ }
@@ -0,0 +1,54 @@
1
+ const SPINNER_FRAMES = [
2
+ "⠋",
3
+ "⠙",
4
+ "⠹",
5
+ "⠸",
6
+ "⠼",
7
+ "⠴",
8
+ "⠦",
9
+ "⠧",
10
+ "⠇",
11
+ "⠏",
12
+ ] as const;
13
+ const FRAME_INTERVAL_MS = 80;
14
+
15
+ function isWritableStream(
16
+ stream: NodeJS.WriteStream | undefined,
17
+ ): stream is NodeJS.WriteStream {
18
+ return Boolean(stream && typeof stream.write === "function");
19
+ }
20
+
21
+ /**
22
+ * CLI用の簡易スピナーを開始する。
23
+ * 戻り値の関数を呼び出すとスピナーが停止して行をクリアします。
24
+ */
25
+ export function startSpinner(message: string): () => void {
26
+ const stream = process.stdout;
27
+
28
+ if (!isWritableStream(stream) || stream.isTTY === false) {
29
+ return () => {};
30
+ }
31
+
32
+ let frameIndex = 0;
33
+ let active = true;
34
+ const padding = " ".repeat(message.length + 2);
35
+
36
+ const render = () => {
37
+ if (!active) return;
38
+ const frame = SPINNER_FRAMES[frameIndex];
39
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
40
+ stream.write(`\r${frame} ${message}`);
41
+ };
42
+
43
+ render();
44
+ const timer = setInterval(render, FRAME_INTERVAL_MS);
45
+
46
+ const stop = () => {
47
+ if (!active) return;
48
+ active = false;
49
+ clearInterval(timer);
50
+ stream.write(`\r${padding}\r`);
51
+ };
52
+
53
+ return stop;
54
+ }