khal-os 1.260324.2

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 (408) hide show
  1. package/.env.example +23 -0
  2. package/.genie/mailbox/cli-sent.jsonl +3 -0
  3. package/.genie/mailbox/ds1-wave2-engineer-1.json +15 -0
  4. package/.genie/mailbox/ds1-wave2-engineer-2.json +15 -0
  5. package/.genie/mailbox/ds1-wave2-engineer-3.json +15 -0
  6. package/.genie/state/os-observability.json +39 -0
  7. package/.genie/state/tmux-control-mode-terminal.json +28 -0
  8. package/.genie/wishes/genieos-one-theme/WISH.md +417 -0
  9. package/.genie/wishes/workos-prod-rbac/WISH.md +345 -0
  10. package/.github/workflows/ci.yml +39 -0
  11. package/.github/workflows/release.yml +78 -0
  12. package/.github/workflows/version.yml +122 -0
  13. package/.husky/pre-commit +1 -0
  14. package/.pnpm-approve-builds.json +1 -0
  15. package/CLAUDE.md +117 -0
  16. package/LICENSE +21 -0
  17. package/README.md +38 -0
  18. package/biome.json +124 -0
  19. package/bun.lock +1249 -0
  20. package/docs/workos-setup.md +116 -0
  21. package/ecosystem.config.cjs +26 -0
  22. package/instrumentation.ts +8 -0
  23. package/knip.json +35 -0
  24. package/nats.conf +7 -0
  25. package/next.config.ts +25 -0
  26. package/package.json +78 -0
  27. package/packages/dev3000-app/components.ts +12 -0
  28. package/packages/dev3000-app/manifest.ts +19 -0
  29. package/packages/dev3000-app/package.json +23 -0
  30. package/packages/dev3000-app/views/dev3000/Dev3000App.tsx +758 -0
  31. package/packages/dev3000-app/views/dev3000/ErrorsPanel.tsx +160 -0
  32. package/packages/dev3000-app/views/dev3000/dev3000-context.tsx +21 -0
  33. package/packages/dev3000-app/views/dev3000/index.ts +4 -0
  34. package/packages/dev3000-app/views/dev3000/schema.ts +55 -0
  35. package/packages/dev3000-app/views/dev3000/service/index.ts +358 -0
  36. package/packages/dev3000-app/views/dev3000/service/runtime +1 -0
  37. package/packages/dev3000-app/views/dev3000/subjects.ts +9 -0
  38. package/packages/dev3000-app/views/dev3000/types.ts +77 -0
  39. package/packages/files-app/components.ts +12 -0
  40. package/packages/files-app/manifest.ts +19 -0
  41. package/packages/files-app/package.json +23 -0
  42. package/packages/files-app/views/files/ContextMenu.tsx +151 -0
  43. package/packages/files-app/views/files/DeleteConfirmDialog.tsx +39 -0
  44. package/packages/files-app/views/files/FileItem.tsx +128 -0
  45. package/packages/files-app/views/files/FilesApp.tsx +509 -0
  46. package/packages/files-app/views/files/FilesListView.tsx +201 -0
  47. package/packages/files-app/views/files/FilesToolbar.tsx +117 -0
  48. package/packages/files-app/views/files/GridView.tsx +90 -0
  49. package/packages/files-app/views/files/InlineInput.tsx +131 -0
  50. package/packages/files-app/views/files/UploadOverlay.tsx +61 -0
  51. package/packages/files-app/views/files/schema.ts +49 -0
  52. package/packages/files-app/views/files/service/index.ts +184 -0
  53. package/packages/files-app/views/files/service/runtime +1 -0
  54. package/packages/files-app/views/files/use-files.ts +201 -0
  55. package/packages/files-app/views/files/use-upload.ts +105 -0
  56. package/packages/genie-app/components.ts +12 -0
  57. package/packages/genie-app/lib/subjects.ts +87 -0
  58. package/packages/genie-app/manifest.ts +19 -0
  59. package/packages/genie-app/package.json +29 -0
  60. package/packages/genie-app/views/genie/service/agent-lifecycle.ts +136 -0
  61. package/packages/genie-app/views/genie/service/cli.ts +114 -0
  62. package/packages/genie-app/views/genie/service/comms.ts +141 -0
  63. package/packages/genie-app/views/genie/service/directory.ts +167 -0
  64. package/packages/genie-app/views/genie/service/index.ts +219 -0
  65. package/packages/genie-app/views/genie/service/system.ts +123 -0
  66. package/packages/genie-app/views/genie/service/teams.ts +191 -0
  67. package/packages/genie-app/views/genie/service/terminal-proxy.ts +184 -0
  68. package/packages/genie-app/views/genie/service/tmux-control.ts +318 -0
  69. package/packages/genie-app/views/genie/service/wishes.ts +270 -0
  70. package/packages/genie-app/views/genie/ui/GenieApp.tsx +5 -0
  71. package/packages/genie-app/views/genie/ui/PaneCard.tsx +307 -0
  72. package/packages/genie-app/views/genie/ui/Sidebar.tsx +212 -0
  73. package/packages/genie-app/views/genie/ui/TabBar.tsx +70 -0
  74. package/packages/genie-app/views/genie/ui/WorkspaceCanvas.tsx +343 -0
  75. package/packages/genie-app/views/genie/ui/XTermPane.tsx +306 -0
  76. package/packages/genie-app/views/genie/ui/hooks/useNatsAction.ts +54 -0
  77. package/packages/genie-app/views/genie/ui/hooks/useNatsRequest.ts +68 -0
  78. package/packages/genie-app/views/genie/ui/panels/AgentsPanel.tsx +399 -0
  79. package/packages/genie-app/views/genie/ui/panels/ChatPanel.tsx +351 -0
  80. package/packages/genie-app/views/genie/ui/panels/SystemPanel.tsx +195 -0
  81. package/packages/genie-app/views/genie/ui/panels/TeamsPanel.tsx +560 -0
  82. package/packages/genie-app/views/genie/ui/panels/WishesPanel.tsx +424 -0
  83. package/packages/nats-viewer-app/components.ts +12 -0
  84. package/packages/nats-viewer-app/manifest.ts +18 -0
  85. package/packages/nats-viewer-app/package.json +14 -0
  86. package/packages/nats-viewer-app/views/nats-viewer/ActiveSubs.tsx +34 -0
  87. package/packages/nats-viewer-app/views/nats-viewer/MessageLog.tsx +247 -0
  88. package/packages/nats-viewer-app/views/nats-viewer/NatsViewer.tsx +209 -0
  89. package/packages/nats-viewer-app/views/nats-viewer/PublishPanel.tsx +111 -0
  90. package/packages/nats-viewer-app/views/nats-viewer/RequestPanel.tsx +165 -0
  91. package/packages/nats-viewer-app/views/nats-viewer/Sidebar.tsx +59 -0
  92. package/packages/nats-viewer-app/views/nats-viewer/SubjectCatalog.tsx +63 -0
  93. package/packages/nats-viewer-app/views/nats-viewer/SubscribeInput.tsx +59 -0
  94. package/packages/nats-viewer-app/views/nats-viewer/index.ts +5 -0
  95. package/packages/nats-viewer-app/views/nats-viewer/nats-viewer-context.tsx +31 -0
  96. package/packages/nats-viewer-app/views/nats-viewer/types.ts +7 -0
  97. package/packages/nats-viewer-app/views/nats-viewer/use-message-buffer.ts +55 -0
  98. package/packages/os-cli/package.json +18 -0
  99. package/packages/os-cli/src/commands/events.ts +176 -0
  100. package/packages/os-cli/src/commands/logs.ts +96 -0
  101. package/packages/os-cli/src/commands/status.ts +53 -0
  102. package/packages/os-cli/src/commands/traces.ts +115 -0
  103. package/packages/os-cli/src/index.ts +15 -0
  104. package/packages/os-cli/src/lib/formatter.ts +123 -0
  105. package/packages/os-cli/src/lib/nats.ts +16 -0
  106. package/packages/os-cli/src/lib/trace-tree.ts +144 -0
  107. package/packages/os-cli/tsconfig.json +12 -0
  108. package/packages/os-sdk/package.json +27 -0
  109. package/packages/os-sdk/src/api/handler.ts +67 -0
  110. package/packages/os-sdk/src/config.ts +68 -0
  111. package/packages/os-sdk/src/db/factory.test.ts +42 -0
  112. package/packages/os-sdk/src/db/factory.ts +72 -0
  113. package/packages/os-sdk/src/db/migrate.ts +140 -0
  114. package/packages/os-sdk/src/db/provision.ts +44 -0
  115. package/packages/os-sdk/src/index.ts +36 -0
  116. package/packages/os-sdk/src/service/console-intercept.ts +60 -0
  117. package/packages/os-sdk/src/service/logger.ts +88 -0
  118. package/packages/os-sdk/src/service/o11y-streams.ts +88 -0
  119. package/packages/os-sdk/src/service/runtime.ts +259 -0
  120. package/packages/os-sdk/src/service/trace.ts +71 -0
  121. package/packages/os-sdk/tsconfig.json +16 -0
  122. package/packages/os-ui/package.json +13 -0
  123. package/packages/os-ui/src/index.ts +29 -0
  124. package/packages/os-ui/src/server.ts +4 -0
  125. package/packages/os-ui/tsconfig.json +19 -0
  126. package/packages/settings-app/components.ts +12 -0
  127. package/packages/settings-app/manifest.ts +18 -0
  128. package/packages/settings-app/package.json +14 -0
  129. package/packages/settings-app/views/settings/Settings.tsx +492 -0
  130. package/packages/terminal-app/components.ts +12 -0
  131. package/packages/terminal-app/manifest.ts +20 -0
  132. package/packages/terminal-app/package.json +23 -0
  133. package/packages/terminal-app/views/terminal/schema.ts +82 -0
  134. package/packages/terminal-app/views/terminal/service/index.ts +133 -0
  135. package/packages/terminal-app/views/terminal/service/runtime +1 -0
  136. package/packages/terminal-app/views/terminal/service/session.ts +290 -0
  137. package/packages/terminal-app/views/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
  138. package/packages/terminal-app/views/terminal/types.ts +26 -0
  139. package/packages/terminal-app/views/terminal/ui/MultiTerminalApp.tsx +615 -0
  140. package/packages/terminal-app/views/terminal/ui/SplitDragHandle.tsx +91 -0
  141. package/packages/terminal-app/views/terminal/ui/SplitPaneRenderer.tsx +112 -0
  142. package/packages/terminal-app/views/terminal/ui/TerminalPane.tsx +478 -0
  143. package/packages/terminal-app/views/terminal/ui/TerminalTabBar.tsx +131 -0
  144. package/pnpm-workspace.yaml +9 -0
  145. package/postcss.config.mjs +7 -0
  146. package/public/file.svg +1 -0
  147. package/public/globe.svg +1 -0
  148. package/public/icons/code-server.svg +6 -0
  149. package/public/icons/default.svg +5 -0
  150. package/public/icons/dusk/1password.svg +1 -0
  151. package/public/icons/dusk/activity_monitor.svg +1 -0
  152. package/public/icons/dusk/app_store.svg +1 -0
  153. package/public/icons/dusk/atom.svg +1 -0
  154. package/public/icons/dusk/brave.svg +1 -0
  155. package/public/icons/dusk/calculator.svg +1 -0
  156. package/public/icons/dusk/calendar.svg +1 -0
  157. package/public/icons/dusk/chrome.svg +1 -0
  158. package/public/icons/dusk/chrome2.svg +1 -0
  159. package/public/icons/dusk/dashboard.svg +13 -0
  160. package/public/icons/dusk/discord.svg +1 -0
  161. package/public/icons/dusk/dropbox.svg +1 -0
  162. package/public/icons/dusk/electron.svg +1 -0
  163. package/public/icons/dusk/figma.svg +1 -0
  164. package/public/icons/dusk/finder.svg +1 -0
  165. package/public/icons/dusk/finder2.svg +1 -0
  166. package/public/icons/dusk/finder3.svg +1 -0
  167. package/public/icons/dusk/firefox.svg +1 -0
  168. package/public/icons/dusk/framer.svg +1 -0
  169. package/public/icons/dusk/gimp.svg +1 -0
  170. package/public/icons/dusk/github_desktop.svg +1 -0
  171. package/public/icons/dusk/hyper.svg +1 -0
  172. package/public/icons/dusk/hyper3.svg +1 -0
  173. package/public/icons/dusk/intellij.svg +1 -0
  174. package/public/icons/dusk/iterm2.svg +1 -0
  175. package/public/icons/dusk/itunes.svg +1 -0
  176. package/public/icons/dusk/mail.svg +1 -0
  177. package/public/icons/dusk/messenger.svg +1 -0
  178. package/public/icons/dusk/mongodb.svg +1 -0
  179. package/public/icons/dusk/notes.svg +1 -0
  180. package/public/icons/dusk/notion.svg +1 -0
  181. package/public/icons/dusk/obs.svg +1 -0
  182. package/public/icons/dusk/pages.svg +1 -0
  183. package/public/icons/dusk/photos.svg +1 -0
  184. package/public/icons/dusk/postman.svg +1 -0
  185. package/public/icons/dusk/preview.svg +1 -0
  186. package/public/icons/dusk/reminders.svg +1 -0
  187. package/public/icons/dusk/safari.svg +1 -0
  188. package/public/icons/dusk/sequel_pro.svg +1 -0
  189. package/public/icons/dusk/sketch.svg +1 -0
  190. package/public/icons/dusk/skype.svg +1 -0
  191. package/public/icons/dusk/slack.svg +1 -0
  192. package/public/icons/dusk/slack2.svg +1 -0
  193. package/public/icons/dusk/spotify.svg +1 -0
  194. package/public/icons/dusk/steam.svg +1 -0
  195. package/public/icons/dusk/system_preferences.svg +1 -0
  196. package/public/icons/dusk/tableplus.svg +1 -0
  197. package/public/icons/dusk/teams.svg +1 -0
  198. package/public/icons/dusk/telegram.svg +1 -0
  199. package/public/icons/dusk/terminal.svg +1 -0
  200. package/public/icons/dusk/todoist.svg +1 -0
  201. package/public/icons/dusk/trash.svg +1 -0
  202. package/public/icons/dusk/trello.svg +1 -0
  203. package/public/icons/dusk/vivaldi.svg +1 -0
  204. package/public/icons/dusk/vlc.svg +1 -0
  205. package/public/icons/dusk/vscode.svg +1 -0
  206. package/public/icons/dusk/whatsapp.svg +1 -0
  207. package/public/icons/dusk/xeyes.svg +1 -0
  208. package/public/icons/dusk/zoom.svg +1 -0
  209. package/public/icons/files.svg +5 -0
  210. package/public/icons/pwa/icon-192.png +0 -0
  211. package/public/icons/pwa/icon-512.png +0 -0
  212. package/public/icons/settings.svg +14 -0
  213. package/public/icons/terminal.svg +5 -0
  214. package/public/icons/text-editor.svg +7 -0
  215. package/public/manifest.json +38 -0
  216. package/public/next.svg +1 -0
  217. package/public/sw.js +41 -0
  218. package/public/vercel.svg +1 -0
  219. package/public/wallpapers/default.svg +10 -0
  220. package/public/window.svg +1 -0
  221. package/scripts/generate-pwa-icons.mjs +33 -0
  222. package/scripts/install-nats.sh +37 -0
  223. package/sentry.client.config.ts +21 -0
  224. package/sentry.edge.config.ts +12 -0
  225. package/sentry.server.config.ts +12 -0
  226. package/src/app/api/files/download/route.ts +81 -0
  227. package/src/app/api/files/download-zip/route.ts +102 -0
  228. package/src/app/api/files/upload/route.ts +58 -0
  229. package/src/app/api/webhooks/workos/route.ts +98 -0
  230. package/src/app/auth/callback/route.ts +16 -0
  231. package/src/app/auth/logout/route.ts +15 -0
  232. package/src/app/desktop/desktop-shell.tsx +110 -0
  233. package/src/app/desktop/layout.tsx +8 -0
  234. package/src/app/desktop/page.tsx +24 -0
  235. package/src/app/favicon.ico +0 -0
  236. package/src/app/globals.css +7 -0
  237. package/src/app/layout.tsx +64 -0
  238. package/src/app/offline/page.tsx +83 -0
  239. package/src/app/page.tsx +5 -0
  240. package/src/app/standalone/[appId]/page.tsx +28 -0
  241. package/src/app/standalone/layout.tsx +10 -0
  242. package/src/components/app-icon.tsx +55 -0
  243. package/src/components/apps/_echo/schema.ts +14 -0
  244. package/src/components/apps/_echo/service/index.ts +42 -0
  245. package/src/components/apps/app-manifest.ts +97 -0
  246. package/src/components/apps/app-registry.ts +55 -0
  247. package/src/components/apps/dev3000/Dev3000App.tsx +224 -0
  248. package/src/components/apps/dev3000/ErrorsPanel.tsx +160 -0
  249. package/src/components/apps/dev3000/Sidebar.tsx +41 -0
  250. package/src/components/apps/dev3000/TimelineLog.tsx +173 -0
  251. package/src/components/apps/dev3000/dev3000-context.tsx +29 -0
  252. package/src/components/apps/dev3000/index.ts +4 -0
  253. package/src/components/apps/dev3000/schema.ts +48 -0
  254. package/src/components/apps/dev3000/service/index.ts +520 -0
  255. package/src/components/apps/dev3000/service/runtime +1 -0
  256. package/src/components/apps/dev3000/types.ts +15 -0
  257. package/src/components/apps/dev3000/use-message-buffer.ts +46 -0
  258. package/src/components/apps/files/ContextMenu.tsx +151 -0
  259. package/src/components/apps/files/DeleteConfirmDialog.tsx +78 -0
  260. package/src/components/apps/files/FileItem.tsx +128 -0
  261. package/src/components/apps/files/FilesApp.tsx +509 -0
  262. package/src/components/apps/files/FilesListView.tsx +201 -0
  263. package/src/components/apps/files/FilesToolbar.tsx +117 -0
  264. package/src/components/apps/files/GridView.tsx +90 -0
  265. package/src/components/apps/files/InlineInput.tsx +131 -0
  266. package/src/components/apps/files/UploadOverlay.tsx +61 -0
  267. package/src/components/apps/files/schema.ts +49 -0
  268. package/src/components/apps/files/service/index.ts +227 -0
  269. package/src/components/apps/files/service/runtime +1 -0
  270. package/src/components/apps/files/use-files.ts +201 -0
  271. package/src/components/apps/files/use-upload.ts +105 -0
  272. package/src/components/apps/nats-viewer/ActiveSubs.tsx +34 -0
  273. package/src/components/apps/nats-viewer/MessageLog.tsx +247 -0
  274. package/src/components/apps/nats-viewer/NatsViewer.tsx +209 -0
  275. package/src/components/apps/nats-viewer/PublishPanel.tsx +113 -0
  276. package/src/components/apps/nats-viewer/RequestPanel.tsx +167 -0
  277. package/src/components/apps/nats-viewer/Sidebar.tsx +62 -0
  278. package/src/components/apps/nats-viewer/SubjectCatalog.tsx +64 -0
  279. package/src/components/apps/nats-viewer/SubscribeInput.tsx +59 -0
  280. package/src/components/apps/nats-viewer/index.ts +5 -0
  281. package/src/components/apps/nats-viewer/nats-viewer-context.tsx +31 -0
  282. package/src/components/apps/nats-viewer/types.ts +7 -0
  283. package/src/components/apps/nats-viewer/use-message-buffer.ts +55 -0
  284. package/src/components/apps/settings/Settings.tsx +492 -0
  285. package/src/components/apps/terminal/schema.ts +82 -0
  286. package/src/components/apps/terminal/service/index.ts +189 -0
  287. package/src/components/apps/terminal/service/runtime +1 -0
  288. package/src/components/apps/terminal/service/session.ts +296 -0
  289. package/src/components/apps/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
  290. package/src/components/apps/terminal/types.ts +26 -0
  291. package/src/components/apps/terminal/ui/MultiTerminalApp.tsx +617 -0
  292. package/src/components/apps/terminal/ui/SplitDragHandle.tsx +91 -0
  293. package/src/components/apps/terminal/ui/SplitPaneRenderer.tsx +112 -0
  294. package/src/components/apps/terminal/ui/TerminalPane.tsx +476 -0
  295. package/src/components/apps/terminal/ui/TerminalTabBar.tsx +131 -0
  296. package/src/components/desktop/AnimatedBackground.tsx +69 -0
  297. package/src/components/desktop/Desktop.tsx +79 -0
  298. package/src/components/desktop/DesktopBackground.tsx +16 -0
  299. package/src/components/desktop/DesktopIcon.tsx +49 -0
  300. package/src/components/desktop/ShortcutViewer.tsx +136 -0
  301. package/src/components/desktop/WindowRenderer.tsx +34 -0
  302. package/src/components/desktop/WindowSwitcher.tsx +42 -0
  303. package/src/components/notifications/NotificationCenter.tsx +153 -0
  304. package/src/components/notifications/NotificationToasts.tsx +66 -0
  305. package/src/components/notifications/OrphanSessionToast.tsx +293 -0
  306. package/src/components/os-primitives/collapsible-sidebar.tsx +226 -0
  307. package/src/components/os-primitives/dialog.tsx +76 -0
  308. package/src/components/os-primitives/empty-state.tsx +43 -0
  309. package/src/components/os-primitives/index.ts +21 -0
  310. package/src/components/os-primitives/list-view.tsx +155 -0
  311. package/src/components/os-primitives/property-panel.tsx +108 -0
  312. package/src/components/os-primitives/section-header.tsx +19 -0
  313. package/src/components/os-primitives/sidebar-nav.tsx +110 -0
  314. package/src/components/os-primitives/split-pane.tsx +146 -0
  315. package/src/components/os-primitives/status-badge.tsx +10 -0
  316. package/src/components/os-primitives/status-bar.tsx +100 -0
  317. package/src/components/os-primitives/toolbar.tsx +152 -0
  318. package/src/components/taskbar/AppLauncher.tsx +114 -0
  319. package/src/components/taskbar/RunningApps.tsx +71 -0
  320. package/src/components/taskbar/SystemTray.tsx +134 -0
  321. package/src/components/taskbar/Taskbar.tsx +45 -0
  322. package/src/components/taskbar/UserMenu.tsx +138 -0
  323. package/src/components/taskbar/WorkspaceSwitcher.tsx +9 -0
  324. package/src/components/ui/ContextMenu.tsx +130 -0
  325. package/src/components/ui/badge.tsx +39 -0
  326. package/src/components/ui/button.tsx +102 -0
  327. package/src/components/ui/command.tsx +165 -0
  328. package/src/components/ui/dropdown-menu.tsx +233 -0
  329. package/src/components/ui/input.tsx +48 -0
  330. package/src/components/ui/note.tsx +55 -0
  331. package/src/components/ui/separator.tsx +25 -0
  332. package/src/components/ui/spinner.tsx +42 -0
  333. package/src/components/ui/switch.tsx +36 -0
  334. package/src/components/ui/theme-provider.tsx +24 -0
  335. package/src/components/ui/theme-switcher.tsx +51 -0
  336. package/src/components/ui/tooltip.tsx +62 -0
  337. package/src/components/window/MobileWindowStack.tsx +218 -0
  338. package/src/components/window/SnapPreview.tsx +37 -0
  339. package/src/components/window/StandaloneFrame.tsx +170 -0
  340. package/src/components/window/Window.tsx +423 -0
  341. package/src/components/window/WindowContent.tsx +14 -0
  342. package/src/components/window/WindowControlsOverlay.tsx +89 -0
  343. package/src/components/window/WindowFrame.tsx +124 -0
  344. package/src/lib/auth/index.ts +27 -0
  345. package/src/lib/auth/roles.ts +50 -0
  346. package/src/lib/auth/types.ts +32 -0
  347. package/src/lib/auth/use-auth.ts +53 -0
  348. package/src/lib/auth/webhook-handler.ts +87 -0
  349. package/src/lib/auth/workos.ts +67 -0
  350. package/src/lib/constants.ts +1 -0
  351. package/src/lib/desktop/dedup.ts +57 -0
  352. package/src/lib/desktop/schema.ts +55 -0
  353. package/src/lib/files/filename-validation.ts +41 -0
  354. package/src/lib/files/safe-path.ts +49 -0
  355. package/src/lib/hooks/use-desktop-nats.ts +438 -0
  356. package/src/lib/hooks/use-is-mobile.ts +23 -0
  357. package/src/lib/hooks/use-launch-app.ts +79 -0
  358. package/src/lib/hooks/use-nats-notifications.ts +84 -0
  359. package/src/lib/hooks/use-nats.ts +60 -0
  360. package/src/lib/hooks/use-visual-viewport.ts +72 -0
  361. package/src/lib/icons/resolve-window-icon.ts +10 -0
  362. package/src/lib/keyboard/defaults.ts +146 -0
  363. package/src/lib/keyboard/types.ts +52 -0
  364. package/src/lib/keyboard/use-global-keybinds.ts +231 -0
  365. package/src/lib/nats-client.ts +255 -0
  366. package/src/lib/nats.ts +35 -0
  367. package/src/lib/notifications/schema.ts +12 -0
  368. package/src/lib/service-loader.ts +171 -0
  369. package/src/lib/subjects.ts +64 -0
  370. package/src/lib/utils.ts +6 -0
  371. package/src/lib/ws-bridge.ts +288 -0
  372. package/src/lib/ws-protocol.ts +53 -0
  373. package/src/lib/ws-server.ts +167 -0
  374. package/src/middleware.ts +57 -0
  375. package/src/stores/desktop-store.ts +112 -0
  376. package/src/stores/keybind-store.ts +66 -0
  377. package/src/stores/notification-store.ts +271 -0
  378. package/src/stores/theme-store.ts +25 -0
  379. package/src/stores/window-store.ts +294 -0
  380. package/src/theme/animations.css +68 -0
  381. package/src/theme/base.css +123 -0
  382. package/src/theme/controls.css +35 -0
  383. package/src/theme/design-tokens.css +276 -0
  384. package/src/theme/index.css +23 -0
  385. package/src/theme/menus.css +45 -0
  386. package/src/theme/status.css +41 -0
  387. package/src/theme/surfaces.css +94 -0
  388. package/src/theme/tailwind-map.css +138 -0
  389. package/src/theme/taskbar.css +25 -0
  390. package/src/theme/terminal.css +55 -0
  391. package/src/theme/typography.css +26 -0
  392. package/src/theme/utilities.css +156 -0
  393. package/src/theme/window.css +103 -0
  394. package/src/types/desktop-entry.ts +12 -0
  395. package/src/types/use-descendants.d.ts +13 -0
  396. package/src/types/window.ts +28 -0
  397. package/src/types.d.ts +9 -0
  398. package/tauri/Cargo.lock +5464 -0
  399. package/tauri/Cargo.toml +19 -0
  400. package/tauri/build.rs +3 -0
  401. package/tauri/capabilities/default.json +36 -0
  402. package/tauri/icons/128x128.png +0 -0
  403. package/tauri/icons/128x128@2x.png +0 -0
  404. package/tauri/icons/32x32.png +0 -0
  405. package/tauri/icons/icon.png +0 -0
  406. package/tauri/src/main.rs +396 -0
  407. package/tauri/tauri.conf.json +23 -0
  408. package/tsconfig.json +43 -0
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+
3
+ import { ChevronLeft, ChevronRight, FolderPlus, Grid, List, RefreshCw, Upload } from 'lucide-react';
4
+ import { useCallback } from 'react';
5
+ import { Toolbar } from '@/components/os-primitives/toolbar';
6
+
7
+ export type ViewMode = 'grid' | 'list';
8
+
9
+ interface FilesToolbarProps {
10
+ currentPath: string;
11
+ canGoBack: boolean;
12
+ viewMode: ViewMode;
13
+ loading: boolean;
14
+ onGoBack: () => void;
15
+ onGoUp: () => void;
16
+ onNavigateTo: (path: string) => void;
17
+ onViewModeChange: (mode: ViewMode) => void;
18
+ onNewFolder: () => void;
19
+ onUpload: () => void;
20
+ onRefresh: () => void;
21
+ }
22
+
23
+ export function FilesToolbar({
24
+ currentPath,
25
+ canGoBack,
26
+ viewMode,
27
+ loading,
28
+ onGoBack,
29
+ onGoUp,
30
+ onNavigateTo,
31
+ onViewModeChange,
32
+ onNewFolder,
33
+ onUpload,
34
+ onRefresh,
35
+ }: FilesToolbarProps) {
36
+ const segments = currentPath.split('/').filter(Boolean);
37
+
38
+ const handleBreadcrumbClick = useCallback(
39
+ (index: number) => {
40
+ const path = `/${segments.slice(0, index + 1).join('/')}`;
41
+ onNavigateTo(path);
42
+ },
43
+ [segments, onNavigateTo]
44
+ );
45
+
46
+ const handleRootClick = useCallback(() => {
47
+ onNavigateTo('/');
48
+ }, [onNavigateTo]);
49
+
50
+ return (
51
+ <Toolbar>
52
+ <Toolbar.Group>
53
+ <Toolbar.Button tooltip="Back" onClick={onGoBack} disabled={!canGoBack}>
54
+ <ChevronLeft />
55
+ </Toolbar.Button>
56
+ <Toolbar.Button tooltip="Up" onClick={onGoUp} disabled={currentPath === '/'}>
57
+ <ChevronRight />
58
+ </Toolbar.Button>
59
+ </Toolbar.Group>
60
+
61
+ <Toolbar.Separator />
62
+
63
+ {/* Breadcrumb */}
64
+ <div className="flex items-center gap-0.5 overflow-hidden text-label-13">
65
+ <button
66
+ type="button"
67
+ onClick={handleRootClick}
68
+ className="shrink-0 rounded-sm px-1 py-0.5 text-gray-900 transition-colors hover:bg-gray-alpha-200 hover:text-gray-1000"
69
+ >
70
+ ~
71
+ </button>
72
+ {segments.map((segment, i) => (
73
+ <span key={`/${segments.slice(0, i + 1).join('/')}`} className="flex items-center gap-0.5">
74
+ <span className="text-gray-600">/</span>
75
+ <button
76
+ type="button"
77
+ onClick={() => handleBreadcrumbClick(i)}
78
+ className={`truncate rounded-sm px-1 py-0.5 transition-colors hover:bg-gray-alpha-200 ${
79
+ i === segments.length - 1 ? 'font-medium text-gray-1000' : 'text-gray-900 hover:text-gray-1000'
80
+ }`}
81
+ >
82
+ {segment}
83
+ </button>
84
+ </span>
85
+ ))}
86
+ </div>
87
+
88
+ <Toolbar.Spacer />
89
+
90
+ <Toolbar.Group>
91
+ <Toolbar.Button tooltip="Grid view" active={viewMode === 'grid'} onClick={() => onViewModeChange('grid')}>
92
+ <Grid />
93
+ </Toolbar.Button>
94
+ <Toolbar.Button tooltip="List view" active={viewMode === 'list'} onClick={() => onViewModeChange('list')}>
95
+ <List />
96
+ </Toolbar.Button>
97
+ </Toolbar.Group>
98
+
99
+ <Toolbar.Separator />
100
+
101
+ <Toolbar.Group>
102
+ <Toolbar.Button tooltip="New Folder" onClick={onNewFolder}>
103
+ <FolderPlus />
104
+ </Toolbar.Button>
105
+ <Toolbar.Button tooltip="Upload" onClick={onUpload}>
106
+ <Upload />
107
+ </Toolbar.Button>
108
+ </Toolbar.Group>
109
+
110
+ <Toolbar.Separator />
111
+
112
+ <Toolbar.Button tooltip="Refresh" onClick={onRefresh} active={loading}>
113
+ <RefreshCw />
114
+ </Toolbar.Button>
115
+ </Toolbar>
116
+ );
117
+ }
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import { Folder } from 'lucide-react';
4
+ import { useCallback } from 'react';
5
+ import { formatSize, getFileIcon } from './FileItem';
6
+ import { InlineRenameInput, NewFolderInput } from './InlineInput';
7
+ import type { FsEntry } from './schema';
8
+
9
+ interface GridViewProps {
10
+ entries: FsEntry[];
11
+ selectedNames: Set<string>;
12
+ onSelect: (name: string, e: React.MouseEvent) => void;
13
+ onClearSelection: () => void;
14
+ onNavigate: (entry: FsEntry) => void;
15
+ onContextMenu: (e: React.MouseEvent, entry: FsEntry) => void;
16
+ renamingName: string | null;
17
+ onRenameSubmit: (oldName: string, newName: string) => void;
18
+ creatingFolder: boolean;
19
+ onNewFolderSubmit: (name: string) => void;
20
+ }
21
+
22
+ export function GridView({
23
+ entries,
24
+ selectedNames,
25
+ onSelect,
26
+ onClearSelection,
27
+ onNavigate,
28
+ onContextMenu,
29
+ renamingName,
30
+ onRenameSubmit,
31
+ creatingFolder,
32
+ onNewFolderSubmit,
33
+ }: GridViewProps) {
34
+ const handleDoubleClick = useCallback(
35
+ (entry: FsEntry) => {
36
+ if (entry.isDir) {
37
+ onNavigate(entry);
38
+ }
39
+ },
40
+ [onNavigate]
41
+ );
42
+
43
+ return (
44
+ <div className="h-full overflow-y-auto p-3" onClick={onClearSelection} onKeyDown={undefined} role="presentation">
45
+ <div
46
+ className="grid auto-rows-auto gap-1"
47
+ style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))' }}
48
+ >
49
+ {creatingFolder && (
50
+ <div className="flex cursor-default flex-col items-center gap-1 rounded-lg bg-blue-700/15 p-2">
51
+ <Folder className="h-8 w-8 text-blue-700" strokeWidth={1.5} />
52
+ <NewFolderInput onSubmit={onNewFolderSubmit} className="text-center" />
53
+ </div>
54
+ )}
55
+ {entries.map((entry) => {
56
+ const Icon = getFileIcon(entry);
57
+ const isSelected = selectedNames.has(entry.name);
58
+ const isRenaming = renamingName === entry.name;
59
+
60
+ return (
61
+ <button
62
+ type="button"
63
+ key={entry.name}
64
+ className={`flex cursor-default flex-col items-center gap-1 rounded-lg p-2 transition-colors ${
65
+ isSelected ? 'bg-blue-700/15 text-gray-1000' : 'text-gray-1000 hover:bg-gray-alpha-100'
66
+ }`}
67
+ onClick={(e) => {
68
+ e.stopPropagation();
69
+ onSelect(entry.name, e);
70
+ }}
71
+ onDoubleClick={() => handleDoubleClick(entry)}
72
+ onContextMenu={(e) => {
73
+ e.stopPropagation();
74
+ onContextMenu(e, entry);
75
+ }}
76
+ >
77
+ <Icon className={`h-8 w-8 ${entry.isDir ? 'text-blue-700' : 'text-gray-700'}`} strokeWidth={1.5} />
78
+ {isRenaming ? (
79
+ <InlineRenameInput name={entry.name} onSubmit={onRenameSubmit} className="text-center" />
80
+ ) : (
81
+ <span className="w-full truncate text-center text-label-13">{entry.name}</span>
82
+ )}
83
+ {!entry.isDir && <span className="text-[11px] text-gray-700">{formatSize(entry.size)}</span>}
84
+ </button>
85
+ );
86
+ })}
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+
5
+ interface InlineRenameInputProps {
6
+ name: string;
7
+ onSubmit: (oldName: string, newName: string) => void;
8
+ className?: string;
9
+ }
10
+
11
+ /**
12
+ * Inline rename input. Enter confirms, Escape and blur cancel (revert to original name).
13
+ */
14
+ export function InlineRenameInput({ name, onSubmit, className }: InlineRenameInputProps) {
15
+ const [value, setValue] = useState(name);
16
+ const inputRef = useRef<HTMLInputElement>(null);
17
+ const confirmedRef = useRef(false);
18
+
19
+ useEffect(() => {
20
+ const input = inputRef.current;
21
+ if (input) {
22
+ input.focus();
23
+ const dotIndex = name.lastIndexOf('.');
24
+ if (dotIndex > 0) {
25
+ input.setSelectionRange(0, dotIndex);
26
+ } else {
27
+ input.select();
28
+ }
29
+ }
30
+ }, [name]);
31
+
32
+ const handleConfirm = useCallback(() => {
33
+ if (confirmedRef.current) return;
34
+ confirmedRef.current = true;
35
+ onSubmit(name, value);
36
+ }, [name, value, onSubmit]);
37
+
38
+ const handleCancel = useCallback(() => {
39
+ if (confirmedRef.current) return;
40
+ confirmedRef.current = true;
41
+ onSubmit(name, name);
42
+ }, [name, onSubmit]);
43
+
44
+ const handleKeyDown = useCallback(
45
+ (e: React.KeyboardEvent) => {
46
+ if (e.key === 'Enter') {
47
+ e.preventDefault();
48
+ handleConfirm();
49
+ } else if (e.key === 'Escape') {
50
+ e.preventDefault();
51
+ handleCancel();
52
+ }
53
+ },
54
+ [handleConfirm, handleCancel]
55
+ );
56
+
57
+ return (
58
+ <input
59
+ ref={inputRef}
60
+ type="text"
61
+ value={value}
62
+ onChange={(e) => setValue(e.target.value)}
63
+ onBlur={handleCancel}
64
+ onKeyDown={handleKeyDown}
65
+ className={`w-full rounded border border-blue-500 bg-transparent px-1 text-label-13 outline-none ${className ?? ''}`}
66
+ style={{ color: 'var(--khal-text-primary)' }}
67
+ onClick={(e) => e.stopPropagation()}
68
+ />
69
+ );
70
+ }
71
+
72
+ interface NewFolderInputProps {
73
+ onSubmit: (name: string) => void;
74
+ className?: string;
75
+ }
76
+
77
+ /**
78
+ * Inline new-folder name input. Enter confirms, Escape and blur cancel.
79
+ */
80
+ export function NewFolderInput({ onSubmit, className }: NewFolderInputProps) {
81
+ const [value, setValue] = useState('New Folder');
82
+ const inputRef = useRef<HTMLInputElement>(null);
83
+ const confirmedRef = useRef(false);
84
+
85
+ useEffect(() => {
86
+ const input = inputRef.current;
87
+ if (input) {
88
+ input.focus();
89
+ input.select();
90
+ }
91
+ }, []);
92
+
93
+ const handleConfirm = useCallback(() => {
94
+ if (confirmedRef.current) return;
95
+ confirmedRef.current = true;
96
+ onSubmit(value);
97
+ }, [value, onSubmit]);
98
+
99
+ const handleCancel = useCallback(() => {
100
+ if (confirmedRef.current) return;
101
+ confirmedRef.current = true;
102
+ onSubmit('');
103
+ }, [onSubmit]);
104
+
105
+ const handleKeyDown = useCallback(
106
+ (e: React.KeyboardEvent) => {
107
+ if (e.key === 'Enter') {
108
+ e.preventDefault();
109
+ handleConfirm();
110
+ } else if (e.key === 'Escape') {
111
+ e.preventDefault();
112
+ handleCancel();
113
+ }
114
+ },
115
+ [handleConfirm, handleCancel]
116
+ );
117
+
118
+ return (
119
+ <input
120
+ ref={inputRef}
121
+ type="text"
122
+ value={value}
123
+ onChange={(e) => setValue(e.target.value)}
124
+ onBlur={handleCancel}
125
+ onKeyDown={handleKeyDown}
126
+ className={`w-full rounded border border-blue-500 bg-transparent px-1 text-label-13 outline-none ${className ?? ''}`}
127
+ style={{ color: 'var(--khal-text-primary)' }}
128
+ onClick={(e) => e.stopPropagation()}
129
+ />
130
+ );
131
+ }
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import { Upload } from 'lucide-react';
4
+ import type { UploadState } from './use-upload';
5
+
6
+ interface UploadOverlayProps {
7
+ /** True when the user is dragging files over the drop zone */
8
+ isDragging: boolean;
9
+ /** Current upload state from useUpload */
10
+ uploadState: UploadState;
11
+ }
12
+
13
+ export function UploadOverlay({ isDragging, uploadState }: UploadOverlayProps) {
14
+ if (!isDragging && !uploadState.uploading && !uploadState.error) {
15
+ return null;
16
+ }
17
+
18
+ // Drag overlay — shown while files hover over the drop zone
19
+ if (isDragging) {
20
+ return (
21
+ <div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center rounded-md border-2 border-dashed border-blue-700 bg-blue-700/10">
22
+ <div className="flex flex-col items-center gap-2 text-blue-700">
23
+ <Upload className="h-8 w-8" />
24
+ <span className="text-label-14 font-medium">Drop files to upload</span>
25
+ </div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ // Upload progress
31
+ if (uploadState.uploading) {
32
+ return (
33
+ <div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center bg-background-100/80">
34
+ <div className="flex w-64 flex-col items-center gap-3 rounded-lg border border-gray-alpha-200 bg-background-100 p-6 shadow-lg">
35
+ <Upload className="h-6 w-6 text-blue-700" />
36
+ <span className="truncate text-label-13 text-gray-1000">{uploadState.fileName}</span>
37
+ <div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-alpha-200">
38
+ <div
39
+ className="h-full rounded-full bg-blue-700 transition-all duration-200"
40
+ style={{ width: `${uploadState.progress}%` }}
41
+ />
42
+ </div>
43
+ <span className="text-label-13 text-gray-700">{uploadState.progress}%</span>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ // Error state
50
+ if (uploadState.error) {
51
+ return (
52
+ <div className="pointer-events-none absolute bottom-3 left-3 right-3 z-30">
53
+ <div className="rounded-md border border-red-400/30 bg-red-50 px-3 py-2 text-label-13 text-red-700 dark:bg-red-900/20 dark:text-red-400">
54
+ Upload failed: {uploadState.error}
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ return null;
61
+ }
@@ -0,0 +1,49 @@
1
+ import { type Static, Type } from '@sinclair/typebox';
2
+
3
+ // Note: userId is NOT included in request schemas.
4
+ // The WS bridge injects `_authUserId` from the authenticated session
5
+ // into all outbound NATS messages. Services read that field instead.
6
+
7
+ // fs.list
8
+ export const FsListRequest = Type.Object({
9
+ path: Type.String(),
10
+ });
11
+ export type FsListRequest = Static<typeof FsListRequest>;
12
+
13
+ export const FsEntry = Type.Object({
14
+ name: Type.String(),
15
+ size: Type.Number(),
16
+ mtime: Type.Number(), // epoch ms
17
+ isDir: Type.Boolean(),
18
+ });
19
+ export type FsEntry = Static<typeof FsEntry>;
20
+
21
+ export const FsListResponse = Type.Object({
22
+ entries: Type.Array(FsEntry),
23
+ path: Type.String(), // resolved relative path
24
+ root: Type.String(), // absolute filesystem root
25
+ });
26
+ export type FsListResponse = Static<typeof FsListResponse>;
27
+
28
+ // fs.write — multiplexed operations
29
+ export const FsWriteRequest = Type.Union([
30
+ Type.Object({ op: Type.Literal('mkdir'), path: Type.String() }),
31
+ Type.Object({ op: Type.Literal('rename'), path: Type.String(), newName: Type.String() }),
32
+ Type.Object({ op: Type.Literal('move'), path: Type.String(), dest: Type.String() }),
33
+ Type.Object({ op: Type.Literal('delete'), path: Type.String() }),
34
+ ]);
35
+ export type FsWriteRequest = Static<typeof FsWriteRequest>;
36
+
37
+ export const FsWriteResponse = Type.Object({
38
+ ok: Type.Boolean(),
39
+ error: Type.Optional(Type.String()),
40
+ });
41
+ export type FsWriteResponse = Static<typeof FsWriteResponse>;
42
+
43
+ // fs.watch event
44
+ export const FsWatchEvent = Type.Object({
45
+ type: Type.Union([Type.Literal('create'), Type.Literal('delete'), Type.Literal('rename'), Type.Literal('change')]),
46
+ path: Type.String(),
47
+ name: Type.String(),
48
+ });
49
+ export type FsWatchEvent = Static<typeof FsWatchEvent>;
@@ -0,0 +1,227 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { connect } from '@nats-io/transport-node';
5
+ import { getFilesRoot, resolveSafePath, validateFilename } from '@/lib/files/safe-path';
6
+ import type { FsListRequest, FsWatchEvent, FsWriteRequest } from '../schema';
7
+
8
+ const MAX_ENTRIES = 500;
9
+
10
+ const ROOT = path.resolve(getFilesRoot());
11
+
12
+ /**
13
+ * Compute a short hash for a directory path, used for watch subject routing.
14
+ */
15
+ function pathHash(dirPath: string): string {
16
+ return crypto.createHash('sha256').update(dirPath).digest('hex').slice(0, 12);
17
+ }
18
+
19
+ /**
20
+ * Get the relative path from root, always starting with '/'.
21
+ */
22
+ function relativePath(root: string, absPath: string): string {
23
+ const rel = path.relative(root, absPath);
24
+ if (rel === '') return '/';
25
+ return `/${rel}`;
26
+ }
27
+
28
+ type NatsConnection = Awaited<ReturnType<typeof connect>>;
29
+
30
+ function handleWriteOp(nc: NatsConnection, orgId: string, request: FsWriteRequest): { ok: boolean; error?: string } {
31
+ switch (request.op) {
32
+ case 'mkdir': {
33
+ const safePath = resolveSafePath(ROOT, request.path);
34
+ fs.mkdirSync(safePath, { recursive: true });
35
+ console.log(`[fs-service] mkdir: ${safePath}`);
36
+
37
+ const parentDir = path.dirname(safePath);
38
+ publishWatchEvent(nc, orgId, parentDir, {
39
+ type: 'create',
40
+ path: relativePath(ROOT, safePath),
41
+ name: path.basename(safePath),
42
+ });
43
+ return { ok: true };
44
+ }
45
+
46
+ case 'rename': {
47
+ const nameError = validateFilename(request.newName);
48
+ if (nameError) return { ok: false, error: nameError };
49
+
50
+ const safePath = resolveSafePath(ROOT, request.path);
51
+ const parentDir = path.dirname(safePath);
52
+ const newPath = resolveSafePath(ROOT, path.join(relativePath(ROOT, parentDir), request.newName));
53
+
54
+ fs.renameSync(safePath, newPath);
55
+ console.log(`[fs-service] rename: ${safePath} -> ${newPath}`);
56
+
57
+ publishWatchEvent(nc, orgId, parentDir, {
58
+ type: 'rename',
59
+ path: relativePath(ROOT, newPath),
60
+ name: request.newName,
61
+ });
62
+ return { ok: true };
63
+ }
64
+
65
+ case 'move': {
66
+ const safeSrc = resolveSafePath(ROOT, request.path);
67
+ const safeDest = resolveSafePath(ROOT, request.dest);
68
+ const targetPath = path.join(safeDest, path.basename(safeSrc));
69
+ resolveSafePath(ROOT, relativePath(ROOT, targetPath));
70
+
71
+ fs.renameSync(safeSrc, targetPath);
72
+ console.log(`[fs-service] move: ${safeSrc} -> ${targetPath}`);
73
+
74
+ const srcParent = path.dirname(safeSrc);
75
+ publishWatchEvent(nc, orgId, srcParent, {
76
+ type: 'delete',
77
+ path: relativePath(ROOT, safeSrc),
78
+ name: path.basename(safeSrc),
79
+ });
80
+ publishWatchEvent(nc, orgId, safeDest, {
81
+ type: 'create',
82
+ path: relativePath(ROOT, targetPath),
83
+ name: path.basename(safeSrc),
84
+ });
85
+ return { ok: true };
86
+ }
87
+
88
+ case 'delete': {
89
+ const safePath = resolveSafePath(ROOT, request.path);
90
+ const parentDir = path.dirname(safePath);
91
+ const entryName = path.basename(safePath);
92
+
93
+ const stat = fs.statSync(safePath);
94
+ if (stat.isDirectory()) {
95
+ fs.rmSync(safePath, { recursive: true, force: true });
96
+ } else {
97
+ fs.unlinkSync(safePath);
98
+ }
99
+ console.log(`[fs-service] delete: ${safePath}`);
100
+
101
+ publishWatchEvent(nc, orgId, parentDir, {
102
+ type: 'delete',
103
+ path: relativePath(ROOT, safePath),
104
+ name: entryName,
105
+ });
106
+ return { ok: true };
107
+ }
108
+
109
+ default:
110
+ return { ok: false, error: 'Unknown operation' };
111
+ }
112
+ }
113
+
114
+ async function main() {
115
+ // Ensure root directory exists
116
+ fs.mkdirSync(ROOT, { recursive: true });
117
+ console.log(`[fs-service] root directory: ${ROOT}`);
118
+
119
+ const nc = await connect({
120
+ servers: process.env.NATS_URL || 'nats://localhost:4222',
121
+ });
122
+
123
+ console.log('[fs-service] connected to NATS, subscribing to khal.*.fs.*');
124
+
125
+ // --- khal.*.fs.list (request-reply) ---
126
+ const listSub = nc.subscribe('khal.*.fs.list');
127
+ (async () => {
128
+ for await (const msg of listSub) {
129
+ try {
130
+ const request = msg.json<FsListRequest & { _authUserId?: string }>();
131
+ const safePath = resolveSafePath(ROOT, request.path);
132
+
133
+ const stat = fs.statSync(safePath);
134
+ if (!stat.isDirectory()) {
135
+ msg.respond(JSON.stringify({ error: 'Not a directory' }));
136
+ continue;
137
+ }
138
+
139
+ const dirents = fs.readdirSync(safePath, { withFileTypes: true });
140
+ const entries = dirents.slice(0, MAX_ENTRIES).map((dirent) => {
141
+ const fullPath = path.join(safePath, dirent.name);
142
+ try {
143
+ const entryStat = fs.statSync(fullPath);
144
+ return {
145
+ name: dirent.name,
146
+ size: entryStat.size,
147
+ mtime: entryStat.mtimeMs,
148
+ isDir: dirent.isDirectory(),
149
+ };
150
+ } catch {
151
+ // Entry may have been deleted between readdir and stat
152
+ return {
153
+ name: dirent.name,
154
+ size: 0,
155
+ mtime: 0,
156
+ isDir: dirent.isDirectory(),
157
+ };
158
+ }
159
+ });
160
+
161
+ msg.respond(
162
+ JSON.stringify({
163
+ entries,
164
+ path: relativePath(ROOT, safePath),
165
+ root: ROOT,
166
+ })
167
+ );
168
+ } catch (err) {
169
+ console.error('[fs-service] list error:', err);
170
+ msg.respond(JSON.stringify({ error: (err as Error).message }));
171
+ }
172
+ }
173
+ })();
174
+
175
+ // --- khal.*.fs.write (request-reply) ---
176
+ const writeSub = nc.subscribe('khal.*.fs.write');
177
+ (async () => {
178
+ for await (const msg of writeSub) {
179
+ try {
180
+ const orgId = msg.subject.split('.')[1];
181
+ const request = msg.json<FsWriteRequest & { _authUserId?: string }>();
182
+ const result = handleWriteOp(nc, orgId, request);
183
+ msg.respond(JSON.stringify(result));
184
+ } catch (err) {
185
+ console.error('[fs-service] write error:', err);
186
+ msg.respond(JSON.stringify({ ok: false, error: (err as Error).message }));
187
+ }
188
+ }
189
+ })();
190
+
191
+ // --- Graceful shutdown ---
192
+ const shutdown = async () => {
193
+ console.log('[fs-service] shutting down...');
194
+
195
+ listSub.unsubscribe();
196
+ writeSub.unsubscribe();
197
+
198
+ await nc.close();
199
+ process.exit(0);
200
+ };
201
+
202
+ process.on('SIGINT', shutdown);
203
+ process.on('SIGTERM', shutdown);
204
+
205
+ // Keep alive
206
+ await nc.closed();
207
+ }
208
+
209
+ /**
210
+ * Publish a watch event to the NATS subject for the given directory.
211
+ */
212
+ function publishWatchEvent(
213
+ nc: Awaited<ReturnType<typeof connect>>,
214
+ orgId: string,
215
+ dirAbsPath: string,
216
+ event: FsWatchEvent
217
+ ) {
218
+ const hash = pathHash(dirAbsPath);
219
+ const subject = `khal.${orgId}.fs.watch.${hash}`;
220
+ nc.publish(subject, JSON.stringify(event));
221
+ console.log(`[fs-service] watch event -> ${subject}:`, event);
222
+ }
223
+
224
+ main().catch((err) => {
225
+ console.error('[fs-service] fatal:', err);
226
+ process.exit(1);
227
+ });
@@ -0,0 +1 @@
1
+ node