@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,149 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { io, type Socket } from "socket.io-client";
5
+
6
+ import { CODEX_WS_NAMESPACE, SERVER_URL } from "@/lib/constants";
7
+ import { configureCodexAuth, getCodexAuthStatus } from "../api/auth.api";
8
+
9
+ export type CodexAuthState = "checking" | "authenticated" | "unauthenticated" | "not-installed";
10
+ export type CodexLoginState = "idle" | "pending" | "done" | "error";
11
+ export type CodexLoginMethod = "device" | "apikey";
12
+
13
+ const URL_REGEX = /https?:\/\/[\w./-]+/g;
14
+ const CODE_REGEX = /(?<![A-Z0-9])([A-Z0-9]{4}-[A-Z0-9]{4,6})(?![A-Z0-9])/;
15
+
16
+ export function extractUrls(text: string): string[] {
17
+ return Array.from(new Set(text.match(URL_REGEX) ?? []));
18
+ }
19
+
20
+ export function extractDeviceCode(text: string): string | null {
21
+ return text.match(CODE_REGEX)?.[1] ?? null;
22
+ }
23
+
24
+ export function useCodexAuth() {
25
+ const [authState, setAuthState] = useState<CodexAuthState>("checking");
26
+ const [loginMethod, setLoginMethod] = useState<CodexLoginMethod>("device");
27
+
28
+ // device auth state
29
+ const [loginState, setLoginState] = useState<CodexLoginState>("idle");
30
+ const [loginOutput, setLoginOutput] = useState("");
31
+ const [loginUrls, setLoginUrls] = useState<string[]>([]);
32
+ const [deviceCode, setDeviceCode] = useState<string | null>(null);
33
+
34
+ // api key state
35
+ const [apiKeyLoginState, setApiKeyLoginState] = useState<CodexLoginState>("idle");
36
+ const [configError, setConfigError] = useState("");
37
+
38
+ const socketRef = useRef<Socket | null>(null);
39
+ const outputRef = useRef("");
40
+ const isLoginActiveRef = useRef(false);
41
+
42
+ const checkAuth = useCallback(async () => {
43
+ setAuthState("checking");
44
+ try {
45
+ const data = await getCodexAuthStatus();
46
+ if (!data.installed) setAuthState("not-installed");
47
+ else setAuthState(data.loggedIn ? "authenticated" : "unauthenticated");
48
+ } catch {
49
+ setAuthState("unauthenticated");
50
+ }
51
+ }, []);
52
+
53
+ useEffect(() => { void checkAuth(); }, [checkAuth]);
54
+
55
+ // ── Device auth (WebSocket) ───────────────────────────────────────────
56
+
57
+ const startDeviceLogin = useCallback(() => {
58
+ if (loginState === "pending") return;
59
+
60
+ outputRef.current = "";
61
+ setLoginOutput("");
62
+ setLoginUrls([]);
63
+ setDeviceCode(null);
64
+ setLoginState("pending");
65
+ isLoginActiveRef.current = true;
66
+
67
+ const socket = io(`${SERVER_URL}${CODEX_WS_NAMESPACE}`, { transports: ["websocket"] });
68
+ socketRef.current = socket;
69
+
70
+ socket.on("connect", () => socket.emit("auth:login:start"));
71
+
72
+ socket.on("auth:output", ({ text }: { text: string }) => {
73
+ outputRef.current += text;
74
+ setLoginOutput(outputRef.current);
75
+ setLoginUrls(extractUrls(outputRef.current));
76
+ const code = extractDeviceCode(outputRef.current);
77
+ if (code) setDeviceCode(code);
78
+ });
79
+
80
+ socket.on("auth:done", ({ success }: { success: boolean }) => {
81
+ isLoginActiveRef.current = false;
82
+ socket.disconnect();
83
+ socketRef.current = null;
84
+ setLoginState(success ? "done" : "error");
85
+ if (success) setAuthState("authenticated");
86
+ });
87
+
88
+ socket.on("disconnect", () => {
89
+ if (isLoginActiveRef.current) {
90
+ isLoginActiveRef.current = false;
91
+ setLoginState("error");
92
+ }
93
+ });
94
+ }, [loginState]);
95
+
96
+ const cancelDeviceLogin = useCallback(() => {
97
+ isLoginActiveRef.current = false;
98
+ socketRef.current?.emit("auth:login:cancel");
99
+ socketRef.current?.disconnect();
100
+ socketRef.current = null;
101
+ setLoginState("idle");
102
+ setLoginOutput("");
103
+ setLoginUrls([]);
104
+ setDeviceCode(null);
105
+ outputRef.current = "";
106
+ }, []);
107
+
108
+ // ── API key ───────────────────────────────────────────────────────────
109
+
110
+ const saveApiKey = useCallback(async (apiKey: string) => {
111
+ setConfigError("");
112
+ setApiKeyLoginState("pending");
113
+ try {
114
+ await configureCodexAuth(apiKey);
115
+ setApiKeyLoginState("done");
116
+ setAuthState("authenticated");
117
+ } catch {
118
+ setConfigError("API 키 저장에 실패했습니다. 서버 연결을 확인하세요.");
119
+ setApiKeyLoginState("error");
120
+ }
121
+ }, []);
122
+
123
+ const resetApiKeyLogin = useCallback(() => {
124
+ setApiKeyLoginState("idle");
125
+ setConfigError("");
126
+ }, []);
127
+
128
+ return {
129
+ authState,
130
+ loginMethod,
131
+ setLoginMethod,
132
+
133
+ // device auth
134
+ loginState,
135
+ loginOutput,
136
+ loginUrls,
137
+ deviceCode,
138
+ startDeviceLogin,
139
+ cancelDeviceLogin,
140
+
141
+ // api key
142
+ apiKeyLoginState,
143
+ configError,
144
+ saveApiKey,
145
+ resetApiKeyLogin,
146
+
147
+ checkAuth,
148
+ };
149
+ }
@@ -0,0 +1,125 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { io, type Socket } from "socket.io-client";
5
+
6
+ import { GEMINI_WS_NAMESPACE, SERVER_URL } from "@/lib/constants";
7
+ import { configureGeminiAuth, getGeminiAuthStatus } from "../api/auth.api";
8
+
9
+ export type GeminiAuthState = "checking" | "authenticated" | "unauthenticated" | "not-installed";
10
+ export type GeminiLoginState = "idle" | "pending" | "done" | "error";
11
+ export type GeminiAuthMethod = "api-key" | "gca";
12
+
13
+ const URL_REGEX = /https?:\/\/[^\s\-"<>\\^`{|}~]+/g;
14
+
15
+ function extractUrls(text: string): string[] {
16
+ return Array.from(new Set(text.match(URL_REGEX) ?? []));
17
+ }
18
+
19
+ export function useGeminiAuth() {
20
+ const [authState, setAuthState] = useState<GeminiAuthState>("checking");
21
+ const [loginState, setLoginState] = useState<GeminiLoginState>("idle");
22
+ const [loginOutput, setLoginOutput] = useState("");
23
+ const [loginUrls, setLoginUrls] = useState<string[]>([]);
24
+ const [configError, setConfigError] = useState("");
25
+
26
+ const socketRef = useRef<Socket | null>(null);
27
+ const outputRef = useRef("");
28
+ const isLoginActiveRef = useRef(false);
29
+
30
+ const checkAuth = useCallback(async () => {
31
+ setAuthState("checking");
32
+ try {
33
+ const data = await getGeminiAuthStatus();
34
+ if (!data.installed) setAuthState("not-installed");
35
+ else setAuthState(data.loggedIn ? "authenticated" : "unauthenticated");
36
+ } catch {
37
+ setAuthState("unauthenticated");
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => { void checkAuth(); }, [checkAuth]);
42
+
43
+ // ── API Key 방식 ────────────────────────────────────────────────────────────
44
+ const saveApiKey = useCallback(async (apiKey: string) => {
45
+ setConfigError("");
46
+ setLoginState("pending");
47
+ try {
48
+ await configureGeminiAuth("api-key", apiKey);
49
+ setLoginState("done");
50
+ setAuthState("authenticated");
51
+ } catch {
52
+ setConfigError("API 키 저장에 실패했습니다. 서버 연결을 확인하세요.");
53
+ setLoginState("error");
54
+ }
55
+ }, []);
56
+
57
+ // ── GCA 방식 (gcloud auth application-default login) ────────────────────────
58
+ const startGcaLogin = useCallback(() => {
59
+ if (loginState === "pending") return;
60
+
61
+ outputRef.current = "";
62
+ setLoginOutput("");
63
+ setLoginUrls([]);
64
+ setLoginState("pending");
65
+ isLoginActiveRef.current = true;
66
+
67
+ const socket = io(`${SERVER_URL}${GEMINI_WS_NAMESPACE}`, { transports: ["websocket"] });
68
+ socketRef.current = socket;
69
+
70
+ socket.on("connect", () => socket.emit("auth:gca:start"));
71
+
72
+ socket.on("auth:output", ({ text }: { text: string }) => {
73
+ outputRef.current += text;
74
+ setLoginOutput(outputRef.current);
75
+ setLoginUrls(extractUrls(outputRef.current));
76
+ });
77
+
78
+ socket.on("auth:done", ({ success }: { success: boolean }) => {
79
+ isLoginActiveRef.current = false;
80
+ socket.disconnect();
81
+ socketRef.current = null;
82
+ setLoginState(success ? "done" : "error");
83
+ if (success) setAuthState("authenticated");
84
+ });
85
+
86
+ socket.on("disconnect", () => {
87
+ if (isLoginActiveRef.current) {
88
+ isLoginActiveRef.current = false;
89
+ setLoginState("error");
90
+ }
91
+ });
92
+ }, [loginState]);
93
+
94
+ const cancelLogin = useCallback(() => {
95
+ isLoginActiveRef.current = false;
96
+ socketRef.current?.emit("auth:login:cancel");
97
+ socketRef.current?.disconnect();
98
+ socketRef.current = null;
99
+ setLoginState("idle");
100
+ setLoginOutput("");
101
+ setLoginUrls([]);
102
+ outputRef.current = "";
103
+ }, []);
104
+
105
+ const resetLogin = useCallback(() => {
106
+ setLoginState("idle");
107
+ setLoginOutput("");
108
+ setLoginUrls([]);
109
+ setConfigError("");
110
+ outputRef.current = "";
111
+ }, []);
112
+
113
+ return {
114
+ authState,
115
+ loginState,
116
+ loginOutput,
117
+ loginUrls,
118
+ configError,
119
+ saveApiKey,
120
+ startGcaLogin,
121
+ cancelLogin,
122
+ resetLogin,
123
+ checkAuth,
124
+ };
125
+ }
@@ -0,0 +1,302 @@
1
+ "use client";
2
+
3
+ import { useRef } from "react";
4
+
5
+ import type { CodexLoginMethod, CodexLoginState } from "../hooks/useCodexAuth";
6
+
7
+ const OPENAI_LOGO = (
8
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5 shrink-0 text-gray-900/80 dark:text-white/80" aria-hidden="true">
9
+ <path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />
10
+ </svg>
11
+ );
12
+
13
+ // ─── Method Tab ───────────────────────────────────────────────────────────────
14
+
15
+ interface TabProps {
16
+ method: CodexLoginMethod;
17
+ selected: CodexLoginMethod;
18
+ onSelect: (m: CodexLoginMethod) => void;
19
+ label: string;
20
+ }
21
+
22
+ function MethodTab({ method, selected, onSelect, label }: TabProps) {
23
+ return (
24
+ <button
25
+ type="button"
26
+ onClick={() => onSelect(method)}
27
+ className={[
28
+ "flex-1 rounded-lg px-4 py-2 text-xs font-medium transition-colors",
29
+ selected === method
30
+ ? "bg-gray-900/[0.07] text-gray-900/75 dark:bg-white/[0.07] dark:text-white/75"
31
+ : "text-gray-900/35 hover:text-gray-900/60 dark:text-white/35 dark:hover:text-white/60",
32
+ ].join(" ")}
33
+ >
34
+ {label}
35
+ </button>
36
+ );
37
+ }
38
+
39
+ // ─── Device Auth Panel ────────────────────────────────────────────────────────
40
+
41
+ interface DeviceAuthProps {
42
+ loginState: CodexLoginState;
43
+ loginOutput: string;
44
+ loginUrls: string[];
45
+ deviceCode: string | null;
46
+ onStart: () => void;
47
+ onCancel: () => void;
48
+ }
49
+
50
+ function DeviceAuthView({ loginState, loginOutput, loginUrls, deviceCode, onStart, onCancel }: DeviceAuthProps) {
51
+ if (loginState === "idle") {
52
+ return (
53
+ <button
54
+ onClick={onStart}
55
+ className="rounded-xl bg-gray-900 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-gray-800 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100"
56
+ >
57
+ ChatGPT로 로그인
58
+ </button>
59
+ );
60
+ }
61
+
62
+ if (loginState === "pending") {
63
+ return (
64
+ <div className="flex w-full max-w-sm flex-col gap-4">
65
+ {loginUrls.length > 0 ? (
66
+ <>
67
+ <div className="flex flex-col gap-2">
68
+ <p className="text-xs font-medium text-gray-900/35 dark:text-white/35">
69
+ 1. 아래 링크를 브라우저에서 열어 로그인하세요
70
+ </p>
71
+ {loginUrls.map((url) => (
72
+ <a
73
+ key={url}
74
+ href={url}
75
+ target="_blank"
76
+ rel="noopener noreferrer"
77
+ className="break-all rounded-lg border border-emerald-700/40 bg-emerald-950/20 px-4 py-3 text-xs font-mono text-emerald-700 transition-colors hover:border-emerald-500 hover:text-emerald-600 dark:text-emerald-300 dark:hover:text-emerald-200"
78
+ >
79
+ {url}
80
+ </a>
81
+ ))}
82
+ </div>
83
+ {deviceCode && (
84
+ <div className="flex flex-col gap-1.5">
85
+ <p className="text-xs font-medium text-gray-900/35 dark:text-white/35">
86
+ 2. 이 일회용 코드를 입력하세요 <span className="text-gray-900/20 dark:text-white/20">(15분 내 유효)</span>
87
+ </p>
88
+ <div className="flex items-center justify-center rounded-xl border border-gray-900/[0.10] bg-gray-900/[0.03] py-4 dark:border-white/[0.10] dark:bg-white/[0.03]">
89
+ <span className="font-mono text-2xl font-bold tracking-widest text-gray-900/80 dark:text-white/80">
90
+ {deviceCode}
91
+ </span>
92
+ </div>
93
+ </div>
94
+ )}
95
+ </>
96
+ ) : (
97
+ <div className="flex items-center justify-center gap-2 text-sm text-gray-900/30 dark:text-white/30">
98
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-gray-900/[0.08] border-t-gray-900/60 dark:border-white/[0.08] dark:border-t-white/60" />
99
+ 연결 중…
100
+ </div>
101
+ )}
102
+
103
+ {loginOutput && !loginUrls.length && (
104
+ <pre className="max-h-40 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">
105
+ {loginOutput}
106
+ </pre>
107
+ )}
108
+
109
+ <button
110
+ onClick={onCancel}
111
+ className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50"
112
+ >
113
+ 취소
114
+ </button>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ if (loginState === "done") {
120
+ return (
121
+ <div className="flex flex-col items-center gap-2 text-center">
122
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500/[0.10]">
123
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-emerald-500 dark:text-emerald-400">
124
+ <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" />
125
+ </svg>
126
+ </div>
127
+ <p className="text-sm font-medium text-emerald-600 dark:text-emerald-400">로그인 완료</p>
128
+ <p className="text-xs text-gray-900/25 dark:text-white/25">잠시 후 자동으로 이동합니다…</p>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ // error
134
+ return (
135
+ <div className="flex flex-col items-center gap-3 text-center">
136
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-red-500/[0.10]">
137
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-red-500 dark:text-red-400">
138
+ <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" />
139
+ </svg>
140
+ </div>
141
+ <p className="text-sm text-red-500 dark:text-red-400">로그인 중 문제가 발생했습니다.</p>
142
+ {loginOutput && (
143
+ <pre className="max-h-32 w-full max-w-sm 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">
144
+ {loginOutput}
145
+ </pre>
146
+ )}
147
+ <button
148
+ onClick={onStart}
149
+ className="rounded-xl bg-gray-900 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-gray-800 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100"
150
+ >
151
+ 다시 시도
152
+ </button>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ // ─── API Key Panel ────────────────────────────────────────────────────────────
158
+
159
+ interface ApiKeyProps {
160
+ loginState: CodexLoginState;
161
+ configError: string;
162
+ onSave: (key: string) => void;
163
+ }
164
+
165
+ function ApiKeyView({ loginState, configError, onSave }: ApiKeyProps) {
166
+ const inputRef = useRef<HTMLInputElement>(null);
167
+
168
+ const handleSubmit = (e: React.FormEvent) => {
169
+ e.preventDefault();
170
+ const key = inputRef.current?.value.trim() ?? "";
171
+ if (key) onSave(key);
172
+ };
173
+
174
+ if (loginState === "done") {
175
+ return (
176
+ <div className="flex flex-col items-center gap-2 text-center">
177
+ <div className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500/[0.10]">
178
+ <svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-emerald-500 dark:text-emerald-400">
179
+ <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" />
180
+ </svg>
181
+ </div>
182
+ <p className="text-sm font-medium text-emerald-600 dark:text-emerald-400">인증 완료</p>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ return (
188
+ <form onSubmit={handleSubmit} className="flex w-full max-w-sm flex-col gap-4">
189
+ <div className="flex flex-col gap-1.5">
190
+ <label className="text-xs font-medium text-gray-900/50 dark:text-white/50">
191
+ OpenAI API 키
192
+ </label>
193
+ <input
194
+ ref={inputRef}
195
+ type="password"
196
+ placeholder="sk-..."
197
+ autoFocus
198
+ required
199
+ 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-gray-900/30 focus:ring-2 focus:ring-gray-900/5 dark:border-white/[0.10] dark:bg-white/[0.03] dark:text-white/85 dark:placeholder-white/20 dark:focus:border-white/30 dark:focus:ring-white/5"
200
+ />
201
+ <p className="text-[11px] text-gray-900/30 dark:text-white/30">
202
+ <a
203
+ href="https://platform.openai.com/api-keys"
204
+ target="_blank"
205
+ rel="noopener noreferrer"
206
+ className="text-gray-900/55 underline-offset-2 hover:underline dark:text-white/55"
207
+ >
208
+ OpenAI 플랫폼
209
+ </a>
210
+ 에서 API 키를 발급받을 수 있습니다.
211
+ </p>
212
+ {configError && (
213
+ <p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
214
+ )}
215
+ </div>
216
+
217
+ <button
218
+ type="submit"
219
+ disabled={loginState === "pending"}
220
+ className="flex items-center justify-center gap-2 rounded-xl bg-gray-900 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-gray-800 disabled:opacity-50 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100"
221
+ >
222
+ {loginState === "pending" && (
223
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white dark:border-gray-900/30 dark:border-t-gray-900" />
224
+ )}
225
+ {loginState === "pending" ? "저장 중…" : "저장"}
226
+ </button>
227
+ </form>
228
+ );
229
+ }
230
+
231
+ // ─── CodexLoginPanel ──────────────────────────────────────────────────────────
232
+
233
+ export type { CodexLoginMethod };
234
+
235
+ interface Props {
236
+ loginMethod: CodexLoginMethod;
237
+ onMethodChange: (m: CodexLoginMethod) => void;
238
+ // device auth
239
+ loginState: CodexLoginState;
240
+ loginOutput: string;
241
+ loginUrls: string[];
242
+ deviceCode: string | null;
243
+ onStartDeviceLogin: () => void;
244
+ onCancelDeviceLogin: () => void;
245
+ // api key
246
+ apiKeyLoginState: CodexLoginState;
247
+ configError: string;
248
+ onSaveApiKey: (key: string) => void;
249
+ }
250
+
251
+ export function CodexLoginPanel({
252
+ loginMethod,
253
+ onMethodChange,
254
+ loginState,
255
+ loginOutput,
256
+ loginUrls,
257
+ deviceCode,
258
+ onStartDeviceLogin,
259
+ onCancelDeviceLogin,
260
+ apiKeyLoginState,
261
+ configError,
262
+ onSaveApiKey,
263
+ }: Props) {
264
+ return (
265
+ <div className="flex flex-1 flex-col items-center justify-center gap-6 px-6">
266
+ {/* Header */}
267
+ <div className="flex flex-col items-center gap-3 text-center">
268
+ <div className="flex h-16 w-16 items-center justify-center rounded-2xl border border-gray-900/[0.08] bg-gray-900/[0.04] dark:border-white/[0.08] dark:bg-white/[0.04]">
269
+ {OPENAI_LOGO}
270
+ </div>
271
+ <h2 className="text-xl font-semibold text-gray-900/90 dark:text-white/90">Codex CLI 인증</h2>
272
+ <p className="max-w-sm text-sm text-gray-900/40 dark:text-white/40">
273
+ ChatGPT 계정으로 로그인하거나 API 키를 입력해 주세요.
274
+ </p>
275
+ </div>
276
+
277
+ {/* Method tabs */}
278
+ <div className="flex w-full max-w-sm rounded-xl border border-gray-900/[0.08] p-1 dark:border-white/[0.08]">
279
+ <MethodTab method="device" selected={loginMethod} onSelect={onMethodChange} label="ChatGPT 로그인" />
280
+ <MethodTab method="apikey" selected={loginMethod} onSelect={onMethodChange} label="API 키" />
281
+ </div>
282
+
283
+ {/* Content */}
284
+ {loginMethod === "device" ? (
285
+ <DeviceAuthView
286
+ loginState={loginState}
287
+ loginOutput={loginOutput}
288
+ loginUrls={loginUrls}
289
+ deviceCode={deviceCode}
290
+ onStart={onStartDeviceLogin}
291
+ onCancel={onCancelDeviceLogin}
292
+ />
293
+ ) : (
294
+ <ApiKeyView
295
+ loginState={apiKeyLoginState}
296
+ configError={configError}
297
+ onSave={onSaveApiKey}
298
+ />
299
+ )}
300
+ </div>
301
+ );
302
+ }