@jcjeon/integration-cli 0.2.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 (264) hide show
  1. package/.gitignore +23 -0
  2. package/.npmignore +21 -0
  3. package/.prettierignore +6 -0
  4. package/.prettierrc +26 -0
  5. package/AGENTS.md +10 -0
  6. package/CLAUDE.md +10 -0
  7. package/README.md +384 -0
  8. package/apps/server/README.md +294 -0
  9. package/apps/server/eslint.config.mjs +20 -0
  10. package/apps/server/nest-cli.json +8 -0
  11. package/apps/server/package.json +89 -0
  12. package/apps/server/scripts/postinstall.js +53 -0
  13. package/apps/server/src/__mocks__/glob.js +6 -0
  14. package/apps/server/src/__mocks__/uuid.js +5 -0
  15. package/apps/server/src/app.controller.spec.ts +24 -0
  16. package/apps/server/src/app.controller.ts +13 -0
  17. package/apps/server/src/app.module.ts +18 -0
  18. package/apps/server/src/app.service.ts +8 -0
  19. package/apps/server/src/common/ji-paths.ts +41 -0
  20. package/apps/server/src/database/database.module.ts +27 -0
  21. package/apps/server/src/database/entities/agent-changelog.entity.ts +39 -0
  22. package/apps/server/src/database/entities/agent-session.entity.ts +29 -0
  23. package/apps/server/src/database/entities/conversation.entity.ts +41 -0
  24. package/apps/server/src/database/entities/session.entity.ts +16 -0
  25. package/apps/server/src/database/entities/task-agent-run.entity.ts +40 -0
  26. package/apps/server/src/database/entities/task-agent.entity.ts +42 -0
  27. package/apps/server/src/database/entities/task-requirement.entity.ts +27 -0
  28. package/apps/server/src/database/entities/task-run.entity.ts +41 -0
  29. package/apps/server/src/database/entities/task.entity.ts +44 -0
  30. package/apps/server/src/main.ts +65 -0
  31. package/apps/server/src/modules/agents/agent-model-settings.spec.ts +80 -0
  32. package/apps/server/src/modules/agents/agents.module.ts +11 -0
  33. package/apps/server/src/modules/agents/claude/claude-auth.manager.ts +83 -0
  34. package/apps/server/src/modules/agents/claude/claude-pty.manager.ts +380 -0
  35. package/apps/server/src/modules/agents/claude/claude.controller.ts +85 -0
  36. package/apps/server/src/modules/agents/claude/claude.gateway.ts +158 -0
  37. package/apps/server/src/modules/agents/claude/claude.module.ts +18 -0
  38. package/apps/server/src/modules/agents/claude/claude.service.ts +67 -0
  39. package/apps/server/src/modules/agents/claude/dto/create-session.dto.ts +24 -0
  40. package/apps/server/src/modules/agents/claude/dto/resize-session.dto.ts +13 -0
  41. package/apps/server/src/modules/agents/claude/dto/send-input.dto.ts +9 -0
  42. package/apps/server/src/modules/agents/claude/interfaces/claude-session.interface.ts +26 -0
  43. package/apps/server/src/modules/agents/claude/interfaces/pty-event.interface.ts +10 -0
  44. package/apps/server/src/modules/agents/claude/interfaces/stream-event.interface.ts +61 -0
  45. package/apps/server/src/modules/agents/codex/codex-auth.manager.ts +107 -0
  46. package/apps/server/src/modules/agents/codex/codex-session.manager.ts +357 -0
  47. package/apps/server/src/modules/agents/codex/codex.controller.ts +64 -0
  48. package/apps/server/src/modules/agents/codex/codex.gateway.ts +97 -0
  49. package/apps/server/src/modules/agents/codex/codex.module.ts +17 -0
  50. package/apps/server/src/modules/agents/codex/dto/configure-auth.dto.ts +7 -0
  51. package/apps/server/src/modules/agents/gemini/dto/configure-auth.dto.ts +15 -0
  52. package/apps/server/src/modules/agents/gemini/dto/create-session.dto.ts +9 -0
  53. package/apps/server/src/modules/agents/gemini/dto/send-input.dto.ts +9 -0
  54. package/apps/server/src/modules/agents/gemini/gemini-auth.manager.ts +157 -0
  55. package/apps/server/src/modules/agents/gemini/gemini-session.manager.ts +287 -0
  56. package/apps/server/src/modules/agents/gemini/gemini.controller.ts +93 -0
  57. package/apps/server/src/modules/agents/gemini/gemini.gateway.ts +149 -0
  58. package/apps/server/src/modules/agents/gemini/gemini.module.ts +17 -0
  59. package/apps/server/src/modules/agents/gemini/interfaces/gemini-session.interface.ts +18 -0
  60. package/apps/server/src/modules/agents/gemini/interfaces/stream-event.interface.ts +14 -0
  61. package/apps/server/src/modules/agents/session-termination.spec.ts +103 -0
  62. package/apps/server/src/modules/changelog/changelog.controller.ts +20 -0
  63. package/apps/server/src/modules/changelog/changelog.module.ts +14 -0
  64. package/apps/server/src/modules/changelog/changelog.service.spec.ts +531 -0
  65. package/apps/server/src/modules/changelog/changelog.service.ts +690 -0
  66. package/apps/server/src/modules/conversations/conversation.controller.spec.ts +106 -0
  67. package/apps/server/src/modules/conversations/conversation.controller.ts +60 -0
  68. package/apps/server/src/modules/conversations/conversation.module.ts +14 -0
  69. package/apps/server/src/modules/conversations/conversation.service.spec.ts +176 -0
  70. package/apps/server/src/modules/conversations/conversation.service.ts +54 -0
  71. package/apps/server/src/modules/conversations/dto/create-conversation.dto.ts +37 -0
  72. package/apps/server/src/modules/conversations/enums/conversation.enum.ts +13 -0
  73. package/apps/server/src/modules/fs/fs.controller.ts +29 -0
  74. package/apps/server/src/modules/fs/fs.module.ts +8 -0
  75. package/apps/server/src/modules/harness/dto/save-harness.dto.ts +9 -0
  76. package/apps/server/src/modules/harness/harness.controller.spec.ts +95 -0
  77. package/apps/server/src/modules/harness/harness.controller.ts +35 -0
  78. package/apps/server/src/modules/harness/harness.module.ts +11 -0
  79. package/apps/server/src/modules/harness/harness.service.spec.ts +217 -0
  80. package/apps/server/src/modules/harness/harness.service.ts +112 -0
  81. package/apps/server/src/modules/sessions/session.controller.spec.ts +68 -0
  82. package/apps/server/src/modules/sessions/session.controller.ts +43 -0
  83. package/apps/server/src/modules/sessions/session.module.ts +14 -0
  84. package/apps/server/src/modules/sessions/session.service.spec.ts +106 -0
  85. package/apps/server/src/modules/sessions/session.service.ts +35 -0
  86. package/apps/server/src/modules/tasks/dto/create-task.dto.ts +54 -0
  87. package/apps/server/src/modules/tasks/dto/execute-task.dto.ts +22 -0
  88. package/apps/server/src/modules/tasks/dto/merge-file.dto.ts +7 -0
  89. package/apps/server/src/modules/tasks/dto/rerun-task.dto.ts +14 -0
  90. package/apps/server/src/modules/tasks/dto/update-task.dto.ts +55 -0
  91. package/apps/server/src/modules/tasks/task-execution.service.ts +978 -0
  92. package/apps/server/src/modules/tasks/task.gateway.ts +140 -0
  93. package/apps/server/src/modules/tasks/tasks.controller.spec.ts +210 -0
  94. package/apps/server/src/modules/tasks/tasks.controller.ts +139 -0
  95. package/apps/server/src/modules/tasks/tasks.module.ts +30 -0
  96. package/apps/server/src/modules/tasks/tasks.service.spec.ts +552 -0
  97. package/apps/server/src/modules/tasks/tasks.service.ts +333 -0
  98. package/apps/server/test/app.e2e-spec.ts +28 -0
  99. package/apps/server/test/jest-e2e.json +9 -0
  100. package/apps/server/tsconfig.build.json +4 -0
  101. package/apps/server/tsconfig.json +13 -0
  102. package/apps/web/AGENTS.md +7 -0
  103. package/apps/web/CLAUDE.md +1 -0
  104. package/apps/web/README.md +36 -0
  105. package/apps/web/eslint.config.mjs +21 -0
  106. package/apps/web/next-env.d.ts +6 -0
  107. package/apps/web/next.config.ts +7 -0
  108. package/apps/web/package.json +49 -0
  109. package/apps/web/postcss.config.mjs +7 -0
  110. package/apps/web/public/file.svg +1 -0
  111. package/apps/web/public/globe.svg +1 -0
  112. package/apps/web/public/next.svg +1 -0
  113. package/apps/web/public/vercel.svg +1 -0
  114. package/apps/web/public/window.svg +1 -0
  115. package/apps/web/src/app/claude/page.tsx +5 -0
  116. package/apps/web/src/app/codex/page.tsx +126 -0
  117. package/apps/web/src/app/favicon.ico +0 -0
  118. package/apps/web/src/app/gemini/page.tsx +130 -0
  119. package/apps/web/src/app/globals.css +149 -0
  120. package/apps/web/src/app/layout.tsx +40 -0
  121. package/apps/web/src/app/login/page.tsx +67 -0
  122. package/apps/web/src/app/page.tsx +497 -0
  123. package/apps/web/src/app/task/[id]/page.tsx +11 -0
  124. package/apps/web/src/app/test/page.tsx +298 -0
  125. package/apps/web/src/components/ui/Modal.tsx +78 -0
  126. package/apps/web/src/components/ui/WorkingDirPicker.tsx +195 -0
  127. package/apps/web/src/components/ui/__tests__/Modal.test.tsx +68 -0
  128. package/apps/web/src/features/auth/api/__tests__/auth.api.test.ts +83 -0
  129. package/apps/web/src/features/auth/api/auth.api.ts +81 -0
  130. package/apps/web/src/features/auth/hooks/__tests__/useClaudeAuth.test.ts +166 -0
  131. package/apps/web/src/features/auth/hooks/__tests__/useCodexAuth.test.ts +127 -0
  132. package/apps/web/src/features/auth/hooks/__tests__/useGeminiAuth.test.ts +120 -0
  133. package/apps/web/src/features/auth/hooks/useClaudeAuth.ts +88 -0
  134. package/apps/web/src/features/auth/hooks/useCodexAuth.ts +149 -0
  135. package/apps/web/src/features/auth/hooks/useGeminiAuth.ts +125 -0
  136. package/apps/web/src/features/auth/ui/CodexLoginPanel.tsx +302 -0
  137. package/apps/web/src/features/auth/ui/GeminiLoginPanel.tsx +316 -0
  138. package/apps/web/src/features/auth/ui/LoginForm.tsx +190 -0
  139. package/apps/web/src/features/auth/ui/LoginPanel.tsx +114 -0
  140. package/apps/web/src/features/auth/ui/__tests__/LoginPanel.test.tsx +105 -0
  141. package/apps/web/src/features/chat/api/__tests__/sessions.api.test.ts +187 -0
  142. package/apps/web/src/features/chat/api/sessions.api.ts +161 -0
  143. package/apps/web/src/features/chat/container/ClaudePageContainer.tsx +152 -0
  144. package/apps/web/src/features/chat/hooks/__tests__/useCodexSessions.test.ts +131 -0
  145. package/apps/web/src/features/chat/hooks/__tests__/useGeminiSessions.test.ts +130 -0
  146. package/apps/web/src/features/chat/hooks/useAgentModelSettings.ts +54 -0
  147. package/apps/web/src/features/chat/hooks/useClaudeSessions.ts +323 -0
  148. package/apps/web/src/features/chat/hooks/useCodexSessions.ts +275 -0
  149. package/apps/web/src/features/chat/hooks/useGeminiSessions.ts +255 -0
  150. package/apps/web/src/features/chat/hooks/useSessionCommand.ts +66 -0
  151. package/apps/web/src/features/chat/hooks/useSessionRename.ts +61 -0
  152. package/apps/web/src/features/chat/hooks/useSessionWorkingDirectories.ts +34 -0
  153. package/apps/web/src/features/chat/hooks/useUnifiedSessions.ts +156 -0
  154. package/apps/web/src/features/chat/lib/agentModelOptions.ts +72 -0
  155. package/apps/web/src/features/chat/ui/AgentModelPicker.tsx +134 -0
  156. package/apps/web/src/features/chat/ui/AgentSelectModal.tsx +236 -0
  157. package/apps/web/src/features/chat/ui/ChatInput.tsx +162 -0
  158. package/apps/web/src/features/chat/ui/ChatMessage.tsx +204 -0
  159. package/apps/web/src/features/chat/ui/ChatWorkspace.tsx +207 -0
  160. package/apps/web/src/features/chat/ui/CheckingSkeleton.tsx +44 -0
  161. package/apps/web/src/features/chat/ui/ClaudeLoginView.tsx +44 -0
  162. package/apps/web/src/features/chat/ui/PermissionCard.tsx +37 -0
  163. package/apps/web/src/features/chat/ui/SessionSidebar.tsx +280 -0
  164. package/apps/web/src/features/chat/ui/__tests__/AgentSelectModal.test.tsx +58 -0
  165. package/apps/web/src/features/chat/ui/__tests__/ChatInput.test.tsx +134 -0
  166. package/apps/web/src/features/chat/ui/__tests__/ChatMessage.test.tsx +106 -0
  167. package/apps/web/src/features/chat/ui/__tests__/ChatWorkspace.test.tsx +66 -0
  168. package/apps/web/src/features/diff/ui/DiffFileRow.tsx +73 -0
  169. package/apps/web/src/features/diff/ui/DiffHunk.tsx +61 -0
  170. package/apps/web/src/features/diff/ui/FileChangeBadge.tsx +23 -0
  171. package/apps/web/src/features/diff/ui/__tests__/DiffFileRow.test.tsx +40 -0
  172. package/apps/web/src/features/diff/ui/__tests__/DiffHunk.test.tsx +24 -0
  173. package/apps/web/src/features/diff/ui/__tests__/FileChangeBadge.test.tsx +16 -0
  174. package/apps/web/src/features/fs/api/fs.api.ts +14 -0
  175. package/apps/web/src/features/fs/hooks/useDirBrowser.ts +50 -0
  176. package/apps/web/src/features/harness/api/__tests__/harness.api.test.ts +73 -0
  177. package/apps/web/src/features/harness/api/harness.api.ts +46 -0
  178. package/apps/web/src/features/harness/hooks/__tests__/useHarness.test.ts +65 -0
  179. package/apps/web/src/features/harness/hooks/useHarness.ts +66 -0
  180. package/apps/web/src/features/harness/ui/HarnessModal.tsx +171 -0
  181. package/apps/web/src/features/harness/ui/__tests__/HarnessModal.test.tsx +46 -0
  182. package/apps/web/src/features/status/ui/AgentStatusModal.tsx +267 -0
  183. package/apps/web/src/features/status/ui/__tests__/AgentStatusModal.test.tsx +71 -0
  184. package/apps/web/src/features/tasks/api/__tests__/changelog.api.test.ts +89 -0
  185. package/apps/web/src/features/tasks/api/__tests__/tasks.api.test.ts +282 -0
  186. package/apps/web/src/features/tasks/api/changelog.api.ts +52 -0
  187. package/apps/web/src/features/tasks/api/tasks.api.ts +175 -0
  188. package/apps/web/src/features/tasks/container/TaskDetailPageContainer.tsx +69 -0
  189. package/apps/web/src/features/tasks/hooks/__tests__/useChangelogCodeCopy.test.ts +48 -0
  190. package/apps/web/src/features/tasks/hooks/__tests__/useTaskChangelog.test.ts +48 -0
  191. package/apps/web/src/features/tasks/hooks/__tests__/useTaskCreate.test.ts +217 -0
  192. package/apps/web/src/features/tasks/hooks/__tests__/useTaskEdit.test.ts +152 -0
  193. package/apps/web/src/features/tasks/hooks/__tests__/useTaskExecution.test.ts +143 -0
  194. package/apps/web/src/features/tasks/hooks/__tests__/useTaskList.test.ts +168 -0
  195. package/apps/web/src/features/tasks/hooks/__tests__/useTaskNotification.test.ts +125 -0
  196. package/apps/web/src/features/tasks/hooks/__tests__/useTaskRuns.test.ts +51 -0
  197. package/apps/web/src/features/tasks/hooks/useChangelogCodeCopy.ts +52 -0
  198. package/apps/web/src/features/tasks/hooks/useCopyToClipboard.ts +47 -0
  199. package/apps/web/src/features/tasks/hooks/useTaskChangelog.ts +32 -0
  200. package/apps/web/src/features/tasks/hooks/useTaskCreate.ts +137 -0
  201. package/apps/web/src/features/tasks/hooks/useTaskDetail.ts +217 -0
  202. package/apps/web/src/features/tasks/hooks/useTaskEdit.ts +130 -0
  203. package/apps/web/src/features/tasks/hooks/useTaskExecution.ts +137 -0
  204. package/apps/web/src/features/tasks/hooks/useTaskList.ts +159 -0
  205. package/apps/web/src/features/tasks/hooks/useTaskNotification.ts +80 -0
  206. package/apps/web/src/features/tasks/hooks/useTaskRuns.ts +32 -0
  207. package/apps/web/src/features/tasks/ui/AgentOutputPanel.tsx +203 -0
  208. package/apps/web/src/features/tasks/ui/AgentRoleSelect.tsx +97 -0
  209. package/apps/web/src/features/tasks/ui/ChangelogPanel.tsx +321 -0
  210. package/apps/web/src/features/tasks/ui/RunHistoryPanel.tsx +193 -0
  211. package/apps/web/src/features/tasks/ui/TaskCreateModal.tsx +205 -0
  212. package/apps/web/src/features/tasks/ui/TaskDetailView.tsx +413 -0
  213. package/apps/web/src/features/tasks/ui/TaskEditModal.tsx +165 -0
  214. package/apps/web/src/features/tasks/ui/TaskListModal.tsx +591 -0
  215. package/apps/web/src/features/tasks/ui/__tests__/AgentRoleSelect.test.tsx +91 -0
  216. package/apps/web/src/features/tasks/ui/__tests__/ChangelogPanel.test.tsx +94 -0
  217. package/apps/web/src/features/tasks/ui/__tests__/RunHistoryPanel.test.tsx +71 -0
  218. package/apps/web/src/features/tasks/ui/__tests__/TaskCreateModal.test.tsx +153 -0
  219. package/apps/web/src/features/tasks/ui/__tests__/TaskEditModal.test.tsx +75 -0
  220. package/apps/web/src/features/tasks/ui/__tests__/TaskListModal.test.tsx +243 -0
  221. package/apps/web/src/hooks/useWorkingDir.ts +28 -0
  222. package/apps/web/src/lib/__tests__/ansi.test.ts +88 -0
  223. package/apps/web/src/lib/ansi.ts +105 -0
  224. package/apps/web/src/lib/constants.ts +4 -0
  225. package/apps/web/src/lib/quota.ts +22 -0
  226. package/apps/web/src/lib/theme.tsx +78 -0
  227. package/apps/web/src/lib/toast.tsx +175 -0
  228. package/apps/web/src/store/agentStatusStore.ts +38 -0
  229. package/apps/web/tsconfig.json +18 -0
  230. package/apps/web/vitest.config.ts +25 -0
  231. package/apps/web/vitest.setup.ts +10 -0
  232. package/package.json +85 -0
  233. package/packages/cli/dist/commands/check.d.ts +1 -0
  234. package/packages/cli/dist/commands/check.js +89 -0
  235. package/packages/cli/dist/commands/init.d.ts +5 -0
  236. package/packages/cli/dist/commands/init.js +183 -0
  237. package/packages/cli/dist/commands/start.d.ts +4 -0
  238. package/packages/cli/dist/commands/start.js +188 -0
  239. package/packages/cli/dist/index.d.ts +2 -0
  240. package/packages/cli/dist/index.js +71 -0
  241. package/packages/cli/dist/utils/agent-tools.d.ts +28 -0
  242. package/packages/cli/dist/utils/agent-tools.js +193 -0
  243. package/packages/cli/dist/utils/project-init.d.ts +12 -0
  244. package/packages/cli/dist/utils/project-init.js +258 -0
  245. package/packages/cli/dist/utils/proxy.d.ts +8 -0
  246. package/packages/cli/dist/utils/proxy.js +138 -0
  247. package/packages/cli/package.json +30 -0
  248. package/packages/cli/src/commands/check.ts +77 -0
  249. package/packages/cli/src/commands/init.ts +209 -0
  250. package/packages/cli/src/commands/start.ts +183 -0
  251. package/packages/cli/src/index.ts +91 -0
  252. package/packages/cli/src/utils/agent-tools.ts +201 -0
  253. package/packages/cli/src/utils/project-init.ts +252 -0
  254. package/packages/cli/src/utils/proxy.ts +123 -0
  255. package/packages/cli/tsconfig.json +14 -0
  256. package/packages/eslint-config/base.mjs +31 -0
  257. package/packages/eslint-config/nest.mjs +55 -0
  258. package/packages/eslint-config/next.mjs +23 -0
  259. package/packages/eslint-config/package.json +20 -0
  260. package/packages/typescript-config/base.json +16 -0
  261. package/packages/typescript-config/nestjs.json +17 -0
  262. package/packages/typescript-config/nextjs.json +15 -0
  263. package/packages/typescript-config/package.json +11 -0
  264. package/turbo.json +28 -0
