@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,83 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { getAuthStatus, getClaudeStatus } from "../auth.api";
4
+
5
+ const mockFetch = vi.fn();
6
+
7
+ beforeEach(() => { vi.stubGlobal("fetch", mockFetch); });
8
+ afterEach(() => { vi.unstubAllGlobals(); });
9
+
10
+ function ok(body: unknown) {
11
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(body) } as Response);
12
+ }
13
+ function err(status: number) {
14
+ return Promise.resolve({ ok: false, status } as Response);
15
+ }
16
+
17
+ describe("getAuthStatus", () => {
18
+ it("returns auth data on success", async () => {
19
+ const data = { loggedIn: true, authMethod: "claude.ai", apiProvider: "firstParty" };
20
+ mockFetch.mockReturnValueOnce(ok(data));
21
+
22
+ const result = await getAuthStatus();
23
+
24
+ expect(result.loggedIn).toBe(true);
25
+ expect(result.authMethod).toBe("claude.ai");
26
+ expect(mockFetch).toHaveBeenCalledWith(
27
+ expect.stringContaining("/agents/claude/auth/status"),
28
+ );
29
+ });
30
+
31
+ it("returns unauthenticated data when not logged in", async () => {
32
+ const data = { loggedIn: false, authMethod: "none", apiProvider: "firstParty" };
33
+ mockFetch.mockReturnValueOnce(ok(data));
34
+ const result = await getAuthStatus();
35
+ expect(result.loggedIn).toBe(false);
36
+ });
37
+
38
+ it("throws on HTTP error", async () => {
39
+ mockFetch.mockReturnValueOnce(err(401));
40
+ await expect(getAuthStatus()).rejects.toThrow("HTTP 401");
41
+ });
42
+
43
+ it("includes optional fields when present", async () => {
44
+ const data = {
45
+ loggedIn: true,
46
+ authMethod: "claude.ai",
47
+ apiProvider: "firstParty",
48
+ email: "user@example.com",
49
+ orgName: "Acme",
50
+ subscriptionType: "pro",
51
+ };
52
+ mockFetch.mockReturnValueOnce(ok(data));
53
+ const result = await getAuthStatus();
54
+ expect(result.email).toBe("user@example.com");
55
+ expect(result.subscriptionType).toBe("pro");
56
+ });
57
+ });
58
+
59
+ describe("getClaudeStatus", () => {
60
+ it("returns full status on success", async () => {
61
+ const data = {
62
+ version: "2.1.0 (Claude Code)",
63
+ auth: { loggedIn: true, authMethod: "claude.ai", apiProvider: "firstParty" },
64
+ activeSessions: 3,
65
+ platform: "darwin arm64",
66
+ };
67
+ mockFetch.mockReturnValueOnce(ok(data));
68
+
69
+ const result = await getClaudeStatus();
70
+
71
+ expect(result.version).toBe("2.1.0 (Claude Code)");
72
+ expect(result.activeSessions).toBe(3);
73
+ expect(result.platform).toBe("darwin arm64");
74
+ expect(mockFetch).toHaveBeenCalledWith(
75
+ expect.stringContaining("/agents/claude/status"),
76
+ );
77
+ });
78
+
79
+ it("throws on HTTP error", async () => {
80
+ mockFetch.mockReturnValueOnce(err(500));
81
+ await expect(getClaudeStatus()).rejects.toThrow("HTTP 500");
82
+ });
83
+ });
@@ -0,0 +1,81 @@
1
+ import { SERVER_URL } from "@/lib/constants";
2
+
3
+ export interface AuthStatus {
4
+ loggedIn: boolean;
5
+ authMethod: string;
6
+ apiProvider: string;
7
+ email?: string;
8
+ orgName?: string;
9
+ subscriptionType?: string;
10
+ }
11
+
12
+ export interface ClaudeStatus {
13
+ version: string;
14
+ auth: AuthStatus;
15
+ activeSessions: number;
16
+ platform: string;
17
+ }
18
+
19
+ export interface GeminiAuthStatus {
20
+ loggedIn: boolean;
21
+ authMethod: string;
22
+ installed: boolean;
23
+ email?: string;
24
+ }
25
+
26
+ export async function getAuthStatus(): Promise<AuthStatus> {
27
+ const res = await fetch(`${SERVER_URL}/agents/claude/auth/status`);
28
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
29
+ return res.json();
30
+ }
31
+
32
+ export async function getClaudeStatus(): Promise<ClaudeStatus> {
33
+ const res = await fetch(`${SERVER_URL}/agents/claude/status`);
34
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
35
+ return res.json();
36
+ }
37
+
38
+ export async function getGeminiAuthStatus(): Promise<GeminiAuthStatus> {
39
+ const res = await fetch(`${SERVER_URL}/agents/gemini/auth/status`);
40
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
41
+ return res.json();
42
+ }
43
+
44
+ export async function configureGeminiAuth(
45
+ authType: "api-key",
46
+ apiKey: string,
47
+ ): Promise<void>;
48
+ export async function configureGeminiAuth(authType: "gca"): Promise<void>;
49
+ export async function configureGeminiAuth(
50
+ authType: "api-key" | "gca",
51
+ apiKey?: string,
52
+ ): Promise<void> {
53
+ const res = await fetch(`${SERVER_URL}/agents/gemini/auth/configure`, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify({ authType, ...(apiKey ? { apiKey } : {}) }),
57
+ });
58
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
59
+ }
60
+
61
+ // ─── Codex ────────────────────────────────────────────────────────────────────
62
+
63
+ export interface CodexAuthStatus {
64
+ installed: boolean;
65
+ loggedIn: boolean;
66
+ }
67
+
68
+ export async function getCodexAuthStatus(): Promise<CodexAuthStatus> {
69
+ const res = await fetch(`${SERVER_URL}/agents/codex/auth/status`);
70
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
71
+ return res.json();
72
+ }
73
+
74
+ export async function configureCodexAuth(apiKey: string): Promise<void> {
75
+ const res = await fetch(`${SERVER_URL}/agents/codex/auth/configure`, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify({ apiKey }),
79
+ });
80
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
81
+ }
@@ -0,0 +1,166 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { extractUrls, useClaudeAuth } from "../useClaudeAuth";
5
+
6
+ // ─── socket.io-client mock ────────────────────────────────────────────────────
7
+
8
+ const mockSocket = {
9
+ on: vi.fn(),
10
+ emit: vi.fn(),
11
+ disconnect: vi.fn(),
12
+ };
13
+
14
+ vi.mock("socket.io-client", () => ({
15
+ io: vi.fn(() => mockSocket),
16
+ }));
17
+
18
+ // ─── fetch mock ───────────────────────────────────────────────────────────────
19
+
20
+ const mockFetch = vi.fn();
21
+
22
+ beforeEach(() => {
23
+ vi.stubGlobal("fetch", mockFetch);
24
+ vi.clearAllMocks();
25
+ // Default: not logged in
26
+ mockFetch.mockResolvedValue({
27
+ ok: true,
28
+ json: () => Promise.resolve({ loggedIn: false, authMethod: "none", apiProvider: "firstParty" }),
29
+ });
30
+ });
31
+
32
+ afterEach(() => {
33
+ vi.unstubAllGlobals();
34
+ });
35
+
36
+ // ─── extractUrls ─────────────────────────────────────────────────────────────
37
+
38
+ describe("extractUrls", () => {
39
+ it("extracts a single URL", () => {
40
+ expect(extractUrls("visit https://claude.ai/auth")).toEqual(["https://claude.ai/auth"]);
41
+ });
42
+
43
+ it("extracts multiple URLs and deduplicates", () => {
44
+ const text = "go to https://a.com and https://a.com or https://b.com";
45
+ expect(extractUrls(text)).toEqual(["https://a.com", "https://b.com"]);
46
+ });
47
+
48
+ it("returns empty array for text without URLs", () => {
49
+ expect(extractUrls("no link here")).toEqual([]);
50
+ });
51
+
52
+ it("handles http URLs", () => {
53
+ expect(extractUrls("http://localhost:3000")).toEqual(["http://localhost:3000"]);
54
+ });
55
+ });
56
+
57
+ // ─── useClaudeAuth ────────────────────────────────────────────────────────────
58
+
59
+ describe("useClaudeAuth", () => {
60
+ it("starts with authState=checking then resolves to unauthenticated", async () => {
61
+ const { result } = renderHook(() => useClaudeAuth());
62
+ expect(result.current.authState).toBe("checking");
63
+
64
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
65
+ });
66
+
67
+ it("resolves to authenticated when loggedIn=true", async () => {
68
+ mockFetch.mockResolvedValueOnce({
69
+ ok: true,
70
+ json: () => Promise.resolve({ loggedIn: true, authMethod: "claude.ai", apiProvider: "firstParty" }),
71
+ });
72
+
73
+ const { result } = renderHook(() => useClaudeAuth());
74
+
75
+ await waitFor(() => expect(result.current.authState).toBe("authenticated"));
76
+ });
77
+
78
+ it("falls back to unauthenticated when fetch fails", async () => {
79
+ mockFetch.mockRejectedValueOnce(new Error("Network error"));
80
+
81
+ const { result } = renderHook(() => useClaudeAuth());
82
+
83
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
84
+ });
85
+
86
+ it("starts loginState as idle", async () => {
87
+ const { result } = renderHook(() => useClaudeAuth());
88
+ await waitFor(() => expect(result.current.authState).not.toBe("checking"));
89
+ expect(result.current.loginState).toBe("idle");
90
+ });
91
+
92
+ it("startLogin transitions loginState to pending and connects socket", async () => {
93
+ const { result } = renderHook(() => useClaudeAuth());
94
+ await waitFor(() => expect(result.current.authState).not.toBe("checking"));
95
+
96
+ act(() => { result.current.startLogin(); });
97
+
98
+ expect(result.current.loginState).toBe("pending");
99
+ expect(mockSocket.on).toHaveBeenCalledWith("connect", expect.any(Function));
100
+ expect(mockSocket.on).toHaveBeenCalledWith("auth:output", expect.any(Function));
101
+ expect(mockSocket.on).toHaveBeenCalledWith("auth:done", expect.any(Function));
102
+ });
103
+
104
+ it("ignores duplicate startLogin when already pending", async () => {
105
+ const { result } = renderHook(() => useClaudeAuth());
106
+ await waitFor(() => expect(result.current.authState).not.toBe("checking"));
107
+
108
+ const { io } = await import("socket.io-client");
109
+ const mockIo = vi.mocked(io);
110
+ mockIo.mockClear();
111
+
112
+ act(() => { result.current.startLogin(); });
113
+ act(() => { result.current.startLogin(); }); // second call should be ignored
114
+
115
+ expect(mockIo).toHaveBeenCalledTimes(1);
116
+ });
117
+
118
+ it("cancelLogin resets loginState to idle", async () => {
119
+ const { result } = renderHook(() => useClaudeAuth());
120
+ await waitFor(() => expect(result.current.authState).not.toBe("checking"));
121
+
122
+ act(() => { result.current.startLogin(); });
123
+ act(() => { result.current.cancelLogin(); });
124
+
125
+ expect(result.current.loginState).toBe("idle");
126
+ expect(mockSocket.disconnect).toHaveBeenCalled();
127
+ });
128
+
129
+ it("auth:done with success=true sets authenticated and done", async () => {
130
+ const { result } = renderHook(() => useClaudeAuth());
131
+ await waitFor(() => expect(result.current.authState).not.toBe("checking"));
132
+
133
+ act(() => { result.current.startLogin(); });
134
+
135
+ // Simulate auth:done event
136
+ const doneHandler = mockSocket.on.mock.calls.find(([event]) => event === "auth:done")?.[1];
137
+ act(() => { doneHandler?.({ success: true }); });
138
+
139
+ expect(result.current.loginState).toBe("done");
140
+ expect(result.current.authState).toBe("authenticated");
141
+ });
142
+
143
+ it("auth:done with success=false sets error state", async () => {
144
+ const { result } = renderHook(() => useClaudeAuth());
145
+ await waitFor(() => expect(result.current.authState).not.toBe("checking"));
146
+
147
+ act(() => { result.current.startLogin(); });
148
+ const doneHandler = mockSocket.on.mock.calls.find(([event]) => event === "auth:done")?.[1];
149
+ act(() => { doneHandler?.({ success: false }); });
150
+
151
+ expect(result.current.loginState).toBe("error");
152
+ });
153
+
154
+ it("auth:output accumulates text and extracts URLs", async () => {
155
+ const { result } = renderHook(() => useClaudeAuth());
156
+ await waitFor(() => expect(result.current.authState).not.toBe("checking"));
157
+
158
+ act(() => { result.current.startLogin(); });
159
+ const outputHandler = mockSocket.on.mock.calls.find(([event]) => event === "auth:output")?.[1];
160
+
161
+ act(() => { outputHandler?.({ text: "Open https://claude.ai/auth to login\n" }); });
162
+
163
+ expect(result.current.loginOutput).toContain("https://claude.ai/auth");
164
+ expect(result.current.loginUrls).toContain("https://claude.ai/auth");
165
+ });
166
+ });
@@ -0,0 +1,127 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import * as authApi from "../../api/auth.api";
5
+ import { extractDeviceCode, extractUrls, useCodexAuth } from "../useCodexAuth";
6
+
7
+ vi.mock("../../api/auth.api", () => ({
8
+ configureCodexAuth: vi.fn(),
9
+ getCodexAuthStatus: vi.fn(),
10
+ }));
11
+
12
+ const socketMock = vi.hoisted(() => {
13
+ const handlers: Record<string, (payload?: unknown) => void> = {};
14
+ const socket = {
15
+ on: vi.fn((event: string, handler: (payload?: unknown) => void) => {
16
+ handlers[event] = handler;
17
+ return socket;
18
+ }),
19
+ emit: vi.fn(),
20
+ disconnect: vi.fn(),
21
+ };
22
+ return {
23
+ handlers,
24
+ socket,
25
+ io: vi.fn(() => socket),
26
+ reset() {
27
+ for (const key of Object.keys(handlers)) delete handlers[key];
28
+ socket.on.mockClear();
29
+ socket.emit.mockClear();
30
+ socket.disconnect.mockClear();
31
+ this.io.mockClear();
32
+ },
33
+ };
34
+ });
35
+
36
+ vi.mock("socket.io-client", () => ({ io: socketMock.io }));
37
+
38
+ const mockGetStatus = vi.mocked(authApi.getCodexAuthStatus);
39
+ const mockConfigure = vi.mocked(authApi.configureCodexAuth);
40
+
41
+ beforeEach(() => {
42
+ mockGetStatus.mockResolvedValue({ installed: true, loggedIn: false });
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.clearAllMocks();
47
+ socketMock.reset();
48
+ });
49
+
50
+ describe("Codex auth helpers", () => {
51
+ it("extracts unique URLs and device code", () => {
52
+ expect(extractUrls("go https://a.test/path then https://a.test/path")).toEqual(["https://a.test/path"]);
53
+ expect(extractDeviceCode("Your code is ABCD-123456")).toBe("ABCD-123456");
54
+ expect(extractDeviceCode("no code")).toBeNull();
55
+ });
56
+ });
57
+
58
+ describe("useCodexAuth", () => {
59
+ it("checks auth status on mount", async () => {
60
+ const { result } = renderHook(() => useCodexAuth());
61
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
62
+ });
63
+
64
+ it("switches login method", async () => {
65
+ const { result } = renderHook(() => useCodexAuth());
66
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
67
+
68
+ act(() => { result.current.setLoginMethod("apikey"); });
69
+ expect(result.current.loginMethod).toBe("apikey");
70
+ });
71
+
72
+ it("saves API key and can reset API key login state", async () => {
73
+ const { result } = renderHook(() => useCodexAuth());
74
+
75
+ await act(async () => { await result.current.saveApiKey("key"); });
76
+ expect(mockConfigure).toHaveBeenCalledWith("key");
77
+ expect(result.current.apiKeyLoginState).toBe("done");
78
+
79
+ act(() => { result.current.resetApiKeyLogin(); });
80
+ expect(result.current.apiKeyLoginState).toBe("idle");
81
+ });
82
+
83
+ it("stores config error when API key save fails", async () => {
84
+ mockConfigure.mockRejectedValueOnce(new Error("bad"));
85
+ const { result } = renderHook(() => useCodexAuth());
86
+
87
+ await act(async () => { await result.current.saveApiKey("bad"); });
88
+
89
+ expect(result.current.apiKeyLoginState).toBe("error");
90
+ expect(result.current.configError).toMatch(/API 키 저장/);
91
+ });
92
+
93
+ it("runs device login, extracts URL and code, and handles done", async () => {
94
+ const { result } = renderHook(() => useCodexAuth());
95
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
96
+
97
+ act(() => { result.current.startDeviceLogin(); });
98
+ act(() => { socketMock.handlers.connect?.(); });
99
+ expect(socketMock.socket.emit).toHaveBeenCalledWith("auth:login:start");
100
+
101
+ act(() => {
102
+ socketMock.handlers["auth:output"]?.({ text: "Open https://auth.test and enter WXYZ-1234" });
103
+ socketMock.handlers["auth:done"]?.({ success: true });
104
+ });
105
+
106
+ expect(result.current.loginUrls).toEqual(["https://auth.test"]);
107
+ expect(result.current.deviceCode).toBe("WXYZ-1234");
108
+ expect(result.current.loginState).toBe("done");
109
+ expect(result.current.authState).toBe("authenticated");
110
+ });
111
+
112
+ it("cancels device login and handles disconnect during active login", async () => {
113
+ const { result } = renderHook(() => useCodexAuth());
114
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
115
+
116
+ act(() => { result.current.startDeviceLogin(); });
117
+ act(() => { socketMock.handlers.disconnect?.(); });
118
+ expect(result.current.loginState).toBe("error");
119
+
120
+ act(() => {
121
+ result.current.startDeviceLogin();
122
+ result.current.cancelDeviceLogin();
123
+ });
124
+ expect(socketMock.socket.emit).toHaveBeenCalledWith("auth:login:cancel");
125
+ expect(result.current.loginState).toBe("idle");
126
+ });
127
+ });
@@ -0,0 +1,120 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import * as authApi from "../../api/auth.api";
5
+ import { useGeminiAuth } from "../useGeminiAuth";
6
+
7
+ vi.mock("../../api/auth.api", () => ({
8
+ configureGeminiAuth: vi.fn(),
9
+ getGeminiAuthStatus: vi.fn(),
10
+ }));
11
+
12
+ const socketMock = vi.hoisted(() => {
13
+ const handlers: Record<string, (payload?: unknown) => void> = {};
14
+ const socket = {
15
+ on: vi.fn((event: string, handler: (payload?: unknown) => void) => {
16
+ handlers[event] = handler;
17
+ return socket;
18
+ }),
19
+ emit: vi.fn(),
20
+ disconnect: vi.fn(),
21
+ };
22
+ return {
23
+ handlers,
24
+ socket,
25
+ io: vi.fn(() => socket),
26
+ reset() {
27
+ for (const key of Object.keys(handlers)) delete handlers[key];
28
+ socket.on.mockClear();
29
+ socket.emit.mockClear();
30
+ socket.disconnect.mockClear();
31
+ this.io.mockClear();
32
+ },
33
+ };
34
+ });
35
+
36
+ vi.mock("socket.io-client", () => ({ io: socketMock.io }));
37
+
38
+ const mockGetStatus = vi.mocked(authApi.getGeminiAuthStatus);
39
+ const mockConfigure = vi.mocked(authApi.configureGeminiAuth);
40
+
41
+ beforeEach(() => {
42
+ mockGetStatus.mockResolvedValue({ installed: true, loggedIn: false, authMethod: "none" });
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.clearAllMocks();
47
+ socketMock.reset();
48
+ });
49
+
50
+ describe("useGeminiAuth", () => {
51
+ it("checks auth status on mount", async () => {
52
+ const { result } = renderHook(() => useGeminiAuth());
53
+
54
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
55
+ });
56
+
57
+ it("marks not-installed and handles status failures", async () => {
58
+ mockGetStatus.mockResolvedValueOnce({ installed: false, loggedIn: false, authMethod: "none" });
59
+ const { result, rerender } = renderHook(() => useGeminiAuth());
60
+ await waitFor(() => expect(result.current.authState).toBe("not-installed"));
61
+
62
+ mockGetStatus.mockRejectedValueOnce(new Error("down"));
63
+ await act(async () => { await result.current.checkAuth(); });
64
+ rerender();
65
+ expect(result.current.authState).toBe("unauthenticated");
66
+ });
67
+
68
+ it("saves API key and authenticates", async () => {
69
+ const { result } = renderHook(() => useGeminiAuth());
70
+ await act(async () => { await result.current.saveApiKey("key"); });
71
+
72
+ expect(mockConfigure).toHaveBeenCalledWith("api-key", "key");
73
+ expect(result.current.loginState).toBe("done");
74
+ expect(result.current.authState).toBe("authenticated");
75
+ });
76
+
77
+ it("stores config error when API key save fails", async () => {
78
+ mockConfigure.mockRejectedValueOnce(new Error("bad"));
79
+ const { result } = renderHook(() => useGeminiAuth());
80
+
81
+ await act(async () => { await result.current.saveApiKey("bad"); });
82
+
83
+ expect(result.current.loginState).toBe("error");
84
+ expect(result.current.configError).toMatch(/API 키 저장/);
85
+ });
86
+
87
+ it("runs GCA login over socket, extracts URLs, and handles success", async () => {
88
+ const { result } = renderHook(() => useGeminiAuth());
89
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
90
+
91
+ act(() => { result.current.startGcaLogin(); });
92
+ act(() => { socketMock.handlers.connect?.(); });
93
+ expect(socketMock.socket.emit).toHaveBeenCalledWith("auth:gca:start");
94
+
95
+ act(() => {
96
+ socketMock.handlers["auth:output"]?.({ text: "Open https://example.com/auth\n" });
97
+ socketMock.handlers["auth:done"]?.({ success: true });
98
+ });
99
+
100
+ expect(result.current.loginUrls).toEqual(["https://example.com/auth"]);
101
+ expect(result.current.loginState).toBe("done");
102
+ expect(result.current.authState).toBe("authenticated");
103
+ });
104
+
105
+ it("cancels an active GCA login and resets output", async () => {
106
+ const { result } = renderHook(() => useGeminiAuth());
107
+ await waitFor(() => expect(result.current.authState).toBe("unauthenticated"));
108
+
109
+ act(() => {
110
+ result.current.startGcaLogin();
111
+ socketMock.handlers["auth:output"]?.({ text: "https://example.com" });
112
+ result.current.cancelLogin();
113
+ });
114
+
115
+ expect(socketMock.socket.emit).toHaveBeenCalledWith("auth:login:cancel");
116
+ expect(socketMock.socket.disconnect).toHaveBeenCalled();
117
+ expect(result.current.loginState).toBe("idle");
118
+ expect(result.current.loginOutput).toBe("");
119
+ });
120
+ });
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { io, type Socket } from "socket.io-client";
5
+
6
+ import { CLAUDE_WS_NAMESPACE, SERVER_URL } from "@/lib/constants";
7
+ import { getAuthStatus } from "../api/auth.api";
8
+
9
+ export type AuthState = "checking" | "authenticated" | "unauthenticated";
10
+ export type LoginState = "idle" | "pending" | "done" | "error";
11
+
12
+ const URL_REGEX = /https?:\/\/[^\s\-"<>\\^`{|}~]+/g;
13
+
14
+ export function extractUrls(text: string): string[] {
15
+ return Array.from(new Set(text.match(URL_REGEX) ?? []));
16
+ }
17
+
18
+ export function useClaudeAuth() {
19
+ const [authState, setAuthState] = useState<AuthState>("checking");
20
+ const [loginState, setLoginState] = useState<LoginState>("idle");
21
+ const [loginOutput, setLoginOutput] = useState("");
22
+ const [loginUrls, setLoginUrls] = useState<string[]>([]);
23
+
24
+ const socketRef = useRef<Socket | null>(null);
25
+ const outputRef = useRef("");
26
+ const isLoginActiveRef = useRef(false);
27
+
28
+ const checkAuth = useCallback(async () => {
29
+ setAuthState("checking");
30
+ try {
31
+ const data = await getAuthStatus();
32
+ setAuthState(data.loggedIn ? "authenticated" : "unauthenticated");
33
+ } catch {
34
+ setAuthState("unauthenticated");
35
+ }
36
+ }, []);
37
+
38
+ useEffect(() => { void checkAuth(); }, [checkAuth]);
39
+
40
+ const startLogin = useCallback(() => {
41
+ if (loginState === "pending") return;
42
+
43
+ outputRef.current = "";
44
+ setLoginOutput("");
45
+ setLoginUrls([]);
46
+ setLoginState("pending");
47
+ isLoginActiveRef.current = true;
48
+
49
+ const socket = io(`${SERVER_URL}${CLAUDE_WS_NAMESPACE}`, { transports: ["websocket"] });
50
+ socketRef.current = socket;
51
+
52
+ socket.on("connect", () => socket.emit("auth:login:start"));
53
+
54
+ socket.on("auth:output", ({ text }: { text: string }) => {
55
+ outputRef.current += text;
56
+ setLoginOutput(outputRef.current);
57
+ setLoginUrls(extractUrls(outputRef.current));
58
+ });
59
+
60
+ socket.on("auth:done", ({ success }: { success: boolean }) => {
61
+ isLoginActiveRef.current = false;
62
+ socket.disconnect();
63
+ socketRef.current = null;
64
+ setLoginState(success ? "done" : "error");
65
+ if (success) setAuthState("authenticated");
66
+ });
67
+
68
+ socket.on("disconnect", () => {
69
+ if (isLoginActiveRef.current) {
70
+ isLoginActiveRef.current = false;
71
+ setLoginState("error");
72
+ }
73
+ });
74
+ }, [loginState]);
75
+
76
+ const cancelLogin = useCallback(() => {
77
+ isLoginActiveRef.current = false;
78
+ socketRef.current?.emit("auth:login:cancel");
79
+ socketRef.current?.disconnect();
80
+ socketRef.current = null;
81
+ setLoginState("idle");
82
+ setLoginOutput("");
83
+ setLoginUrls([]);
84
+ outputRef.current = "";
85
+ }, []);
86
+
87
+ return { authState, loginState, loginOutput, loginUrls, startLogin, cancelLogin, checkAuth };
88
+ }