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.
- package/README.md +230 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +84 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +147 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +114 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +101 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +327 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +362 -0
- package/src/lib/skills-catalog.ts +138 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +427 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
package/README.md
CHANGED
|
@@ -1,3 +1,231 @@
|
|
|
1
|
-
#
|
|
1
|
+
# kcgcode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Control your [OpenCode](https://opencode.ai) agents from your phone. kcgcode runs a local bridge
|
|
4
|
+
on your machine and serves a mobile-friendly web app (PWA) where you chat with agents, answer
|
|
5
|
+
their permission prompts, manage projects and skills, and keep it all behind a lock screen when
|
|
6
|
+
you reach it through a tunnel.
|
|
7
|
+
|
|
8
|
+
> Only OpenCode is supported as an agent right now. Claude Code support is planned.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Chat with agents from anywhere.** Start sessions per project, stream replies live, stop a
|
|
13
|
+
running turn, and pick the model and agent mode (build, plan, or your own agents).
|
|
14
|
+
- **Answer prompts on the go.** Permission requests and multi-question prompts from the agent
|
|
15
|
+
show up as cards you can approve, deny, or answer from your phone.
|
|
16
|
+
- **Projects & sessions.** Organise work by folder inside a sandbox. The sidebar groups every
|
|
17
|
+
session by project, with live status and a quick search (`Ctrl/⌘ + K`).
|
|
18
|
+
- **Rich input.** Mention files with `@`, attach images, and set per-project custom
|
|
19
|
+
instructions.
|
|
20
|
+
- **Skills from [skills.sh](https://skills.sh).** Search the directory, see security audit
|
|
21
|
+
results before you install, and add skills to a project in one click. Installs run in the
|
|
22
|
+
background with live output, survive page refreshes, and can be cancelled. The agent picks up
|
|
23
|
+
new skills automatically, waiting for any running chat to finish first.
|
|
24
|
+
- **App lock.** Protect the app with a PIN or password before exposing it. It comes with a lock
|
|
25
|
+
screen showing your profile photo or avatar, auto-lock after inactivity, signed-in device
|
|
26
|
+
management, and brute-force protection.
|
|
27
|
+
- **Installable PWA** with light, dark, or system theme.
|
|
28
|
+
- **Terminal dashboard.** `kcgcode` shows the server status right in your terminal.
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- [Bun](https://bun.com) 1.1 or newer
|
|
33
|
+
- [OpenCode](https://opencode.ai) installed and on your `PATH` (kcgcode starts `opencode serve`
|
|
34
|
+
for each project)
|
|
35
|
+
- Internet access for skills.sh search and skill installs (installs use `bunx skills`)
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bun i -g kcgcode
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or from a local clone:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bun pm pack
|
|
47
|
+
bun i -g ./kcgcode-0.1.0.tgz
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# create ~/.kcgcode (config, sandbox, data)
|
|
54
|
+
kcgcode init
|
|
55
|
+
|
|
56
|
+
# start the server + terminal dashboard (works from any directory)
|
|
57
|
+
kcgcode
|
|
58
|
+
|
|
59
|
+
# custom port, open the browser
|
|
60
|
+
kcgcode start --port 4000 --open
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open the printed URL in your browser. Projects are folders inside the sandbox root from your
|
|
64
|
+
config.
|
|
65
|
+
|
|
66
|
+
## Using it from your phone
|
|
67
|
+
|
|
68
|
+
By default the server only listens on `127.0.0.1`, so it's reachable from this machine only.
|
|
69
|
+
To use it from your phone:
|
|
70
|
+
|
|
71
|
+
1. **Set a lock first.** Open **Settings → Security** and turn on the app lock (PIN or
|
|
72
|
+
password). Until a lock is set, anyone who can reach the address can control your agents and
|
|
73
|
+
run commands on your machine. The sidebar shows a "Not protected" warning while there's no
|
|
74
|
+
lock.
|
|
75
|
+
2. **Expose it.** Either:
|
|
76
|
+
- Turn on **Settings → Remote access** (built in, see below), or
|
|
77
|
+
- Use an HTTPS tunnel such as Cloudflare Tunnel or ngrok, pointed at the kcgcode port, or
|
|
78
|
+
- Bind to your LAN with `kcgcode --host 0.0.0.0` and open `http://<your-ip>:3000` on the
|
|
79
|
+
same network. Traffic isn't encrypted on plain HTTP, so prefer a tunnel.
|
|
80
|
+
3. Open the URL on your phone, unlock, and optionally add it to your home screen.
|
|
81
|
+
|
|
82
|
+
Only tunnel the kcgcode port. The per-project `opencode serve` processes have no password of
|
|
83
|
+
their own and should stay local.
|
|
84
|
+
|
|
85
|
+
### Remote access (`https://<name>.<your-domain>`)
|
|
86
|
+
|
|
87
|
+
kcgcode can publish itself at your own subdomain through a kcgcode tunnel server you host
|
|
88
|
+
(see [`reverse-proxy/`](reverse-proxy)).
|
|
89
|
+
|
|
90
|
+
1. Set an app lock. Remote access can't be turned on without one, and removing the lock turns
|
|
91
|
+
it off.
|
|
92
|
+
2. **Settings → Remote access → Connect.** kcgcode shows a code, a link, and a QR code.
|
|
93
|
+
3. Open the link on any device (your phone works), sign in with Google, check that the code
|
|
94
|
+
matches, and approve. First time only: pick a name (3–20 lowercase letters, numbers, or
|
|
95
|
+
hyphens).
|
|
96
|
+
4. kcgcode connects and turns the tunnel on. It downloads `frpc` v0.71.0 to `~/.kcgcode/bin`
|
|
97
|
+
(SHA-256 verified; an existing `frpc` on `PATH` is used instead). The tunnel reconnects on its
|
|
98
|
+
own and comes back after a restart.
|
|
99
|
+
|
|
100
|
+
Google sign-in happens entirely on the tunnel service, so kcgcode never holds Google
|
|
101
|
+
credentials. What it stores is a device token (revocable, per machine) and the tunnel secret, in
|
|
102
|
+
SQLite plus `frpc.toml` (mode 0600) next to it. Neither is ever sent to the browser.
|
|
103
|
+
Under **Advanced**, **Unlink this computer** revokes the device token on the server and
|
|
104
|
+
**Reset security key** issues a new frp secret if you think it leaked.
|
|
105
|
+
|
|
106
|
+
The tunnel server URL is baked into the published npm package, so users don't configure
|
|
107
|
+
anything. `KCG_TUNNEL_API_URL` overrides it, for example to point at your own server.
|
|
108
|
+
|
|
109
|
+
| Environment variable | Default |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| `KCG_TUNNEL_API_URL` | The URL built into the package (none in a git checkout) |
|
|
112
|
+
| `KCG_FRPC_PATH` | auto (`~/.kcgcode/bin/frpc`, then `PATH`, then download) |
|
|
113
|
+
|
|
114
|
+
## Security
|
|
115
|
+
|
|
116
|
+
When a lock is set:
|
|
117
|
+
|
|
118
|
+
- Every API route and the WebSocket require a signed-in session. The only exceptions are the
|
|
119
|
+
lock screen's status, login, and profile photo.
|
|
120
|
+
- The PIN or password is stored as an argon2id hash. Sessions use an `HttpOnly`,
|
|
121
|
+
`SameSite=Strict` cookie, marked `Secure` over HTTPS. The database only stores a hash of the
|
|
122
|
+
session token.
|
|
123
|
+
- Requests from other sites are rejected (Origin check, including the WebSocket).
|
|
124
|
+
- Auto-lock (5 min to 1 hour, or never) is enforced by the server, not just the UI. Locking,
|
|
125
|
+
changing the lock, or signing out a device also closes that device's live connection.
|
|
126
|
+
- Failed attempts are rate limited per IP and globally, since tunnels can hide the real IP.
|
|
127
|
+
Repeated failures pause all logins for up to 15 minutes.
|
|
128
|
+
- PINs must be 6–12 digits, and trivial patterns like `111111` or `123456` are rejected.
|
|
129
|
+
Passwords need at least 8 characters.
|
|
130
|
+
|
|
131
|
+
**Forgot your PIN or password?** Run this on the host machine:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
kcgcode reset-lock
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
It removes the lock and signs out every device, but keeps your profile. Set a new lock right
|
|
138
|
+
away if the app is exposed.
|
|
139
|
+
|
|
140
|
+
Skills run with your agent's full permissions. Review a skill and its audit before installing.
|
|
141
|
+
|
|
142
|
+
## CLI
|
|
143
|
+
|
|
144
|
+
| Command | Description |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `kcgcode` / `kcgcode start` | Run the server + live dashboard |
|
|
147
|
+
| `kcgcode init` | Create `~/.kcgcode` (config, sandbox, data) |
|
|
148
|
+
| `kcgcode reset-lock` | Remove the app lock and sign out all devices |
|
|
149
|
+
| `kcgcode --help` | Show help |
|
|
150
|
+
| `kcgcode --version` | Show version |
|
|
151
|
+
|
|
152
|
+
| Option | Description |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `-p, --port <n>` | HTTP port (default `3000`, env `KCG_PORT`) |
|
|
155
|
+
| `-H, --host <h>` | Bind host (default `127.0.0.1`, env `KCG_HOST`) |
|
|
156
|
+
| `-c, --config <f>` | Config path (default `~/.kcgcode/config.json`) |
|
|
157
|
+
| `--open` | Open the web app in your browser |
|
|
158
|
+
| `--allow-sleep` | Let the OS sleep while running (by default kcgcode keeps the machine awake so the tunnel stays connected) |
|
|
159
|
+
|
|
160
|
+
Dashboard keys: `q` quit · `o` open browser · `Ctrl+C` stop.
|
|
161
|
+
|
|
162
|
+
| Environment variable | Description |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `KCG_PORT` | HTTP port |
|
|
165
|
+
| `KCG_HOST` | Bind host |
|
|
166
|
+
| `KCG_CONFIG_PATH` | Config file path |
|
|
167
|
+
| `KCG_DB_PATH` | SQLite path (default `~/.kcgcode/data/kcg-code.sqlite`) |
|
|
168
|
+
| `KCG_ALLOW_SLEEP=1` | Same as `--allow-sleep` |
|
|
169
|
+
|
|
170
|
+
## Config & data location
|
|
171
|
+
|
|
172
|
+
The runtime mode is detected from the entrypoint:
|
|
173
|
+
|
|
174
|
+
| | **dev** (`bun dev` / `src/index.ts`) | **cli** (`kcgcode` global binary) |
|
|
175
|
+
| --- | --- | --- |
|
|
176
|
+
| Config | `./kcg-code.config.json` if present, else `~/.kcgcode/config.json` | `~/.kcgcode/config.json` |
|
|
177
|
+
| SQLite | `./data/kcg-code.sqlite` | `~/.kcgcode/data/kcg-code.sqlite` |
|
|
178
|
+
| Uploads | `./data/uploads` | `~/.kcgcode/data/uploads` |
|
|
179
|
+
| Sandbox (init) | `./sandbox` | `~/.kcgcode/sandbox` |
|
|
180
|
+
| HMR | on | off (`NODE_ENV=production`) |
|
|
181
|
+
|
|
182
|
+
Global CLI layout:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
~/.kcgcode/
|
|
186
|
+
config.json # sandboxRoot
|
|
187
|
+
sandbox/ # default agent workspace
|
|
188
|
+
data/
|
|
189
|
+
kcg-code.sqlite # projects, sessions, messages, app lock
|
|
190
|
+
uploads/ # image attachments
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Config file resolution order:
|
|
194
|
+
|
|
195
|
+
1. `--config <path>` / `KCG_CONFIG_PATH` (always wins)
|
|
196
|
+
2. **cli**: `~/.kcgcode/config.json` (cwd is ignored)
|
|
197
|
+
3. **dev**: `./kcg-code.config.json` if present, else `~/.kcgcode/config.json`
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"sandboxRoot": "/absolute/path/to/your/workspace"
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Skills installed from the Skills page go into the project folder under `.agents/skills/`.
|
|
206
|
+
|
|
207
|
+
## Develop
|
|
208
|
+
|
|
209
|
+
Copy `.env.example` to `.env`. Bun loads it automatically, and it's gitignored. Set
|
|
210
|
+
`KCG_TUNNEL_API_URL` to your tunnel server to use Remote access while developing.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
bun install
|
|
214
|
+
bun dev # dev server with HMR (uses ./kcg-code.config.json if present)
|
|
215
|
+
bun test
|
|
216
|
+
bun run lint
|
|
217
|
+
bun run typecheck
|
|
218
|
+
bun run build
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Publishing
|
|
222
|
+
|
|
223
|
+
`bun publish` (or `bun pm pack`) runs `prepack`, which writes `KCG_TUNNEL_API_URL` from `.env`
|
|
224
|
+
into `src/build-config.ts`. `postpack` then resets it, so the URL never ends up in git.
|
|
225
|
+
Publishing fails if the variable is missing or isn't `https://`. Check what gets published with:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
bun pm pack && tar -xOzf kcgcode-*.tgz package/src/build-config.ts
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Built with [Bun](https://bun.com).
|
package/bin/kcgcode.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Entry global CLI `kcgcode` (`bun i -g kcgcode`).
|
|
4
|
+
*
|
|
5
|
+
* Menandai mode runtime `cli` lebih dulu agar config/db/uploads
|
|
6
|
+
* resolve ke `~/.kcgcode`, bukan path lokal cwd.
|
|
7
|
+
*/
|
|
8
|
+
import { setRunMode } from "../src/runtime";
|
|
9
|
+
|
|
10
|
+
setRunMode("cli");
|
|
11
|
+
|
|
12
|
+
const { runCli } = await import("../src/cli/index");
|
|
13
|
+
await runCli(process.argv.slice(2));
|
package/bun-env.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Generated by `bun init`
|
|
2
|
+
|
|
3
|
+
declare module "*.svg" {
|
|
4
|
+
/**
|
|
5
|
+
* A path to the SVG file
|
|
6
|
+
*/
|
|
7
|
+
const path: `${string}.svg`;
|
|
8
|
+
export = path;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "*.css" {}
|
|
12
|
+
|
|
13
|
+
declare module "*.module.css" {
|
|
14
|
+
/**
|
|
15
|
+
* A record of class names to their corresponding CSS module classes
|
|
16
|
+
*/
|
|
17
|
+
const classes: { readonly [key: string]: string };
|
|
18
|
+
export = classes;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kcg-code",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Control CLI AI agents (OpenCode / Claude Code) from your phone — mobile PWA + local headless bridge.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kcgcode": "./bin/kcgcode.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"public",
|
|
14
|
+
"styles",
|
|
15
|
+
"bun-env.d.ts",
|
|
16
|
+
"tsconfig.json",
|
|
17
|
+
"README.md",
|
|
18
|
+
"kcg-code.config.example.json"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.1.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"bun",
|
|
25
|
+
"cli",
|
|
26
|
+
"tui",
|
|
27
|
+
"opencode",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"ai-agent",
|
|
30
|
+
"pwa",
|
|
31
|
+
"mobile"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "bun --hot src/index.ts",
|
|
35
|
+
"start": "NODE_ENV=production bun src/index.ts",
|
|
36
|
+
"kcgcode": "bun bin/kcgcode.ts",
|
|
37
|
+
"build": "bun run build.ts",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"lint": "biome check .",
|
|
40
|
+
"lint:fix": "biome check --write .",
|
|
41
|
+
"format": "biome format --write .",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"prepack": "bun scripts/stamp-build-config.ts",
|
|
44
|
+
"postpack": "bun scripts/stamp-build-config.ts --reset"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.296",
|
|
48
|
+
"@radix-ui/react-label": "^2.1.7",
|
|
49
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
50
|
+
"@radix-ui/react-slot": "^1.2.3",
|
|
51
|
+
"@shadcn/react": "^0.3.0",
|
|
52
|
+
"@simplewebauthn/browser": "14.0.0",
|
|
53
|
+
"@simplewebauthn/server": "14.0.3",
|
|
54
|
+
"bun-plugin-tailwind": "^0.1.2",
|
|
55
|
+
"class-variance-authority": "^0.7.1",
|
|
56
|
+
"clsx": "^2.1.1",
|
|
57
|
+
"cn": "^0.2.5",
|
|
58
|
+
"ink": "^7.1.1",
|
|
59
|
+
"lucide-react": "^1",
|
|
60
|
+
"motion": "13.4.6",
|
|
61
|
+
"picocolors": "^1.1.1",
|
|
62
|
+
"radix-ui": "^1.6.7",
|
|
63
|
+
"react": "^19",
|
|
64
|
+
"react-dom": "^19",
|
|
65
|
+
"react-markdown": "10.1.0",
|
|
66
|
+
"remark-gfm": "4.0.1",
|
|
67
|
+
"shiki": "4.4.3",
|
|
68
|
+
"sonner": "2.0.8",
|
|
69
|
+
"tailwind-merge": "^3.3.1",
|
|
70
|
+
"tw-animate-css": "^1.4.0",
|
|
71
|
+
"uqr": "0.1.3"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@biomejs/biome": "^2.5.7",
|
|
75
|
+
"@types/bun": "latest",
|
|
76
|
+
"@types/react": "^19",
|
|
77
|
+
"@types/react-dom": "^19",
|
|
78
|
+
"fast-check": "^4.9.0",
|
|
79
|
+
"react-reconciler": "0.33.0",
|
|
80
|
+
"tailwindcss": "^4.1.11",
|
|
81
|
+
"typescript": "^6"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"typescript": "^6"
|
|
85
|
+
}
|
|
86
|
+
}
|
package/public/logo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "KCG Code",
|
|
3
|
+
"short_name": "KCG Code",
|
|
4
|
+
"description": "Kontrol CLI AI Agent (OpenCode / Claude Code) dari perangkat mobile.",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#0a0a0a",
|
|
8
|
+
"theme_color": "#0a0a0a",
|
|
9
|
+
"lang": "id",
|
|
10
|
+
"icons": [
|
|
11
|
+
{
|
|
12
|
+
"src": "/logo.svg",
|
|
13
|
+
"sizes": "any",
|
|
14
|
+
"type": "image/svg+xml",
|
|
15
|
+
"purpose": "any"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/public/sw.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// KCG Code service worker (Requirement 8.1).
|
|
2
|
+
// - Cache-first untuk asset statis (JS/CSS bundle hasil build.ts).
|
|
3
|
+
// - Network-only (pass-through) untuk `/api/*` dan koneksi WebSocket `/ws`,
|
|
4
|
+
// agar kontrol CLI_Agent tidak pernah disajikan dari cache basi.
|
|
5
|
+
|
|
6
|
+
const CACHE_NAME = "kcg-code-v1";
|
|
7
|
+
const PRECACHE = ["/", "/manifest.json", "/logo.svg"];
|
|
8
|
+
|
|
9
|
+
self.addEventListener("install", (event) => {
|
|
10
|
+
event.waitUntil(
|
|
11
|
+
caches
|
|
12
|
+
.open(CACHE_NAME)
|
|
13
|
+
.then((cache) => cache.addAll(PRECACHE))
|
|
14
|
+
.catch(() => {}),
|
|
15
|
+
);
|
|
16
|
+
self.skipWaiting();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
self.addEventListener("activate", (event) => {
|
|
20
|
+
event.waitUntil(
|
|
21
|
+
caches
|
|
22
|
+
.keys()
|
|
23
|
+
.then((keys) =>
|
|
24
|
+
Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
self.clients.claim();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
self.addEventListener("fetch", (event) => {
|
|
31
|
+
const url = new URL(event.request.url);
|
|
32
|
+
|
|
33
|
+
// Network-only untuk API dan WebSocket.
|
|
34
|
+
if (url.pathname.startsWith("/api/") || url.pathname === "/ws") return;
|
|
35
|
+
|
|
36
|
+
// Cache-first untuk asset statis.
|
|
37
|
+
event.respondWith(
|
|
38
|
+
caches.match(event.request).then((cached) => {
|
|
39
|
+
if (cached) return cached;
|
|
40
|
+
return fetch(event.request).then((response) => {
|
|
41
|
+
if (response.ok && url.origin === self.location.origin) {
|
|
42
|
+
const clone = response.clone();
|
|
43
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
44
|
+
}
|
|
45
|
+
return response;
|
|
46
|
+
});
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
});
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Akar aplikasi KCG Code.
|
|
3
|
+
*
|
|
4
|
+
* Layout, navigasi, dan pemetaan URL -> halaman dipegang `AppShell`
|
|
5
|
+
* (`components/layout/AppShell.tsx`) — pola yang sama dengan kcgcode
|
|
6
|
+
* (`src/App.tsx` merender shell, halaman hidup di `src/pages/*`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { domAnimation, LazyMotion, MotionConfig } from "motion/react";
|
|
10
|
+
import { AuthGate } from "@/components/auth/AuthGate";
|
|
11
|
+
import { AppShell } from "@/components/layout/AppShell";
|
|
12
|
+
import { Toaster } from "@/components/ui/sonner";
|
|
13
|
+
import "./index.css";
|
|
14
|
+
|
|
15
|
+
export function App() {
|
|
16
|
+
// Kunci aplikasi: shell (dan semua request datanya) baru dirender setelah
|
|
17
|
+
// lock screen dibuka.
|
|
18
|
+
//
|
|
19
|
+
// Motion (animasi masuk/keluar komponen):
|
|
20
|
+
// - `LazyMotion` + `domAnimation` + komponen `m.*` (`motion/react-m`)
|
|
21
|
+
// menjaga bundle kecil (tanpa fitur drag/layout). `strict` melempar
|
|
22
|
+
// error bila ada yang tak sengaja memakai `motion.*` (bundle penuh).
|
|
23
|
+
// - `reducedMotion="user"`: animasi transform dimatikan bila pengguna
|
|
24
|
+
// memilih reduce motion di sistemnya (opacity tetap).
|
|
25
|
+
return (
|
|
26
|
+
<LazyMotion features={domAnimation} strict>
|
|
27
|
+
<MotionConfig reducedMotion="user">
|
|
28
|
+
<AuthGate>
|
|
29
|
+
<AppShell />
|
|
30
|
+
</AuthGate>
|
|
31
|
+
{/* Toast global (juga tampil di lock screen). Atas-tengah agar tidak
|
|
32
|
+
menutupi kartu instalasi skill di kanan bawah. */}
|
|
33
|
+
<Toaster position="top-center" closeButton />
|
|
34
|
+
</MotionConfig>
|
|
35
|
+
</LazyMotion>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default App;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test validitas PWA manifest (task 21.4, Requirement 8.1).
|
|
3
|
+
*
|
|
4
|
+
* Memverifikasi field wajib web app manifest (`name`, `short_name`,
|
|
5
|
+
* `start_url`, `display`, `icons`) ada dan bernilai valid agar Client dapat
|
|
6
|
+
* di-install sebagai Progressive Web App.
|
|
7
|
+
*/
|
|
8
|
+
import { expect, test } from "bun:test";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
interface ManifestIcon {
|
|
12
|
+
src: string;
|
|
13
|
+
sizes: string;
|
|
14
|
+
type: string;
|
|
15
|
+
purpose?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Manifest {
|
|
19
|
+
name: string;
|
|
20
|
+
short_name: string;
|
|
21
|
+
start_url: string;
|
|
22
|
+
display: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
icons: ManifestIcon[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readManifest(): Manifest {
|
|
28
|
+
const raw = readFileSync("public/manifest.json", "utf8");
|
|
29
|
+
return JSON.parse(raw) as Manifest;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("21.4: manifest.json adalah JSON valid", () => {
|
|
33
|
+
expect(() => readManifest()).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("21.4: field wajib ada dan bernilai valid", () => {
|
|
37
|
+
const manifest = readManifest();
|
|
38
|
+
|
|
39
|
+
// name & short_name berupa string non-kosong
|
|
40
|
+
expect(typeof manifest.name).toBe("string");
|
|
41
|
+
expect(manifest.name.length).toBeGreaterThan(0);
|
|
42
|
+
expect(typeof manifest.short_name).toBe("string");
|
|
43
|
+
expect(manifest.short_name.length).toBeGreaterThan(0);
|
|
44
|
+
|
|
45
|
+
// start_url adalah path/URL absolut valid
|
|
46
|
+
expect(typeof manifest.start_url).toBe("string");
|
|
47
|
+
expect(manifest.start_url.startsWith("/")).toBe(true);
|
|
48
|
+
|
|
49
|
+
// display stand-alone (prasyarat instalasi PWA)
|
|
50
|
+
expect(manifest.display).toBe("standalone");
|
|
51
|
+
|
|
52
|
+
// icons: array non-kosong dengan src & type valid
|
|
53
|
+
expect(Array.isArray(manifest.icons)).toBe(true);
|
|
54
|
+
expect(manifest.icons.length).toBeGreaterThan(0);
|
|
55
|
+
for (const icon of manifest.icons) {
|
|
56
|
+
expect(typeof icon.src).toBe("string");
|
|
57
|
+
expect(icon.src.startsWith("/")).toBe(true);
|
|
58
|
+
expect(typeof icon.type).toBe("string");
|
|
59
|
+
expect(icon.type.length).toBeGreaterThan(0);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time constants, stamped into the npm package by `scripts/stamp-build-config.ts`
|
|
3
|
+
* (`prepack`) and reset to empty by `postpack`, so the repo never contains a
|
|
4
|
+
* deployment-specific value.
|
|
5
|
+
*
|
|
6
|
+
* Do not edit by hand: set `KCG_TUNNEL_API_URL` in `.env` for development, and
|
|
7
|
+
* it's baked in automatically when you run `bun publish` / `bun pm pack`.
|
|
8
|
+
*/
|
|
9
|
+
export const BUILD_TUNNEL_API_URL: string = "https://code.kcgapp.tech";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parseArgs } from "../args";
|
|
6
|
+
import { childArgv, daemonStatePath, isAlive, runningDaemon, writeDaemonState } from "../daemon";
|
|
7
|
+
|
|
8
|
+
describe("parseArgs daemon", () => {
|
|
9
|
+
test("start -d + command stop/status/logs", () => {
|
|
10
|
+
expect(parseArgs(["start", "-d"]).detach).toBe(true);
|
|
11
|
+
expect(parseArgs(["--detach", "-p", "4000"])).toMatchObject({
|
|
12
|
+
command: "start",
|
|
13
|
+
detach: true,
|
|
14
|
+
port: 4000,
|
|
15
|
+
});
|
|
16
|
+
expect(parseArgs(["stop"]).command).toBe("stop");
|
|
17
|
+
expect(parseArgs(["status"]).command).toBe("status");
|
|
18
|
+
expect(parseArgs(["logs", "-f", "-n", "20"])).toMatchObject({
|
|
19
|
+
command: "logs",
|
|
20
|
+
follow: true,
|
|
21
|
+
lines: 20,
|
|
22
|
+
});
|
|
23
|
+
expect(parseArgs(["logs", "-n", "abc"]).command).toBe("invalid");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("childArgv meneruskan opsi server, tanpa -d / --open", () => {
|
|
27
|
+
const args = parseArgs([
|
|
28
|
+
"start",
|
|
29
|
+
"-d",
|
|
30
|
+
"--open",
|
|
31
|
+
"-p",
|
|
32
|
+
"4000",
|
|
33
|
+
"-H",
|
|
34
|
+
"0.0.0.0",
|
|
35
|
+
"--allow-sleep",
|
|
36
|
+
]);
|
|
37
|
+
expect(childArgv(args)).toEqual([
|
|
38
|
+
"start",
|
|
39
|
+
"--port",
|
|
40
|
+
"4000",
|
|
41
|
+
"--host",
|
|
42
|
+
"0.0.0.0",
|
|
43
|
+
"--allow-sleep",
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("daemon state", () => {
|
|
49
|
+
let dir: string;
|
|
50
|
+
const prevDb = process.env.KCG_DB_PATH;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
dir = mkdtempSync(path.join(tmpdir(), "kcg-daemon-"));
|
|
54
|
+
process.env.KCG_DB_PATH = path.join(dir, "kcg.sqlite");
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
if (prevDb === undefined) delete process.env.KCG_DB_PATH;
|
|
58
|
+
else process.env.KCG_DB_PATH = prevDb;
|
|
59
|
+
rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const state = (pid: number) => ({
|
|
63
|
+
pid,
|
|
64
|
+
port: 3000,
|
|
65
|
+
hostname: "127.0.0.1",
|
|
66
|
+
localUrl: "http://127.0.0.1:3000",
|
|
67
|
+
publicUrl: null,
|
|
68
|
+
startedAt: Date.now(),
|
|
69
|
+
logFile: path.join(dir, "kcgcode.log"),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("state dengan pid hidup dikembalikan", async () => {
|
|
73
|
+
await writeDaemonState(state(process.pid));
|
|
74
|
+
expect(isAlive(process.pid)).toBe(true);
|
|
75
|
+
expect((await runningDaemon())?.pid).toBe(process.pid);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("state basi (pid mati) dibersihkan", async () => {
|
|
79
|
+
const dead = Bun.spawnSync(["true"]).pid;
|
|
80
|
+
await writeDaemonState(state(dead));
|
|
81
|
+
expect(await runningDaemon()).toBeNull();
|
|
82
|
+
expect(existsSync(daemonStatePath())).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|