@@ -0,0 +1,316 @@
1
+ "use client";
2
+
3
+ import { useRef, useState } from "react";
4
+
5
+ import type { GeminiAuthMethod, GeminiLoginState } from "../hooks/useGeminiAuth";
6
+
7
+ interface GeminiLoginPanelProps {
8
+ loginState: GeminiLoginState;
9
+ loginOutput: string;
10
+ loginUrls: string[];
11
+ configError: string;
12
+ onSaveApiKey: (key: string) => void;
13
+ onStartGca: () => void;
14
+ onCancel: () => void;
15
+ onReset: () => void;
16
+ }
17
+
18
+ const GOOGLE_LOGO = (
19
+ <svg viewBox="0 0 24 24" className="h-5 w-5 shrink-0" aria-hidden="true">
20
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" />
21
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
22
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
23
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
24
+ </svg>
25
+ );
26
+
27
+ // ─── Method Select ────────────────────────────────────────────────────────────
28
+
29
+ interface MethodSelectProps {
30
+ onSelect: (method: GeminiAuthMethod) => void;
31
+ }
32
+
33
+ function MethodSelect({ onSelect }: MethodSelectProps) {
34
+ return (
35
+ <div className="flex w-full max-w-sm flex-col gap-3">
36
+ <button
37
+ onClick={() => onSelect("api-key")}
38
+ className="flex items-center gap-3 rounded-xl border border-gray-900/[0.08] bg-gray-900/[0.02] px-5 py-4 text-left transition-all hover:border-blue-500/40 hover:bg-blue-500/[0.04] dark:border-white/[0.08] dark:bg-white/[0.02] dark:hover:border-blue-400/40 dark:hover:bg-blue-400/[0.04]"
39
+ >
40
+ <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-blue-500/[0.10]">
41
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-4.5 w-4.5 text-blue-500 dark:text-blue-400">
42
+ <path fillRule="evenodd" d="M8 7a5 5 0 113.61 4.804l-1.903 1.903A1 1 0 019 14H8v1a1 1 0 01-1 1H6v1a1 1 0 01-1 1H3a1 1 0 01-1-1v-2a1 1 0 01.293-.707L8.196 8.39A5.002 5.002 0 018 7zm5-3a.75.75 0 000 1.5A1.5 1.5 0 0114.5 7 .75.75 0 0016 7a3 3 0 00-3-3z" clipRule="evenodd" />
43
+ </svg>
44
+ </span>
45
+ <div>
46
+ <p className="text-sm font-semibold text-gray-900/85 dark:text-white/85">API 키</p>
47
+ <p className="text-xs text-gray-900/35 dark:text-white/35">Google AI Studio에서 발급한 키 입력</p>
48
+ </div>
49
+ </button>
50
+
51
+ <button
52
+ onClick={() => onSelect("gca")}
53
+ className="flex items-center gap-3 rounded-xl border border-gray-900/[0.08] bg-gray-900/[0.02] px-5 py-4 text-left transition-all hover:border-blue-500/40 hover:bg-blue-500/[0.04] dark:border-white/[0.08] dark:bg-white/[0.02] dark:hover:border-blue-400/40 dark:hover:bg-blue-400/[0.04]"
54
+ >
55
+ <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-blue-500/[0.10]">
56
+ {GOOGLE_LOGO}
57
+ </span>
58
+ <div>
59
+ <p className="text-sm font-semibold text-gray-900/85 dark:text-white/85">Google Cloud (GCA)</p>
60
+ <p className="text-xs text-gray-900/35 dark:text-white/35">gcloud CLI로 Application Default Credentials 설정</p>
61
+ </div>
62
+ </button>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ // ─── API Key Form ─────────────────────────────────────────────────────────────
68
+
69
+ interface ApiKeyFormProps {
70
+ loginState: GeminiLoginState;
71
+ configError: string;
72
+ onSubmit: (key: string) => void;
73
+ onBack: () => void;
74
+ }
75
+
76
+ function ApiKeyForm({ loginState, configError, onSubmit, onBack }: ApiKeyFormProps) {
77
+ const inputRef = useRef<HTMLInputElement>(null);
78
+
79
+ const handleSubmit = (e: React.FormEvent) => {
80
+ e.preventDefault();
81
+ const key = inputRef.current?.value.trim() ?? "";
82
+ if (key) onSubmit(key);
83
+ };
84
+
85
+ if (loginState === "done") return <SuccessView />;
86
+
87
+ return (
88
+ <form onSubmit={handleSubmit} className="flex w-full max-w-sm flex-col gap-4">
89
+ <div className="flex flex-col gap-1.5">
90
+ <label className="text-xs font-medium text-gray-900/50 dark:text-white/50">
91
+ Gemini API 키
92
+ </label>
93
+ <input
94
+ ref={inputRef}
95
+ type="password"
96
+ placeholder="AIza..."
97
+ autoFocus
98
+ required
99
+ className="rounded-lg border border-gray-900/[0.10] bg-gray-900/[0.03] px-3.5 py-2.5 font-mono text-sm text-gray-900/85 placeholder-gray-900/20 outline-none transition-colors focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/10 dark:border-white/[0.10] dark:bg-white/[0.03] dark:text-white/85 dark:placeholder-white/20 dark:focus:border-blue-400/50 dark:focus:ring-blue-400/10"
100
+ />
101
+ <p className="text-[11px] text-gray-900/30 dark:text-white/30">
102
+ <a
103
+ href="https://aistudio.google.com/app/apikey"
104
+ target="_blank"
105
+ rel="noopener noreferrer"
106
+ className="text-blue-500 hover:underline dark:text-blue-400"
107
+ >
108
+ Google AI Studio
109
+ </a>
110
+ 에서 API 키를 발급받을 수 있습니다.
111
+ </p>
112
+ {configError && (
113
+ <p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
114
+ )}
115
+ </div>
116
+
117
+ <div className="flex flex-col gap-2">
118
+ <button
119
+ type="submit"
120
+ disabled={loginState === "pending"}
121
+ className="flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-500 disabled:opacity-50"
122
+ >
123
+ {loginState === "pending" && (
124
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
125
+ )}
126
+ {loginState === "pending" ? "저장 중…" : "저장"}
127
+ </button>
128
+ <button
129
+ type="button"
130
+ onClick={onBack}
131
+ className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50"
132
+ >
133
+ ← 뒤로
134
+ </button>
135
+ </div>
136
+ </form>
137
+ );
138
+ }
139
+
140
+ // ─── GCA Form ─────────────────────────────────────────────────────────────────
141
+
142
+ interface GcaFormProps {
143
+ loginState: GeminiLoginState;
144
+ loginOutput: string;
145
+ loginUrls: string[];
146
+ onStart: () => void;
147
+ onCancel: () => void;
148
+ onBack: () => void;
149
+ }
150
+
151
+ function GcaForm({ loginState, loginOutput, loginUrls, onStart, onCancel, onBack }: GcaFormProps) {
152
+ if (loginState === "done") return <SuccessView />;
153
+
154
+ const isPending = loginState === "pending";
155
+
156
+ return (
157
+ <div className="flex w-full max-w-lg flex-col gap-4">
158
+ {loginState === "idle" || loginState === "error" ? (
159
+ <>
160
+ <div className="flex flex-col gap-2 rounded-xl border border-gray-900/[0.07] bg-gray-900/[0.025] p-4 dark:border-white/[0.07] dark:bg-white/[0.025]">
161
+ <p className="text-sm font-medium text-gray-900/70 dark:text-white/70">
162
+ gcloud CLI가 필요합니다
163
+ </p>
164
+ <p className="text-xs text-gray-900/40 dark:text-white/40">
165
+ 먼저{" "}
166
+ <a
167
+ href="https://cloud.google.com/sdk/docs/install"
168
+ target="_blank"
169
+ rel="noopener noreferrer"
170
+ className="text-blue-500 hover:underline dark:text-blue-400"
171
+ >
172
+ Google Cloud SDK
173
+ </a>
174
+ 를 설치한 후 아래 버튼을 클릭하세요.
175
+ </p>
176
+ <code className="mt-1 rounded-md bg-gray-900/[0.04] px-2.5 py-1.5 font-mono text-xs text-gray-900/50 dark:bg-white/[0.04] dark:text-white/50">
177
+ gcloud auth application-default login
178
+ </code>
179
+ </div>
180
+
181
+ {loginState === "error" && loginOutput && (
182
+ <pre className="max-h-32 overflow-y-auto rounded-lg border border-red-200 bg-red-50 px-4 py-3 font-mono text-xs whitespace-pre-wrap text-red-600 dark:border-red-900/50 dark:bg-red-950/40 dark:text-red-400">
183
+ {loginOutput}
184
+ </pre>
185
+ )}
186
+
187
+ <div className="flex flex-col gap-2">
188
+ <button
189
+ onClick={onStart}
190
+ className="flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-500"
191
+ >
192
+ {GOOGLE_LOGO}
193
+ Google 계정으로 로그인
194
+ </button>
195
+ <button
196
+ onClick={onBack}
197
+ className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50"
198
+ >
199
+ ← 뒤로
200
+ </button>
201
+ </div>
202
+ </>
203
+ ) : null}
204
+
205
+ {isPending && (
206
+ <>
207
+ {loginUrls.length > 0 && (
208
+ <div className="flex flex-col gap-2">
209
+ <p className="text-xs font-medium text-gray-900/35 dark:text-white/35">브라우저에서 아래 링크를 열어 인증을 완료하세요:</p>
210
+ {loginUrls.map((url) => (
211
+ <a
212
+ key={url}
213
+ href={url}
214
+ target="_blank"
215
+ rel="noopener noreferrer"
216
+ className="break-all rounded-lg border border-blue-700/50 bg-blue-950/30 px-4 py-3 text-xs font-mono text-blue-700 transition-colors hover:border-blue-500 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-200"
217
+ >
218
+ {url}
219
+ </a>
220
+ ))}
221
+ </div>
222
+ )}
223
+ {loginOutput && (
224
+ <pre className="max-h-48 overflow-y-auto rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.02] px-4 py-3 font-mono text-xs leading-relaxed whitespace-pre-wrap text-gray-900/45 dark:border-white/[0.07] dark:bg-white/[0.02] dark:text-white/45">
225
+ {loginOutput}
226
+ </pre>
227
+ )}
228
+ {loginUrls.length === 0 && (
229
+ <div className="flex items-center justify-center gap-2 text-sm text-gray-900/30 dark:text-white/30">
230
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-gray-900/[0.08] border-t-blue-500 dark:border-white/[0.08]" />
231
+ gcloud 인증 진행 중…
232
+ </div>
233
+ )}
234
+ <button onClick={onCancel} className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50">
235
+ 취소
236
+ </button>
237
+ </>
238
+ )}
239
+ </div>
240
+ );
241
+ }
242
+
243
+ // ─── Success ──────────────────────────────────────────────────────────────────
244
+
245
+ function SuccessView() {
246
+ return (
247
+ <div className="flex flex-col items-center gap-2 text-center">
248
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500/[0.10]">
249
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-emerald-500 dark:text-emerald-400">
250
+ <path fillRule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clipRule="evenodd" />
251
+ </svg>
252
+ </div>
253
+ <p className="text-sm font-medium text-emerald-600 dark:text-emerald-400">인증 완료</p>
254
+ </div>
255
+ );
256
+ }
257
+
258
+ // ─── Panel Root ───────────────────────────────────────────────────────────────
259
+
260
+ export function GeminiLoginPanel({
261
+ loginState,
262
+ loginOutput,
263
+ loginUrls,
264
+ configError,
265
+ onSaveApiKey,
266
+ onStartGca,
267
+ onCancel,
268
+ onReset,
269
+ }: GeminiLoginPanelProps) {
270
+ const [selectedMethod, setSelectedMethod] = useState<GeminiAuthMethod | null>(null);
271
+
272
+ const handleBack = () => {
273
+ onReset();
274
+ setSelectedMethod(null);
275
+ };
276
+
277
+ return (
278
+ <div className="flex flex-1 flex-col items-center justify-center gap-6 px-6">
279
+ {/* Header */}
280
+ <div className="flex flex-col items-center gap-3 text-center">
281
+ <div className="flex h-16 w-16 items-center justify-center rounded-2xl border border-gray-900/[0.08] bg-blue-500/[0.08] dark:border-white/[0.08]">
282
+ {GOOGLE_LOGO}
283
+ </div>
284
+ <h2 className="text-xl font-semibold text-gray-900/90 dark:text-white/90">Gemini CLI 인증</h2>
285
+ {!selectedMethod && (
286
+ <p className="max-w-sm text-sm text-gray-900/40 dark:text-white/40">
287
+ 사용할 인증 방식을 선택해 주세요.
288
+ </p>
289
+ )}
290
+ </div>
291
+
292
+ {/* Content */}
293
+ {!selectedMethod && <MethodSelect onSelect={setSelectedMethod} />}
294
+
295
+ {selectedMethod === "api-key" && (
296
+ <ApiKeyForm
297
+ loginState={loginState}
298
+ configError={configError}
299
+ onSubmit={onSaveApiKey}
300
+ onBack={handleBack}
301
+ />
302
+ )}
303
+
304
+ {selectedMethod === "gca" && (
305
+ <GcaForm
306
+ loginState={loginState}
307
+ loginOutput={loginOutput}
308
+ loginUrls={loginUrls}
309
+ onStart={onStartGca}
310
+ onCancel={onCancel}
311
+ onBack={handleBack}
312
+ />
313
+ )}
314
+ </div>
315
+ );
316
+ }
@@ -0,0 +1,190 @@
1
+ "use client";
2
+
3
+ // ─── Atoms ────────────────────────────────────────────────────────────────────
4
+
5
+ interface LogoMarkProps {
6
+ className?: string;
7
+ }
8
+
9
+ function LogoMark({ className }: LogoMarkProps) {
10
+ return (
11
+ <div className={`relative ${className ?? ""}`}>
12
+ <div className="flex h-12 w-64 items-center justify-center rounded-[16px] border border-gray-900/[0.10] bg-gray-900/[0.05] shadow-[inset_0_1px_0_rgba(0,0,0,0.06)] backdrop-blur-sm dark:border-white/[0.10] dark:bg-white/[0.05] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]">
13
+ <span className="font-mono text-sm font-bold tracking-widest text-gray-900/65 dark:text-white/65">
14
+ INTEGRATION-CLI
15
+ </span>
16
+ </div>
17
+ <div className="absolute -inset-2 rounded-[20px] bg-orange-500/10 blur-xl" />
18
+ </div>
19
+ );
20
+ }
21
+
22
+ interface InputFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
23
+ label: string;
24
+ id: string;
25
+ }
26
+
27
+ function InputField({ label, id, ...props }: InputFieldProps) {
28
+ return (
29
+ <div className="flex flex-col gap-1.5">
30
+ <label htmlFor={id} className="text-xs font-medium text-gray-900/50 dark:text-white/50">
31
+ {label}
32
+ </label>
33
+ <input
34
+ id={id}
35
+ {...props}
36
+ className="w-full rounded-xl border border-gray-900/[0.10] bg-gray-900/[0.03] px-4 py-3 text-sm text-gray-900/90 placeholder:text-gray-900/25 outline-none ring-0 transition-all duration-200 focus:border-orange-400/60 focus:ring-2 focus:ring-orange-400/15 dark:border-white/[0.10] dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/25 dark:focus:border-orange-400/50 dark:focus:ring-orange-400/10"
37
+ />
38
+ </div>
39
+ );
40
+ }
41
+
42
+ interface SubmitButtonProps {
43
+ label: string;
44
+ loading?: boolean;
45
+ onClick?: () => void;
46
+ }
47
+
48
+ function SubmitButton({ label, loading }: SubmitButtonProps) {
49
+ return (
50
+ <button
51
+ type="submit"
52
+ disabled={loading}
53
+ className="relative w-full overflow-hidden rounded-xl bg-orange-600 py-3 text-sm font-semibold text-white transition-all duration-200 hover:bg-orange-500 active:scale-[0.99] disabled:opacity-60"
54
+ >
55
+ {loading ? (
56
+ <span className="flex items-center justify-center gap-2">
57
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
58
+ 로그인 중…
59
+ </span>
60
+ ) : (
61
+ label
62
+ )}
63
+ </button>
64
+ );
65
+ }
66
+
67
+ interface DividerProps {
68
+ label: string;
69
+ }
70
+
71
+ function Divider({ label }: DividerProps) {
72
+ return (
73
+ <div className="flex items-center gap-3">
74
+ <div className="h-px flex-1 bg-gray-900/[0.07] dark:bg-white/[0.07]" />
75
+ <span className="text-xs text-gray-900/25 dark:text-white/25">{label}</span>
76
+ <div className="h-px flex-1 bg-gray-900/[0.07] dark:bg-white/[0.07]" />
77
+ </div>
78
+ );
79
+ }
80
+
81
+ // ─── Form ─────────────────────────────────────────────────────────────────────
82
+
83
+ export interface LoginFormProps {
84
+ email: string;
85
+ password: string;
86
+ loading: boolean;
87
+ error: string | null;
88
+ onEmailChange: (value: string) => void;
89
+ onPasswordChange: (value: string) => void;
90
+ onSubmit: (e: React.FormEvent) => void;
91
+ onGuestLogin: () => void;
92
+ }
93
+
94
+ export function LoginForm({
95
+ email,
96
+ password,
97
+ loading,
98
+ error,
99
+ onEmailChange,
100
+ onPasswordChange,
101
+ onSubmit,
102
+ onGuestLogin,
103
+ }: LoginFormProps) {
104
+ return (
105
+ <div className="flex w-full max-w-[400px] flex-col items-center gap-8 animate-fade-in-up">
106
+ {/* Logo */}
107
+ <header className="flex flex-col items-center gap-5 text-center">
108
+ <LogoMark />
109
+ <div>
110
+ <h1
111
+ className="text-[2.25rem] font-bold leading-none tracking-[-0.03em]"
112
+ style={{
113
+ background: "var(--heading-gradient)",
114
+ WebkitBackgroundClip: "text",
115
+ WebkitTextFillColor: "transparent",
116
+ backgroundClip: "text",
117
+ }}
118
+ >
119
+ JC-CLI
120
+ </h1>
121
+ <p className="mt-2 text-[13px] font-medium uppercase tracking-[0.06em] text-gray-900/28 dark:text-white/28">
122
+ 하나의 플랫폼에서 모든 AI CLI를 제어합니다
123
+ </p>
124
+ </div>
125
+ </header>
126
+
127
+ {/* Card */}
128
+ <div className="w-full rounded-2xl border border-gray-900/[0.08] bg-gray-900/[0.025] p-6 shadow-[0_1px_3px_rgba(0,0,0,0.04),0_8px_24px_-4px_rgba(0,0,0,0.04)] backdrop-blur-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
129
+ {/* Top accent */}
130
+ <div className="absolute inset-x-0 top-0 h-px rounded-t-2xl bg-gradient-to-r from-transparent via-orange-400/50 to-transparent" />
131
+
132
+ <div className="flex flex-col gap-1.5 mb-6">
133
+ <h2 className="text-[15px] font-semibold text-gray-900/90 dark:text-white/90">
134
+ 로그인
135
+ </h2>
136
+ <p className="text-[13px] text-gray-900/35 dark:text-white/35">
137
+ 계정에 로그인하여 서비스를 이용하세요.
138
+ </p>
139
+ </div>
140
+
141
+ <form onSubmit={onSubmit} className="flex flex-col gap-4">
142
+ <InputField
143
+ id="email"
144
+ label="이메일"
145
+ type="email"
146
+ placeholder="you@example.com"
147
+ autoComplete="email"
148
+ value={email}
149
+ onChange={(e) => onEmailChange(e.target.value)}
150
+ />
151
+ <InputField
152
+ id="password"
153
+ label="비밀번호"
154
+ type="password"
155
+ placeholder="••••••••"
156
+ autoComplete="current-password"
157
+ value={password}
158
+ onChange={(e) => onPasswordChange(e.target.value)}
159
+ />
160
+
161
+ {error && (
162
+ <p className="rounded-lg border border-red-500/20 bg-red-500/[0.07] px-3 py-2 text-xs text-red-500 dark:text-red-400">
163
+ {error}
164
+ </p>
165
+ )}
166
+
167
+ <SubmitButton label="로그인" loading={loading} />
168
+ </form>
169
+
170
+ <div className="mt-4 flex flex-col gap-4">
171
+ <Divider label="또는" />
172
+ <button
173
+ type="button"
174
+ onClick={onGuestLogin}
175
+ className="w-full rounded-xl border border-gray-900/[0.09] bg-transparent py-3 text-sm font-medium text-gray-900/60 transition-all duration-200 hover:border-gray-900/[0.14] hover:bg-gray-900/[0.03] hover:text-gray-900/80 dark:border-white/[0.09] dark:text-white/60 dark:hover:border-white/[0.14] dark:hover:bg-white/[0.03] dark:hover:text-white/80"
176
+ >
177
+ 게스트로 계속하기
178
+ </button>
179
+ </div>
180
+ </div>
181
+
182
+ <p className="text-xs text-gray-900/25 dark:text-white/25">
183
+ 계정이 없으신가요?{" "}
184
+ <button type="button" className="text-orange-600 transition-colors hover:text-orange-500 dark:text-orange-400 dark:hover:text-orange-300">
185
+ 회원가입
186
+ </button>
187
+ </p>
188
+ </div>
189
+ );
190
+ }
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import type { LoginState } from "../hooks/useClaudeAuth";
4
+
5
+ interface LoginPanelProps {
6
+ loginState: LoginState;
7
+ loginOutput: string;
8
+ loginUrls: string[];
9
+ onStart: () => void;
10
+ onCancel: () => void;
11
+ }
12
+
13
+ export function LoginPanel({ loginState, loginOutput, loginUrls, onStart, onCancel }: LoginPanelProps) {
14
+ const isPending = loginState === "pending";
15
+ const isDone = loginState === "done";
16
+ const isError = loginState === "error";
17
+
18
+ return (
19
+ <div className="flex flex-1 flex-col items-center justify-center gap-6 px-6">
20
+ <div className="flex flex-col items-center gap-3 text-center">
21
+ <div className="flex h-16 w-16 items-center justify-center rounded-2xl border border-gray-900/[0.08] bg-orange-500/[0.08] dark:border-white/[0.08]">
22
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="h-8 w-8 text-orange-500 dark:text-orange-400/80">
23
+ <path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
24
+ </svg>
25
+ </div>
26
+ <h2 className="text-xl font-semibold text-gray-900/90 dark:text-white/90">Claude Code 로그인 필요</h2>
27
+ <p className="max-w-sm text-sm text-gray-900/40 dark:text-white/40">
28
+ Claude CLI를 사용하려면 Anthropic 계정으로 로그인해야 합니다.
29
+ </p>
30
+ </div>
31
+
32
+ {loginState === "idle" && (
33
+ <button
34
+ onClick={onStart}
35
+ className="rounded-xl bg-orange-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-orange-500"
36
+ >
37
+ Claude Code 로그인
38
+ </button>
39
+ )}
40
+
41
+ {isPending && (
42
+ <div className="flex w-full max-w-lg flex-col gap-4">
43
+ {loginUrls.length > 0 && (
44
+ <div className="flex flex-col gap-2">
45
+ <p className="text-xs font-medium text-gray-900/35 dark:text-white/35">브라우저에서 아래 링크를 열어 인증을 완료하세요:</p>
46
+ {loginUrls.map((url) => (
47
+ <a
48
+ key={url}
49
+ href={url}
50
+ target="_blank"
51
+ rel="noopener noreferrer"
52
+ className="break-all rounded-lg border border-orange-700/50 bg-orange-950/30 px-4 py-3 text-xs font-mono text-orange-700 transition-colors hover:border-orange-500 hover:text-orange-600 dark:text-orange-300 dark:hover:text-orange-200"
53
+ >
54
+ {url}
55
+ </a>
56
+ ))}
57
+ </div>
58
+ )}
59
+
60
+ {loginOutput && (
61
+ <pre className="max-h-48 overflow-y-auto rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.02] px-4 py-3 font-mono text-xs leading-relaxed whitespace-pre-wrap text-gray-900/45 dark:border-white/[0.07] dark:bg-white/[0.02] dark:text-white/45">
62
+ {loginOutput}
63
+ </pre>
64
+ )}
65
+
66
+ {loginUrls.length === 0 && (
67
+ <div className="flex items-center justify-center gap-2 text-sm text-gray-900/30 dark:text-white/30">
68
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-gray-900/[0.08] border-t-orange-500 dark:border-white/[0.08]" />
69
+ 로그인 프로세스를 시작하는 중…
70
+ </div>
71
+ )}
72
+
73
+ <button onClick={onCancel} className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50">
74
+ 취소
75
+ </button>
76
+ </div>
77
+ )}
78
+
79
+ {isDone && (
80
+ <div className="flex flex-col items-center gap-2 text-center">
81
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500/[0.10]">
82
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-emerald-500 dark:text-emerald-400">
83
+ <path fillRule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clipRule="evenodd" />
84
+ </svg>
85
+ </div>
86
+ <p className="text-sm font-medium text-emerald-600 dark:text-emerald-400">로그인 완료</p>
87
+ <p className="text-xs text-gray-900/25 dark:text-white/25">잠시 후 자동으로 이동합니다…</p>
88
+ </div>
89
+ )}
90
+
91
+ {isError && (
92
+ <div className="flex flex-col items-center gap-3 text-center">
93
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-red-500/[0.10]">
94
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-red-500 dark:text-red-400">
95
+ <path fillRule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
96
+ </svg>
97
+ </div>
98
+ <p className="text-sm text-red-500 dark:text-red-400">로그인 중 문제가 발생했습니다.</p>
99
+ {loginOutput && (
100
+ <pre className="max-h-32 w-full max-w-lg overflow-y-auto rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.02] px-4 py-3 font-mono text-xs whitespace-pre-wrap text-gray-900/40 dark:border-white/[0.07] dark:bg-white/[0.02] dark:text-white/40">
101
+ {loginOutput}
102
+ </pre>
103
+ )}
104
+ <button
105
+ onClick={onStart}
106
+ className="rounded-xl bg-orange-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-orange-500"
107
+ >
108
+ 다시 시도
109
+ </button>
110
+ </div>
111
+ )}
112
+ </div>
113
+ );
114
+ }