kcg-code 0.0.0-stage → 0.1.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 (286) hide show
  1. package/README.md +228 -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 +83 -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__/dashboard.test.ts +62 -0
  13. package/src/cli/args.ts +123 -0
  14. package/src/cli/commands.ts +174 -0
  15. package/src/cli/dashboard.tsx +343 -0
  16. package/src/cli/index.ts +37 -0
  17. package/src/cli/theme.ts +100 -0
  18. package/src/components/auth/AuthGate.tsx +182 -0
  19. package/src/components/auth/AvatarEditor.tsx +236 -0
  20. package/src/components/auth/LockScreen.tsx +714 -0
  21. package/src/components/auth/ProfileAvatar.tsx +60 -0
  22. package/src/components/auth/WelcomeScreen.tsx +672 -0
  23. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  24. package/src/components/auth/avatar-icons.tsx +114 -0
  25. package/src/components/layout/AppShell.tsx +142 -0
  26. package/src/components/layout/AppSidebar.tsx +419 -0
  27. package/src/components/layout/SearchDialog.tsx +189 -0
  28. package/src/components/layout/SidebarAccount.tsx +111 -0
  29. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  30. package/src/components/layout/ThemeToggle.tsx +67 -0
  31. package/src/components/projects/FolderBrowser.tsx +142 -0
  32. package/src/components/projects/HomeChat.tsx +250 -0
  33. package/src/components/projects/NewProjectDialog.tsx +249 -0
  34. package/src/components/projects/PanelSection.tsx +102 -0
  35. package/src/components/projects/ProjectInstructions.tsx +229 -0
  36. package/src/components/projects/ProjectRow.tsx +58 -0
  37. package/src/components/projects/ProjectSidePanel.tsx +448 -0
  38. package/src/components/sessions/AgentModeList.tsx +265 -0
  39. package/src/components/sessions/AgentPicker.tsx +123 -0
  40. package/src/components/sessions/AssistantTurn.tsx +145 -0
  41. package/src/components/sessions/AttachmentTile.tsx +161 -0
  42. package/src/components/sessions/CodeBlock.tsx +107 -0
  43. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  44. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  45. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
  46. package/src/components/sessions/ContextPanel.tsx +561 -0
  47. package/src/components/sessions/DropPlate.tsx +165 -0
  48. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  49. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  50. package/src/components/sessions/ImageLightbox.tsx +173 -0
  51. package/src/components/sessions/MarkdownContent.tsx +187 -0
  52. package/src/components/sessions/Mascot.tsx +47 -0
  53. package/src/components/sessions/MessageActions.tsx +60 -0
  54. package/src/components/sessions/MessageRail.tsx +121 -0
  55. package/src/components/sessions/ModelPicker.tsx +209 -0
  56. package/src/components/sessions/ModelSearchList.tsx +151 -0
  57. package/src/components/sessions/NewSessionDialog.tsx +189 -0
  58. package/src/components/sessions/PendingAttachments.tsx +127 -0
  59. package/src/components/sessions/PromptCard.tsx +1085 -0
  60. package/src/components/sessions/PromptPanel.tsx +112 -0
  61. package/src/components/sessions/SessionComposer.tsx +640 -0
  62. package/src/components/sessions/SessionHeader.tsx +175 -0
  63. package/src/components/sessions/SessionList.tsx +396 -0
  64. package/src/components/sessions/SessionRow.tsx +137 -0
  65. package/src/components/sessions/SessionTimeline.tsx +275 -0
  66. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  67. package/src/components/sessions/SessionView.tsx +236 -0
  68. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  69. package/src/components/sessions/UserMessage.tsx +177 -0
  70. package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
  71. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  72. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  73. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  74. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  75. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  76. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  77. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  78. package/src/components/settings/AboutGroup.tsx +170 -0
  79. package/src/components/settings/AccountGroup.tsx +122 -0
  80. package/src/components/settings/DevicesGroup.tsx +151 -0
  81. package/src/components/settings/LhrRemoteAccess.tsx +262 -0
  82. package/src/components/settings/PasskeysGroup.tsx +551 -0
  83. package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
  84. package/src/components/settings/SecurityGroup.tsx +566 -0
  85. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  86. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  87. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  88. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  89. package/src/components/settings/prefs.tsx +265 -0
  90. package/src/components/skills/InstallNotifications.tsx +325 -0
  91. package/src/components/skills/InstallSkillDialog.tsx +233 -0
  92. package/src/components/skills/SkillCards.tsx +210 -0
  93. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  94. package/src/components/ui/alert-dialog.tsx +179 -0
  95. package/src/components/ui/alert.tsx +60 -0
  96. package/src/components/ui/badge.tsx +46 -0
  97. package/src/components/ui/bubble.tsx +125 -0
  98. package/src/components/ui/button.tsx +62 -0
  99. package/src/components/ui/card.tsx +75 -0
  100. package/src/components/ui/collapsible.tsx +19 -0
  101. package/src/components/ui/dialog.tsx +142 -0
  102. package/src/components/ui/dropdown-menu.tsx +225 -0
  103. package/src/components/ui/empty.tsx +94 -0
  104. package/src/components/ui/field.tsx +234 -0
  105. package/src/components/ui/input-group.tsx +161 -0
  106. package/src/components/ui/input.tsx +21 -0
  107. package/src/components/ui/label.tsx +19 -0
  108. package/src/components/ui/message-scroller.tsx +131 -0
  109. package/src/components/ui/message.tsx +85 -0
  110. package/src/components/ui/popover.tsx +46 -0
  111. package/src/components/ui/select.tsx +172 -0
  112. package/src/components/ui/separator.tsx +28 -0
  113. package/src/components/ui/sheet.tsx +138 -0
  114. package/src/components/ui/sidebar.tsx +699 -0
  115. package/src/components/ui/skeleton.tsx +15 -0
  116. package/src/components/ui/sonner.tsx +45 -0
  117. package/src/components/ui/spinner.tsx +16 -0
  118. package/src/components/ui/switch.tsx +32 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/components/ui/tooltip.tsx +51 -0
  121. package/src/config.ts +85 -0
  122. package/src/db.ts +146 -0
  123. package/src/frontend.tsx +51 -0
  124. package/src/hooks/__tests__/useMention.test.ts +79 -0
  125. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  126. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  127. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  128. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  129. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  130. package/src/hooks/use-mobile.ts +16 -0
  131. package/src/hooks/useAgentPicker.ts +97 -0
  132. package/src/hooks/useMediaQuery.ts +49 -0
  133. package/src/hooks/useMention.ts +236 -0
  134. package/src/hooks/useProjects.ts +88 -0
  135. package/src/hooks/useResyncOnReconnect.ts +28 -0
  136. package/src/hooks/useRouter.ts +92 -0
  137. package/src/hooks/useSessionChat.ts +427 -0
  138. package/src/hooks/useSessionContext.ts +106 -0
  139. package/src/hooks/useSessionList.ts +234 -0
  140. package/src/hooks/useSidebarData.ts +108 -0
  141. package/src/hooks/useSmoothText.ts +122 -0
  142. package/src/hooks/useTheme.ts +164 -0
  143. package/src/hooks/useWebSocket.ts +196 -0
  144. package/src/index.css +27 -0
  145. package/src/index.html +14 -0
  146. package/src/index.ts +49 -0
  147. package/src/lib/__tests__/attachments.test.ts +58 -0
  148. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  149. package/src/lib/__tests__/collapsible.test.ts +101 -0
  150. package/src/lib/__tests__/composer.test.ts +118 -0
  151. package/src/lib/__tests__/highlight.test.ts +42 -0
  152. package/src/lib/__tests__/home-chat.test.ts +64 -0
  153. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  154. package/src/lib/__tests__/model-picker.test.ts +205 -0
  155. package/src/lib/__tests__/onboarding.test.ts +41 -0
  156. package/src/lib/__tests__/permission-info.test.ts +61 -0
  157. package/src/lib/__tests__/project-extensions.test.ts +52 -0
  158. package/src/lib/__tests__/project-overview.test.ts +173 -0
  159. package/src/lib/__tests__/references.test.ts +121 -0
  160. package/src/lib/__tests__/routes.test.ts +134 -0
  161. package/src/lib/__tests__/session-summary.test.ts +115 -0
  162. package/src/lib/__tests__/session-title.test.ts +58 -0
  163. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  164. package/src/lib/__tests__/skill-installs.test.ts +134 -0
  165. package/src/lib/__tests__/skills-catalog.test.ts +96 -0
  166. package/src/lib/__tests__/tone.test.ts +37 -0
  167. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  168. package/src/lib/api.ts +306 -0
  169. package/src/lib/attachments.ts +203 -0
  170. package/src/lib/auth.ts +92 -0
  171. package/src/lib/avatar-presets.ts +31 -0
  172. package/src/lib/collapsible.ts +75 -0
  173. package/src/lib/composer.ts +89 -0
  174. package/src/lib/data-events.ts +17 -0
  175. package/src/lib/headlines.ts +30 -0
  176. package/src/lib/highlight.ts +186 -0
  177. package/src/lib/layout.ts +20 -0
  178. package/src/lib/lock-screen.ts +54 -0
  179. package/src/lib/model-picker.ts +133 -0
  180. package/src/lib/onboarding.ts +38 -0
  181. package/src/lib/passkeys.ts +113 -0
  182. package/src/lib/pending-prompt.ts +53 -0
  183. package/src/lib/permission-info.ts +101 -0
  184. package/src/lib/project-extensions.ts +74 -0
  185. package/src/lib/project-overview.ts +117 -0
  186. package/src/lib/prompts.ts +87 -0
  187. package/src/lib/references.ts +106 -0
  188. package/src/lib/routes.ts +115 -0
  189. package/src/lib/session-handoff.ts +25 -0
  190. package/src/lib/session-status.ts +45 -0
  191. package/src/lib/session-summary.ts +68 -0
  192. package/src/lib/session-title.ts +63 -0
  193. package/src/lib/sidebar-groups.ts +78 -0
  194. package/src/lib/skill-installs.ts +360 -0
  195. package/src/lib/skills-catalog.ts +99 -0
  196. package/src/lib/time.ts +22 -0
  197. package/src/lib/tone.ts +80 -0
  198. package/src/lib/turns.ts +320 -0
  199. package/src/lib/utils.ts +6 -0
  200. package/src/logo.svg +31 -0
  201. package/src/pages/projects/ProjectDetailPage.tsx +130 -0
  202. package/src/pages/projects/ProjectsPage.tsx +97 -0
  203. package/src/pages/sessions/SessionPage.tsx +122 -0
  204. package/src/pages/settings/SettingsPage.tsx +299 -0
  205. package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
  206. package/src/pages/skills/SkillsPage.tsx +701 -0
  207. package/src/paths.ts +121 -0
  208. package/src/runtime.ts +24 -0
  209. package/src/server/__tests__/app.test.ts +1031 -0
  210. package/src/server/__tests__/auth.test.ts +370 -0
  211. package/src/server/__tests__/config.test.ts +101 -0
  212. package/src/server/__tests__/db.test.ts +485 -0
  213. package/src/server/__tests__/host.test.ts +14 -0
  214. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  215. package/src/server/app.ts +415 -0
  216. package/src/server/db/auth.ts +311 -0
  217. package/src/server/db/messages.ts +50 -0
  218. package/src/server/db/projects.ts +106 -0
  219. package/src/server/db/prompts.ts +78 -0
  220. package/src/server/db/rows.ts +150 -0
  221. package/src/server/db/schema.ts +221 -0
  222. package/src/server/db/sessions.ts +239 -0
  223. package/src/server/db/tunnel.ts +124 -0
  224. package/src/server/host.ts +12 -0
  225. package/src/server/result.ts +15 -0
  226. package/src/server/routes/auth-guard.ts +156 -0
  227. package/src/server/routes/auth.routes.ts +287 -0
  228. package/src/server/routes/helpers.ts +165 -0
  229. package/src/server/routes/meta.routes.ts +24 -0
  230. package/src/server/routes/passkeys.routes.ts +161 -0
  231. package/src/server/routes/projects.routes.ts +157 -0
  232. package/src/server/routes/sessions.routes.ts +197 -0
  233. package/src/server/routes/skills.routes.ts +117 -0
  234. package/src/server/routes/tunnel.routes.ts +132 -0
  235. package/src/server/routes/types.ts +35 -0
  236. package/src/server/routes/uploads.routes.ts +54 -0
  237. package/src/server/routes/workspace.routes.ts +127 -0
  238. package/src/server/services/__tests__/app-info.test.ts +18 -0
  239. package/src/server/services/__tests__/attachments.test.ts +159 -0
  240. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  241. package/src/server/services/__tests__/auth.test.ts +341 -0
  242. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  243. package/src/server/services/__tests__/git-info.test.ts +74 -0
  244. package/src/server/services/__tests__/opencode.test.ts +320 -0
  245. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  246. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  247. package/src/server/services/__tests__/session-manager.test.ts +3469 -0
  248. package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
  249. package/src/server/services/__tests__/skills-registry.test.ts +359 -0
  250. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  251. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  252. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  253. package/src/server/services/__tests__/workspace.test.ts +249 -0
  254. package/src/server/services/app-info.ts +29 -0
  255. package/src/server/services/attachments.ts +193 -0
  256. package/src/server/services/auth-passkeys.ts +459 -0
  257. package/src/server/services/auth.ts +633 -0
  258. package/src/server/services/frpc-installer.ts +183 -0
  259. package/src/server/services/git-info.ts +87 -0
  260. package/src/server/services/opencode-client.ts +714 -0
  261. package/src/server/services/opencode-server.ts +319 -0
  262. package/src/server/services/opencode-sse.ts +82 -0
  263. package/src/server/services/project-manager.ts +147 -0
  264. package/src/server/services/sandbox.ts +83 -0
  265. package/src/server/services/session-events.ts +256 -0
  266. package/src/server/services/session-manager.ts +1374 -0
  267. package/src/server/services/skill-install-jobs.ts +262 -0
  268. package/src/server/services/skills-registry.ts +468 -0
  269. package/src/server/services/tunnel-api.ts +207 -0
  270. package/src/server/services/tunnel-config.ts +45 -0
  271. package/src/server/services/tunnel-lhr.ts +387 -0
  272. package/src/server/services/tunnel.ts +633 -0
  273. package/src/server/services/turn-stream.ts +339 -0
  274. package/src/server/services/websocket-gateway.ts +374 -0
  275. package/src/server/services/workspace.ts +223 -0
  276. package/src/server/services/ws-transport.ts +121 -0
  277. package/src/types/context.ts +69 -0
  278. package/src/types/index.ts +13 -0
  279. package/src/types/message.ts +58 -0
  280. package/src/types/project.ts +16 -0
  281. package/src/types/prompt.ts +118 -0
  282. package/src/types/session.ts +52 -0
  283. package/src/types/turns.ts +47 -0
  284. package/src/ws-protocol.ts +93 -0
  285. package/styles/globals.css +311 -0
  286. package/tsconfig.json +37 -0
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Parser argumen CLI kcgcode (tanpa dependency eksternal).
3
+ */
4
+
5
+ export interface CliArgs {
6
+ command: "start" | "init" | "reset-lock" | "help" | "version" | "invalid";
7
+ port?: number;
8
+ host?: string;
9
+ /** Path berkas konfigurasi (override KCG_CONFIG_PATH). */
10
+ config?: string;
11
+ open: boolean;
12
+ /** Argumen tak dikenal / pesan error. */
13
+ error?: string;
14
+ }
15
+
16
+ const HELP_TEXT = `kcgcode — control CLI AI agents from your phone
17
+
18
+ USAGE
19
+ kcgcode [start] [options]
20
+ kcgcode init [options]
21
+ kcgcode help | --help | -h
22
+ kcgcode version | --version | -v
23
+
24
+ COMMANDS
25
+ start (default) Run server + terminal dashboard
26
+ init Create kcg-code.config.json & local sandbox folder
27
+ reset-lock Remove the app lock (forgot PIN/password) & sign out all devices
28
+
29
+ OPTIONS
30
+ -p, --port <n> HTTP port (default 3000 / env KCG_PORT)
31
+ -H, --host <h> Bind host (default 127.0.0.1 / env KCG_HOST)
32
+ -c, --config <f> Config path (default ~/.kcgcode/config.json)
33
+ --open Open the web dashboard in a browser after start
34
+ -h, --help Show help
35
+ -v, --version Show version
36
+
37
+ ENV
38
+ KCG_PORT HTTP port
39
+ KCG_HOST Bind host
40
+ KCG_CONFIG_PATH Config file path
41
+ KCG_DB_PATH SQLite path (default ~/.kcgcode/data/kcg-code.sqlite)
42
+
43
+ EXAMPLES
44
+ kcgcode
45
+ kcgcode init
46
+ kcgcode start --port 4000 --open
47
+ kcgcode init --config ~/.kcgcode/config.json
48
+ `;
49
+
50
+ export function helpText(): string {
51
+ return HELP_TEXT;
52
+ }
53
+
54
+ export function parseArgs(argv: string[]): CliArgs {
55
+ const args: CliArgs = { command: "start", open: false };
56
+ const rest = [...argv];
57
+
58
+ // Command eksplisit di posisi pertama (bukan flag)
59
+ const first = rest[0];
60
+ if (first === "init") {
61
+ args.command = "init";
62
+ rest.shift();
63
+ } else if (first === "reset-lock") {
64
+ args.command = "reset-lock";
65
+ rest.shift();
66
+ } else if (first === "start") {
67
+ args.command = "start";
68
+ rest.shift();
69
+ } else if (first === "help") {
70
+ args.command = "help";
71
+ rest.shift();
72
+ } else if (first === "version") {
73
+ args.command = "version";
74
+ rest.shift();
75
+ }
76
+
77
+ for (let i = 0; i < rest.length; i++) {
78
+ const a = rest[i];
79
+ if (a === undefined) continue;
80
+
81
+ if (a === "-h" || a === "--help") {
82
+ return { command: "help", open: false };
83
+ }
84
+ if (a === "-v" || a === "--version") {
85
+ return { command: "version", open: false };
86
+ }
87
+ if (a === "--open") {
88
+ args.open = true;
89
+ continue;
90
+ }
91
+ if (a === "-p" || a === "--port") {
92
+ const raw = rest[++i];
93
+ if (raw === undefined)
94
+ return { ...args, command: "invalid", error: `Missing value for ${a}` };
95
+ const n = Number(raw);
96
+ if (!Number.isInteger(n) || n <= 0 || n > 65535) {
97
+ return { ...args, command: "invalid", error: `Invalid port "${raw}"` };
98
+ }
99
+ args.port = n;
100
+ continue;
101
+ }
102
+ if (a === "-H" || a === "--host") {
103
+ const raw = rest[++i];
104
+ if (raw === undefined)
105
+ return { ...args, command: "invalid", error: `Missing value for ${a}` };
106
+ args.host = raw;
107
+ continue;
108
+ }
109
+ if (a === "-c" || a === "--config") {
110
+ const raw = rest[++i];
111
+ if (raw === undefined)
112
+ return { ...args, command: "invalid", error: `Missing value for ${a}` };
113
+ args.config = raw;
114
+ continue;
115
+ }
116
+ if (a.startsWith("-")) {
117
+ return { ...args, command: "invalid", error: `Unknown option "${a}"` };
118
+ }
119
+ return { ...args, command: "invalid", error: `Unknown command "${a}"` };
120
+ }
121
+
122
+ return args;
123
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Perintah CLI: start (server + dashboard) dan init (scaffold config).
3
+ */
4
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { resolveConfig } from "../config";
7
+ import { openSessionStore } from "../db";
8
+ import spa from "../index.html";
9
+ import {
10
+ defaultSandboxDir,
11
+ ensureKcgHome,
12
+ KCG_HOME,
13
+ resolveEffectiveConfigPath,
14
+ resolveEffectiveDbPath,
15
+ resolveEffectiveUploadsDir,
16
+ } from "../paths";
17
+ import { isCliMode } from "../runtime";
18
+ import { createKcgServer } from "../server/app";
19
+ import { resolveHostname } from "../server/host";
20
+ import { createAuthService } from "../server/services/auth";
21
+ import type { CliArgs } from "./args";
22
+ import { openBrowser, startDashboard } from "./dashboard";
23
+ import { c, panel, row, symbols } from "./theme";
24
+
25
+ const SPA_PATHS = ["/", "/projects", "/projects/*", "/skills", "/settings", "/settings/*"];
26
+
27
+ function applyEnvOverrides(args: CliArgs): void {
28
+ if (args.port !== undefined) process.env.KCG_PORT = String(args.port);
29
+ if (args.host !== undefined) process.env.KCG_HOST = args.host;
30
+ if (args.config !== undefined) process.env.KCG_CONFIG_PATH = args.config;
31
+ }
32
+
33
+ function failConfig(message: string): never {
34
+ console.error(`${c.redBright(symbols.cross)} ${message}`);
35
+ console.error(
36
+ `\n${c.dim("Run")} ${c.cyan("kcgcode init")} ${c.dim("to create")} ${c.cyan("~/.kcgcode/config.json")}${c.dim(".")}`,
37
+ );
38
+ process.exit(1);
39
+ }
40
+
41
+ export async function runStart(args: CliArgs): Promise<void> {
42
+ // CLI global = mode produksi: matikan HMR dev yang noisy di luar repo.
43
+ if (process.env.NODE_ENV === undefined) {
44
+ process.env.NODE_ENV = "production";
45
+ }
46
+ applyEnvOverrides(args);
47
+
48
+ const cfg = resolveConfig();
49
+ if (!cfg.ok) failConfig(cfg.error);
50
+
51
+ const port = args.port ?? Number(process.env.KCG_PORT ?? 3000);
52
+ const hostname = args.host ?? resolveHostname();
53
+
54
+ const app = createKcgServer({
55
+ config: cfg.data,
56
+ spa,
57
+ spaPaths: SPA_PATHS,
58
+ port,
59
+ hostname,
60
+ uploadsRoot: resolveEffectiveUploadsDir(),
61
+ });
62
+
63
+ const resolvedPort = app.server.port ?? port;
64
+ const primaryUrl = `http://${hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname}:${resolvedPort}`;
65
+
66
+ let shuttingDown = false;
67
+ let dash: { stop(): void } | null = null;
68
+
69
+ async function shutdown(reason: string): Promise<void> {
70
+ if (shuttingDown) return;
71
+ shuttingDown = true;
72
+ dash?.stop();
73
+ console.log(`\n${c.dim(`(${reason})`)} ${c.yellow("Saving session state and stopping…")}`);
74
+ try {
75
+ await app.close();
76
+ } catch (err) {
77
+ console.error(c.red("Shutdown error:"), err);
78
+ }
79
+ console.log(c.greenBright(`${symbols.check} KCG Code stopped.`));
80
+ process.exit(0);
81
+ }
82
+
83
+ dash = await startDashboard({
84
+ server: app,
85
+ port: resolvedPort,
86
+ hostname,
87
+ onQuit: (reason) => void shutdown(reason),
88
+ onOpenUrl: () => openBrowser(primaryUrl),
89
+ });
90
+
91
+ if (args.open) {
92
+ void openBrowser(primaryUrl);
93
+ }
94
+
95
+ process.on("SIGINT", () => void shutdown("SIGINT"));
96
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
97
+ }
98
+
99
+ export function runInit(args: CliArgs): void {
100
+ if (isCliMode()) ensureKcgHome();
101
+
102
+ const configPath = resolveEffectiveConfigPath(args.config);
103
+ const sandboxPath = args.config
104
+ ? path.join(path.dirname(configPath), "sandbox")
105
+ : defaultSandboxDir();
106
+ const dataDir = path.dirname(resolveEffectiveDbPath());
107
+
108
+ if (existsSync(configPath)) {
109
+ console.log(
110
+ panel("INIT", [
111
+ row("Config", c.yellow("already exists — skipped")),
112
+ row("Path", c.dim(configPath)),
113
+ row("Mode", c.dim(isCliMode() ? "cli (~/.kcgcode)" : "dev (local)")),
114
+ row("Home", c.dim(KCG_HOME)),
115
+ "",
116
+ c.dim("Edit sandboxRoot in the config if needed."),
117
+ ]),
118
+ );
119
+ return;
120
+ }
121
+
122
+ mkdirSync(path.dirname(configPath), { recursive: true });
123
+ mkdirSync(sandboxPath, { recursive: true });
124
+ mkdirSync(dataDir, { recursive: true });
125
+ const body = `${JSON.stringify({ sandboxRoot: sandboxPath }, null, 2)}\n`;
126
+ writeFileSync(configPath, body, "utf8");
127
+
128
+ console.log(
129
+ panel("INIT", [
130
+ row("Config", c.greenBright(`${symbols.check} created`)),
131
+ row("Path", c.white(configPath)),
132
+ row("Sandbox", c.white(sandboxPath)),
133
+ row("Data", c.dim(dataDir)),
134
+ row("Mode", c.dim(isCliMode() ? "cli (~/.kcgcode)" : "dev (local)")),
135
+ "",
136
+ c.dim(
137
+ isCliMode()
138
+ ? `Run ${c.cyan("kcgcode")} to start the dashboard.`
139
+ : `Run ${c.cyan("bun dev")} in the project, or ${c.cyan("kcgcode")} if installed globally.`,
140
+ ),
141
+ ]),
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Lupa PIN/password: hapus kunci aplikasi & cabut semua sesi langsung di
147
+ * database lokal. Hanya bisa dijalankan dari mesin ini (akses shell), jadi
148
+ * aman sebagai jalur pemulihan. Nickname & foto profil tidak diubah.
149
+ */
150
+ export function runResetLock(): void {
151
+ const dbPath = resolveEffectiveDbPath();
152
+ if (!existsSync(dbPath)) {
153
+ console.log(`${c.dim("No database at")} ${c.cyan(dbPath)} ${c.dim("— nothing to reset.")}`);
154
+ return;
155
+ }
156
+ const store = openSessionStore(dbPath);
157
+ try {
158
+ const wasLocked = store.getAuthSettings(Date.now()).lockKind !== null;
159
+ createAuthService({ store }).resetLock();
160
+ console.log(
161
+ wasLocked
162
+ ? `${c.greenBright(symbols.check)} App lock removed and all devices signed out.`
163
+ : `${c.greenBright(symbols.check)} App was not locked. All device sessions cleared.`,
164
+ );
165
+ console.log(
166
+ `${c.dim("Set a new PIN or password in")} ${c.cyan("Settings → Security")}${c.dim(".")}`,
167
+ );
168
+ console.log(
169
+ `${c.yellow("!")} ${c.dim("If KCG Code is running, anyone who can reach it can use it until you set a new lock.")}`,
170
+ );
171
+ } finally {
172
+ store.close();
173
+ }
174
+ }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Dashboard terminal live untuk server kcgcode — dirender lewat Ink.
3
+ *
4
+ * Alternate screen + frame update in-place; layout modern dengan card.
5
+ */
6
+ import { networkInterfaces } from "node:os";
7
+ import { Box, render, renderToString, Text, useInput } from "ink";
8
+ import { type ReactNode, useEffect, useReducer } from "react";
9
+ import type { KcgServer } from "../server/app";
10
+ import { logoLines, readPackageVersion } from "./theme";
11
+
12
+ export interface DashboardInfo {
13
+ server: KcgServer;
14
+ port: number;
15
+ hostname: string;
16
+ }
17
+
18
+ export interface DashboardCallbacks {
19
+ /** Panggil saat user minta berhenti (q / Ctrl+C). */
20
+ onQuit: (reason: string) => void;
21
+ /** Buka URL dashboard di browser default. */
22
+ onOpenUrl: () => void | Promise<void>;
23
+ }
24
+
25
+ const REFRESH_MS = 2000;
26
+ const PAD = 2;
27
+
28
+ function localUrls(hostname: string, port: number): { label: string; url: string }[] {
29
+ const urls: { label: string; url: string }[] = [];
30
+ const host = hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname;
31
+ urls.push({ label: "local", url: `http://${host}:${port}` });
32
+
33
+ if (hostname === "127.0.0.1" || hostname === "localhost") {
34
+ return urls;
35
+ }
36
+
37
+ for (const infos of Object.values(networkInterfaces())) {
38
+ for (const info of infos ?? []) {
39
+ if (info.family === "IPv4" && !info.internal) {
40
+ urls.push({ label: "network", url: `http://${info.address}:${port}` });
41
+ }
42
+ }
43
+ }
44
+ return urls;
45
+ }
46
+
47
+ function activityStats(app: KcgServer) {
48
+ const projects = app.store.listProjects().length;
49
+ const sessions = app.store.listSessions();
50
+ const running = sessions.filter((s) => s.status === "running");
51
+ return { projects, sessions: sessions.length, running };
52
+ }
53
+
54
+ function Card({ title, children }: { title: string; children: ReactNode }) {
55
+ return (
56
+ <Box borderStyle="round" borderColor="cyan" flexDirection="column" paddingX={1} gap={0}>
57
+ <Text color="cyan" bold>
58
+ {title}
59
+ </Text>
60
+ <Box flexDirection="column" gap={0}>
61
+ {children}
62
+ </Box>
63
+ </Box>
64
+ );
65
+ }
66
+
67
+ function Key({ children }: { children: string }) {
68
+ return <Text dimColor>{children}</Text>;
69
+ }
70
+
71
+ function Metric({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
72
+ return (
73
+ <Box gap={1}>
74
+ <Text dimColor>{label.padEnd(10)}</Text>
75
+ <Text bold color={accent ? "green" : undefined}>
76
+ {value}
77
+ </Text>
78
+ </Box>
79
+ );
80
+ }
81
+
82
+ function Brand({ version }: { version: string }) {
83
+ const logos = logoLines();
84
+ return (
85
+ <Box gap={2}>
86
+ <Box flexDirection="column">
87
+ {logos.map((line, i) => (
88
+ // biome-ignore lint/suspicious/noArrayIndexKey: static logo lines
89
+ <Text key={i} color="cyan" bold>
90
+ {line}
91
+ </Text>
92
+ ))}
93
+ </Box>
94
+ <Box flexDirection="column">
95
+ <Box gap={1}>
96
+ <Text bold color="cyan">
97
+ kcgcode
98
+ </Text>
99
+ <Text dimColor>v{version}</Text>
100
+ </Box>
101
+ <Text dimColor>mobile control for CLI agents</Text>
102
+ <Text dimColor>opencode · claude code</Text>
103
+ </Box>
104
+ </Box>
105
+ );
106
+ }
107
+
108
+ const TUNNEL_COLOR: Record<string, string> = {
109
+ online: "green",
110
+ connecting: "yellow",
111
+ starting: "yellow",
112
+ reconnecting: "yellow",
113
+ error: "red",
114
+ };
115
+
116
+ /** Status tunnel publik (`https://<username>.<domain>`). */
117
+ function TunnelCard({ server }: { server: KcgServer }) {
118
+ const t = server.tunnel.status();
119
+ if (!t.account) {
120
+ return (
121
+ <Card title="TUNNEL">
122
+ {t.pairing ? (
123
+ <>
124
+ <Box gap={1}>
125
+ <Text color="yellow">○</Text>
126
+ <Text dimColor>{"code".padEnd(8)}</Text>
127
+ <Text bold color="yellow">
128
+ {t.pairing.userCode}
129
+ </Text>
130
+ </Box>
131
+ <Box gap={1}>
132
+ <Text color="gray"> </Text>
133
+ <Text dimColor>{"open".padEnd(8)}</Text>
134
+ <Text color="cyan" underline>
135
+ {t.pairing.verificationUriComplete}
136
+ </Text>
137
+ </Box>
138
+ </>
139
+ ) : (
140
+ <Box gap={1}>
141
+ <Text color="gray">○</Text>
142
+ <Text dimColor>off · connect in Settings → Remote access</Text>
143
+ </Box>
144
+ )}
145
+ </Card>
146
+ );
147
+ }
148
+ const color = TUNNEL_COLOR[t.phase] ?? "gray";
149
+ return (
150
+ <Card title="TUNNEL">
151
+ <Box gap={1}>
152
+ <Text color={color}>{t.phase === "online" ? "●" : "○"}</Text>
153
+ <Text dimColor>{"public".padEnd(8)}</Text>
154
+ <Text color="cyan" underline={t.phase === "online"}>
155
+ {t.account.url}
156
+ </Text>
157
+ </Box>
158
+ <Box gap={1}>
159
+ <Text dimColor>{"status".padEnd(10)}</Text>
160
+ <Text color={color} bold>
161
+ {t.phase === "stopped" ? "off" : t.phase}
162
+ </Text>
163
+ {t.error && <Text color="red">({t.error})</Text>}
164
+ </Box>
165
+ </Card>
166
+ );
167
+ }
168
+
169
+ interface DashboardUIProps {
170
+ info: DashboardInfo;
171
+ version: string;
172
+ }
173
+
174
+ function DashboardUI({ info, version }: DashboardUIProps) {
175
+ const { server, port, hostname } = info;
176
+ const urls = localUrls(hostname, port);
177
+ const { projects, sessions, running } = activityStats(server);
178
+
179
+ return (
180
+ <Box flexDirection="column" paddingX={PAD} paddingY={1} gap={1}>
181
+ <Brand version={version} />
182
+
183
+ <Card title="SERVER">
184
+ {urls.map((u, idx) => (
185
+ <Box key={u.url} gap={1}>
186
+ <Text color={idx === 0 ? "green" : "gray"}>{idx === 0 ? "●" : "○"}</Text>
187
+ <Text dimColor>{u.label.padEnd(8)}</Text>
188
+ <Text color="cyan" underline>
189
+ {u.url}
190
+ </Text>
191
+ </Box>
192
+ ))}
193
+ <Box marginTop={0} gap={1}>
194
+ <Text dimColor>{"process".padEnd(8)}</Text>
195
+ <Text color="green" bold>
196
+ running
197
+ </Text>
198
+ </Box>
199
+ </Card>
200
+
201
+ <TunnelCard server={server} />
202
+
203
+ <Card title="ACTIVITY">
204
+ <Metric label="projects" value={String(projects)} />
205
+ <Metric label="sessions" value={String(sessions)} />
206
+ <Metric label="running" value={String(running.length)} accent={running.length > 0} />
207
+
208
+ {running.length > 0 && (
209
+ <Box flexDirection="column" marginTop={1}>
210
+ {running.slice(0, 5).map((s) => (
211
+ <Box key={s.id} gap={1}>
212
+ <Text color="green">●</Text>
213
+ <Text>{s.title?.trim() || s.id.slice(0, 12)}</Text>
214
+ </Box>
215
+ ))}
216
+ </Box>
217
+ )}
218
+ </Card>
219
+
220
+ <Box gap={2}>
221
+ <Box gap={1}>
222
+ <Key>q</Key>
223
+ <Text dimColor>quit</Text>
224
+ </Box>
225
+ <Box gap={1}>
226
+ <Key>o</Key>
227
+ <Text dimColor>open</Text>
228
+ </Box>
229
+ <Box gap={1}>
230
+ <Key>ctrl+c</Key>
231
+ <Text dimColor>stop</Text>
232
+ </Box>
233
+ </Box>
234
+ </Box>
235
+ );
236
+ }
237
+
238
+ /** Frame string (non-TTY / test). */
239
+ export function renderDashboard(info: DashboardInfo, version: string): string {
240
+ return renderToString(<DashboardUI info={info} version={version} />);
241
+ }
242
+
243
+ interface DashboardAppProps {
244
+ info: DashboardInfo;
245
+ version: string;
246
+ onQuit: DashboardCallbacks["onQuit"];
247
+ onOpenUrl: DashboardCallbacks["onOpenUrl"];
248
+ }
249
+
250
+ function DashboardApp({ info, version, onQuit, onOpenUrl }: DashboardAppProps) {
251
+ const [, tick] = useReducer((n: number) => n + 1, 0);
252
+
253
+ useEffect(() => {
254
+ const id = setInterval(tick, REFRESH_MS);
255
+ return () => clearInterval(id);
256
+ }, []);
257
+
258
+ useInput((input, key) => {
259
+ if (key.ctrl && input === "c") {
260
+ onQuit("Ctrl+C");
261
+ return;
262
+ }
263
+ if (input === "q" || input === "Q") {
264
+ onQuit("quit");
265
+ return;
266
+ }
267
+ if (input === "o" || input === "O") {
268
+ void onOpenUrl();
269
+ }
270
+ });
271
+
272
+ return <DashboardUI info={info} version={version} />;
273
+ }
274
+
275
+ export interface RunningDashboard {
276
+ stop(): void;
277
+ /** Frame sudah live via Ink; API dipertahankan untuk kompatibilitas. */
278
+ redraw(): void;
279
+ }
280
+
281
+ /**
282
+ * Mulai dashboard live di alternate screen. Kembalikan handle untuk stop.
283
+ * Pastikan `stop()` dipanggil saat shutdown agar terminal pulih.
284
+ */
285
+ export async function startDashboard(
286
+ info: DashboardInfo & Partial<DashboardCallbacks>,
287
+ ): Promise<RunningDashboard> {
288
+ const version = await readPackageVersion();
289
+ const stdout = process.stdout;
290
+ const isTty = Boolean(stdout.isTTY);
291
+
292
+ const onQuit = info.onQuit ?? (() => {});
293
+ const onOpenUrl = info.onOpenUrl ?? (() => {});
294
+
295
+ if (!isTty) {
296
+ stdout.write(`${renderDashboard(info, version)}\n`);
297
+ return {
298
+ stop() {},
299
+ redraw() {
300
+ stdout.write(`${renderDashboard(info, version)}\n`);
301
+ },
302
+ };
303
+ }
304
+
305
+ const instance = render(
306
+ <DashboardApp info={info} version={version} onQuit={onQuit} onOpenUrl={onOpenUrl} />,
307
+ {
308
+ alternateScreen: true,
309
+ patchConsole: true,
310
+ exitOnCtrlC: false,
311
+ incrementalRendering: true,
312
+ },
313
+ );
314
+
315
+ return {
316
+ stop() {
317
+ instance.unmount();
318
+ },
319
+ redraw() {
320
+ instance.rerender(
321
+ <DashboardApp info={info} version={version} onQuit={onQuit} onOpenUrl={onOpenUrl} />,
322
+ );
323
+ },
324
+ };
325
+ }
326
+
327
+ /** Coba buka URL di browser default OS. */
328
+ export async function openBrowser(url: string): Promise<void> {
329
+ const platform = process.platform;
330
+ let cmd: string[];
331
+ if (platform === "win32") {
332
+ cmd = ["cmd", "/c", "start", "", url];
333
+ } else if (platform === "darwin") {
334
+ cmd = ["open", url];
335
+ } else {
336
+ cmd = ["xdg-open", url];
337
+ }
338
+ try {
339
+ Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" });
340
+ } catch {
341
+ // diam — user bisa buka manual
342
+ }
343
+ }
@@ -0,0 +1,37 @@
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 { c, readPackageVersion, symbols } from "./theme";
7
+
8
+ export async function runCli(argv: string[]): Promise<void> {
9
+ const args = parseArgs(argv);
10
+
11
+ switch (args.command) {
12
+ case "help":
13
+ console.log(helpText());
14
+ return;
15
+ case "version": {
16
+ const v = await readPackageVersion();
17
+ console.log(`kcgcode ${v}`);
18
+ return;
19
+ }
20
+ case "invalid":
21
+ console.error(`${c.redBright(symbols.cross)} ${args.error}`);
22
+ console.error(`\n${c.dim("Run")} ${c.cyan("kcgcode --help")} ${c.dim("for usage.")}`);
23
+ process.exit(1);
24
+ return;
25
+ case "init":
26
+ runInit(args);
27
+ return;
28
+ case "reset-lock":
29
+ runResetLock();
30
+ return;
31
+ case "start":
32
+ await runStart(args);
33
+ return;
34
+ }
35
+ }
36
+
37
+ export type { CliArgs };