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.
- package/.env.example +23 -0
- package/.genie/mailbox/cli-sent.jsonl +3 -0
- package/.genie/mailbox/ds1-wave2-engineer-1.json +15 -0
- package/.genie/mailbox/ds1-wave2-engineer-2.json +15 -0
- package/.genie/mailbox/ds1-wave2-engineer-3.json +15 -0
- package/.genie/state/os-observability.json +39 -0
- package/.genie/state/tmux-control-mode-terminal.json +28 -0
- package/.genie/wishes/genieos-one-theme/WISH.md +417 -0
- package/.genie/wishes/workos-prod-rbac/WISH.md +345 -0
- package/.github/workflows/ci.yml +39 -0
- package/.github/workflows/release.yml +78 -0
- package/.github/workflows/version.yml +122 -0
- package/.husky/pre-commit +1 -0
- package/.pnpm-approve-builds.json +1 -0
- package/CLAUDE.md +117 -0
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/biome.json +124 -0
- package/bun.lock +1249 -0
- package/docs/workos-setup.md +116 -0
- package/ecosystem.config.cjs +26 -0
- package/instrumentation.ts +8 -0
- package/knip.json +35 -0
- package/nats.conf +7 -0
- package/next.config.ts +25 -0
- package/package.json +78 -0
- package/packages/dev3000-app/components.ts +12 -0
- package/packages/dev3000-app/manifest.ts +19 -0
- package/packages/dev3000-app/package.json +23 -0
- package/packages/dev3000-app/views/dev3000/Dev3000App.tsx +758 -0
- package/packages/dev3000-app/views/dev3000/ErrorsPanel.tsx +160 -0
- package/packages/dev3000-app/views/dev3000/dev3000-context.tsx +21 -0
- package/packages/dev3000-app/views/dev3000/index.ts +4 -0
- package/packages/dev3000-app/views/dev3000/schema.ts +55 -0
- package/packages/dev3000-app/views/dev3000/service/index.ts +358 -0
- package/packages/dev3000-app/views/dev3000/service/runtime +1 -0
- package/packages/dev3000-app/views/dev3000/subjects.ts +9 -0
- package/packages/dev3000-app/views/dev3000/types.ts +77 -0
- package/packages/files-app/components.ts +12 -0
- package/packages/files-app/manifest.ts +19 -0
- package/packages/files-app/package.json +23 -0
- package/packages/files-app/views/files/ContextMenu.tsx +151 -0
- package/packages/files-app/views/files/DeleteConfirmDialog.tsx +39 -0
- package/packages/files-app/views/files/FileItem.tsx +128 -0
- package/packages/files-app/views/files/FilesApp.tsx +509 -0
- package/packages/files-app/views/files/FilesListView.tsx +201 -0
- package/packages/files-app/views/files/FilesToolbar.tsx +117 -0
- package/packages/files-app/views/files/GridView.tsx +90 -0
- package/packages/files-app/views/files/InlineInput.tsx +131 -0
- package/packages/files-app/views/files/UploadOverlay.tsx +61 -0
- package/packages/files-app/views/files/schema.ts +49 -0
- package/packages/files-app/views/files/service/index.ts +184 -0
- package/packages/files-app/views/files/service/runtime +1 -0
- package/packages/files-app/views/files/use-files.ts +201 -0
- package/packages/files-app/views/files/use-upload.ts +105 -0
- package/packages/genie-app/components.ts +12 -0
- package/packages/genie-app/lib/subjects.ts +87 -0
- package/packages/genie-app/manifest.ts +19 -0
- package/packages/genie-app/package.json +29 -0
- package/packages/genie-app/views/genie/service/agent-lifecycle.ts +136 -0
- package/packages/genie-app/views/genie/service/cli.ts +114 -0
- package/packages/genie-app/views/genie/service/comms.ts +141 -0
- package/packages/genie-app/views/genie/service/directory.ts +167 -0
- package/packages/genie-app/views/genie/service/index.ts +219 -0
- package/packages/genie-app/views/genie/service/system.ts +123 -0
- package/packages/genie-app/views/genie/service/teams.ts +191 -0
- package/packages/genie-app/views/genie/service/terminal-proxy.ts +184 -0
- package/packages/genie-app/views/genie/service/tmux-control.ts +318 -0
- package/packages/genie-app/views/genie/service/wishes.ts +270 -0
- package/packages/genie-app/views/genie/ui/GenieApp.tsx +5 -0
- package/packages/genie-app/views/genie/ui/PaneCard.tsx +307 -0
- package/packages/genie-app/views/genie/ui/Sidebar.tsx +212 -0
- package/packages/genie-app/views/genie/ui/TabBar.tsx +70 -0
- package/packages/genie-app/views/genie/ui/WorkspaceCanvas.tsx +343 -0
- package/packages/genie-app/views/genie/ui/XTermPane.tsx +306 -0
- package/packages/genie-app/views/genie/ui/hooks/useNatsAction.ts +54 -0
- package/packages/genie-app/views/genie/ui/hooks/useNatsRequest.ts +68 -0
- package/packages/genie-app/views/genie/ui/panels/AgentsPanel.tsx +399 -0
- package/packages/genie-app/views/genie/ui/panels/ChatPanel.tsx +351 -0
- package/packages/genie-app/views/genie/ui/panels/SystemPanel.tsx +195 -0
- package/packages/genie-app/views/genie/ui/panels/TeamsPanel.tsx +560 -0
- package/packages/genie-app/views/genie/ui/panels/WishesPanel.tsx +424 -0
- package/packages/nats-viewer-app/components.ts +12 -0
- package/packages/nats-viewer-app/manifest.ts +18 -0
- package/packages/nats-viewer-app/package.json +14 -0
- package/packages/nats-viewer-app/views/nats-viewer/ActiveSubs.tsx +34 -0
- package/packages/nats-viewer-app/views/nats-viewer/MessageLog.tsx +247 -0
- package/packages/nats-viewer-app/views/nats-viewer/NatsViewer.tsx +209 -0
- package/packages/nats-viewer-app/views/nats-viewer/PublishPanel.tsx +111 -0
- package/packages/nats-viewer-app/views/nats-viewer/RequestPanel.tsx +165 -0
- package/packages/nats-viewer-app/views/nats-viewer/Sidebar.tsx +59 -0
- package/packages/nats-viewer-app/views/nats-viewer/SubjectCatalog.tsx +63 -0
- package/packages/nats-viewer-app/views/nats-viewer/SubscribeInput.tsx +59 -0
- package/packages/nats-viewer-app/views/nats-viewer/index.ts +5 -0
- package/packages/nats-viewer-app/views/nats-viewer/nats-viewer-context.tsx +31 -0
- package/packages/nats-viewer-app/views/nats-viewer/types.ts +7 -0
- package/packages/nats-viewer-app/views/nats-viewer/use-message-buffer.ts +55 -0
- package/packages/os-cli/package.json +18 -0
- package/packages/os-cli/src/commands/events.ts +176 -0
- package/packages/os-cli/src/commands/logs.ts +96 -0
- package/packages/os-cli/src/commands/status.ts +53 -0
- package/packages/os-cli/src/commands/traces.ts +115 -0
- package/packages/os-cli/src/index.ts +15 -0
- package/packages/os-cli/src/lib/formatter.ts +123 -0
- package/packages/os-cli/src/lib/nats.ts +16 -0
- package/packages/os-cli/src/lib/trace-tree.ts +144 -0
- package/packages/os-cli/tsconfig.json +12 -0
- package/packages/os-sdk/package.json +27 -0
- package/packages/os-sdk/src/api/handler.ts +67 -0
- package/packages/os-sdk/src/config.ts +68 -0
- package/packages/os-sdk/src/db/factory.test.ts +42 -0
- package/packages/os-sdk/src/db/factory.ts +72 -0
- package/packages/os-sdk/src/db/migrate.ts +140 -0
- package/packages/os-sdk/src/db/provision.ts +44 -0
- package/packages/os-sdk/src/index.ts +36 -0
- package/packages/os-sdk/src/service/console-intercept.ts +60 -0
- package/packages/os-sdk/src/service/logger.ts +88 -0
- package/packages/os-sdk/src/service/o11y-streams.ts +88 -0
- package/packages/os-sdk/src/service/runtime.ts +259 -0
- package/packages/os-sdk/src/service/trace.ts +71 -0
- package/packages/os-sdk/tsconfig.json +16 -0
- package/packages/os-ui/package.json +13 -0
- package/packages/os-ui/src/index.ts +29 -0
- package/packages/os-ui/src/server.ts +4 -0
- package/packages/os-ui/tsconfig.json +19 -0
- package/packages/settings-app/components.ts +12 -0
- package/packages/settings-app/manifest.ts +18 -0
- package/packages/settings-app/package.json +14 -0
- package/packages/settings-app/views/settings/Settings.tsx +492 -0
- package/packages/terminal-app/components.ts +12 -0
- package/packages/terminal-app/manifest.ts +20 -0
- package/packages/terminal-app/package.json +23 -0
- package/packages/terminal-app/views/terminal/schema.ts +82 -0
- package/packages/terminal-app/views/terminal/service/index.ts +133 -0
- package/packages/terminal-app/views/terminal/service/runtime +1 -0
- package/packages/terminal-app/views/terminal/service/session.ts +290 -0
- package/packages/terminal-app/views/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
- package/packages/terminal-app/views/terminal/types.ts +26 -0
- package/packages/terminal-app/views/terminal/ui/MultiTerminalApp.tsx +615 -0
- package/packages/terminal-app/views/terminal/ui/SplitDragHandle.tsx +91 -0
- package/packages/terminal-app/views/terminal/ui/SplitPaneRenderer.tsx +112 -0
- package/packages/terminal-app/views/terminal/ui/TerminalPane.tsx +478 -0
- package/packages/terminal-app/views/terminal/ui/TerminalTabBar.tsx +131 -0
- package/pnpm-workspace.yaml +9 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icons/code-server.svg +6 -0
- package/public/icons/default.svg +5 -0
- package/public/icons/dusk/1password.svg +1 -0
- package/public/icons/dusk/activity_monitor.svg +1 -0
- package/public/icons/dusk/app_store.svg +1 -0
- package/public/icons/dusk/atom.svg +1 -0
- package/public/icons/dusk/brave.svg +1 -0
- package/public/icons/dusk/calculator.svg +1 -0
- package/public/icons/dusk/calendar.svg +1 -0
- package/public/icons/dusk/chrome.svg +1 -0
- package/public/icons/dusk/chrome2.svg +1 -0
- package/public/icons/dusk/dashboard.svg +13 -0
- package/public/icons/dusk/discord.svg +1 -0
- package/public/icons/dusk/dropbox.svg +1 -0
- package/public/icons/dusk/electron.svg +1 -0
- package/public/icons/dusk/figma.svg +1 -0
- package/public/icons/dusk/finder.svg +1 -0
- package/public/icons/dusk/finder2.svg +1 -0
- package/public/icons/dusk/finder3.svg +1 -0
- package/public/icons/dusk/firefox.svg +1 -0
- package/public/icons/dusk/framer.svg +1 -0
- package/public/icons/dusk/gimp.svg +1 -0
- package/public/icons/dusk/github_desktop.svg +1 -0
- package/public/icons/dusk/hyper.svg +1 -0
- package/public/icons/dusk/hyper3.svg +1 -0
- package/public/icons/dusk/intellij.svg +1 -0
- package/public/icons/dusk/iterm2.svg +1 -0
- package/public/icons/dusk/itunes.svg +1 -0
- package/public/icons/dusk/mail.svg +1 -0
- package/public/icons/dusk/messenger.svg +1 -0
- package/public/icons/dusk/mongodb.svg +1 -0
- package/public/icons/dusk/notes.svg +1 -0
- package/public/icons/dusk/notion.svg +1 -0
- package/public/icons/dusk/obs.svg +1 -0
- package/public/icons/dusk/pages.svg +1 -0
- package/public/icons/dusk/photos.svg +1 -0
- package/public/icons/dusk/postman.svg +1 -0
- package/public/icons/dusk/preview.svg +1 -0
- package/public/icons/dusk/reminders.svg +1 -0
- package/public/icons/dusk/safari.svg +1 -0
- package/public/icons/dusk/sequel_pro.svg +1 -0
- package/public/icons/dusk/sketch.svg +1 -0
- package/public/icons/dusk/skype.svg +1 -0
- package/public/icons/dusk/slack.svg +1 -0
- package/public/icons/dusk/slack2.svg +1 -0
- package/public/icons/dusk/spotify.svg +1 -0
- package/public/icons/dusk/steam.svg +1 -0
- package/public/icons/dusk/system_preferences.svg +1 -0
- package/public/icons/dusk/tableplus.svg +1 -0
- package/public/icons/dusk/teams.svg +1 -0
- package/public/icons/dusk/telegram.svg +1 -0
- package/public/icons/dusk/terminal.svg +1 -0
- package/public/icons/dusk/todoist.svg +1 -0
- package/public/icons/dusk/trash.svg +1 -0
- package/public/icons/dusk/trello.svg +1 -0
- package/public/icons/dusk/vivaldi.svg +1 -0
- package/public/icons/dusk/vlc.svg +1 -0
- package/public/icons/dusk/vscode.svg +1 -0
- package/public/icons/dusk/whatsapp.svg +1 -0
- package/public/icons/dusk/xeyes.svg +1 -0
- package/public/icons/dusk/zoom.svg +1 -0
- package/public/icons/files.svg +5 -0
- package/public/icons/pwa/icon-192.png +0 -0
- package/public/icons/pwa/icon-512.png +0 -0
- package/public/icons/settings.svg +14 -0
- package/public/icons/terminal.svg +5 -0
- package/public/icons/text-editor.svg +7 -0
- package/public/manifest.json +38 -0
- package/public/next.svg +1 -0
- package/public/sw.js +41 -0
- package/public/vercel.svg +1 -0
- package/public/wallpapers/default.svg +10 -0
- package/public/window.svg +1 -0
- package/scripts/generate-pwa-icons.mjs +33 -0
- package/scripts/install-nats.sh +37 -0
- package/sentry.client.config.ts +21 -0
- package/sentry.edge.config.ts +12 -0
- package/sentry.server.config.ts +12 -0
- package/src/app/api/files/download/route.ts +81 -0
- package/src/app/api/files/download-zip/route.ts +102 -0
- package/src/app/api/files/upload/route.ts +58 -0
- package/src/app/api/webhooks/workos/route.ts +98 -0
- package/src/app/auth/callback/route.ts +16 -0
- package/src/app/auth/logout/route.ts +15 -0
- package/src/app/desktop/desktop-shell.tsx +110 -0
- package/src/app/desktop/layout.tsx +8 -0
- package/src/app/desktop/page.tsx +24 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +7 -0
- package/src/app/layout.tsx +64 -0
- package/src/app/offline/page.tsx +83 -0
- package/src/app/page.tsx +5 -0
- package/src/app/standalone/[appId]/page.tsx +28 -0
- package/src/app/standalone/layout.tsx +10 -0
- package/src/components/app-icon.tsx +55 -0
- package/src/components/apps/_echo/schema.ts +14 -0
- package/src/components/apps/_echo/service/index.ts +42 -0
- package/src/components/apps/app-manifest.ts +97 -0
- package/src/components/apps/app-registry.ts +55 -0
- package/src/components/apps/dev3000/Dev3000App.tsx +224 -0
- package/src/components/apps/dev3000/ErrorsPanel.tsx +160 -0
- package/src/components/apps/dev3000/Sidebar.tsx +41 -0
- package/src/components/apps/dev3000/TimelineLog.tsx +173 -0
- package/src/components/apps/dev3000/dev3000-context.tsx +29 -0
- package/src/components/apps/dev3000/index.ts +4 -0
- package/src/components/apps/dev3000/schema.ts +48 -0
- package/src/components/apps/dev3000/service/index.ts +520 -0
- package/src/components/apps/dev3000/service/runtime +1 -0
- package/src/components/apps/dev3000/types.ts +15 -0
- package/src/components/apps/dev3000/use-message-buffer.ts +46 -0
- package/src/components/apps/files/ContextMenu.tsx +151 -0
- package/src/components/apps/files/DeleteConfirmDialog.tsx +78 -0
- package/src/components/apps/files/FileItem.tsx +128 -0
- package/src/components/apps/files/FilesApp.tsx +509 -0
- package/src/components/apps/files/FilesListView.tsx +201 -0
- package/src/components/apps/files/FilesToolbar.tsx +117 -0
- package/src/components/apps/files/GridView.tsx +90 -0
- package/src/components/apps/files/InlineInput.tsx +131 -0
- package/src/components/apps/files/UploadOverlay.tsx +61 -0
- package/src/components/apps/files/schema.ts +49 -0
- package/src/components/apps/files/service/index.ts +227 -0
- package/src/components/apps/files/service/runtime +1 -0
- package/src/components/apps/files/use-files.ts +201 -0
- package/src/components/apps/files/use-upload.ts +105 -0
- package/src/components/apps/nats-viewer/ActiveSubs.tsx +34 -0
- package/src/components/apps/nats-viewer/MessageLog.tsx +247 -0
- package/src/components/apps/nats-viewer/NatsViewer.tsx +209 -0
- package/src/components/apps/nats-viewer/PublishPanel.tsx +113 -0
- package/src/components/apps/nats-viewer/RequestPanel.tsx +167 -0
- package/src/components/apps/nats-viewer/Sidebar.tsx +62 -0
- package/src/components/apps/nats-viewer/SubjectCatalog.tsx +64 -0
- package/src/components/apps/nats-viewer/SubscribeInput.tsx +59 -0
- package/src/components/apps/nats-viewer/index.ts +5 -0
- package/src/components/apps/nats-viewer/nats-viewer-context.tsx +31 -0
- package/src/components/apps/nats-viewer/types.ts +7 -0
- package/src/components/apps/nats-viewer/use-message-buffer.ts +55 -0
- package/src/components/apps/settings/Settings.tsx +492 -0
- package/src/components/apps/terminal/schema.ts +82 -0
- package/src/components/apps/terminal/service/index.ts +189 -0
- package/src/components/apps/terminal/service/runtime +1 -0
- package/src/components/apps/terminal/service/session.ts +296 -0
- package/src/components/apps/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
- package/src/components/apps/terminal/types.ts +26 -0
- package/src/components/apps/terminal/ui/MultiTerminalApp.tsx +617 -0
- package/src/components/apps/terminal/ui/SplitDragHandle.tsx +91 -0
- package/src/components/apps/terminal/ui/SplitPaneRenderer.tsx +112 -0
- package/src/components/apps/terminal/ui/TerminalPane.tsx +476 -0
- package/src/components/apps/terminal/ui/TerminalTabBar.tsx +131 -0
- package/src/components/desktop/AnimatedBackground.tsx +69 -0
- package/src/components/desktop/Desktop.tsx +79 -0
- package/src/components/desktop/DesktopBackground.tsx +16 -0
- package/src/components/desktop/DesktopIcon.tsx +49 -0
- package/src/components/desktop/ShortcutViewer.tsx +136 -0
- package/src/components/desktop/WindowRenderer.tsx +34 -0
- package/src/components/desktop/WindowSwitcher.tsx +42 -0
- package/src/components/notifications/NotificationCenter.tsx +153 -0
- package/src/components/notifications/NotificationToasts.tsx +66 -0
- package/src/components/notifications/OrphanSessionToast.tsx +293 -0
- package/src/components/os-primitives/collapsible-sidebar.tsx +226 -0
- package/src/components/os-primitives/dialog.tsx +76 -0
- package/src/components/os-primitives/empty-state.tsx +43 -0
- package/src/components/os-primitives/index.ts +21 -0
- package/src/components/os-primitives/list-view.tsx +155 -0
- package/src/components/os-primitives/property-panel.tsx +108 -0
- package/src/components/os-primitives/section-header.tsx +19 -0
- package/src/components/os-primitives/sidebar-nav.tsx +110 -0
- package/src/components/os-primitives/split-pane.tsx +146 -0
- package/src/components/os-primitives/status-badge.tsx +10 -0
- package/src/components/os-primitives/status-bar.tsx +100 -0
- package/src/components/os-primitives/toolbar.tsx +152 -0
- package/src/components/taskbar/AppLauncher.tsx +114 -0
- package/src/components/taskbar/RunningApps.tsx +71 -0
- package/src/components/taskbar/SystemTray.tsx +134 -0
- package/src/components/taskbar/Taskbar.tsx +45 -0
- package/src/components/taskbar/UserMenu.tsx +138 -0
- package/src/components/taskbar/WorkspaceSwitcher.tsx +9 -0
- package/src/components/ui/ContextMenu.tsx +130 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/button.tsx +102 -0
- package/src/components/ui/command.tsx +165 -0
- package/src/components/ui/dropdown-menu.tsx +233 -0
- package/src/components/ui/input.tsx +48 -0
- package/src/components/ui/note.tsx +55 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/spinner.tsx +42 -0
- package/src/components/ui/switch.tsx +36 -0
- package/src/components/ui/theme-provider.tsx +24 -0
- package/src/components/ui/theme-switcher.tsx +51 -0
- package/src/components/ui/tooltip.tsx +62 -0
- package/src/components/window/MobileWindowStack.tsx +218 -0
- package/src/components/window/SnapPreview.tsx +37 -0
- package/src/components/window/StandaloneFrame.tsx +170 -0
- package/src/components/window/Window.tsx +423 -0
- package/src/components/window/WindowContent.tsx +14 -0
- package/src/components/window/WindowControlsOverlay.tsx +89 -0
- package/src/components/window/WindowFrame.tsx +124 -0
- package/src/lib/auth/index.ts +27 -0
- package/src/lib/auth/roles.ts +50 -0
- package/src/lib/auth/types.ts +32 -0
- package/src/lib/auth/use-auth.ts +53 -0
- package/src/lib/auth/webhook-handler.ts +87 -0
- package/src/lib/auth/workos.ts +67 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/desktop/dedup.ts +57 -0
- package/src/lib/desktop/schema.ts +55 -0
- package/src/lib/files/filename-validation.ts +41 -0
- package/src/lib/files/safe-path.ts +49 -0
- package/src/lib/hooks/use-desktop-nats.ts +438 -0
- package/src/lib/hooks/use-is-mobile.ts +23 -0
- package/src/lib/hooks/use-launch-app.ts +79 -0
- package/src/lib/hooks/use-nats-notifications.ts +84 -0
- package/src/lib/hooks/use-nats.ts +60 -0
- package/src/lib/hooks/use-visual-viewport.ts +72 -0
- package/src/lib/icons/resolve-window-icon.ts +10 -0
- package/src/lib/keyboard/defaults.ts +146 -0
- package/src/lib/keyboard/types.ts +52 -0
- package/src/lib/keyboard/use-global-keybinds.ts +231 -0
- package/src/lib/nats-client.ts +255 -0
- package/src/lib/nats.ts +35 -0
- package/src/lib/notifications/schema.ts +12 -0
- package/src/lib/service-loader.ts +171 -0
- package/src/lib/subjects.ts +64 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/ws-bridge.ts +288 -0
- package/src/lib/ws-protocol.ts +53 -0
- package/src/lib/ws-server.ts +167 -0
- package/src/middleware.ts +57 -0
- package/src/stores/desktop-store.ts +112 -0
- package/src/stores/keybind-store.ts +66 -0
- package/src/stores/notification-store.ts +271 -0
- package/src/stores/theme-store.ts +25 -0
- package/src/stores/window-store.ts +294 -0
- package/src/theme/animations.css +68 -0
- package/src/theme/base.css +123 -0
- package/src/theme/controls.css +35 -0
- package/src/theme/design-tokens.css +276 -0
- package/src/theme/index.css +23 -0
- package/src/theme/menus.css +45 -0
- package/src/theme/status.css +41 -0
- package/src/theme/surfaces.css +94 -0
- package/src/theme/tailwind-map.css +138 -0
- package/src/theme/taskbar.css +25 -0
- package/src/theme/terminal.css +55 -0
- package/src/theme/typography.css +26 -0
- package/src/theme/utilities.css +156 -0
- package/src/theme/window.css +103 -0
- package/src/types/desktop-entry.ts +12 -0
- package/src/types/use-descendants.d.ts +13 -0
- package/src/types/window.ts +28 -0
- package/src/types.d.ts +9 -0
- package/tauri/Cargo.lock +5464 -0
- package/tauri/Cargo.toml +19 -0
- package/tauri/build.rs +3 -0
- package/tauri/capabilities/default.json +36 -0
- package/tauri/icons/128x128.png +0 -0
- package/tauri/icons/128x128@2x.png +0 -0
- package/tauri/icons/32x32.png +0 -0
- package/tauri/icons/icon.png +0 -0
- package/tauri/src/main.rs +396 -0
- package/tauri/tauri.conf.json +23 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Inbox } from 'lucide-react';
|
|
4
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import { Spinner } from '@/components/ui/spinner';
|
|
8
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
9
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
10
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
11
|
+
import type { LogEntry } from './types';
|
|
12
|
+
|
|
13
|
+
function buildQuickPickSubjects(orgId: string): string[] {
|
|
14
|
+
return [SUBJECTS.echo(orgId), SUBJECTS.pty.create(orgId), SUBJECTS.pty.list(orgId)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type RequestState =
|
|
18
|
+
| { status: 'idle' }
|
|
19
|
+
| { status: 'loading' }
|
|
20
|
+
| { status: 'success'; data: unknown }
|
|
21
|
+
| { status: 'error'; message: string };
|
|
22
|
+
|
|
23
|
+
interface RequestPanelProps {
|
|
24
|
+
/** Optional callback to push an entry to the message buffer. */
|
|
25
|
+
onMessage?: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function RequestPanel({ onMessage }: RequestPanelProps) {
|
|
29
|
+
const { connected, request } = useNats();
|
|
30
|
+
const auth = useKhalAuth();
|
|
31
|
+
const orgId = auth?.orgId ?? 'default';
|
|
32
|
+
const quickPicks = useMemo(() => buildQuickPickSubjects(orgId), [orgId]);
|
|
33
|
+
|
|
34
|
+
const [subject, setSubject] = useState('');
|
|
35
|
+
const [payload, setPayload] = useState('');
|
|
36
|
+
const [timeout, setTimeout_] = useState(5000);
|
|
37
|
+
const [jsonError, setJsonError] = useState<string | null>(null);
|
|
38
|
+
const [reqState, setReqState] = useState<RequestState>({ status: 'idle' });
|
|
39
|
+
|
|
40
|
+
const handleSend = useCallback(async () => {
|
|
41
|
+
setJsonError(null);
|
|
42
|
+
|
|
43
|
+
let parsed: unknown;
|
|
44
|
+
if (payload.trim()) {
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(payload);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
setJsonError(`Invalid JSON: ${(e as Error).message}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Clear previous response and start loading
|
|
54
|
+
setReqState({ status: 'loading' });
|
|
55
|
+
|
|
56
|
+
// Push outgoing request to buffer
|
|
57
|
+
onMessage?.({ subject, payload: parsed, direction: 'out' });
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await request(subject, parsed, timeout);
|
|
61
|
+
setReqState({ status: 'success', data: response });
|
|
62
|
+
|
|
63
|
+
// Push response to buffer
|
|
64
|
+
onMessage?.({ subject: `${subject} (reply)`, payload: response, direction: 'in' });
|
|
65
|
+
} catch (e) {
|
|
66
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
67
|
+
setReqState({ status: 'error', message });
|
|
68
|
+
}
|
|
69
|
+
}, [subject, payload, timeout, request, onMessage]);
|
|
70
|
+
|
|
71
|
+
const canSend = subject.trim().length > 0 && connected && reqState.status !== 'loading';
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex flex-col gap-2">
|
|
75
|
+
{/* Subject input */}
|
|
76
|
+
<Input
|
|
77
|
+
size="small"
|
|
78
|
+
placeholder={SUBJECTS.echo(orgId)}
|
|
79
|
+
value={subject}
|
|
80
|
+
onChange={(e) => {
|
|
81
|
+
setSubject(e.target.value);
|
|
82
|
+
setJsonError(null);
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
{/* Quick-pick subjects */}
|
|
87
|
+
<div className="flex flex-wrap gap-1">
|
|
88
|
+
{quickPicks.map((s) => (
|
|
89
|
+
<button
|
|
90
|
+
key={s}
|
|
91
|
+
type="button"
|
|
92
|
+
className="rounded border border-gray-alpha-300 px-1.5 py-0.5 text-[10px] font-mono text-gray-800 hover:bg-gray-alpha-100 transition-colors"
|
|
93
|
+
onClick={() => setSubject(s)}
|
|
94
|
+
>
|
|
95
|
+
{s}
|
|
96
|
+
</button>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Payload textarea */}
|
|
101
|
+
<textarea
|
|
102
|
+
className="w-full rounded-md border border-gray-alpha-400 bg-background-100 px-2 py-1.5 font-mono text-xs text-gray-1000 placeholder:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-1 resize-none"
|
|
103
|
+
rows={4}
|
|
104
|
+
placeholder="{}"
|
|
105
|
+
value={payload}
|
|
106
|
+
onChange={(e) => {
|
|
107
|
+
setPayload(e.target.value);
|
|
108
|
+
setJsonError(null);
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
{/* Timeout input */}
|
|
113
|
+
<div className="flex items-center gap-2">
|
|
114
|
+
<label className="text-[11px] text-gray-800 shrink-0">Timeout</label>
|
|
115
|
+
<Input
|
|
116
|
+
size="small"
|
|
117
|
+
type="number"
|
|
118
|
+
className="w-20 text-xs"
|
|
119
|
+
value={timeout}
|
|
120
|
+
onChange={(e) => setTimeout_(Number(e.target.value) || 5000)}
|
|
121
|
+
/>
|
|
122
|
+
<span className="text-[11px] text-gray-700">ms</span>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* JSON error */}
|
|
126
|
+
{jsonError && <p className="text-[11px] text-red-600">{jsonError}</p>}
|
|
127
|
+
|
|
128
|
+
{/* Send button */}
|
|
129
|
+
<div className="flex items-center gap-2">
|
|
130
|
+
<Button
|
|
131
|
+
size="small"
|
|
132
|
+
disabled={!canSend}
|
|
133
|
+
onClick={handleSend}
|
|
134
|
+
loading={reqState.status === 'loading'}
|
|
135
|
+
prefix={<Inbox className="h-3 w-3" />}
|
|
136
|
+
>
|
|
137
|
+
Send Request
|
|
138
|
+
</Button>
|
|
139
|
+
{!connected && <span className="text-[11px] text-gray-600">Not connected</span>}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Response area */}
|
|
143
|
+
{reqState.status === 'loading' && (
|
|
144
|
+
<div className="flex items-center gap-2 rounded border border-gray-alpha-300 bg-gray-alpha-50 px-2 py-2">
|
|
145
|
+
<Spinner size="sm" />
|
|
146
|
+
<span className="text-[11px] text-gray-800">Waiting for reply...</span>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{reqState.status === 'success' && (
|
|
151
|
+
<div className="rounded border border-gray-alpha-400 bg-gray-alpha-50 p-2 overflow-auto max-h-48">
|
|
152
|
+
<pre className="font-mono text-xs text-gray-1000 whitespace-pre-wrap break-all">
|
|
153
|
+
{typeof reqState.data === 'string' ? reqState.data : JSON.stringify(reqState.data, null, 2)}
|
|
154
|
+
</pre>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{reqState.status === 'error' && (
|
|
159
|
+
<div className="rounded border border-red-300 bg-red-50 dark:bg-red-950/20 p-2">
|
|
160
|
+
<p className="font-mono text-[11px] text-red-600 break-all">{reqState.message}</p>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Radio } from 'lucide-react';
|
|
4
|
+
import { Separator } from '@/components/ui/separator';
|
|
5
|
+
import { ActiveSubs } from './ActiveSubs';
|
|
6
|
+
import { useNatsViewer } from './nats-viewer-context';
|
|
7
|
+
import { SubjectCatalog } from './SubjectCatalog';
|
|
8
|
+
import { SubscribeInput } from './SubscribeInput';
|
|
9
|
+
|
|
10
|
+
export function Sidebar() {
|
|
11
|
+
const { subscriptions, addSubscription, removeSubscription } = useNatsViewer();
|
|
12
|
+
const catchAll = 'os.>';
|
|
13
|
+
const catchAllActive = subscriptions.has(catchAll);
|
|
14
|
+
|
|
15
|
+
const toggleCatchAll = () => {
|
|
16
|
+
if (catchAllActive) {
|
|
17
|
+
removeSubscription(catchAll);
|
|
18
|
+
} else {
|
|
19
|
+
addSubscription(catchAll);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col gap-3 overflow-y-auto">
|
|
25
|
+
{/* Catch-all toggle */}
|
|
26
|
+
<button
|
|
27
|
+
onClick={toggleCatchAll}
|
|
28
|
+
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
|
|
29
|
+
catchAllActive
|
|
30
|
+
? 'bg-green-500/15 text-green-700 hover:bg-green-500/25'
|
|
31
|
+
: 'bg-gray-alpha-100 text-gray-900 hover:bg-gray-alpha-200'
|
|
32
|
+
}`}
|
|
33
|
+
>
|
|
34
|
+
<Radio className="h-3.5 w-3.5" />
|
|
35
|
+
<span className="font-mono">{catchAll}</span>
|
|
36
|
+
<span className="ml-auto text-[11px]">{catchAllActive ? 'ON' : 'OFF'}</span>
|
|
37
|
+
</button>
|
|
38
|
+
|
|
39
|
+
{/* Custom subscribe input */}
|
|
40
|
+
<SubscribeInput />
|
|
41
|
+
|
|
42
|
+
<Separator />
|
|
43
|
+
|
|
44
|
+
{/* Known Subjects */}
|
|
45
|
+
<div>
|
|
46
|
+
<h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Known Subjects</h3>
|
|
47
|
+
<SubjectCatalog />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<Separator />
|
|
51
|
+
|
|
52
|
+
{/* Active Subscriptions */}
|
|
53
|
+
<div>
|
|
54
|
+
<h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Active Subscriptions</h3>
|
|
55
|
+
<ActiveSubs />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useKhalAuth } from '@/lib/auth/use-auth';
|
|
5
|
+
import { useNats } from '@/lib/hooks/use-nats';
|
|
6
|
+
import { SUBJECTS } from '@/lib/subjects';
|
|
7
|
+
import { useNatsViewer } from './nats-viewer-context';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a list of known static subjects.
|
|
11
|
+
* Skips session-scoped subjects like pty.data.
|
|
12
|
+
*/
|
|
13
|
+
function buildKnownSubjects(orgId: string, userId: string): string[] {
|
|
14
|
+
const subjects = [
|
|
15
|
+
SUBJECTS.echo(orgId),
|
|
16
|
+
SUBJECTS.system.health(orgId),
|
|
17
|
+
SUBJECTS.pty.create(orgId),
|
|
18
|
+
SUBJECTS.pty.destroy(orgId),
|
|
19
|
+
SUBJECTS.pty.list(orgId),
|
|
20
|
+
SUBJECTS.fs.list(orgId),
|
|
21
|
+
SUBJECTS.fs.read(orgId),
|
|
22
|
+
SUBJECTS.fs.write(orgId),
|
|
23
|
+
SUBJECTS.fs.search(orgId),
|
|
24
|
+
SUBJECTS.notify.broadcast(orgId),
|
|
25
|
+
];
|
|
26
|
+
if (userId) {
|
|
27
|
+
subjects.push(SUBJECTS.desktop.cmd.all(orgId, userId), SUBJECTS.desktop.event.all(orgId, userId));
|
|
28
|
+
}
|
|
29
|
+
return subjects.sort();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function SubjectCatalog() {
|
|
33
|
+
const { subscriptions, addSubscription, removeSubscription } = useNatsViewer();
|
|
34
|
+
const { userId } = useNats();
|
|
35
|
+
const auth = useKhalAuth();
|
|
36
|
+
const orgId = auth?.orgId ?? 'default';
|
|
37
|
+
|
|
38
|
+
const knownSubjects = useMemo(() => buildKnownSubjects(orgId, userId), [orgId, userId]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex flex-col gap-0.5">
|
|
42
|
+
{knownSubjects.map((subject) => {
|
|
43
|
+
const active = subscriptions.has(subject);
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
key={subject}
|
|
47
|
+
onClick={() => (active ? removeSubscription(subject) : addSubscription(subject))}
|
|
48
|
+
className="group flex items-center gap-2 rounded px-1.5 py-0.5 text-left transition-colors hover:bg-gray-alpha-100"
|
|
49
|
+
>
|
|
50
|
+
<span
|
|
51
|
+
className={`inline-block h-2 w-2 shrink-0 rounded-full transition-colors ${
|
|
52
|
+
active ? 'bg-green-500' : 'bg-gray-400 group-hover:bg-gray-500'
|
|
53
|
+
}`}
|
|
54
|
+
/>
|
|
55
|
+
<span className="min-w-0 truncate font-mono text-xs text-gray-900 group-hover:text-gray-1000">
|
|
56
|
+
{subject}
|
|
57
|
+
</span>
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Plus } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { useNatsViewer } from './nats-viewer-context';
|
|
6
|
+
|
|
7
|
+
export function SubscribeInput() {
|
|
8
|
+
const { addSubscription } = useNatsViewer();
|
|
9
|
+
const [value, setValue] = useState('');
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
|
|
12
|
+
const handleSubmit = () => {
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
if (!trimmed) return;
|
|
15
|
+
|
|
16
|
+
if (!trimmed.startsWith('os.')) {
|
|
17
|
+
setError('Subject must start with "os."');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setError('');
|
|
22
|
+
addSubscription(trimmed);
|
|
23
|
+
setValue('');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
27
|
+
if (e.key === 'Enter') {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
handleSubmit();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex flex-col gap-1">
|
|
35
|
+
<div className="flex items-center gap-1">
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
value={value}
|
|
39
|
+
onChange={(e) => {
|
|
40
|
+
setValue(e.target.value);
|
|
41
|
+
if (error) setError('');
|
|
42
|
+
}}
|
|
43
|
+
onKeyDown={handleKeyDown}
|
|
44
|
+
placeholder="os.custom.subject"
|
|
45
|
+
className="h-7 flex-1 rounded border border-gray-alpha-400 bg-background-100 px-2 font-mono text-xs text-gray-1000 placeholder:text-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-700"
|
|
46
|
+
/>
|
|
47
|
+
<button
|
|
48
|
+
onClick={handleSubmit}
|
|
49
|
+
disabled={!value.trim()}
|
|
50
|
+
className="flex h-7 shrink-0 items-center gap-1 rounded border border-gray-alpha-400 bg-background-100 px-2 text-xs text-gray-900 transition-colors hover:bg-gray-alpha-100 hover:text-gray-1000 disabled:opacity-40 disabled:pointer-events-none"
|
|
51
|
+
>
|
|
52
|
+
<Plus className="h-3 w-3" />
|
|
53
|
+
Sub
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
{error && <p className="px-0.5 text-[11px] text-red-600">{error}</p>}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import type { LogEntry } from './types';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Context — shared state for sidebar panels and the main log area
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface NatsViewerContextValue {
|
|
11
|
+
subscriptions: Set<string>;
|
|
12
|
+
addSubscription: (subject: string) => void;
|
|
13
|
+
removeSubscription: (subject: string) => void;
|
|
14
|
+
buffer: {
|
|
15
|
+
entries: LogEntry[];
|
|
16
|
+
push: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
};
|
|
19
|
+
filter: string;
|
|
20
|
+
setFilter: (f: string) => void;
|
|
21
|
+
paused: boolean;
|
|
22
|
+
setPaused: (p: boolean) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const NatsViewerContext = createContext<NatsViewerContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
export function useNatsViewer(): NatsViewerContextValue {
|
|
28
|
+
const ctx = useContext(NatsViewerContext);
|
|
29
|
+
if (!ctx) throw new Error('useNatsViewer must be used within <NatsViewer>');
|
|
30
|
+
return ctx;
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback, useReducer } from 'react';
|
|
2
|
+
import type { LogEntry } from './types';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CAPACITY = 1000;
|
|
5
|
+
|
|
6
|
+
type Action =
|
|
7
|
+
| { type: 'push'; entry: Omit<LogEntry, 'id' | 'timestamp'> }
|
|
8
|
+
| { type: 'clear' }
|
|
9
|
+
| { type: 'set-capacity'; capacity: number };
|
|
10
|
+
|
|
11
|
+
interface State {
|
|
12
|
+
entries: LogEntry[];
|
|
13
|
+
capacity: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function reducer(state: State, action: Action): State {
|
|
17
|
+
switch (action.type) {
|
|
18
|
+
case 'push': {
|
|
19
|
+
const entry: LogEntry = {
|
|
20
|
+
...action.entry,
|
|
21
|
+
id: crypto.randomUUID(),
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
};
|
|
24
|
+
const next = [...state.entries, entry];
|
|
25
|
+
// Drop oldest entries when over capacity
|
|
26
|
+
if (next.length > state.capacity) {
|
|
27
|
+
return { ...state, entries: next.slice(next.length - state.capacity) };
|
|
28
|
+
}
|
|
29
|
+
return { ...state, entries: next };
|
|
30
|
+
}
|
|
31
|
+
case 'clear':
|
|
32
|
+
return { ...state, entries: [] };
|
|
33
|
+
case 'set-capacity': {
|
|
34
|
+
const entries =
|
|
35
|
+
state.entries.length > action.capacity
|
|
36
|
+
? state.entries.slice(state.entries.length - action.capacity)
|
|
37
|
+
: state.entries;
|
|
38
|
+
return { capacity: action.capacity, entries };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useMessageBuffer(capacity = DEFAULT_CAPACITY) {
|
|
44
|
+
const [state, dispatch] = useReducer(reducer, { entries: [], capacity });
|
|
45
|
+
|
|
46
|
+
const push = useCallback((entry: Omit<LogEntry, 'id' | 'timestamp'>) => {
|
|
47
|
+
dispatch({ type: 'push', entry });
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const clear = useCallback(() => {
|
|
51
|
+
dispatch({ type: 'clear' });
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
return { entries: state.entries, push, clear };
|
|
55
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@genie-os/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"genie-os": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "bun build src/index.ts --outfile dist/index.js --target node"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"commander": "^12.1.0",
|
|
14
|
+
"chalk": "^5.4.1",
|
|
15
|
+
"@nats-io/transport-node": "^3.3.1",
|
|
16
|
+
"@nats-io/jetstream": "^3.3.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { DeliverPolicy, jetstream } from '@nats-io/jetstream';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { connectNats, decode } from '../lib/nats.js';
|
|
5
|
+
|
|
6
|
+
/** Parse a duration string like "5m", "1h", "30s" into milliseconds. */
|
|
7
|
+
function parseDuration(input: string): number {
|
|
8
|
+
const match = input.match(/^(\d+)(s|m|h|d)$/);
|
|
9
|
+
if (!match) throw new Error(`Invalid duration: "${input}". Use format like 5m, 1h, 30s`);
|
|
10
|
+
const value = Number.parseInt(match[1], 10);
|
|
11
|
+
const unit = match[2];
|
|
12
|
+
const multipliers: Record<string, number> = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
13
|
+
return value * multipliers[unit];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface EventEntry {
|
|
17
|
+
subject: string;
|
|
18
|
+
service: string;
|
|
19
|
+
duration_ms: number;
|
|
20
|
+
payload_bytes?: number;
|
|
21
|
+
trace_id: string;
|
|
22
|
+
span_id: string;
|
|
23
|
+
parent_span_id?: string;
|
|
24
|
+
ts: string;
|
|
25
|
+
error?: { message: string; stack?: string };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Palette of distinct colors for service names. */
|
|
29
|
+
const SERVICE_COLORS = [
|
|
30
|
+
chalk.cyan,
|
|
31
|
+
chalk.magenta,
|
|
32
|
+
chalk.green,
|
|
33
|
+
chalk.yellow,
|
|
34
|
+
chalk.blue,
|
|
35
|
+
chalk.red,
|
|
36
|
+
chalk.white,
|
|
37
|
+
chalk.gray,
|
|
38
|
+
];
|
|
39
|
+
const serviceColorMap = new Map<string, (text: string) => string>();
|
|
40
|
+
let colorIndex = 0;
|
|
41
|
+
|
|
42
|
+
function getServiceColor(service: string): (text: string) => string {
|
|
43
|
+
const existing = serviceColorMap.get(service);
|
|
44
|
+
if (existing) return existing;
|
|
45
|
+
const color = SERVICE_COLORS[colorIndex % SERVICE_COLORS.length];
|
|
46
|
+
serviceColorMap.set(service, color);
|
|
47
|
+
colorIndex++;
|
|
48
|
+
return color;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatEvent(entry: EventEntry): string {
|
|
52
|
+
const ts = formatTimestamp(entry.ts);
|
|
53
|
+
const serviceColor = getServiceColor(entry.service);
|
|
54
|
+
const service = serviceColor(`[${entry.service}]`);
|
|
55
|
+
const subject = chalk.white(entry.subject);
|
|
56
|
+
const hasError = !!entry.error;
|
|
57
|
+
|
|
58
|
+
const durationMs = entry.duration_ms;
|
|
59
|
+
let duration: string;
|
|
60
|
+
if (durationMs < 1) duration = chalk.green(`${durationMs.toFixed(2)}ms`);
|
|
61
|
+
else if (durationMs < 100) duration = chalk.green(`${durationMs.toFixed(1)}ms`);
|
|
62
|
+
else if (durationMs < 1000) duration = chalk.yellow(`${durationMs.toFixed(0)}ms`);
|
|
63
|
+
else duration = chalk.red(`${(durationMs / 1000).toFixed(2)}s`);
|
|
64
|
+
|
|
65
|
+
const status = hasError ? chalk.red('ERR') : chalk.green('OK');
|
|
66
|
+
const traceId = entry.trace_id ? chalk.dim(`trace=${entry.trace_id.slice(0, 8)}`) : '';
|
|
67
|
+
|
|
68
|
+
let line = `${chalk.dim(ts)} ${status} ${service} ${subject} ${duration} ${traceId}`;
|
|
69
|
+
|
|
70
|
+
if (hasError && entry.error) {
|
|
71
|
+
line += `\n ${chalk.red(entry.error.message)}`;
|
|
72
|
+
if (entry.error.stack) {
|
|
73
|
+
const stackLines = entry.error.stack.split('\n').slice(1, 4);
|
|
74
|
+
for (const sl of stackLines) {
|
|
75
|
+
line += `\n ${chalk.dim(sl.trim())}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return line;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatTimestamp(ts: string): string {
|
|
84
|
+
try {
|
|
85
|
+
const d = new Date(ts);
|
|
86
|
+
const h = String(d.getHours()).padStart(2, '0');
|
|
87
|
+
const m = String(d.getMinutes()).padStart(2, '0');
|
|
88
|
+
const s = String(d.getSeconds()).padStart(2, '0');
|
|
89
|
+
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
90
|
+
return `${h}:${m}:${s}.${ms}`;
|
|
91
|
+
} catch {
|
|
92
|
+
return ts;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const eventsCommand = new Command('events')
|
|
97
|
+
.description('Tail handler events with timing and status')
|
|
98
|
+
.argument('[subject-pattern]', 'NATS subject pattern to filter events (e.g. os.genie.teams.*)')
|
|
99
|
+
.option('--all', 'Subscribe to all handler events')
|
|
100
|
+
.option('--since <duration>', 'Replay events from JetStream (e.g. 5m, 1h, 30s)')
|
|
101
|
+
.option('--json', 'Output raw JSON lines')
|
|
102
|
+
.action(async (subjectPattern: string | undefined, opts: { all?: boolean; since?: string; json?: boolean }) => {
|
|
103
|
+
if (!subjectPattern && !opts.all) {
|
|
104
|
+
console.error('Error: specify a subject pattern or use --all');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const nc = await connectNats();
|
|
109
|
+
|
|
110
|
+
// Build the NATS subject to subscribe to.
|
|
111
|
+
// User passes a pattern like "os.genie.teams.*" — we prefix with "os.o11y.events."
|
|
112
|
+
const subject = opts.all ? 'os.o11y.events.>' : `os.o11y.events.${subjectPattern}`;
|
|
113
|
+
|
|
114
|
+
if (opts.since) {
|
|
115
|
+
await replayThenTail(nc, subject, opts);
|
|
116
|
+
} else {
|
|
117
|
+
await liveTail(nc, subject, opts);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
async function liveTail(
|
|
122
|
+
nc: Awaited<ReturnType<typeof connectNats>>,
|
|
123
|
+
subject: string,
|
|
124
|
+
opts: { json?: boolean }
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const sub = nc.subscribe(subject);
|
|
127
|
+
|
|
128
|
+
for await (const msg of sub) {
|
|
129
|
+
try {
|
|
130
|
+
const raw = decode(msg.data);
|
|
131
|
+
const entry: EventEntry = JSON.parse(raw);
|
|
132
|
+
|
|
133
|
+
if (opts.json) {
|
|
134
|
+
process.stdout.write(raw + '\n');
|
|
135
|
+
} else {
|
|
136
|
+
process.stdout.write(formatEvent(entry) + '\n');
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Skip malformed messages
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function replayThenTail(
|
|
145
|
+
nc: Awaited<ReturnType<typeof connectNats>>,
|
|
146
|
+
subject: string,
|
|
147
|
+
opts: { since?: string; json?: boolean }
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
const sinceMs = parseDuration(opts.since!);
|
|
150
|
+
const startTime = new Date(Date.now() - sinceMs);
|
|
151
|
+
|
|
152
|
+
const js = jetstream(nc);
|
|
153
|
+
|
|
154
|
+
const consumer = await js.consumers.get('OS_O11Y_EVENTS', {
|
|
155
|
+
filter_subjects: [subject],
|
|
156
|
+
deliver_policy: DeliverPolicy.StartTime,
|
|
157
|
+
opt_start_time: startTime.toISOString(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const messages = await consumer.consume();
|
|
161
|
+
|
|
162
|
+
for await (const msg of messages) {
|
|
163
|
+
try {
|
|
164
|
+
const raw = decode(msg.data);
|
|
165
|
+
const entry: EventEntry = JSON.parse(raw);
|
|
166
|
+
|
|
167
|
+
if (opts.json) {
|
|
168
|
+
process.stdout.write(raw + '\n');
|
|
169
|
+
} else {
|
|
170
|
+
process.stdout.write(formatEvent(entry) + '\n');
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// Skip malformed messages
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|