@polderlabs/bizar 3.23.1 → 4.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 +83 -16
- package/bizar-dash/.bizar/activity.log +3 -0
- package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
- package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
- package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
- package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
- package/bizar-dash/.bizar/graph/graph.html +307 -0
- package/bizar-dash/.bizar/graph/graph.json +40267 -0
- package/bizar-dash/.bizar/graph/manifest.json +762 -0
- package/bizar-dash/.graphifyignore +40 -0
- package/bizar-dash/.obsidian/README.md +14 -0
- package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
- package/bizar-dash/.obsidian/vault.json +7 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
- package/bizar-dash/CHANGELOG.md +474 -0
- package/bizar-dash/DESIGN.md +136 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
- package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
- package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
- package/bizar-dash/dist/index.html +40 -0
- package/bizar-dash/dist/mobile.html +22 -0
- package/bizar-dash/plans/debug-plan/comments.json +1 -0
- package/bizar-dash/plans/debug-plan/meta.json +10 -0
- package/bizar-dash/plans/debug-plan/plan.json +174 -0
- package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
- package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
- package/bizar-dash/src/cli.mjs +522 -0
- package/bizar-dash/src/server/activity-log.mjs +174 -0
- package/bizar-dash/src/server/agents-store.mjs +465 -0
- package/bizar-dash/src/server/api.mjs +138 -0
- package/bizar-dash/src/server/artifact-template.html +810 -0
- package/bizar-dash/src/server/artifacts-store.mjs +733 -0
- package/bizar-dash/src/server/auth.mjs +381 -0
- package/bizar-dash/src/server/background-store.mjs +447 -0
- package/bizar-dash/src/server/bg-poller.mjs +305 -0
- package/bizar-dash/src/server/bg-retry.mjs +574 -0
- package/bizar-dash/src/server/browser.mjs +40 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
- package/bizar-dash/src/server/dialog-poller.mjs +105 -0
- package/bizar-dash/src/server/dialog-store.mjs +177 -0
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
- package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
- package/bizar-dash/src/server/memory-git.mjs +329 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
- package/bizar-dash/src/server/memory-schema.mjs +145 -0
- package/bizar-dash/src/server/memory-secrets.mjs +84 -0
- package/bizar-dash/src/server/memory-store.mjs +471 -0
- package/bizar-dash/src/server/mod-security.mjs +398 -0
- package/bizar-dash/src/server/mods-loader.mjs +1067 -0
- package/bizar-dash/src/server/notifications-store.mjs +243 -0
- package/bizar-dash/src/server/obsidian-store.mjs +318 -0
- package/bizar-dash/src/server/opencode-runner.mjs +279 -0
- package/bizar-dash/src/server/pair-store.mjs +138 -0
- package/bizar-dash/src/server/projects-store.mjs +364 -0
- package/bizar-dash/src/server/providers-store.mjs +789 -0
- package/bizar-dash/src/server/routes/_shared.mjs +415 -0
- package/bizar-dash/src/server/routes/activity.mjs +110 -0
- package/bizar-dash/src/server/routes/agents.mjs +134 -0
- package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
- package/bizar-dash/src/server/routes/auth.mjs +69 -0
- package/bizar-dash/src/server/routes/background.mjs +165 -0
- package/bizar-dash/src/server/routes/chat.mjs +485 -0
- package/bizar-dash/src/server/routes/config.mjs +129 -0
- package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
- package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
- package/bizar-dash/src/server/routes/fs.mjs +429 -0
- package/bizar-dash/src/server/routes/history.mjs +78 -0
- package/bizar-dash/src/server/routes/memory.mjs +658 -0
- package/bizar-dash/src/server/routes/misc.mjs +89 -0
- package/bizar-dash/src/server/routes/mods.mjs +359 -0
- package/bizar-dash/src/server/routes/notifications.mjs +54 -0
- package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
- package/bizar-dash/src/server/routes/overview.mjs +91 -0
- package/bizar-dash/src/server/routes/pair.mjs +67 -0
- package/bizar-dash/src/server/routes/projects.mjs +175 -0
- package/bizar-dash/src/server/routes/providers.mjs +47 -0
- package/bizar-dash/src/server/routes/schedules.mjs +104 -0
- package/bizar-dash/src/server/routes/settings.mjs +51 -0
- package/bizar-dash/src/server/routes/skills.mjs +76 -0
- package/bizar-dash/src/server/routes/tasks.mjs +468 -0
- package/bizar-dash/src/server/routes/themes.mjs +51 -0
- package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
- package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
- package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
- package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
- package/bizar-dash/src/server/schedules-runner.mjs +369 -0
- package/bizar-dash/src/server/schedules-store.mjs +268 -0
- package/bizar-dash/src/server/search-store.mjs +220 -0
- package/bizar-dash/src/server/serve-info.mjs +684 -0
- package/bizar-dash/src/server/server.mjs +796 -0
- package/bizar-dash/src/server/settings-store.mjs +45 -0
- package/bizar-dash/src/server/skills-store.mjs +300 -0
- package/bizar-dash/src/server/state.mjs +437 -0
- package/bizar-dash/src/server/tailscale-store.mjs +113 -0
- package/bizar-dash/src/server/task-delegator.mjs +824 -0
- package/bizar-dash/src/server/tasks-store.mjs +508 -0
- package/bizar-dash/src/server/tui.mjs +923 -0
- package/bizar-dash/src/server/update-store.mjs +168 -0
- package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
- package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
- package/bizar-dash/src/server/watcher.mjs +81 -0
- package/bizar-dash/src/server/yaml.mjs +238 -0
- package/bizar-dash/src/web/App.tsx +718 -0
- package/bizar-dash/src/web/MobileApp.tsx +408 -0
- package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
- package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
- package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
- package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
- package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
- package/bizar-dash/src/web/components/Button.tsx +56 -0
- package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
- package/bizar-dash/src/web/components/Card.tsx +40 -0
- package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
- package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
- package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
- package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
- package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
- package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
- package/bizar-dash/src/web/components/Modal.tsx +215 -0
- package/bizar-dash/src/web/components/Notifications.tsx +241 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
- package/bizar-dash/src/web/components/Spinner.tsx +19 -0
- package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
- package/bizar-dash/src/web/components/Tag.tsx +28 -0
- package/bizar-dash/src/web/components/Toast.tsx +150 -0
- package/bizar-dash/src/web/components/Topbar.tsx +368 -0
- package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
- package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
- package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
- package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
- package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
- package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
- package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
- package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
- package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
- package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
- package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
- package/bizar-dash/src/web/components/chat/index.ts +20 -0
- package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
- package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
- package/bizar-dash/src/web/index.html +37 -0
- package/bizar-dash/src/web/lib/api.ts +193 -0
- package/bizar-dash/src/web/lib/markdown.tsx +59 -0
- package/bizar-dash/src/web/lib/types.ts +668 -0
- package/bizar-dash/src/web/lib/utils.ts +114 -0
- package/bizar-dash/src/web/lib/ws.ts +176 -0
- package/bizar-dash/src/web/main.tsx +34 -0
- package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
- package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
- package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
- package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
- package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
- package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
- package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
- package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
- package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
- package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
- package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
- package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
- package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
- package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
- package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
- package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
- package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
- package/bizar-dash/src/web/mobile.html +20 -0
- package/bizar-dash/src/web/mobile.tsx +13 -0
- package/bizar-dash/src/web/styles/chat.css +198 -0
- package/bizar-dash/src/web/styles/glyphs.css +960 -0
- package/bizar-dash/src/web/styles/main.css +8855 -0
- package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
- package/bizar-dash/src/web/styles/mobile.css +1960 -0
- package/bizar-dash/src/web/views/Activity.tsx +1203 -0
- package/bizar-dash/src/web/views/Agents.tsx +701 -0
- package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
- package/bizar-dash/src/web/views/Chat.tsx +190 -0
- package/bizar-dash/src/web/views/Config.tsx +2063 -0
- package/bizar-dash/src/web/views/History.tsx +335 -0
- package/bizar-dash/src/web/views/ModView.tsx +207 -0
- package/bizar-dash/src/web/views/Mods.tsx +693 -0
- package/bizar-dash/src/web/views/Overview.tsx +812 -0
- package/bizar-dash/src/web/views/Providers.tsx +302 -0
- package/bizar-dash/src/web/views/Schedules.tsx +802 -0
- package/bizar-dash/src/web/views/Settings.tsx +1787 -0
- package/bizar-dash/src/web/views/Skills.tsx +431 -0
- package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
- package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
- package/bizar-dash/templates/mod/FORMAT.md +76 -0
- package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
- package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
- package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
- package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
- package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
- package/bizar-dash/tests/memory-cli.test.mjs +542 -0
- package/bizar-dash/tests/memory-config.test.mjs +422 -0
- package/bizar-dash/tests/memory-git.test.mjs +246 -0
- package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
- package/bizar-dash/tests/memory-schema.test.mjs +198 -0
- package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
- package/bizar-dash/tests/memory-store.test.mjs +390 -0
- package/bizar-dash/tests/memory-sync.test.mjs +141 -0
- package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
- package/bizar-dash/tests/mod-security.test.mjs +273 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
- package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
- package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
- package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
- package/bizar-dash/tests/path-safe.test.mjs +108 -0
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
- package/bizar-dash/tests/smoke-v2.mjs +212 -0
- package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
- package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
- package/bizar-dash/tests/yaml.test.mjs +128 -0
- package/bizar-dash/tsconfig.json +23 -0
- package/bizar-dash/vite.config.ts +27 -0
- package/cli/atomic.mjs +73 -0
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +44 -47
- package/cli/bootstrap.mjs +1 -1
- package/cli/copy.mjs +25 -16
- package/cli/dev-link.mjs +17 -1
- package/cli/doctor.mjs +4 -4
- package/cli/doctor.test.mjs +2 -2
- package/cli/init.mjs +135 -1
- package/cli/install.mjs +31 -39
- package/cli/memory-constants.mjs +59 -0
- package/cli/memory.mjs +1436 -0
- package/cli/service.mjs +5 -6
- package/cli/utils.mjs +6 -3
- package/config/AGENTS.md +7 -7
- package/config/agents/_shared/AGENT_BASELINE.md +59 -61
- package/config/opencode.json +13 -38
- package/config/skills/memory-protocol/SKILL.md +105 -0
- package/config/skills/obsidian/SKILL.md +270 -65
- package/install.sh +59 -19
- package/package.json +49 -14
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +80 -0
- package/packages/sdk/src/client.ts +183 -0
- package/packages/sdk/src/errors.ts +120 -0
- package/packages/sdk/src/events.ts +153 -0
- package/packages/sdk/src/index.ts +63 -0
- package/packages/sdk/src/types.ts +176 -0
- package/packages/sdk/src/version.ts +4 -0
- package/packages/sdk/tests/client.test.ts +217 -0
- package/packages/sdk/tests/errors.test.ts +108 -0
- package/packages/sdk/tests/events.test.ts +139 -0
- package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
- package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
- package/packages/sdk/tsconfig.json +25 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/plugins/bizar/LICENSE +21 -0
- package/plugins/bizar/README.md +448 -0
- package/plugins/bizar/dist/index.js +29901 -0
- package/plugins/bizar/index.ts +1491 -0
- package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
- package/plugins/bizar/src/background-state.ts +551 -0
- package/plugins/bizar/src/background.ts +1250 -0
- package/plugins/bizar/src/commands-impl.ts +464 -0
- package/plugins/bizar/src/commands.ts +1168 -0
- package/plugins/bizar/src/dashboard-client.ts +233 -0
- package/plugins/bizar/src/event-stream.ts +606 -0
- package/plugins/bizar/src/fingerprint.ts +120 -0
- package/plugins/bizar/src/handoff.ts +79 -0
- package/plugins/bizar/src/http-client.ts +467 -0
- package/plugins/bizar/src/key-rotation.ts +218 -0
- package/plugins/bizar/src/logger.ts +144 -0
- package/plugins/bizar/src/loop.ts +176 -0
- package/plugins/bizar/src/opencode-runner.ts +390 -0
- package/plugins/bizar/src/options.ts +421 -0
- package/plugins/bizar/src/plan-fs.ts +323 -0
- package/plugins/bizar/src/reasoning-clean.ts +454 -0
- package/plugins/bizar/src/report.ts +178 -0
- package/plugins/bizar/src/research-prompt.ts +35 -0
- package/plugins/bizar/src/serve-info.ts +228 -0
- package/plugins/bizar/src/serve.ts +496 -0
- package/plugins/bizar/src/settings.ts +349 -0
- package/plugins/bizar/src/state.ts +298 -0
- package/plugins/bizar/src/tools/bg-collect.ts +104 -0
- package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
- package/plugins/bizar/src/tools/bg-kill.ts +87 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
- package/plugins/bizar/src/tools/bg-status.ts +99 -0
- package/plugins/bizar/src/tools/open-kb.ts +191 -0
- package/plugins/bizar/src/tools/plan-action.ts +767 -0
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
- package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
- package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
- package/plugins/bizar/tests/background-state.test.ts +277 -0
- package/plugins/bizar/tests/background.test.ts +402 -0
- package/plugins/bizar/tests/block.test.ts +195 -0
- package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
- package/plugins/bizar/tests/commands-impl.test.ts +442 -0
- package/plugins/bizar/tests/commands.test.ts +584 -0
- package/plugins/bizar/tests/config.test.ts +122 -0
- package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
- package/plugins/bizar/tests/dispose.test.ts +336 -0
- package/plugins/bizar/tests/event-stream.test.ts +409 -0
- package/plugins/bizar/tests/event.test.ts +262 -0
- package/plugins/bizar/tests/fingerprint.test.ts +162 -0
- package/plugins/bizar/tests/http-client.test.ts +404 -0
- package/plugins/bizar/tests/init-helpers.test.ts +203 -0
- package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
- package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
- package/plugins/bizar/tests/key-rotation.test.ts +396 -0
- package/plugins/bizar/tests/loop.test.ts +397 -0
- package/plugins/bizar/tests/options.test.ts +276 -0
- package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
- package/plugins/bizar/tests/serve.test.ts +339 -0
- package/plugins/bizar/tests/settings.test.ts +351 -0
- package/plugins/bizar/tests/stall-think.test.ts +750 -0
- package/plugins/bizar/tests/state.test.ts +276 -0
- package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
- package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
- package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
- package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
- package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
- package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
- package/plugins/bizar/tsconfig.json +29 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/cli/dashboard-ports.mjs
|
|
3
|
+
*
|
|
4
|
+
* v3.20.7 — Discover every running Bizar dashboard on this machine,
|
|
5
|
+
* classify it as healthy / zombie / orphan / dead, and provide cleanup
|
|
6
|
+
* helpers. Solves the long-standing "why is there a dashboard on
|
|
7
|
+
* port 4321 that I can't reach but won't go away?" problem.
|
|
8
|
+
*
|
|
9
|
+
* We can't trust the port file (it tracks only the most-recent start,
|
|
10
|
+
* written to the last successful boot). Use a combination of:
|
|
11
|
+
* 1. Read the canonical PID from ~/.config/bizar/dashboard.pid
|
|
12
|
+
* 2. Scan all node processes via `ps` for `bizar dash start` /
|
|
13
|
+
* `@polderlabs/bizar-dash/src/cli.mjs start`
|
|
14
|
+
* 3. For each candidate PID, walk /proc/<pid>/fd → socket inodes →
|
|
15
|
+
* /proc/net/tcp to find its listening port (Linux only)
|
|
16
|
+
* 4. TCP-probe each candidate port (1.5s timeout, transport-only)
|
|
17
|
+
* 5. HTTP-probe each reachable port's /api/health endpoint
|
|
18
|
+
*
|
|
19
|
+
* Result classification:
|
|
20
|
+
* - 'healthy' PID alive + /api/health returns ok:true
|
|
21
|
+
* - 'zombie' PID alive + port accepts TCP but health probe fails
|
|
22
|
+
* (request handler deadlock, event loop starved, etc.)
|
|
23
|
+
* - 'dead' PID does not exist (orphaned port file)
|
|
24
|
+
* - 'orphan' Port responds to health but no PID file knows about it
|
|
25
|
+
* (started by a different process, port file deleted)
|
|
26
|
+
*
|
|
27
|
+
* Exports:
|
|
28
|
+
* - findAllDashboards() → DashboardInstance[]
|
|
29
|
+
* - killDashboard(pid, signal?) → boolean
|
|
30
|
+
* - summarize(instances) → friendly multi-line report
|
|
31
|
+
*
|
|
32
|
+
* Cross-platform: the /proc-fd walk is Linux-only. On macOS/Windows
|
|
33
|
+
* we fall back to canonical-PID + port range scan.
|
|
34
|
+
*/
|
|
35
|
+
import { execSync, spawn } from 'node:child_process';
|
|
36
|
+
import {
|
|
37
|
+
existsSync,
|
|
38
|
+
readFileSync,
|
|
39
|
+
readlinkSync,
|
|
40
|
+
readdirSync,
|
|
41
|
+
writeFileSync,
|
|
42
|
+
unlinkSync,
|
|
43
|
+
} from 'node:fs';
|
|
44
|
+
import { join } from 'node:path';
|
|
45
|
+
import { homedir } from 'node:os';
|
|
46
|
+
import net from 'node:net';
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_PORT_START = 4321;
|
|
49
|
+
export const DEFAULT_PORT_END = 4530;
|
|
50
|
+
export const HEALTH_PROBE_TIMEOUT_MS = 1500;
|
|
51
|
+
export const TCP_PROBE_TIMEOUT_MS = 800;
|
|
52
|
+
|
|
53
|
+
const HOME = homedir();
|
|
54
|
+
const BIZAR_HOME = process.platform === 'win32'
|
|
55
|
+
? join(process.env.APPDATA || HOME, 'bizar')
|
|
56
|
+
: join(HOME, '.config', 'bizar');
|
|
57
|
+
export const PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
|
|
58
|
+
export const PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} DashboardInstance
|
|
62
|
+
* @property {number|null} pid PID of the node process (null if unknown)
|
|
63
|
+
* @property {number|null} port TCP port the dashboard listens on
|
|
64
|
+
* @property {'healthy'|'zombie'|'dead'|'orphan'|'unknown'} state
|
|
65
|
+
* @property {boolean} portReachable TCP probe succeeded
|
|
66
|
+
* @property {boolean} healthOk /api/health returned ok:true
|
|
67
|
+
* @property {string} cmdline Full process command line (or note)
|
|
68
|
+
* @property {'pid-file'|'ps-scan'|'port-scan'} source How we found it
|
|
69
|
+
* @property {boolean} isCanonical Matches the PID file
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
export function readCanonical() {
|
|
73
|
+
let pid = null;
|
|
74
|
+
let port = null;
|
|
75
|
+
try {
|
|
76
|
+
const v = parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
77
|
+
if (Number.isFinite(v) && v > 0) pid = v;
|
|
78
|
+
} catch { /* ignore */ }
|
|
79
|
+
try {
|
|
80
|
+
const v = parseInt(readFileSync(PORT_FILE, 'utf8').trim(), 10);
|
|
81
|
+
if (Number.isFinite(v) && v > 0 && v <= 65535) port = v;
|
|
82
|
+
} catch { /* ignore */ }
|
|
83
|
+
return { pid, port };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** True if a PID is alive (signal 0 trick). */
|
|
87
|
+
export function pidAlive(pid) {
|
|
88
|
+
if (!pid || !Number.isFinite(pid)) return false;
|
|
89
|
+
try {
|
|
90
|
+
process.kill(pid, 0);
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** TCP connect probe — auth-free, transport-only. */
|
|
98
|
+
export function tcpProbe(port, timeoutMs = TCP_PROBE_TIMEOUT_MS) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const sock = net.createConnection({ port, host: '127.0.0.1' });
|
|
101
|
+
let settled = false;
|
|
102
|
+
const finish = (ok) => {
|
|
103
|
+
if (settled) return;
|
|
104
|
+
settled = true;
|
|
105
|
+
try { sock.destroy(); } catch { /* ignore */ }
|
|
106
|
+
resolve(ok);
|
|
107
|
+
};
|
|
108
|
+
sock.setTimeout(timeoutMs);
|
|
109
|
+
sock.once('connect', () => finish(true));
|
|
110
|
+
sock.once('error', () => finish(false));
|
|
111
|
+
sock.once('timeout', () => finish(false));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** HTTP probe — returns true only if /api/health returns ok:true. */
|
|
116
|
+
export async function httpHealth(port, timeoutMs = HEALTH_PROBE_TIMEOUT_MS) {
|
|
117
|
+
try {
|
|
118
|
+
const ctrl = new AbortController();
|
|
119
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
120
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: ctrl.signal });
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
if (!res.ok) return false;
|
|
123
|
+
let json = null;
|
|
124
|
+
try { json = await res.json(); } catch { return false; }
|
|
125
|
+
return json && json.ok === true;
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find the listening TCP port for a PID by walking /proc/<pid>/fd and
|
|
133
|
+
* matching socket inodes against /proc/net/tcp. Linux-only.
|
|
134
|
+
* Returns null on other platforms or if the PID has no listening socket.
|
|
135
|
+
*/
|
|
136
|
+
export function pidListeningPort(pid) {
|
|
137
|
+
if (process.platform !== 'linux') return null;
|
|
138
|
+
try {
|
|
139
|
+
const fdDir = `/proc/${pid}/fd`;
|
|
140
|
+
if (!existsSync(fdDir)) return null;
|
|
141
|
+
const inodes = new Set();
|
|
142
|
+
for (const fd of readdirSync(fdDir)) {
|
|
143
|
+
try {
|
|
144
|
+
const target = readlinkSync(join(fdDir, fd));
|
|
145
|
+
const m = target.match(/socket:\[(\d+)\]/);
|
|
146
|
+
if (m) inodes.add(m[1]);
|
|
147
|
+
} catch { /* stale fd — ignore */ }
|
|
148
|
+
}
|
|
149
|
+
if (inodes.size === 0) return null;
|
|
150
|
+
const tcp = readFileSync('/proc/net/tcp', 'utf8');
|
|
151
|
+
// 127.0.0.1 in little-endian hex = 0100007F
|
|
152
|
+
// 0.0.0.0 in little-endian hex = 00000000
|
|
153
|
+
const LOCAL_IPS = new Set(['0100007F', '00000000']);
|
|
154
|
+
for (const line of tcp.split('\n').slice(1)) {
|
|
155
|
+
const cols = line.trim().split(/\s+/);
|
|
156
|
+
if (cols.length < 10) continue;
|
|
157
|
+
const local = cols[1];
|
|
158
|
+
if (!local.includes(':')) continue;
|
|
159
|
+
const [ipHex, portHex] = local.split(':');
|
|
160
|
+
if (!LOCAL_IPS.has(ipHex)) continue;
|
|
161
|
+
const port = parseInt(portHex, 16);
|
|
162
|
+
const inode = cols[9];
|
|
163
|
+
if (inodes.has(inode)) return port;
|
|
164
|
+
}
|
|
165
|
+
} catch { /* ignore */ }
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `ps`-based fallback to find every node process whose argv mentions
|
|
171
|
+
* the Bizar dashboard CLI. Returns a list of { pid, cmdline }.
|
|
172
|
+
*
|
|
173
|
+
* v3.20.14 — tightened the pattern + added an explicit cmdline
|
|
174
|
+
* re-verification step that the caller MUST do before trusting the
|
|
175
|
+
* match. The previous version matched anything containing the substring
|
|
176
|
+
* "bizar dash" or "@polderlabs/bizar-dash/src/cli.mjs" — which is fine
|
|
177
|
+
* for finding real dashboards but breaks when:
|
|
178
|
+
* - The PID is recycled to a different process between ps-scan and
|
|
179
|
+
* `pidAlive(pid)` (Linux recycles PIDs in milliseconds).
|
|
180
|
+
* - A previous run wrote a stale `dashboard.pid` file referencing a
|
|
181
|
+
* PID that's now owned by an unrelated process (chrome-headless-shell
|
|
182
|
+
* renderer, ssh session, etc.).
|
|
183
|
+
* - The cmdline contains a path that no longer exists on disk
|
|
184
|
+
* (e.g. /home/drb0rk/.local/npm/bin/bizar after a clean npm install).
|
|
185
|
+
*
|
|
186
|
+
* The fix: only record a ps-scan match if the cmdline actually looks
|
|
187
|
+
* like a node-launched Bizar dashboard CLI. The `cmdlineLooksLikeDashboard`
|
|
188
|
+
* helper does the structural check (node binary + cli.mjs OR a known
|
|
189
|
+
* `bizar` bin path + `dash start`). Then the caller's `pidAlive`
|
|
190
|
+
* step re-reads /proc/<pid>/cmdline and calls the same helper to
|
|
191
|
+
* confirm the PID hasn't been recycled.
|
|
192
|
+
*
|
|
193
|
+
* Skips our own process (the runner) so the CLI doesn't see itself.
|
|
194
|
+
*/
|
|
195
|
+
export function psScanDashboards() {
|
|
196
|
+
const out = new Map(); // pid → cmdline
|
|
197
|
+
if (process.platform === 'win32') {
|
|
198
|
+
// tasklist | findstr — too platform-specific for now; users on
|
|
199
|
+
// Windows who hit this can restart their machine. The canonical
|
|
200
|
+
// PID file path still works on Windows.
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const result = execSync(
|
|
205
|
+
// Match `bizar dash` or `@polderlabs/bizar-dash/src/cli.mjs`. The
|
|
206
|
+
// pipe through grep -v grep is still needed to suppress grep's
|
|
207
|
+
// own argv from matching the regex.
|
|
208
|
+
`ps -eo pid=,args= 2>/dev/null | grep -E '(bizar[ ]+dash|@polderlabs/bizar-dash/src/cli\\.mjs)' | grep -v grep || true`,
|
|
209
|
+
{ encoding: 'utf8', timeout: 5000 },
|
|
210
|
+
);
|
|
211
|
+
for (const line of result.split('\n')) {
|
|
212
|
+
const trimmed = line.trim();
|
|
213
|
+
if (!trimmed) continue;
|
|
214
|
+
// Format: "<pid> <args...>" — pid is the first whitespace-delimited token.
|
|
215
|
+
const m = trimmed.match(/^(\d+)\s+(.+)$/);
|
|
216
|
+
if (!m) continue;
|
|
217
|
+
const pid = parseInt(m[1], 10);
|
|
218
|
+
if (!Number.isFinite(pid)) continue;
|
|
219
|
+
if (pid === process.pid) continue; // skip our own process
|
|
220
|
+
const cmdline = m[2].trim();
|
|
221
|
+
// Structural check: only keep matches that look like a real
|
|
222
|
+
// dashboard launch. Anything else is a false positive (PID
|
|
223
|
+
// recycled, or a process whose argv happens to contain the
|
|
224
|
+
// words "bizar dash" — e.g. a stale node process or a shell
|
|
225
|
+
// command that includes the words in a comment).
|
|
226
|
+
if (!cmdlineLooksLikeDashboard(cmdline)) continue;
|
|
227
|
+
out.set(pid, cmdline);
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// ps not available — fall back to canonical only
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Structural check: does this cmdline look like a Bizar dashboard
|
|
237
|
+
* launch? We accept:
|
|
238
|
+
* - `node <anything>/@polderlabs/bizar-dash/src/cli.mjs [start|tui|...]`
|
|
239
|
+
* - `<path>/bizar dash <subcommand>` where `<path>` is a real-looking
|
|
240
|
+
* filesystem path (contains `/` or starts with `.`).
|
|
241
|
+
*
|
|
242
|
+
* Reject anything that doesn't match those patterns (e.g. a `bash -c`
|
|
243
|
+
* shell command whose body mentions `bizar dash` in a comment).
|
|
244
|
+
*
|
|
245
|
+
* Implementation note: we anchor the `bizar` bin to a path-like prefix
|
|
246
|
+
* by requiring it to be preceded by either the start of the cmdline,
|
|
247
|
+
* a `/`, or a `\`. This rejects strings like
|
|
248
|
+
* `bash -c "echo I want to run bizar dash start"` where `bizar dash
|
|
249
|
+
* start` is mid-string inside a quoted argument — the `bizar` is
|
|
250
|
+
* preceded by a space, not by a path separator.
|
|
251
|
+
*/
|
|
252
|
+
export function cmdlineLooksLikeDashboard(cmdline) {
|
|
253
|
+
if (!cmdline || typeof cmdline !== 'string') return false;
|
|
254
|
+
// Direct CLI invocation: @polderlabs/bizar-dash/src/cli.mjs <verb>
|
|
255
|
+
if (/\/@polderlabs\/bizar-dash\/src\/cli\.mjs\b/.test(cmdline)) return true;
|
|
256
|
+
// Wrapped via the `bizar` bin: <path>/bizar dash <subcommand>.
|
|
257
|
+
// - The `bizar` token must be preceded by a path separator (so it's
|
|
258
|
+
// a binary path, not an arbitrary word in a comment).
|
|
259
|
+
// - The `dash` and subcommand must be whitespace-delimited argv.
|
|
260
|
+
if (/(?:^|[\/\\])\S*bizar\s+dash\s+(?:start|tui|stop|status|cleanup)\b/.test(cmdline)) return true;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Re-read /proc/<pid>/cmdline and verify the cmdline still looks like
|
|
266
|
+
* a dashboard launch. Linux recycles PIDs aggressively — between
|
|
267
|
+
* `psScanDashboards` returning a PID and `pidAlive(pid)` succeeding,
|
|
268
|
+
* the PID may have been reassigned to an unrelated process. We catch
|
|
269
|
+
* that by reading /proc/<pid>/cmdline fresh and re-applying the
|
|
270
|
+
* structural check. Returns the cmdline string on success, null on
|
|
271
|
+
* failure (PID dead, /proc unreadable, or cmdline no longer matches).
|
|
272
|
+
*/
|
|
273
|
+
export function verifyCmdlineAtProbeTime(pid) {
|
|
274
|
+
if (!pid) return null;
|
|
275
|
+
try {
|
|
276
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim();
|
|
277
|
+
if (cmdlineLooksLikeDashboard(cmdline)) return cmdline;
|
|
278
|
+
return null;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Discover every Bizar dashboard process on this machine.
|
|
286
|
+
*
|
|
287
|
+
* @param {Object} opts
|
|
288
|
+
* @param {number} [opts.portStart=4321] Start of port scan range
|
|
289
|
+
* @param {number} [opts.portEnd=4530] End of port scan range
|
|
290
|
+
* @param {boolean} [opts.scanPorts=true] If true, probe ports in [start, end] for orphans
|
|
291
|
+
* @returns {Promise<DashboardInstance[]>}
|
|
292
|
+
*/
|
|
293
|
+
export async function findAllDashboards({
|
|
294
|
+
portStart = DEFAULT_PORT_START,
|
|
295
|
+
portEnd = DEFAULT_PORT_END,
|
|
296
|
+
scanPorts = true,
|
|
297
|
+
} = {}) {
|
|
298
|
+
const canonical = readCanonical();
|
|
299
|
+
/** @type {Map<number, DashboardInstance>} */
|
|
300
|
+
const byPid = new Map();
|
|
301
|
+
|
|
302
|
+
// 1. Canonical PID — even if dead, record it so the user can clean up.
|
|
303
|
+
if (canonical.pid) {
|
|
304
|
+
const alive = pidAlive(canonical.pid);
|
|
305
|
+
let cmdline = '';
|
|
306
|
+
try { cmdline = readFileSync(`/proc/${canonical.pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim(); } catch {}
|
|
307
|
+
if (!cmdline) cmdline = '(no /proc entry — process likely exited)';
|
|
308
|
+
// v3.20.14 — only treat the canonical PID as canonical if the
|
|
309
|
+
// current cmdline still looks like a dashboard launch. A recycled
|
|
310
|
+
// PID (now owned by chrome, ssh, etc.) should NOT count as the
|
|
311
|
+
// canonical dashboard — the PID file is stale.
|
|
312
|
+
const cmdlineMatches = cmdlineLooksLikeDashboard(cmdline);
|
|
313
|
+
byPid.set(canonical.pid, {
|
|
314
|
+
pid: canonical.pid,
|
|
315
|
+
port: canonical.port,
|
|
316
|
+
// If PID is alive but cmdline is no longer a dashboard, the PID
|
|
317
|
+
// has been recycled. Mark as 'dead' so cleanup clears the
|
|
318
|
+
// stale PID file. Same if PID is genuinely dead.
|
|
319
|
+
state: alive && cmdlineMatches ? 'unknown' : 'dead',
|
|
320
|
+
portReachable: false,
|
|
321
|
+
healthOk: false,
|
|
322
|
+
cmdline,
|
|
323
|
+
// Only canonical if PID file path AND cmdline both look real.
|
|
324
|
+
// This prevents the "Found 1 dashboard: [healthy] pid 938619
|
|
325
|
+
// from ps-scan" trap where the PID file's PID has been recycled
|
|
326
|
+
// to an unrelated process.
|
|
327
|
+
isCanonical: cmdlineMatches,
|
|
328
|
+
source: 'pid-file',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 2. ps scan — find every node process running the dashboard CLI.
|
|
333
|
+
const psResults = psScanDashboards();
|
|
334
|
+
for (const [pid, cmdline] of psResults) {
|
|
335
|
+
if (byPid.has(pid)) {
|
|
336
|
+
// Already known — backfill cmdline if it was empty.
|
|
337
|
+
const existing = byPid.get(pid);
|
|
338
|
+
if (!existing.cmdline || existing.cmdline.startsWith('(')) {
|
|
339
|
+
existing.cmdline = cmdline;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
byPid.set(pid, {
|
|
344
|
+
pid,
|
|
345
|
+
port: null,
|
|
346
|
+
state: 'unknown',
|
|
347
|
+
portReachable: false,
|
|
348
|
+
healthOk: false,
|
|
349
|
+
cmdline,
|
|
350
|
+
source: 'ps-scan',
|
|
351
|
+
isCanonical: false,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 3. Probe each PID's listening port + health.
|
|
356
|
+
for (const inst of byPid.values()) {
|
|
357
|
+
if (!pidAlive(inst.pid)) {
|
|
358
|
+
inst.state = 'dead';
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
// v3.20.14 — re-read /proc/<pid>/cmdline fresh and re-verify it
|
|
362
|
+
// still looks like a dashboard launch. Linux recycles PIDs
|
|
363
|
+
// aggressively; a PID that matched ps-scan a moment ago may
|
|
364
|
+
// now belong to an unrelated process (chrome-headless-shell
|
|
365
|
+
// renderer, ssh session, etc.). If the cmdline no longer matches
|
|
366
|
+
// the dashboard pattern, the PID is recycled — mark it dead.
|
|
367
|
+
if (inst.source === 'ps-scan') {
|
|
368
|
+
const fresh = verifyCmdlineAtProbeTime(inst.pid);
|
|
369
|
+
if (fresh === null) {
|
|
370
|
+
inst.state = 'dead';
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
// Update cmdline in case it changed (the argv we saw at scan time
|
|
374
|
+
// might have been a stale /proc snapshot — extremely rare but
|
|
375
|
+
// possible during a process exec).
|
|
376
|
+
if (fresh !== inst.cmdline) inst.cmdline = fresh;
|
|
377
|
+
}
|
|
378
|
+
// Resolve port: prefer /proc fd walk; fall back to canonical port for PID file match.
|
|
379
|
+
if (!inst.port) {
|
|
380
|
+
const discovered = pidListeningPort(inst.pid);
|
|
381
|
+
if (discovered) inst.port = discovered;
|
|
382
|
+
}
|
|
383
|
+
if (!inst.port) continue; // can't probe without a port
|
|
384
|
+
inst.portReachable = await tcpProbe(inst.port);
|
|
385
|
+
if (!inst.portReachable) continue;
|
|
386
|
+
inst.healthOk = await httpHealth(inst.port);
|
|
387
|
+
inst.state = inst.healthOk ? 'healthy' : 'zombie';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 4. Port-range scan for orphans — ports that respond to health but
|
|
391
|
+
// no PID claimed them.
|
|
392
|
+
if (scanPorts) {
|
|
393
|
+
const claimed = new Set(
|
|
394
|
+
Array.from(byPid.values()).filter(i => i.port).map(i => i.port),
|
|
395
|
+
);
|
|
396
|
+
for (let p = portStart; p <= portEnd; p++) {
|
|
397
|
+
if (claimed.has(p)) continue;
|
|
398
|
+
const reachable = await tcpProbe(p);
|
|
399
|
+
if (!reachable) continue;
|
|
400
|
+
const healthOk = await httpHealth(p);
|
|
401
|
+
if (!healthOk) continue;
|
|
402
|
+
// We found a port responding to /api/health that no PID claimed.
|
|
403
|
+
// This is an orphan — typically started by a different user or
|
|
404
|
+
// after the port file was deleted.
|
|
405
|
+
byPid.set(-p, {
|
|
406
|
+
pid: null,
|
|
407
|
+
port: p,
|
|
408
|
+
state: 'orphan',
|
|
409
|
+
portReachable: true,
|
|
410
|
+
healthOk: true,
|
|
411
|
+
cmdline: '(unknown — no PID claimed this port)',
|
|
412
|
+
source: 'port-scan',
|
|
413
|
+
isCanonical: false,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return Array.from(byPid.values()).sort((a, b) => {
|
|
419
|
+
// Canonical first, then by port, then by pid
|
|
420
|
+
if (a.isCanonical !== b.isCanonical) return a.isCanonical ? -1 : 1;
|
|
421
|
+
if (a.port !== b.port) return (a.port || 0) - (b.port || 0);
|
|
422
|
+
return (a.pid || 0) - (b.pid || 0);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Send a signal to a PID. Returns true if the signal was delivered.
|
|
428
|
+
* `signal` defaults to 'SIGTERM' (graceful). Use 'SIGKILL' for stuck procs.
|
|
429
|
+
*/
|
|
430
|
+
export function killDashboard(pid, signal = 'SIGTERM') {
|
|
431
|
+
if (!pid) return false;
|
|
432
|
+
try {
|
|
433
|
+
process.kill(pid, signal);
|
|
434
|
+
return true;
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Clear the canonical PID/port files (only if the PID is dead or matches).
|
|
442
|
+
*/
|
|
443
|
+
export function clearCanonicalFiles(onlyIfPid = null) {
|
|
444
|
+
if (onlyIfPid !== null) {
|
|
445
|
+
const canonical = readCanonical();
|
|
446
|
+
if (canonical.pid !== onlyIfPid) return false;
|
|
447
|
+
}
|
|
448
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
449
|
+
try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Wait for a PID to exit (poll). Returns true if it exited within
|
|
455
|
+
* `timeoutMs`, false otherwise.
|
|
456
|
+
*/
|
|
457
|
+
export async function waitForExit(pid, timeoutMs = 5000, intervalMs = 200) {
|
|
458
|
+
const start = Date.now();
|
|
459
|
+
while (Date.now() - start < timeoutMs) {
|
|
460
|
+
if (!pidAlive(pid)) return true;
|
|
461
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
462
|
+
}
|
|
463
|
+
return !pidAlive(pid);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Render a friendly multi-line summary of dashboard instances for the CLI.
|
|
468
|
+
*/
|
|
469
|
+
export function summarize(instances) {
|
|
470
|
+
if (!instances.length) {
|
|
471
|
+
return 'No dashboards detected.';
|
|
472
|
+
}
|
|
473
|
+
const lines = [];
|
|
474
|
+
lines.push(`Found ${instances.length} dashboard${instances.length === 1 ? '' : 's'}:`);
|
|
475
|
+
lines.push('');
|
|
476
|
+
for (const inst of instances) {
|
|
477
|
+
const tag = inst.isCanonical ? '★ canonical' : ' ';
|
|
478
|
+
const stateTag = `[${inst.state.padEnd(8)}]`;
|
|
479
|
+
const portStr = inst.port ? `:${inst.port}` : '(no port)';
|
|
480
|
+
const pidStr = inst.pid ? `pid ${inst.pid}` : 'pid ?';
|
|
481
|
+
lines.push(` ${tag} ${stateTag} ${pidStr.padEnd(12)} ${portStr.padEnd(10)} ${inst.source}`);
|
|
482
|
+
if (inst.cmdline) {
|
|
483
|
+
const cl = inst.cmdline.length > 100 ? inst.cmdline.slice(0, 97) + '...' : inst.cmdline;
|
|
484
|
+
lines.push(` ${cl}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return lines.join('\n');
|
|
488
|
+
}
|