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,47 @@
1
+ /**
2
+ * Router CLI kcgcode — dipanggil dari `bin/kcgcode.ts`.
3
+ */
4
+ import { type CliArgs, helpText, parseArgs } from "./args";
5
+ import { runInit, runResetLock, runStart } from "./commands";
6
+ import { runLogs, runStatus, runStop } from "./daemon";
7
+ import { c, readPackageVersion, symbols } from "./theme";
8
+
9
+ export async function runCli(argv: string[]): Promise<void> {
10
+ const args = parseArgs(argv);
11
+
12
+ switch (args.command) {
13
+ case "help":
14
+ console.log(helpText());
15
+ return;
16
+ case "version": {
17
+ const v = await readPackageVersion();
18
+ console.log(`kcgcode ${v}`);
19
+ return;
20
+ }
21
+ case "invalid":
22
+ console.error(`${c.redBright(symbols.cross)} ${args.error}`);
23
+ console.error(`\n${c.dim("Run")} ${c.cyan("kcgcode --help")} ${c.dim("for usage.")}`);
24
+ process.exit(1);
25
+ return;
26
+ case "init":
27
+ runInit(args);
28
+ return;
29
+ case "reset-lock":
30
+ runResetLock();
31
+ return;
32
+ case "start":
33
+ await runStart(args);
34
+ return;
35
+ case "stop":
36
+ await runStop();
37
+ return;
38
+ case "status":
39
+ await runStatus();
40
+ return;
41
+ case "logs":
42
+ await runLogs(args);
43
+ return;
44
+ }
45
+ }
46
+
47
+ export type { CliArgs };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Cegah OS masuk sleep selama server kcgcode berjalan — kalau mesin tidur,
3
+ * jaringan putus dan tunnel (kcg/lhr) ikut disconnect.
4
+ *
5
+ * Per platform:
6
+ * - **linux**: `systemd-inhibit --what=sleep:idle … cat`. Child `cat` membaca
7
+ * stdin pipe dari proses ini, jadi bila kcgcode mati (termasuk SIGKILL)
8
+ * pipe tertutup, `cat` keluar, dan inhibitor otomatis dilepas.
9
+ * - **darwin**: `caffeinate -i -s -w <pid>` — berhenti sendiri saat pid mati.
10
+ * - **win32**: `SetThreadExecutionState` via `bun:ffi`; state dilepas OS
11
+ * saat proses berakhir.
12
+ *
13
+ * Hanya mencegah sleep otomatis (idle). Layar tetap boleh mati, dan sleep
14
+ * karena tutup lid laptop tetap mengikuti setelan OS.
15
+ */
16
+ import type { Subprocess } from "bun";
17
+
18
+ export interface KeepAwakeStatus {
19
+ active: boolean;
20
+ /** Mekanisme yang dipakai, mis. "systemd-inhibit". */
21
+ method: string | null;
22
+ error: string | null;
23
+ }
24
+
25
+ export interface KeepAwake {
26
+ status(): KeepAwakeStatus;
27
+ stop(): void;
28
+ }
29
+
30
+ const WHY = "kcgcode is serving remote sessions (keeps the tunnel online)";
31
+
32
+ /** Perintah inhibitor untuk platform child-process; null bila tak didukung. */
33
+ export function inhibitCommand(platform: NodeJS.Platform, pid: number): string[] | null {
34
+ if (platform === "linux") {
35
+ return [
36
+ "systemd-inhibit",
37
+ "--what=sleep:idle",
38
+ "--who=kcgcode",
39
+ `--why=${WHY}`,
40
+ "--mode=block",
41
+ "cat",
42
+ ];
43
+ }
44
+ if (platform === "darwin") {
45
+ return ["caffeinate", "-i", "-s", "-w", String(pid)];
46
+ }
47
+ return null;
48
+ }
49
+
50
+ export function disabledKeepAwake(): KeepAwake {
51
+ return {
52
+ status: () => ({ active: false, method: null, error: "disabled (--allow-sleep)" }),
53
+ stop() {},
54
+ };
55
+ }
56
+
57
+ export async function startKeepAwake(
58
+ platform: NodeJS.Platform = process.platform,
59
+ ): Promise<KeepAwake> {
60
+ if (platform === "win32") return startWindows();
61
+
62
+ const cmd = inhibitCommand(platform, process.pid);
63
+ if (!cmd) {
64
+ return {
65
+ status: () => ({ active: false, method: null, error: `unsupported on ${platform}` }),
66
+ stop() {},
67
+ };
68
+ }
69
+
70
+ const method = cmd[0] as string;
71
+ let state: KeepAwakeStatus = { active: true, method, error: null };
72
+ let proc: Subprocess<"pipe", "ignore", "pipe">;
73
+ try {
74
+ proc = Bun.spawn(cmd, { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
75
+ } catch (err) {
76
+ const reason = (err as Error).message.includes("ENOENT")
77
+ ? `${method} not found`
78
+ : (err as Error).message;
79
+ return {
80
+ status: () => ({ active: false, method, error: reason }),
81
+ stop() {},
82
+ };
83
+ }
84
+
85
+ let stopping = false;
86
+ void proc.exited.then(async (code) => {
87
+ if (stopping) return;
88
+ const stderr = (await new Response(proc.stderr).text()).trim();
89
+ const firstLine = stderr.split("\n")[0];
90
+ state = {
91
+ active: false,
92
+ method,
93
+ error: firstLine || `${method} exited (code ${code})`,
94
+ };
95
+ });
96
+
97
+ return {
98
+ status: () => state,
99
+ stop() {
100
+ if (stopping) return;
101
+ stopping = true;
102
+ state = { active: false, method, error: null };
103
+ try {
104
+ proc.stdin.end();
105
+ } catch {
106
+ // sudah tertutup
107
+ }
108
+ proc.kill();
109
+ },
110
+ };
111
+ }
112
+
113
+ const ES_CONTINUOUS = 0x80000000;
114
+ const ES_SYSTEM_REQUIRED = 0x00000001;
115
+
116
+ async function startWindows(): Promise<KeepAwake> {
117
+ const method = "SetThreadExecutionState";
118
+ try {
119
+ const { dlopen, FFIType } = await import("bun:ffi");
120
+ const lib = dlopen("kernel32.dll", {
121
+ SetThreadExecutionState: { args: [FFIType.u32], returns: FFIType.u32 },
122
+ });
123
+ const set = lib.symbols.SetThreadExecutionState;
124
+ if (set(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) === 0) {
125
+ lib.close();
126
+ return {
127
+ status: () => ({ active: false, method, error: `${method} failed` }),
128
+ stop() {},
129
+ };
130
+ }
131
+ let active = true;
132
+ return {
133
+ status: () => ({ active, method, error: null }),
134
+ stop() {
135
+ if (!active) return;
136
+ active = false;
137
+ set(ES_CONTINUOUS);
138
+ lib.close();
139
+ },
140
+ };
141
+ } catch (err) {
142
+ const error = (err as Error).message;
143
+ return {
144
+ status: () => ({ active: false, method, error }),
145
+ stop() {},
146
+ };
147
+ }
148
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Pemilihan port HTTP saat startup.
3
+ *
4
+ * Prioritas: `--port` > `KCG_PORT` > `port` di config > 3000.
5
+ * Port eksplisit (flag/env) yang terpakai → error; port dari config/default
6
+ * yang terpakai → otomatis pindah ke port kosong berikutnya.
7
+ */
8
+
9
+ export const DEFAULT_PORT = 3000;
10
+ /** Jumlah port berurutan yang dicoba saat fallback. */
11
+ const SCAN_ATTEMPTS = 50;
12
+
13
+ export type PortSource = "flag" | "env" | "config" | "default";
14
+
15
+ const CONNECT_TIMEOUT_MS = 1000;
16
+
17
+ /** true bila ada yang sudah listen (koneksi TCP diterima). */
18
+ async function acceptsConnection(hostname: string, port: number): Promise<boolean> {
19
+ // Alamat wildcard tidak bisa dituju — cek lewat loopback.
20
+ const host = hostname === "0.0.0.0" ? "127.0.0.1" : hostname === "::" ? "::1" : hostname;
21
+ const attempt = Bun.connect({
22
+ hostname: host,
23
+ port,
24
+ socket: { data() {}, open: (s) => void s.end() },
25
+ }).then(
26
+ (s) => {
27
+ s.end();
28
+ return true;
29
+ },
30
+ () => false,
31
+ );
32
+ const timeout = Bun.sleep(CONNECT_TIMEOUT_MS).then(() => false);
33
+ return Promise.race([attempt, timeout]);
34
+ }
35
+
36
+ /**
37
+ * Cek port bisa dipakai. Probe bind saja tidak cukup: setelah server kcgcode
38
+ * berjalan di proses ini, bind ke port yang sudah dipakai proses lain bisa
39
+ * "berhasil" (SO_REUSEPORT). Jadi cek koneksi TCP dulu, lalu probe bind untuk
40
+ * port yang terpakai tapi tidak menerima koneksi (mis. butuh root).
41
+ */
42
+ export async function portIsFree(hostname: string, port: number): Promise<boolean> {
43
+ if (port === 0) return true;
44
+ if (await acceptsConnection(hostname, port)) return false;
45
+ try {
46
+ const probe = Bun.serve({ hostname, port, reusePort: false, fetch: () => new Response() });
47
+ probe.stop(true);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /** Port yang diminta beserta asalnya. `envPort` dibaca sebelum override `--port`. */
55
+ export function requestedPort(opts: { flag?: number; env?: string; config?: number }): {
56
+ port: number;
57
+ source: PortSource;
58
+ } {
59
+ if (opts.flag !== undefined) return { port: opts.flag, source: "flag" };
60
+ if (opts.env !== undefined && opts.env.trim() !== "") {
61
+ const n = Number(opts.env);
62
+ if (Number.isInteger(n) && n > 0 && n <= 65535) return { port: n, source: "env" };
63
+ }
64
+ if (opts.config !== undefined) return { port: opts.config, source: "config" };
65
+ return { port: DEFAULT_PORT, source: "default" };
66
+ }
67
+
68
+ export type PortChoice =
69
+ | { ok: true; port: number; fallbackFrom: number | null }
70
+ | { ok: false; error: string };
71
+
72
+ /** Pilih port final: pakai yang diminta, atau cari pengganti bila boleh. */
73
+ export async function choosePort(
74
+ hostname: string,
75
+ req: { port: number; source: PortSource },
76
+ isFree: (hostname: string, port: number) => boolean | Promise<boolean> = portIsFree,
77
+ ): Promise<PortChoice> {
78
+ if (await isFree(hostname, req.port)) return { ok: true, port: req.port, fallbackFrom: null };
79
+
80
+ if (req.source === "flag" || req.source === "env") {
81
+ const via = req.source === "flag" ? "--port" : "KCG_PORT";
82
+ return { ok: false, error: `Port ${req.port} (from ${via}) is already in use on ${hostname}.` };
83
+ }
84
+
85
+ for (let p = req.port + 1; p < req.port + 1 + SCAN_ATTEMPTS && p <= 65535; p++) {
86
+ if (await isFree(hostname, p)) return { ok: true, port: p, fallbackFrom: req.port };
87
+ }
88
+ return {
89
+ ok: false,
90
+ error: `Port ${req.port} and the next ${SCAN_ATTEMPTS} ports are in use on ${hostname}.`,
91
+ };
92
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Theme terminal untuk dashboard kcgcode.
3
+ * Palet selaras dark mode app — cyan/violet accent.
4
+ */
5
+ import pc from "picocolors";
6
+
7
+ const useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined;
8
+ const paint = (fn: (s: string) => string) => (useColor ? fn : (s: string) => s);
9
+
10
+ export const c = {
11
+ bold: paint(pc.bold),
12
+ dim: paint(pc.dim),
13
+ cyan: paint(pc.cyan),
14
+ cyanBright: paint((s) => pc.bold(pc.cyan(s))),
15
+ violet: paint(pc.magenta),
16
+ violetBright: paint((s) => pc.bold(pc.magenta(s))),
17
+ green: paint(pc.green),
18
+ greenBright: paint((s) => pc.bold(pc.green(s))),
19
+ yellow: paint(pc.yellow),
20
+ yellowBright: paint((s) => pc.bold(pc.yellow(s))),
21
+ red: paint(pc.red),
22
+ redBright: paint((s) => pc.bold(pc.red(s))),
23
+ white: paint(pc.white),
24
+ gray: paint(pc.gray),
25
+ underline: paint(pc.underline),
26
+ };
27
+
28
+ export const symbols = {
29
+ bullet: "●",
30
+ idle: "○",
31
+ arrow: "→",
32
+ check: "✓",
33
+ cross: "✗",
34
+ warn: "!",
35
+ boxH: "─",
36
+ boxV: "│",
37
+ boxTL: "┌",
38
+ boxTR: "┐",
39
+ boxBL: "└",
40
+ boxBR: "┘",
41
+ } as const;
42
+
43
+ /** Versi paket dari package.json. */
44
+ export async function readPackageVersion(): Promise<string> {
45
+ try {
46
+ const file = Bun.file(new URL("../../package.json", import.meta.url));
47
+ if (!(await file.exists())) return "0.1.0";
48
+ const json = (await file.json()) as { version?: string };
49
+ return json.version ?? "0.1.0";
50
+ } catch {
51
+ return "0.1.0";
52
+ }
53
+ }
54
+
55
+ /** Banner ASCII "KCG". */
56
+ export function logoLines(): string[] {
57
+ return [
58
+ "██╗ ██╗ ██████╗ ██████╗ ",
59
+ "██║ ██╔╝██╔════╝ ██╔════╝ ",
60
+ "█████╔╝ ██║ ██║ ███╗",
61
+ "██╔═██╗ ██║ ██║ ██║",
62
+ "██║ ██╗╚██████╗ ╚██████╔╝",
63
+ "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ",
64
+ ];
65
+ }
66
+
67
+ export function stripAnsi(s: string): string {
68
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: strip ANSI SGR
69
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
70
+ }
71
+
72
+ /** Render panel berborder. Lebar menyesuaikan isi. */
73
+ export function panel(title: string | null, lines: string[], minWidth = 48): string {
74
+ const contentW = Math.max(
75
+ title ? title.length + 2 : 0,
76
+ ...lines.map((l) => stripAnsi(l).length),
77
+ minWidth - 4,
78
+ );
79
+ const inner = Math.max(20, contentW + 2);
80
+ const out: string[] = [];
81
+ const titleText = title ? ` ${title} ` : "";
82
+ const pad = Math.max(0, inner - titleText.length);
83
+ const left = Math.floor(pad / 2);
84
+ const right = pad - left;
85
+ out.push(
86
+ `${c.cyan(symbols.boxTL)}${c.cyan(symbols.boxH.repeat(left) + titleText + symbols.boxH.repeat(right))}${c.cyan(symbols.boxTR)}`,
87
+ );
88
+ for (const line of lines) {
89
+ const visible = stripAnsi(line).length;
90
+ const spaces = Math.max(0, inner - visible - 2);
91
+ out.push(`${c.cyan(symbols.boxV)} ${line}${" ".repeat(spaces)} ${c.cyan(symbols.boxV)}`);
92
+ }
93
+ out.push(`${c.cyan(symbols.boxBL)}${c.cyan(symbols.boxH.repeat(inner))}${c.cyan(symbols.boxBR)}`);
94
+ return out.join("\n");
95
+ }
96
+
97
+ /** Baris label + value. */
98
+ export function row(label: string, value: string, labelWidth = 12): string {
99
+ return `${c.dim(label.padEnd(labelWidth))}${value}`;
100
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Gerbang kunci aplikasi. Selama kunci aktif dan sesi belum dibuka, app TIDAK
3
+ * dirender sama sekali — tidak ada request API / WebSocket yang berjalan dan
4
+ * tidak ada data yang tersisa di DOM. Yang tampil hanya `LockScreen`.
5
+ *
6
+ * Animasi (Motion `AnimatePresence`, `initial={false}` -> tidak beranimasi
7
+ * saat app pertama dibuka dalam keadaan terkunci):
8
+ * - Dikunci: lock screen turun dari atas. App lama ditahan di DOM (inert)
9
+ * hanya sampai lock screen mendarat, lalu dilepas.
10
+ * - Dibuka: lock screen naik keluar layar, app sudah ada di bawahnya.
11
+ * - Instalasi baru (`needsOnboarding`): welcome screen (`WelcomeScreen`)
12
+ * memudar masuk di lapisan sendiri; setelah selesai memudar keluar dan
13
+ * app muncul dari baliknya.
14
+ *
15
+ * Kunci otomatis: server menegakkan batas idle; klien menirunya dengan timer
16
+ * aktivitas lokal (agar layar terkunci tepat waktu walau tidak ada request)
17
+ * dan heartbeat status berkala (mendeteksi kunci dari perangkat lain /
18
+ * sesi dicabut).
19
+ */
20
+ import { AnimatePresence, useIsPresent, useReducedMotion } from "motion/react";
21
+ import * as m from "motion/react-m";
22
+ import { type ReactNode, useEffect, useRef, useState } from "react";
23
+ import { LockScreen } from "@/components/auth/LockScreen";
24
+ import { WelcomeScreen } from "@/components/auth/WelcomeScreen";
25
+ import { Button } from "@/components/ui/button";
26
+ import { Spinner } from "@/components/ui/spinner";
27
+ import { lockNow, refreshAuth, useAuth } from "@/lib/auth";
28
+
29
+ /** Interval cek status ke server saat app terbuka. */
30
+ const HEARTBEAT_MS = 60_000;
31
+
32
+ const ACTIVITY_EVENTS = ["pointerdown", "keydown", "wheel", "touchstart"] as const;
33
+
34
+ export function AuthGate({ children }: { children: ReactNode }) {
35
+ const auth = useAuth();
36
+
37
+ useEffect(() => {
38
+ void refreshAuth();
39
+ }, []);
40
+
41
+ const status = auth.phase === "ready" ? auth.status : null;
42
+ const unlocked = status !== null && (!status.protected || status.authenticated);
43
+
44
+ /**
45
+ * Welcome screen di-"latch": begitu tampil, ia bertahan sampai pengguna
46
+ * menyelesaikannya (`onFinished`) — walau di tengah alur server sudah
47
+ * melaporkan `needsOnboarding: false` (mis. setelah kunci dipasang di
48
+ * langkah Lock, heartbeat / refresh status lain tidak boleh memotong
49
+ * layar "Done").
50
+ */
51
+ const [onboarding, setOnboarding] = useState(false);
52
+ // Latch disetel SAAT render (bukan di effect) agar tidak ada satu frame
53
+ // pun app tampil sebelum welcome screen.
54
+ if (status?.needsOnboarding && !onboarding) setOnboarding(true);
55
+ const autoLockMs =
56
+ status?.protected && status.autoLockMinutes > 0 ? status.autoLockMinutes * 60_000 : 0;
57
+
58
+ // Kunci otomatis sisi klien + heartbeat.
59
+ const lastActivity = useRef(Date.now());
60
+ useEffect(() => {
61
+ if (!unlocked || !status?.protected) return;
62
+ lastActivity.current = Date.now();
63
+ const onActivity = () => {
64
+ lastActivity.current = Date.now();
65
+ };
66
+ for (const ev of ACTIVITY_EVENTS) window.addEventListener(ev, onActivity, { passive: true });
67
+
68
+ const tick = setInterval(() => {
69
+ if (autoLockMs > 0 && Date.now() - lastActivity.current >= autoLockMs) {
70
+ void lockNow();
71
+ }
72
+ }, 5_000);
73
+ const heartbeat = setInterval(() => void refreshAuth(), HEARTBEAT_MS);
74
+ // Kembali ke tab / buka app lagi di HP -> cek segera (timer bisa tertunda
75
+ // saat tab di latar belakang).
76
+ const onVisible = () => {
77
+ if (document.visibilityState !== "visible") return;
78
+ if (autoLockMs > 0 && Date.now() - lastActivity.current >= autoLockMs) void lockNow();
79
+ else void refreshAuth();
80
+ };
81
+ document.addEventListener("visibilitychange", onVisible);
82
+
83
+ return () => {
84
+ for (const ev of ACTIVITY_EVENTS) window.removeEventListener(ev, onActivity);
85
+ clearInterval(tick);
86
+ clearInterval(heartbeat);
87
+ document.removeEventListener("visibilitychange", onVisible);
88
+ };
89
+ }, [unlocked, status?.protected, autoLockMs]);
90
+
91
+ if (auth.phase === "loading") {
92
+ return (
93
+ <div className="flex h-dvh items-center justify-center bg-background" role="status">
94
+ <Spinner className="size-6 text-muted-foreground" />
95
+ <span className="sr-only">Loading…</span>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ if (auth.phase === "error") {
101
+ return (
102
+ <div className="flex h-dvh flex-col items-center justify-center gap-4 bg-background px-6 text-center">
103
+ <p className="text-sm text-muted-foreground">Can't reach the KCG Code server.</p>
104
+ <Button type="button" variant="outline" onClick={() => void refreshAuth()}>
105
+ Try again
106
+ </Button>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ const locked = !unlocked && status !== null;
112
+ /**
113
+ * Instalasi baru: welcome screen MENUTUPI app (lapisan sendiri di atas).
114
+ * App baru dirender setelah onboarding selesai — tidak ada request data
115
+ * selama welcome screen — lalu muncul dari balik layar yang memudar.
116
+ */
117
+ const showWelcome = !locked && (onboarding || status?.needsOnboarding === true);
118
+ return (
119
+ <>
120
+ <AnimatePresence initial={false}>
121
+ {locked ? (
122
+ <m.div
123
+ key="lock"
124
+ // Tirai: turun dari atas saat dikunci, naik keluar saat dibuka.
125
+ // Bayangan di tepi bawah hanya terlihat selama bergerak.
126
+ className="fixed inset-0 z-[100] shadow-[0_24px_48px_-12px_rgb(0_0_0/0.45)]"
127
+ initial={{ y: "-100%" }}
128
+ animate={{ y: 0, transition: { duration: 0.55, ease: LOCK_EASE } }}
129
+ exit={{ y: "-100%", transition: { duration: 0.45, ease: LOCK_EASE } }}
130
+ >
131
+ <LockScreen status={status} />
132
+ </m.div>
133
+ ) : showWelcome ? null : (
134
+ <AppLayer key="app">{children}</AppLayer>
135
+ )}
136
+ </AnimatePresence>
137
+
138
+ {/* Lapisan terpisah: SELALU beranimasi masuk (bahkan saat app pertama
139
+ dibuka) dan keluar dengan memudar + sedikit membesar. */}
140
+ <AnimatePresence>
141
+ {showWelcome && status && (
142
+ <m.div
143
+ key="welcome"
144
+ className="fixed inset-0 z-[90]"
145
+ initial={{ opacity: 0 }}
146
+ animate={{ opacity: 1, transition: { duration: 0.4, ease: "easeOut" } }}
147
+ exit={{ opacity: 0, scale: 1.03, transition: { duration: 0.45, ease: LOCK_EASE } }}
148
+ >
149
+ <WelcomeScreen status={status} onFinished={() => setOnboarding(false)} />
150
+ </m.div>
151
+ )}
152
+ </AnimatePresence>
153
+ </>
154
+ );
155
+ }
156
+
157
+ /** Kurva ala sheet iOS: cepat di awal, mendarat halus. */
158
+ const LOCK_EASE: [number, number, number, number] = [0.32, 0.72, 0, 1];
159
+ /** Selama app lama ditahan di bawah lock screen yang sedang turun (detik). */
160
+ const LOCK_COVER_S = 0.55;
161
+
162
+ /**
163
+ * App yang sedang terbuka. Saat dikunci, `AnimatePresence` menahannya di DOM
164
+ * sampai lock screen selesai menutupi layar (`exit` tertunda
165
+ * `LOCK_COVER_S`), lalu melepasnya — tidak ada data yang tersisa. Selama
166
+ * ditahan: `inert` (tak bisa diklik, difokus, atau dibaca pembaca layar).
167
+ * `display: contents` agar pembungkus tidak mengubah layout shell.
168
+ */
169
+ function AppLayer({ children }: { children: ReactNode }) {
170
+ const present = useIsPresent();
171
+ // Reduced motion: lock screen tampil seketika, jadi app tak perlu ditahan.
172
+ const reduced = useReducedMotion();
173
+ return (
174
+ <m.div
175
+ className="contents"
176
+ inert={!present}
177
+ exit={{ opacity: 0, transition: { delay: reduced ? 0 : LOCK_COVER_S, duration: 0 } }}
178
+ >
179
+ {children}
180
+ </m.div>
181
+ );
182
+ }