kcg-code 0.0.0-stage → 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 (307) hide show
  1. package/README.md +230 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +84 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/daemon.test.ts +84 -0
  13. package/src/cli/__tests__/dashboard.test.ts +224 -0
  14. package/src/cli/__tests__/keep-awake.test.ts +44 -0
  15. package/src/cli/__tests__/port.test.ts +86 -0
  16. package/src/cli/args.ts +186 -0
  17. package/src/cli/attach.ts +154 -0
  18. package/src/cli/commands.ts +327 -0
  19. package/src/cli/daemon.ts +275 -0
  20. package/src/cli/dashboard.tsx +1060 -0
  21. package/src/cli/index.ts +47 -0
  22. package/src/cli/keep-awake.ts +148 -0
  23. package/src/cli/port.ts +92 -0
  24. package/src/cli/theme.ts +100 -0
  25. package/src/components/auth/AuthGate.tsx +182 -0
  26. package/src/components/auth/AvatarEditor.tsx +236 -0
  27. package/src/components/auth/LockScreen.tsx +714 -0
  28. package/src/components/auth/ProfileAvatar.tsx +60 -0
  29. package/src/components/auth/WelcomeScreen.tsx +672 -0
  30. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  31. package/src/components/auth/avatar-icons.tsx +114 -0
  32. package/src/components/layout/AppShell.tsx +147 -0
  33. package/src/components/layout/AppSidebar.tsx +419 -0
  34. package/src/components/layout/SearchDialog.tsx +189 -0
  35. package/src/components/layout/SidebarAccount.tsx +111 -0
  36. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  37. package/src/components/layout/ThemeToggle.tsx +67 -0
  38. package/src/components/projects/FolderBrowser.tsx +142 -0
  39. package/src/components/projects/HomeChat.tsx +287 -0
  40. package/src/components/projects/HomeRecentSessions.tsx +90 -0
  41. package/src/components/projects/NewProjectDialog.tsx +249 -0
  42. package/src/components/projects/ProjectInstructions.tsx +218 -0
  43. package/src/components/projects/ProjectRow.tsx +58 -0
  44. package/src/components/projects/ProjectSidePanel.tsx +514 -0
  45. package/src/components/sessions/AgentLogo.tsx +42 -0
  46. package/src/components/sessions/AgentModeList.tsx +265 -0
  47. package/src/components/sessions/AgentPicker.tsx +132 -0
  48. package/src/components/sessions/AssistantTurn.tsx +145 -0
  49. package/src/components/sessions/AttachmentTile.tsx +161 -0
  50. package/src/components/sessions/CodeBlock.tsx +107 -0
  51. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  52. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  53. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
  54. package/src/components/sessions/ContextPanel.tsx +661 -0
  55. package/src/components/sessions/DropPlate.tsx +165 -0
  56. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  57. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  58. package/src/components/sessions/ImageLightbox.tsx +173 -0
  59. package/src/components/sessions/MarkdownContent.tsx +187 -0
  60. package/src/components/sessions/Mascot.tsx +47 -0
  61. package/src/components/sessions/MessageActions.tsx +60 -0
  62. package/src/components/sessions/MessageRail.tsx +121 -0
  63. package/src/components/sessions/ModelPicker.tsx +247 -0
  64. package/src/components/sessions/ModelSearchList.tsx +231 -0
  65. package/src/components/sessions/NewSessionDialog.tsx +172 -0
  66. package/src/components/sessions/PendingAttachments.tsx +127 -0
  67. package/src/components/sessions/PromptCard.tsx +1085 -0
  68. package/src/components/sessions/PromptPanel.tsx +112 -0
  69. package/src/components/sessions/SessionComposer.tsx +641 -0
  70. package/src/components/sessions/SessionHeader.tsx +176 -0
  71. package/src/components/sessions/SessionList.tsx +503 -0
  72. package/src/components/sessions/SessionRow.tsx +237 -0
  73. package/src/components/sessions/SessionTimeline.tsx +275 -0
  74. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  75. package/src/components/sessions/SessionView.tsx +240 -0
  76. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  77. package/src/components/sessions/UserMessage.tsx +177 -0
  78. package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
  79. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  80. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  81. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  82. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  83. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  84. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  85. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  86. package/src/components/settings/AboutGroup.tsx +170 -0
  87. package/src/components/settings/AccountGroup.tsx +122 -0
  88. package/src/components/settings/DevicesGroup.tsx +151 -0
  89. package/src/components/settings/LhrRemoteAccess.tsx +257 -0
  90. package/src/components/settings/PasskeysGroup.tsx +551 -0
  91. package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
  92. package/src/components/settings/SecurityGroup.tsx +566 -0
  93. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  94. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  95. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  96. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  97. package/src/components/settings/prefs.tsx +333 -0
  98. package/src/components/skills/InstallNotifications.tsx +326 -0
  99. package/src/components/skills/InstallSkillDialog.tsx +390 -0
  100. package/src/components/skills/SkillCards.tsx +254 -0
  101. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  102. package/src/components/ui/adw-dialog.tsx +114 -0
  103. package/src/components/ui/adw-toggle-group.tsx +75 -0
  104. package/src/components/ui/alert-dialog.tsx +179 -0
  105. package/src/components/ui/alert.tsx +60 -0
  106. package/src/components/ui/badge.tsx +46 -0
  107. package/src/components/ui/bubble.tsx +125 -0
  108. package/src/components/ui/button.tsx +62 -0
  109. package/src/components/ui/card.tsx +75 -0
  110. package/src/components/ui/collapsible.tsx +19 -0
  111. package/src/components/ui/dialog.tsx +142 -0
  112. package/src/components/ui/dropdown-menu.tsx +225 -0
  113. package/src/components/ui/empty.tsx +94 -0
  114. package/src/components/ui/field.tsx +234 -0
  115. package/src/components/ui/input-group.tsx +161 -0
  116. package/src/components/ui/input.tsx +21 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/message-scroller.tsx +131 -0
  119. package/src/components/ui/message.tsx +85 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/search-entry.tsx +70 -0
  122. package/src/components/ui/select.tsx +172 -0
  123. package/src/components/ui/separator.tsx +28 -0
  124. package/src/components/ui/sheet.tsx +138 -0
  125. package/src/components/ui/sidebar.tsx +699 -0
  126. package/src/components/ui/skeleton.tsx +15 -0
  127. package/src/components/ui/sonner.tsx +45 -0
  128. package/src/components/ui/spinner.tsx +16 -0
  129. package/src/components/ui/switch.tsx +32 -0
  130. package/src/components/ui/textarea.tsx +18 -0
  131. package/src/components/ui/tooltip.tsx +51 -0
  132. package/src/config.ts +114 -0
  133. package/src/db.ts +146 -0
  134. package/src/frontend.tsx +51 -0
  135. package/src/hooks/__tests__/useMention.test.ts +79 -0
  136. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  137. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  138. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  139. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  140. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  141. package/src/hooks/use-mobile.ts +16 -0
  142. package/src/hooks/useAgentPicker.ts +101 -0
  143. package/src/hooks/useMediaQuery.ts +49 -0
  144. package/src/hooks/useMention.ts +236 -0
  145. package/src/hooks/useProjects.ts +88 -0
  146. package/src/hooks/useResyncOnReconnect.ts +28 -0
  147. package/src/hooks/useRouter.ts +92 -0
  148. package/src/hooks/useSessionChat.ts +427 -0
  149. package/src/hooks/useSessionContext.ts +106 -0
  150. package/src/hooks/useSessionList.ts +327 -0
  151. package/src/hooks/useSidebarData.ts +108 -0
  152. package/src/hooks/useSmoothText.ts +122 -0
  153. package/src/hooks/useTheme.ts +164 -0
  154. package/src/hooks/useWebSocket.ts +196 -0
  155. package/src/index.css +27 -0
  156. package/src/index.html +14 -0
  157. package/src/index.ts +49 -0
  158. package/src/lib/__tests__/attachments.test.ts +58 -0
  159. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  160. package/src/lib/__tests__/collapsible.test.ts +101 -0
  161. package/src/lib/__tests__/composer.test.ts +118 -0
  162. package/src/lib/__tests__/highlight.test.ts +42 -0
  163. package/src/lib/__tests__/home-chat.test.ts +64 -0
  164. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  165. package/src/lib/__tests__/model-picker.test.ts +205 -0
  166. package/src/lib/__tests__/onboarding.test.ts +41 -0
  167. package/src/lib/__tests__/permission-info.test.ts +61 -0
  168. package/src/lib/__tests__/project-extensions.test.ts +83 -0
  169. package/src/lib/__tests__/project-overview.test.ts +188 -0
  170. package/src/lib/__tests__/references.test.ts +121 -0
  171. package/src/lib/__tests__/routes.test.ts +134 -0
  172. package/src/lib/__tests__/session-selection.test.ts +41 -0
  173. package/src/lib/__tests__/session-summary.test.ts +115 -0
  174. package/src/lib/__tests__/session-title.test.ts +58 -0
  175. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  176. package/src/lib/__tests__/skill-installs.test.ts +135 -0
  177. package/src/lib/__tests__/skills-catalog.test.ts +124 -0
  178. package/src/lib/__tests__/tone.test.ts +37 -0
  179. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  180. package/src/lib/agent-types.ts +49 -0
  181. package/src/lib/api.ts +306 -0
  182. package/src/lib/attachments.ts +203 -0
  183. package/src/lib/auth.ts +92 -0
  184. package/src/lib/avatar-presets.ts +31 -0
  185. package/src/lib/collapsible.ts +75 -0
  186. package/src/lib/composer.ts +89 -0
  187. package/src/lib/data-events.ts +17 -0
  188. package/src/lib/headlines.ts +30 -0
  189. package/src/lib/highlight.ts +186 -0
  190. package/src/lib/layout.ts +20 -0
  191. package/src/lib/lock-screen.ts +54 -0
  192. package/src/lib/model-picker.ts +133 -0
  193. package/src/lib/onboarding.ts +38 -0
  194. package/src/lib/passkeys.ts +113 -0
  195. package/src/lib/pending-prompt.ts +53 -0
  196. package/src/lib/permission-info.ts +101 -0
  197. package/src/lib/project-extensions.ts +136 -0
  198. package/src/lib/project-overview.ts +123 -0
  199. package/src/lib/prompts.ts +87 -0
  200. package/src/lib/references.ts +106 -0
  201. package/src/lib/routes.ts +124 -0
  202. package/src/lib/session-handoff.ts +25 -0
  203. package/src/lib/session-selection.ts +40 -0
  204. package/src/lib/session-status.ts +45 -0
  205. package/src/lib/session-summary.ts +68 -0
  206. package/src/lib/session-title.ts +63 -0
  207. package/src/lib/sidebar-groups.ts +78 -0
  208. package/src/lib/skill-installs.ts +362 -0
  209. package/src/lib/skills-catalog.ts +138 -0
  210. package/src/lib/time.ts +22 -0
  211. package/src/lib/tone.ts +80 -0
  212. package/src/lib/turns.ts +320 -0
  213. package/src/lib/utils.ts +6 -0
  214. package/src/logo.svg +31 -0
  215. package/src/pages/projects/ProjectDetailPage.tsx +131 -0
  216. package/src/pages/projects/ProjectsPage.tsx +97 -0
  217. package/src/pages/sessions/SessionPage.tsx +122 -0
  218. package/src/pages/settings/SettingsPage.tsx +366 -0
  219. package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
  220. package/src/pages/skills/SkillsPage.tsx +745 -0
  221. package/src/paths.ts +121 -0
  222. package/src/runtime.ts +24 -0
  223. package/src/server/__tests__/app.test.ts +1044 -0
  224. package/src/server/__tests__/auth.test.ts +370 -0
  225. package/src/server/__tests__/config.test.ts +101 -0
  226. package/src/server/__tests__/db.test.ts +485 -0
  227. package/src/server/__tests__/host.test.ts +14 -0
  228. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  229. package/src/server/app.ts +427 -0
  230. package/src/server/db/auth.ts +311 -0
  231. package/src/server/db/messages.ts +50 -0
  232. package/src/server/db/projects.ts +106 -0
  233. package/src/server/db/prompts.ts +78 -0
  234. package/src/server/db/rows.ts +150 -0
  235. package/src/server/db/schema.ts +221 -0
  236. package/src/server/db/sessions.ts +239 -0
  237. package/src/server/db/tunnel.ts +124 -0
  238. package/src/server/host.ts +12 -0
  239. package/src/server/result.ts +15 -0
  240. package/src/server/routes/auth-guard.ts +156 -0
  241. package/src/server/routes/auth.routes.ts +287 -0
  242. package/src/server/routes/helpers.ts +165 -0
  243. package/src/server/routes/meta.routes.ts +24 -0
  244. package/src/server/routes/passkeys.routes.ts +161 -0
  245. package/src/server/routes/projects.routes.ts +167 -0
  246. package/src/server/routes/sessions.routes.ts +197 -0
  247. package/src/server/routes/skills.routes.ts +136 -0
  248. package/src/server/routes/tunnel.routes.ts +132 -0
  249. package/src/server/routes/types.ts +35 -0
  250. package/src/server/routes/uploads.routes.ts +54 -0
  251. package/src/server/routes/workspace.routes.ts +127 -0
  252. package/src/server/services/__tests__/app-info.test.ts +18 -0
  253. package/src/server/services/__tests__/attachments.test.ts +159 -0
  254. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  255. package/src/server/services/__tests__/auth.test.ts +341 -0
  256. package/src/server/services/__tests__/claude-code.test.ts +609 -0
  257. package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
  258. package/src/server/services/__tests__/claude-skills.test.ts +63 -0
  259. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  260. package/src/server/services/__tests__/git-info.test.ts +74 -0
  261. package/src/server/services/__tests__/opencode.test.ts +320 -0
  262. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  263. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  264. package/src/server/services/__tests__/session-manager.test.ts +3509 -0
  265. package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
  266. package/src/server/services/__tests__/skills-registry.test.ts +365 -0
  267. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  268. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  269. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  270. package/src/server/services/__tests__/workspace.test.ts +249 -0
  271. package/src/server/services/app-info.ts +29 -0
  272. package/src/server/services/attachments.ts +193 -0
  273. package/src/server/services/auth-passkeys.ts +459 -0
  274. package/src/server/services/auth.ts +633 -0
  275. package/src/server/services/claude-code-client.ts +1093 -0
  276. package/src/server/services/claude-code-translate.ts +370 -0
  277. package/src/server/services/claude-mcp.ts +122 -0
  278. package/src/server/services/claude-skills.ts +102 -0
  279. package/src/server/services/frpc-installer.ts +183 -0
  280. package/src/server/services/git-info.ts +87 -0
  281. package/src/server/services/opencode-client.ts +714 -0
  282. package/src/server/services/opencode-server.ts +319 -0
  283. package/src/server/services/opencode-sse.ts +82 -0
  284. package/src/server/services/project-manager.ts +147 -0
  285. package/src/server/services/sandbox.ts +83 -0
  286. package/src/server/services/session-events.ts +256 -0
  287. package/src/server/services/session-manager.ts +1439 -0
  288. package/src/server/services/skill-install-jobs.ts +275 -0
  289. package/src/server/services/skills-registry.ts +480 -0
  290. package/src/server/services/tunnel-api.ts +207 -0
  291. package/src/server/services/tunnel-config.ts +45 -0
  292. package/src/server/services/tunnel-lhr.ts +387 -0
  293. package/src/server/services/tunnel.ts +633 -0
  294. package/src/server/services/turn-stream.ts +339 -0
  295. package/src/server/services/websocket-gateway.ts +374 -0
  296. package/src/server/services/workspace.ts +223 -0
  297. package/src/server/services/ws-transport.ts +121 -0
  298. package/src/types/context.ts +69 -0
  299. package/src/types/index.ts +13 -0
  300. package/src/types/message.ts +58 -0
  301. package/src/types/project.ts +16 -0
  302. package/src/types/prompt.ts +118 -0
  303. package/src/types/session.ts +52 -0
  304. package/src/types/turns.ts +47 -0
  305. package/src/ws-protocol.ts +93 -0
  306. package/styles/globals.css +311 -0
  307. package/tsconfig.json +37 -0
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Composition root KCG Code (task 20) — wiring server.
3
+ *
4
+ * `createKcgServer()` merakit seluruh komponen dan menjalankan
5
+ * `Bun.serve({ hostname, port, routes, websocket })`. Mengikuti struktur
6
+ * kcgcode: handler HTTP dikelompokkan per fitur di `routes/*.routes.ts`
7
+ * (di-assemble di sini) dan domain logic di `services/`.
8
+ * - Routes HTTP `/api/projects`, `/api/fs`, `/api/sessions`, upload lampiran,
9
+ * `/api/skills` (tabel rute terpisah) + upgrade WebSocket di `/ws`.
10
+ * - Kunci aplikasi opsional (lock screen, `services/auth.ts`): bila diatur,
11
+ * seluruh `/api/*` & `/ws` mewajibkan cookie sesi. Tanpa kunci, server
12
+ * hanya aman karena bind ke `127.0.0.1` secara default (lihat `host.ts`).
13
+ * - `reconcileOnStartup()` dipanggil sebelum `Bun.serve` menerima koneksi
14
+ * (Requirement 2.4).
15
+ *
16
+ * Dipisah dari `src/index.ts` (entry) agar dapat diuji (task 20.2) dengan
17
+ * injeksi `config`, `store`, dan `spawn` mock — tanpa mengimpor HTML.
18
+ */
19
+
20
+ import path from "node:path";
21
+ import { type HTMLBundle, type Server, type ServerWebSocket, serve } from "bun";
22
+ import { type AppConfig, loadConfig } from "../config";
23
+ import { openSessionStore, type SessionStore } from "../db";
24
+ import {
25
+ PUBLIC_DIR,
26
+ resolveEffectiveDbPath,
27
+ resolveEffectiveUploadsDir,
28
+ resolveFrpcConfigPath,
29
+ } from "../paths";
30
+ import { resolveHostname } from "./host";
31
+ import { authRoutes } from "./routes/auth.routes";
32
+ import { guardRoutes, originAllowed, readSessionToken } from "./routes/auth-guard";
33
+ import { metaRoutes } from "./routes/meta.routes";
34
+ import { passkeyRoutes } from "./routes/passkeys.routes";
35
+ import { projectsRoutes } from "./routes/projects.routes";
36
+ import { sessionsRoutes } from "./routes/sessions.routes";
37
+ import { skillsRoutes } from "./routes/skills.routes";
38
+ import { tunnelRoutes } from "./routes/tunnel.routes";
39
+ import type { ApiRouteContext } from "./routes/types";
40
+ import { uploadsRoutes } from "./routes/uploads.routes";
41
+ import { workspaceRoutes } from "./routes/workspace.routes";
42
+ import { type AttachmentManager, createAttachmentManager } from "./services/attachments";
43
+ import { type AuthService, createAuthService } from "./services/auth";
44
+ import { createPasskeyService } from "./services/auth-passkeys";
45
+ import { createClaudeCodeServerManager } from "./services/claude-code-client";
46
+ import { createFrpcInstaller } from "./services/frpc-installer";
47
+ import {
48
+ createOpenCodeServerManager,
49
+ type OpenCodeServerManager,
50
+ } from "./services/opencode-server";
51
+ import { createProjectManager, type ProjectManager } from "./services/project-manager";
52
+ import { createSessionManager, type SessionManager } from "./services/session-manager";
53
+ import { createSkillInstallJobs } from "./services/skill-install-jobs";
54
+ import {
55
+ createSkillsRegistry,
56
+ isValidSkillId,
57
+ isValidSource,
58
+ type SkillsRegistry,
59
+ } from "./services/skills-registry";
60
+ import { createTunnelManager, type TunnelManager } from "./services/tunnel";
61
+ import { createTunnelApi } from "./services/tunnel-api";
62
+ import { resolveTunnelConfig } from "./services/tunnel-config";
63
+ import { createLhrManager, type LhrManager } from "./services/tunnel-lhr";
64
+ import {
65
+ createWebSocketGateway,
66
+ type Subscriber,
67
+ type WebSocketGateway,
68
+ } from "./services/websocket-gateway";
69
+ import { bunWsSubscriber, dispatchClientMessage } from "./services/ws-transport";
70
+
71
+ /**
72
+ * Data per-koneksi WebSocket: id sesi login pemiliknya (null = kunci belum
73
+ * diatur saat koneksi dibuka). Dipakai untuk menutup socket saat sesi itu
74
+ * dicabut / terkunci.
75
+ */
76
+ type WsData = { authSessionId: string | null };
77
+
78
+ export interface KcgServerOptions {
79
+ config?: AppConfig;
80
+ store?: SessionStore;
81
+ /** Injeksi OpenCode_Server_Manager (untuk pengujian, task 20.2). */
82
+ servers?: OpenCodeServerManager;
83
+ /** Injeksi backend Claude Code (Agent SDK) untuk pengujian. */
84
+ claudeServers?: OpenCodeServerManager;
85
+ /** Injeksi Skills_Registry (skills.sh + CLI `skills`) untuk pengujian. */
86
+ skillsRegistry?: SkillsRegistry;
87
+ /** Injeksi Auth_Service (mis. hash cepat untuk test). */
88
+ auth?: AuthService;
89
+ /**
90
+ * Factory Tunnel_Manager (test menyuntik API/spawn palsu). Menerima
91
+ * dependensi yang hanya diketahui composition root.
92
+ */
93
+ tunnel?: (deps: {
94
+ store: SessionStore;
95
+ auth: AuthService;
96
+ port: number;
97
+ hostname: string;
98
+ }) => TunnelManager;
99
+ /** Factory tunnel localhost.run (test menyuntik spawn palsu). */
100
+ lhr?: (deps: {
101
+ store: SessionStore;
102
+ auth: AuthService;
103
+ port: number;
104
+ hostname: string;
105
+ }) => LhrManager;
106
+ /** Direktori lampiran gambar upload (default: `~/.kcgcode/data/uploads`). */
107
+ uploadsRoot?: string;
108
+ hostname?: string;
109
+ port?: number;
110
+ /** Shell SPA untuk rute halaman terdaftar (`spaPaths`; default: 404). */
111
+ spa?: Response | HTMLBundle;
112
+ /**
113
+ * Rute halaman yang menyajikan shell SPA — didaftarkan eksplisit di entry
114
+ * (`src/index.ts`) persis seperti daftar rute SPA kcgcode; path lain 404.
115
+ */
116
+ spaPaths?: string[];
117
+ }
118
+
119
+ export interface KcgServer {
120
+ server: Server<WsData>;
121
+ store: SessionStore;
122
+ projectManager: ProjectManager;
123
+ sessionManager: SessionManager;
124
+ gateway: WebSocketGateway;
125
+ /** Job instalasi skill (diekspos untuk pengujian). */
126
+ skillInstalls: ApiRouteContext["skillInstalls"];
127
+ /** Kunci aplikasi (diekspos untuk pengujian & CLI). */
128
+ auth: AuthService;
129
+ /** Tunnel publik (diekspos untuk dashboard terminal & pengujian). */
130
+ tunnel: TunnelManager;
131
+ /** Tunnel localhost.run (diekspos untuk dashboard terminal). */
132
+ lhr: LhrManager;
133
+ /** Provider tunnel terpilih: "kcg" | "lhr". */
134
+ tunnelProvider: () => "kcg" | "lhr";
135
+ /** Pola rute `/api/*` yang terdaftar (untuk test cakupan penjaga). */
136
+ apiRoutePatterns: string[];
137
+ /** Shutdown: simpan status running (budget 5s) -> stop server -> tutup store. */
138
+ close(): Promise<void>;
139
+ }
140
+
141
+ /**
142
+ * Port HTTP default 3000 (env KCG_PORT). Nilai tak valid ditolak agar
143
+ * `Bun.serve` tidak menerima NaN dari `Number(env)`.
144
+ */
145
+ function resolvePort(): number {
146
+ const raw = process.env.KCG_PORT;
147
+ if (raw === undefined) return 3000;
148
+ const n = Number(raw);
149
+ if (Number.isInteger(n) && n > 0 && n <= 65535) return n;
150
+ console.warn(`[kcg-code] KCG_PORT tidak valid ("${raw}"); memakai 3000.`);
151
+ return 3000;
152
+ }
153
+
154
+ async function staticFile(filePath: string, contentType: string): Promise<Response> {
155
+ const file = Bun.file(filePath);
156
+ if (!(await file.exists())) return new Response("Not Found", { status: 404 });
157
+ return new Response(file, { headers: { "content-type": contentType } });
158
+ }
159
+
160
+ /**
161
+ * Membangun seluruh komponen KCG Code, me-reconcile status Session, dan
162
+ * menjalankan `Bun.serve`. `reconcileOnStartup()` dieksekusi sebelum server
163
+ * menerima koneksi (Requirement 2.4).
164
+ */
165
+ export function createKcgServer(opts: KcgServerOptions = {}): KcgServer {
166
+ const config = opts.config ?? loadConfig();
167
+ const store = opts.store ?? openSessionStore();
168
+ const hostname = opts.hostname ?? resolveHostname();
169
+ const port = opts.port ?? resolvePort();
170
+ // Direktori lampiran gambar. Absolut sejak awal agar URL `file:///…`
171
+ // valid. Default sesuai mode runtime (dev: ./data/uploads, cli: ~/.kcgcode/...).
172
+ const uploadsRoot = opts.uploadsRoot ?? resolveEffectiveUploadsDir();
173
+
174
+ const projectManager = createProjectManager(config.sandboxRoot, store);
175
+ const servers = opts.servers ?? createOpenCodeServerManager();
176
+ // Proses `claude` baru di-spawn saat Session Claude Code menerima prompt.
177
+ const claudeServers = opts.claudeServers ?? createClaudeCodeServerManager();
178
+ const attachments: AttachmentManager = createAttachmentManager(uploadsRoot);
179
+
180
+ // Skills_Registry & job instalasi dibuat lebih dulu: session manager
181
+ // memberi tahu job instalasi saat refresh skill tertunda selesai. Callback
182
+ // `refreshSkills` baru dipanggil saat instalasi berjalan (setelah init).
183
+ const skillsRegistry = opts.skillsRegistry ?? createSkillsRegistry();
184
+ const skillInstalls = createSkillInstallJobs({
185
+ registry: skillsRegistry,
186
+ refreshSkills: (projectId, agentType) => sessionManager.refreshSkills(projectId, agentType),
187
+ validate: (source, skill) => isValidSource(source) && isValidSkillId(skill),
188
+ });
189
+
190
+ let gateway: WebSocketGateway;
191
+ const sessionManager = createSessionManager({
192
+ store,
193
+ servers,
194
+ claudeServers,
195
+ attachments,
196
+ onMessage: (message) => gateway.notifyMessage(message.sessionId, message),
197
+ onMessagePart: (sessionId, messageId, part) =>
198
+ gateway.notifyMessagePart(sessionId, messageId, part),
199
+ onMessagePartDelta: (sessionId, messageId, partId, field, delta) =>
200
+ gateway.notifyMessagePartDelta(sessionId, messageId, partId, field, delta),
201
+ onPrompt: (prompt) => gateway.notifyPrompt(prompt.sessionId, prompt),
202
+ // Kartu kembar yang ikut terjawab lewat fan-out grup: kirim notif
203
+ // `prompt_resolved` agar hilang dari UI tanpa menunggu reattach.
204
+ onPromptResolved: (sessionId, promptId) => gateway.notifyPromptResolved(sessionId, promptId),
205
+ onStatusChange: (sessionId, status) => gateway.notifySessionStatus(sessionId, status),
206
+ // Judul hasil generate opencode -> daftar Session di Client terbarui live.
207
+ onTitleChange: (sessionId, title) => gateway.notifySessionTitle(sessionId, title),
208
+ onDeleted: (sessionId) => gateway.notifySessionDeleted(sessionId),
209
+ onError: (sessionId, message) => gateway.notifyError(sessionId, "AGENT_ERROR", message),
210
+ // Turn mulai/selesai -> Client tahu kapan model merespon (tombol stop).
211
+ onTurnChange: (sessionId, active) => gateway.notifyTurnActive(sessionId, active),
212
+ // Refresh skill tertunda berjalan otomatis setelah chat selesai -> perbarui
213
+ // notifikasi instalasi terkait ("aktif sekarang").
214
+ onSkillsRefreshed: (projectId) => skillInstalls.markRefreshed(projectId),
215
+ });
216
+ gateway = createWebSocketGateway({ store, sessionManager });
217
+
218
+ // Requirement 2.4: tandai Session "running" tanpa proses sebelum menerima koneksi.
219
+ sessionManager.reconcileOnStartup();
220
+
221
+ // Subscriber per koneksi (identitas stabil untuk Map gateway).
222
+ const wsSubs = new Map<ServerWebSocket<WsData>, Subscriber>();
223
+
224
+ // ---- Kunci aplikasi (lock screen) ----
225
+ const auth = opts.auth ?? createAuthService({ store });
226
+ const passkeys = createPasskeyService({ store, auth });
227
+
228
+ // ---- Tunnel publik (<username>.<domain dari VPS>) — wajib app lock ----
229
+ const tunnel = opts.tunnel
230
+ ? opts.tunnel({ store, auth, port, hostname })
231
+ : (() => {
232
+ const tcfg = resolveTunnelConfig();
233
+ return createTunnelManager({
234
+ store,
235
+ config: tcfg,
236
+ // Tanpa URL API: klien yang selalu gagal (connect() sudah menolak lebih dulu).
237
+ api: createTunnelApi(tcfg.apiUrl ?? "http://tunnel-api.invalid"),
238
+ installer: createFrpcInstaller(),
239
+ localPort: port,
240
+ localHost: hostname,
241
+ configPath: resolveFrpcConfigPath(),
242
+ isProtected: () => auth.isProtected(),
243
+ });
244
+ })();
245
+ // ---- Tunnel localhost.run (gratis, via ssh) — wajib app lock ----
246
+ const lhr = opts.lhr
247
+ ? opts.lhr({ store, auth, port, hostname })
248
+ : createLhrManager({
249
+ store,
250
+ localPort: port,
251
+ localHost: hostname,
252
+ dataDir: path.dirname(resolveFrpcConfigPath()),
253
+ isProtected: () => auth.isProtected(),
254
+ });
255
+ auth.onLockChanged((prot) => {
256
+ tunnel.handleLockChanged(prot);
257
+ lhr.handleLockChanged(prot);
258
+ });
259
+
260
+ // Konteks bersama untuk tabel rute API.
261
+ const routeCtx: ApiRouteContext = {
262
+ projectManager,
263
+ sessionManager,
264
+ attachments,
265
+ skillsRegistry,
266
+ skillInstalls,
267
+ tunnel,
268
+ lhr,
269
+ tunnelProvider: () => store.getTunnelSettings(Date.now()).provider,
270
+ setTunnelProvider: (provider) => store.setTunnelProvider(provider, Date.now()),
271
+ notifyDataChanged: () => gateway.notifyDataChanged(),
272
+ };
273
+ // Sesi dicabut / terkunci / kedaluwarsa -> tutup WebSocket miliknya agar
274
+ // perangkat itu tidak terus menerima data percakapan.
275
+ auth.onSessionEnded((sessionId) => {
276
+ for (const ws of wsSubs.keys()) {
277
+ if (ws.data.authSessionId === sessionId) ws.close(4401, "locked");
278
+ }
279
+ });
280
+ /** Kunci baru diatur -> tutup socket yang dibuka tanpa sesi (sebelum dikunci). */
281
+ function closeUnauthenticatedSockets(): void {
282
+ if (!auth.isProtected()) return;
283
+ for (const ws of wsSubs.keys()) {
284
+ if (ws.data.authSessionId === null) ws.close(4401, "locked");
285
+ }
286
+ }
287
+ // Kunci otomatis ditegakkan server: sapu sesi idle/kedaluwarsa berkala.
288
+ const sweepTimer = setInterval(() => {
289
+ try {
290
+ auth.sweep();
291
+ closeUnauthenticatedSockets();
292
+ } catch (e) {
293
+ console.error("[kcg-code] auth sweep gagal:", e);
294
+ }
295
+ }, 30_000);
296
+ sweepTimer.unref?.();
297
+
298
+ const apiRoutes = guardRoutes(auth, {
299
+ ...authRoutes(auth),
300
+ ...passkeyRoutes(auth, passkeys),
301
+ ...metaRoutes(),
302
+ ...projectsRoutes(routeCtx),
303
+ ...sessionsRoutes(routeCtx),
304
+ ...uploadsRoutes(routeCtx),
305
+ ...skillsRoutes(routeCtx),
306
+ ...tunnelRoutes(routeCtx),
307
+ ...workspaceRoutes(routeCtx, {
308
+ auth,
309
+ configPath: config.configPath,
310
+ // Data (DB, uploads, frpc.toml) & config KCG Code tak boleh berada di
311
+ // dalam workspace — agent bisa membaca hash sandi / token tunnel.
312
+ protectedDirs: [
313
+ path.dirname(resolveEffectiveDbPath()),
314
+ uploadsRoot,
315
+ path.dirname(config.configPath),
316
+ ],
317
+ }),
318
+ });
319
+
320
+ const server = serve<WsData>({
321
+ hostname,
322
+ port,
323
+ routes: {
324
+ // ---- Asset statis PWA dari root paket (bukan cwd) — aman utk global install ----
325
+ "/manifest.json": () =>
326
+ staticFile(path.join(PUBLIC_DIR, "manifest.json"), "application/manifest+json"),
327
+ "/sw.js": () => staticFile(path.join(PUBLIC_DIR, "sw.js"), "text/javascript"),
328
+ "/logo.svg": () => staticFile(path.join(PUBLIC_DIR, "logo.svg"), "image/svg+xml"),
329
+
330
+ // ---- Rute API per fitur (pola kcgcode: tabel rute terpisah) ----
331
+ // Seluruh `/api/*` dijaga kunci aplikasi (kecuali status/login/avatar).
332
+ ...apiRoutes,
333
+
334
+ // ---- WebSocket_Gateway upgrade (Requirement 4) ----
335
+ // WebSocket tidak dibatasi CORS, jadi Origin wajib same-origin dan
336
+ // sesi login wajib valid sebelum upgrade.
337
+ "/ws": (req: Request, server: Server<WsData>) => {
338
+ if (!originAllowed(req)) return new Response("Forbidden", { status: 403 });
339
+ let authSessionId: string | null = null;
340
+ if (auth.isProtected()) {
341
+ const session = auth.authenticate(readSessionToken(req));
342
+ if (!session) return new Response("Unauthorized", { status: 401 });
343
+ authSessionId = session.id;
344
+ }
345
+ const upgraded = server.upgrade(req, { data: { authSessionId } });
346
+ if (!upgraded) return new Response("Upgrade WebSocket gagal", { status: 400 });
347
+ return undefined;
348
+ },
349
+
350
+ // ---- Shell SPA hanya untuk rute halaman terdaftar (disuntik entry,
351
+ // default 404) — path tak dikenal tidak menangkap shell SPA ----
352
+ ...Object.fromEntries(
353
+ (opts.spaPaths ?? []).map((pattern) => [
354
+ pattern,
355
+ opts.spa ?? new Response("Not Found", { status: 404 }),
356
+ ]),
357
+ ),
358
+ },
359
+ websocket: {
360
+ data: { authSessionId: null } as WsData,
361
+ open(ws) {
362
+ wsSubs.set(ws, bunWsSubscriber(ws));
363
+ },
364
+ message(ws, message) {
365
+ // Socket yang tak terdaftar (sudah ditutup) diabaikan.
366
+ const sub = wsSubs.get(ws);
367
+ if (!sub) return;
368
+ // Setiap pesan dianggap aktivitas: sesi tetap hidup selama dipakai,
369
+ // dan socket milik sesi yang sudah terkunci ditutup.
370
+ if (auth.isProtected()) {
371
+ const alive =
372
+ ws.data.authSessionId !== null && auth.isSessionAlive(ws.data.authSessionId);
373
+ if (!alive) {
374
+ ws.close(4401, "locked");
375
+ return;
376
+ }
377
+ }
378
+ dispatchClientMessage(gateway, sub, String(message));
379
+ },
380
+ close(ws) {
381
+ const sub = wsSubs.get(ws);
382
+ if (sub) {
383
+ gateway.detach(sub);
384
+ wsSubs.delete(ws);
385
+ }
386
+ },
387
+ },
388
+ development: process.env.NODE_ENV !== "production" && {
389
+ hmr: true,
390
+ console: true,
391
+ },
392
+ });
393
+
394
+ // Tunnel yang aktif sebelum restart dinyalakan lagi setelah server listen.
395
+ // Port nyata diketahui setelah `serve` (port 0 di test) — factory default
396
+ // memakai `port` yang diminta, sama seperti yang dicetak dashboard.
397
+ // Hanya penyedia terpilih yang dinyalakan ulang (satu tunnel aktif).
398
+ if (store.getTunnelSettings(Date.now()).provider === "lhr") lhr.autoStart();
399
+ else tunnel.autoStart();
400
+
401
+ async function close(): Promise<void> {
402
+ clearInterval(sweepTimer);
403
+ // Hentikan frpc lebih dulu agar URL publik tidak menunjuk server mati.
404
+ await Promise.all([tunnel.shutdown(), lhr.shutdown()]);
405
+ // Hentikan instalasi skill yang berjalan agar proses CLI tidak yatim.
406
+ await skillInstalls.cancelAll();
407
+ // shutdown() menyimpan status running (budget 5s) lalu menghentikan server headless.
408
+ await sessionManager.shutdown();
409
+ await server.stop(true);
410
+ store.close();
411
+ }
412
+
413
+ return {
414
+ server,
415
+ store,
416
+ projectManager,
417
+ sessionManager,
418
+ gateway,
419
+ skillInstalls,
420
+ auth,
421
+ tunnel,
422
+ lhr,
423
+ tunnelProvider: () => store.getTunnelSettings(Date.now()).provider,
424
+ apiRoutePatterns: Object.keys(apiRoutes),
425
+ close,
426
+ };
427
+ }