@aion0/forge 0.5.26 → 0.5.27
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/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
- package/RELEASE_NOTES.md +11 -28
- package/app/api/terminal-bell/route.ts +6 -2
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Executor — runs plugin actions (http, poll, shell, script).
|
|
3
|
+
*
|
|
4
|
+
* Resolves templates ({{config.x}}, {{params.x}}) and executes the action.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import type { PluginAction, PluginActionResult, InstalledPlugin } from './types';
|
|
9
|
+
|
|
10
|
+
// ─── Template Resolution ─────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function resolveTemplate(template: string, ctx: Record<string, any>): string {
|
|
13
|
+
return template.replace(/\{\{(\w+)\.(\w+)\}\}/g, (_, scope, key) => {
|
|
14
|
+
return ctx[scope]?.[key] ?? '';
|
|
15
|
+
}).replace(/\{\{(\w+)\s*\|\s*json\}\}/g, (_, scope) => {
|
|
16
|
+
return JSON.stringify(ctx[scope] || {});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveObject(obj: Record<string, string> | undefined, ctx: Record<string, any>): Record<string, string> {
|
|
21
|
+
if (!obj) return {};
|
|
22
|
+
const result: Record<string, string> = {};
|
|
23
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
24
|
+
result[k] = resolveTemplate(v, ctx);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── JSONPath-like extraction ────────────────────────────
|
|
30
|
+
|
|
31
|
+
function extractValue(data: any, path: string): any {
|
|
32
|
+
if (path === '$body' || path === '$stdout') return typeof data === 'string' ? data : JSON.stringify(data);
|
|
33
|
+
if (path.startsWith('$.')) {
|
|
34
|
+
const keys = path.slice(2).split('.');
|
|
35
|
+
let current = data;
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
if (current == null) return undefined;
|
|
38
|
+
current = current[key];
|
|
39
|
+
}
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractOutputs(data: any, outputSpec: Record<string, string> | undefined): Record<string, any> {
|
|
46
|
+
if (!outputSpec) return { result: data };
|
|
47
|
+
const result: Record<string, any> = {};
|
|
48
|
+
for (const [name, path] of Object.entries(outputSpec)) {
|
|
49
|
+
result[name] = extractValue(data, path);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Executors ───────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
async function executeHttp(action: PluginAction, ctx: Record<string, any>): Promise<PluginActionResult> {
|
|
57
|
+
const url = resolveTemplate(action.url || '', ctx);
|
|
58
|
+
const method = (action.method || 'GET').toUpperCase();
|
|
59
|
+
const headers = resolveObject(action.headers, ctx);
|
|
60
|
+
const body = action.body ? resolveTemplate(action.body, ctx) : undefined;
|
|
61
|
+
|
|
62
|
+
const startTime = Date.now();
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
method,
|
|
66
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
67
|
+
body: method !== 'GET' ? body : undefined,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let data: any;
|
|
71
|
+
const contentType = res.headers.get('content-type') || '';
|
|
72
|
+
if (contentType.includes('json')) {
|
|
73
|
+
data = await res.json();
|
|
74
|
+
} else {
|
|
75
|
+
data = await res.text();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
ok: res.ok,
|
|
80
|
+
output: extractOutputs(data, action.output),
|
|
81
|
+
rawResponse: typeof data === 'string' ? data : JSON.stringify(data),
|
|
82
|
+
duration: Date.now() - startTime,
|
|
83
|
+
};
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
return { ok: false, output: {}, error: err.message, duration: Date.now() - startTime };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function executePoll(action: PluginAction, ctx: Record<string, any>): Promise<PluginActionResult> {
|
|
90
|
+
const url = resolveTemplate(action.url || '', ctx);
|
|
91
|
+
const headers = resolveObject(action.headers, ctx);
|
|
92
|
+
const interval = (action.interval || 30) * 1000;
|
|
93
|
+
const timeout = (action.timeout || 1800) * 1000;
|
|
94
|
+
const untilExpr = action.until || '$.result != null';
|
|
95
|
+
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
|
|
98
|
+
while (Date.now() - startTime < timeout) {
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(url, { headers });
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
|
|
103
|
+
// Evaluate condition
|
|
104
|
+
const conditionMet = evaluateCondition(data, untilExpr);
|
|
105
|
+
if (conditionMet) {
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
output: extractOutputs(data, action.output),
|
|
109
|
+
rawResponse: JSON.stringify(data),
|
|
110
|
+
duration: Date.now() - startTime,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
|
|
115
|
+
// Wait before next poll
|
|
116
|
+
await new Promise(r => setTimeout(r, interval));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { ok: false, output: {}, error: 'Poll timeout', duration: Date.now() - startTime };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function evaluateCondition(data: any, expr: string): boolean {
|
|
123
|
+
// Simple condition parser: "$.field != null", "$.field == value"
|
|
124
|
+
const match = expr.match(/^(\$\.[.\w]+)\s*(==|!=|>|<)\s*(.+)$/);
|
|
125
|
+
if (!match) return false;
|
|
126
|
+
const [, path, op, expected] = match;
|
|
127
|
+
const actual = extractValue(data, path);
|
|
128
|
+
const exp = expected === 'null' ? null : expected === 'true' ? true : expected === 'false' ? false : expected;
|
|
129
|
+
|
|
130
|
+
switch (op) {
|
|
131
|
+
case '==': return actual == exp;
|
|
132
|
+
case '!=': return actual != exp;
|
|
133
|
+
case '>': return Number(actual) > Number(exp);
|
|
134
|
+
case '<': return Number(actual) < Number(exp);
|
|
135
|
+
default: return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function executeShell(action: PluginAction, ctx: Record<string, any>): Promise<PluginActionResult> {
|
|
140
|
+
const command = resolveTemplate(action.command || '', ctx);
|
|
141
|
+
const rawCwd = action.cwd ? resolveTemplate(action.cwd, ctx) : '';
|
|
142
|
+
const cwd = rawCwd || undefined;
|
|
143
|
+
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
const timeout = (action.timeout || 300) * 1000;
|
|
146
|
+
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
let resolved = false;
|
|
149
|
+
const done = (result: PluginActionResult) => {
|
|
150
|
+
if (resolved) return;
|
|
151
|
+
resolved = true;
|
|
152
|
+
resolve(result);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Use spawn with detached: true so the child gets its own process group.
|
|
157
|
+
// This prevents crashes/signals in the child (e.g., Playwright browser crash)
|
|
158
|
+
// from propagating to Forge's process group and killing sibling services.
|
|
159
|
+
const child = spawn('/bin/sh', ['-c', command], {
|
|
160
|
+
cwd,
|
|
161
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
162
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
163
|
+
detached: true,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let stdout = '';
|
|
167
|
+
let stderr = '';
|
|
168
|
+
|
|
169
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
170
|
+
stdout += chunk.toString('utf-8');
|
|
171
|
+
// Cap buffer to prevent memory issues
|
|
172
|
+
if (stdout.length > 10 * 1024 * 1024) {
|
|
173
|
+
stdout = stdout.slice(-5 * 1024 * 1024);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
177
|
+
stderr += chunk.toString('utf-8');
|
|
178
|
+
if (stderr.length > 10 * 1024 * 1024) {
|
|
179
|
+
stderr = stderr.slice(-5 * 1024 * 1024);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
console.log(`[plugin-shell] pid=${child.pid} pgid=new command=${command.slice(0, 80)}`);
|
|
184
|
+
|
|
185
|
+
child.on('error', (e) => {
|
|
186
|
+
console.log(`[plugin-shell] pid=${child.pid} error: ${e.message}`);
|
|
187
|
+
done({
|
|
188
|
+
ok: false,
|
|
189
|
+
output: {},
|
|
190
|
+
error: `Process error: ${e.message}`,
|
|
191
|
+
duration: Date.now() - startTime,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Use 'close' instead of 'exit' to ensure all stdout/stderr data is collected
|
|
196
|
+
child.on('close', (code, signal) => {
|
|
197
|
+
child.unref();
|
|
198
|
+
console.log(`[plugin-shell] pid=${child.pid} closed code=${code} signal=${signal} stdout=${stdout.length}b stderr=${stderr.length}b`);
|
|
199
|
+
const combined = (stdout || stderr || '').trim();
|
|
200
|
+
// Treat SIGTERM (143) with output as a normal completion — many test runners
|
|
201
|
+
// and complex commands send SIGTERM to their process group during cleanup,
|
|
202
|
+
// which kills the shell wrapper but doesn't indicate a real failure.
|
|
203
|
+
const killedBySignal = code === null || code === 143 || code === 130;
|
|
204
|
+
const hasOutput = combined.length > 0;
|
|
205
|
+
const effectiveOk = code === 0 || (killedBySignal && hasOutput);
|
|
206
|
+
|
|
207
|
+
done({
|
|
208
|
+
ok: effectiveOk,
|
|
209
|
+
output: extractOutputs(combined, action.output),
|
|
210
|
+
error: effectiveOk ? undefined : (signal ? `Killed by ${signal}` : `Exit code ${code}`),
|
|
211
|
+
rawResponse: combined.slice(0, 5000),
|
|
212
|
+
duration: Date.now() - startTime,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Timeout: kill the child's process group
|
|
217
|
+
const timer = setTimeout(() => {
|
|
218
|
+
try { process.kill(-child.pid!, 'SIGTERM'); } catch {}
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
try { process.kill(-child.pid!, 'SIGKILL'); } catch {}
|
|
221
|
+
}, 3000);
|
|
222
|
+
done({
|
|
223
|
+
ok: false,
|
|
224
|
+
output: {},
|
|
225
|
+
error: `Command timed out after ${timeout / 1000}s`,
|
|
226
|
+
rawResponse: (stderr || stdout || '').slice(0, 5000),
|
|
227
|
+
duration: Date.now() - startTime,
|
|
228
|
+
});
|
|
229
|
+
}, timeout);
|
|
230
|
+
// Don't let the timer keep the process alive
|
|
231
|
+
timer.unref();
|
|
232
|
+
|
|
233
|
+
child.on('exit', () => clearTimeout(timer));
|
|
234
|
+
} catch (e: any) {
|
|
235
|
+
done({
|
|
236
|
+
ok: false,
|
|
237
|
+
output: {},
|
|
238
|
+
error: `Failed to spawn: ${e.message}`,
|
|
239
|
+
duration: Date.now() - startTime,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Public API ──────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Execute a plugin action.
|
|
249
|
+
* @param plugin - Installed plugin instance
|
|
250
|
+
* @param actionName - Action to execute (e.g., 'trigger', 'wait')
|
|
251
|
+
* @param params - Per-use parameters
|
|
252
|
+
*/
|
|
253
|
+
export async function executePluginAction(
|
|
254
|
+
plugin: InstalledPlugin,
|
|
255
|
+
actionName: string,
|
|
256
|
+
params: Record<string, any> = {},
|
|
257
|
+
): Promise<PluginActionResult> {
|
|
258
|
+
// Auto-resolve action by config.mode prefix: "test" → "docker_test" if mode=docker
|
|
259
|
+
let action = plugin.definition.actions[actionName];
|
|
260
|
+
if (!action && plugin.config.mode) {
|
|
261
|
+
const modeAction = `${plugin.config.mode}_${actionName}`;
|
|
262
|
+
action = plugin.definition.actions[modeAction];
|
|
263
|
+
}
|
|
264
|
+
if (!action) {
|
|
265
|
+
return { ok: false, output: {}, error: `Action "${actionName}" not found in plugin "${plugin.id}"` };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Fill missing config values with definition defaults (handles plugins installed before defaults were added)
|
|
269
|
+
const configWithDefaults = { ...plugin.config };
|
|
270
|
+
if (plugin.definition.config) {
|
|
271
|
+
for (const [k, fieldDef] of Object.entries(plugin.definition.config)) {
|
|
272
|
+
if (configWithDefaults[k] == null && (fieldDef as any).default != null) {
|
|
273
|
+
configWithDefaults[k] = (fieldDef as any).default;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// params can override config values — supports multi-instance scenarios
|
|
279
|
+
// e.g., different Jenkins URLs per pipeline node via params.jenkins_url
|
|
280
|
+
const mergedConfig = { ...configWithDefaults };
|
|
281
|
+
const remainingParams = { ...params };
|
|
282
|
+
for (const k of Object.keys(params)) {
|
|
283
|
+
if (k in plugin.definition.config && params[k] != null) {
|
|
284
|
+
mergedConfig[k] = params[k];
|
|
285
|
+
delete remainingParams[k];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Config fields named "default_xxx" provide fallback for params.xxx
|
|
290
|
+
for (const [k, v] of Object.entries(mergedConfig)) {
|
|
291
|
+
if (k.startsWith('default_')) {
|
|
292
|
+
const paramKey = k.slice(8); // "default_job" → "job"
|
|
293
|
+
if (remainingParams[paramKey] == null && v != null) {
|
|
294
|
+
remainingParams[paramKey] = v;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const ctx = {
|
|
300
|
+
config: mergedConfig,
|
|
301
|
+
params: remainingParams,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
console.log(`[plugin] ${plugin.id}.${actionName}: executing (${action.run})`);
|
|
305
|
+
|
|
306
|
+
switch (action.run) {
|
|
307
|
+
case 'http':
|
|
308
|
+
return executeHttp(action, ctx);
|
|
309
|
+
case 'poll':
|
|
310
|
+
return executePoll(action, ctx);
|
|
311
|
+
case 'shell':
|
|
312
|
+
return executeShell(action, ctx);
|
|
313
|
+
case 'script':
|
|
314
|
+
// TODO: implement script execution
|
|
315
|
+
return { ok: false, output: {}, error: 'Script execution not yet implemented' };
|
|
316
|
+
default:
|
|
317
|
+
return { ok: false, output: {}, error: `Unknown action type: ${action.run}` };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Execute a plugin with auto-wait.
|
|
323
|
+
* Runs the specified action, then if wait=true, runs the 'wait' action.
|
|
324
|
+
*/
|
|
325
|
+
export async function executePluginWithWait(
|
|
326
|
+
plugin: InstalledPlugin,
|
|
327
|
+
actionName: string,
|
|
328
|
+
params: Record<string, any> = {},
|
|
329
|
+
wait: boolean = false,
|
|
330
|
+
): Promise<PluginActionResult> {
|
|
331
|
+
const result = await executePluginAction(plugin, actionName, params);
|
|
332
|
+
if (!result.ok || !wait) return result;
|
|
333
|
+
|
|
334
|
+
// Auto-wait: if plugin has a 'wait' action, run it
|
|
335
|
+
if (plugin.definition.actions['wait']) {
|
|
336
|
+
const waitResult = await executePluginAction(plugin, 'wait', params);
|
|
337
|
+
return {
|
|
338
|
+
ok: waitResult.ok,
|
|
339
|
+
output: { ...result.output, ...waitResult.output },
|
|
340
|
+
rawResponse: waitResult.rawResponse,
|
|
341
|
+
duration: (result.duration || 0) + (waitResult.duration || 0),
|
|
342
|
+
error: waitResult.error,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Registry — load, install, uninstall, list plugins.
|
|
3
|
+
*
|
|
4
|
+
* Plugins are stored as YAML files:
|
|
5
|
+
* - Built-in: lib/builtin-plugins/*.yaml
|
|
6
|
+
* - User-installed: ~/.forge/plugins/<id>/plugin.yaml
|
|
7
|
+
*
|
|
8
|
+
* Installed plugin configs are stored in:
|
|
9
|
+
* - ~/.forge/data/plugin-configs.json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
13
|
+
import { join, dirname } from 'node:path';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import YAML from 'yaml';
|
|
17
|
+
import type { PluginDefinition, InstalledPlugin, PluginSource } from './types';
|
|
18
|
+
|
|
19
|
+
const _filename = typeof __filename !== 'undefined' ? __filename : fileURLToPath(import.meta.url);
|
|
20
|
+
const _dirname = typeof __dirname !== 'undefined' ? __dirname : dirname(_filename);
|
|
21
|
+
|
|
22
|
+
const BUILTIN_DIR_COMPILED = join(_dirname, '..', 'builtin-plugins');
|
|
23
|
+
const BUILTIN_DIR_SOURCE = join(process.cwd(), 'lib', 'builtin-plugins');
|
|
24
|
+
const BUILTIN_DIR = existsSync(BUILTIN_DIR_COMPILED) ? BUILTIN_DIR_COMPILED : BUILTIN_DIR_SOURCE;
|
|
25
|
+
const USER_PLUGINS_DIR = join(homedir(), '.forge', 'plugins');
|
|
26
|
+
const CONFIGS_FILE = join(homedir(), '.forge', 'data', 'plugin-configs.json');
|
|
27
|
+
|
|
28
|
+
// ─── Load Plugin Definition ──────────────────────────────
|
|
29
|
+
|
|
30
|
+
function loadPluginYaml(filePath: string): PluginDefinition | null {
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
33
|
+
const def = YAML.parse(raw) as PluginDefinition;
|
|
34
|
+
if (!def.id || !def.name || !def.actions) return null;
|
|
35
|
+
// Defaults
|
|
36
|
+
if (!def.config) def.config = {};
|
|
37
|
+
if (!def.params) def.params = {};
|
|
38
|
+
if (!def.icon) def.icon = '🔌';
|
|
39
|
+
if (!def.version) def.version = '0.0.1';
|
|
40
|
+
return def;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Config Storage ──────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function loadConfigs(): Record<string, { config: Record<string, any>; installedAt: string; enabled: boolean }> {
|
|
49
|
+
try {
|
|
50
|
+
if (existsSync(CONFIGS_FILE)) {
|
|
51
|
+
return JSON.parse(readFileSync(CONFIGS_FILE, 'utf-8'));
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function saveConfigs(configs: Record<string, any>): void {
|
|
58
|
+
const dir = dirname(CONFIGS_FILE);
|
|
59
|
+
mkdirSync(dir, { recursive: true });
|
|
60
|
+
writeFileSync(CONFIGS_FILE, JSON.stringify(configs, null, 2));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Public API ──────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** List all available plugins (built-in + user-installed) */
|
|
66
|
+
export function listPlugins(): PluginSource[] {
|
|
67
|
+
const sources: PluginSource[] = [];
|
|
68
|
+
const configs = loadConfigs();
|
|
69
|
+
|
|
70
|
+
// Check if any config entry references this plugin (base install or instance)
|
|
71
|
+
const isInstalledOrHasInstance = (id: string) =>
|
|
72
|
+
Object.entries(configs).some(([key, cfg]) => key === id || (cfg as any).source === id);
|
|
73
|
+
|
|
74
|
+
// Built-in plugins
|
|
75
|
+
if (existsSync(BUILTIN_DIR)) {
|
|
76
|
+
for (const file of readdirSync(BUILTIN_DIR).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
|
|
77
|
+
const def = loadPluginYaml(join(BUILTIN_DIR, file));
|
|
78
|
+
if (def) {
|
|
79
|
+
sources.push({
|
|
80
|
+
id: def.id,
|
|
81
|
+
name: def.name,
|
|
82
|
+
icon: def.icon,
|
|
83
|
+
version: def.version,
|
|
84
|
+
author: def.author || 'forge',
|
|
85
|
+
description: def.description || '',
|
|
86
|
+
source: 'builtin',
|
|
87
|
+
installed: isInstalledOrHasInstance(def.id),
|
|
88
|
+
configCount: Object.keys(def.config).length,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// User-installed plugins
|
|
95
|
+
mkdirSync(USER_PLUGINS_DIR, { recursive: true });
|
|
96
|
+
for (const dir of readdirSync(USER_PLUGINS_DIR, { withFileTypes: true })) {
|
|
97
|
+
if (!dir.isDirectory()) continue;
|
|
98
|
+
const yamlPath = join(USER_PLUGINS_DIR, dir.name, 'plugin.yaml');
|
|
99
|
+
const ymlPath = join(USER_PLUGINS_DIR, dir.name, 'plugin.yml');
|
|
100
|
+
const filePath = existsSync(yamlPath) ? yamlPath : existsSync(ymlPath) ? ymlPath : null;
|
|
101
|
+
if (!filePath) continue;
|
|
102
|
+
const def = loadPluginYaml(filePath);
|
|
103
|
+
if (def) {
|
|
104
|
+
// Don't duplicate if also built-in
|
|
105
|
+
if (sources.some(s => s.id === def.id)) continue;
|
|
106
|
+
sources.push({
|
|
107
|
+
id: def.id,
|
|
108
|
+
name: def.name,
|
|
109
|
+
icon: def.icon,
|
|
110
|
+
version: def.version,
|
|
111
|
+
author: def.author || 'local',
|
|
112
|
+
description: def.description || '',
|
|
113
|
+
source: 'local',
|
|
114
|
+
installed: isInstalledOrHasInstance(def.id),
|
|
115
|
+
configCount: Object.keys(def.config).length,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return sources;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Get a plugin definition by ID */
|
|
124
|
+
export function getPlugin(id: string): PluginDefinition | null {
|
|
125
|
+
// Check built-in first
|
|
126
|
+
if (existsSync(BUILTIN_DIR)) {
|
|
127
|
+
for (const file of readdirSync(BUILTIN_DIR).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
|
|
128
|
+
const def = loadPluginYaml(join(BUILTIN_DIR, file));
|
|
129
|
+
if (def?.id === id) return def;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check user plugins
|
|
134
|
+
const yamlPath = join(USER_PLUGINS_DIR, id, 'plugin.yaml');
|
|
135
|
+
const ymlPath = join(USER_PLUGINS_DIR, id, 'plugin.yml');
|
|
136
|
+
if (existsSync(yamlPath)) return loadPluginYaml(yamlPath);
|
|
137
|
+
if (existsSync(ymlPath)) return loadPluginYaml(ymlPath);
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Get an installed plugin with its config */
|
|
143
|
+
export function getInstalledPlugin(id: string): InstalledPlugin | null {
|
|
144
|
+
const configs = loadConfigs();
|
|
145
|
+
const cfg = configs[id] as any;
|
|
146
|
+
if (!cfg) return null;
|
|
147
|
+
// Resolve definition: use source field for instances, fallback to id
|
|
148
|
+
const sourceId = cfg.source || id;
|
|
149
|
+
const def = getPlugin(sourceId);
|
|
150
|
+
if (!def) return null;
|
|
151
|
+
return {
|
|
152
|
+
id,
|
|
153
|
+
definition: def,
|
|
154
|
+
config: cfg.config || {},
|
|
155
|
+
installedAt: cfg.installedAt || new Date().toISOString(),
|
|
156
|
+
enabled: cfg.enabled !== false,
|
|
157
|
+
instanceName: cfg.name,
|
|
158
|
+
source: sourceId !== id ? sourceId : undefined,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** List all installed plugins (including instances) */
|
|
163
|
+
export function listInstalledPlugins(): InstalledPlugin[] {
|
|
164
|
+
const configs = loadConfigs();
|
|
165
|
+
const installed: InstalledPlugin[] = [];
|
|
166
|
+
for (const [id, cfg] of Object.entries(configs)) {
|
|
167
|
+
const sourceId = (cfg as any).source || id;
|
|
168
|
+
const def = getPlugin(sourceId);
|
|
169
|
+
if (def) {
|
|
170
|
+
installed.push({
|
|
171
|
+
id,
|
|
172
|
+
definition: def,
|
|
173
|
+
config: (cfg as any).config || {},
|
|
174
|
+
installedAt: (cfg as any).installedAt || '',
|
|
175
|
+
enabled: (cfg as any).enabled !== false,
|
|
176
|
+
instanceName: (cfg as any).name,
|
|
177
|
+
source: sourceId !== id ? sourceId : undefined,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return installed;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Install a plugin or create an instance */
|
|
185
|
+
export function installPlugin(id: string, config: Record<string, any>, opts?: { source?: string; name?: string }): boolean {
|
|
186
|
+
const sourceId = opts?.source || id;
|
|
187
|
+
const def = getPlugin(sourceId);
|
|
188
|
+
if (!def) return false;
|
|
189
|
+
const configs = loadConfigs();
|
|
190
|
+
configs[id] = {
|
|
191
|
+
config,
|
|
192
|
+
installedAt: new Date().toISOString(),
|
|
193
|
+
enabled: true,
|
|
194
|
+
...(opts?.source ? { source: opts.source } : {}),
|
|
195
|
+
...(opts?.name ? { name: opts.name } : {}),
|
|
196
|
+
};
|
|
197
|
+
saveConfigs(configs);
|
|
198
|
+
console.log(`[plugins] Installed: ${opts?.name || def.name} (${id}${opts?.source ? ' ← ' + opts.source : ''})`);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Uninstall a plugin or instance */
|
|
203
|
+
export function uninstallPlugin(id: string): boolean {
|
|
204
|
+
const configs = loadConfigs();
|
|
205
|
+
if (!configs[id]) return false;
|
|
206
|
+
delete configs[id];
|
|
207
|
+
saveConfigs(configs);
|
|
208
|
+
console.log(`[plugins] Uninstalled: ${id}`);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Update plugin config */
|
|
213
|
+
export function updatePluginConfig(id: string, config: Record<string, any>): boolean {
|
|
214
|
+
const configs = loadConfigs();
|
|
215
|
+
if (!configs[id]) return false;
|
|
216
|
+
(configs[id] as any).config = config;
|
|
217
|
+
saveConfigs(configs);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Enable/disable a plugin */
|
|
222
|
+
export function setPluginEnabled(id: string, enabled: boolean): boolean {
|
|
223
|
+
const configs = loadConfigs();
|
|
224
|
+
if (!configs[id]) return false;
|
|
225
|
+
(configs[id] as any).enabled = enabled;
|
|
226
|
+
saveConfigs(configs);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Types — defines the plugin system for pipeline node extensions.
|
|
3
|
+
*
|
|
4
|
+
* A plugin is a reusable, configurable capability that can be used as a
|
|
5
|
+
* pipeline node. Plugins are declarative YAML files with config schema,
|
|
6
|
+
* params schema, and actions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Plugin action execution type */
|
|
10
|
+
export type PluginActionType = 'http' | 'poll' | 'shell' | 'script';
|
|
11
|
+
|
|
12
|
+
/** Schema field definition for config/params */
|
|
13
|
+
export interface PluginFieldSchema {
|
|
14
|
+
type: 'string' | 'number' | 'boolean' | 'secret' | 'json' | 'select';
|
|
15
|
+
label?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
default?: any;
|
|
19
|
+
options?: string[]; // for select type
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A single action a plugin can perform */
|
|
23
|
+
export interface PluginAction {
|
|
24
|
+
/** Execution type */
|
|
25
|
+
run: PluginActionType;
|
|
26
|
+
|
|
27
|
+
// HTTP action fields
|
|
28
|
+
method?: string; // GET, POST, PUT, DELETE
|
|
29
|
+
url?: string; // URL template (supports {{config.x}}, {{params.x}})
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
body?: string; // body template
|
|
32
|
+
|
|
33
|
+
// Poll action fields (extends HTTP)
|
|
34
|
+
interval?: number; // poll interval in seconds
|
|
35
|
+
until?: string; // JSONPath condition: "$.result != null"
|
|
36
|
+
timeout?: number; // max wait in seconds
|
|
37
|
+
|
|
38
|
+
// Shell action fields
|
|
39
|
+
command?: string; // shell command template
|
|
40
|
+
cwd?: string; // working directory
|
|
41
|
+
|
|
42
|
+
// Script action fields
|
|
43
|
+
script?: string; // path to JS/Python script
|
|
44
|
+
runtime?: 'node' | 'python';
|
|
45
|
+
|
|
46
|
+
// Output extraction
|
|
47
|
+
output?: Record<string, string>; // { fieldName: "$.json.path" or "$body" or "$stdout" }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Plugin definition (loaded from plugin.yaml) */
|
|
51
|
+
export interface PluginDefinition {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
icon: string;
|
|
55
|
+
version: string;
|
|
56
|
+
author?: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
|
|
59
|
+
/** Global config — set once when installing the plugin */
|
|
60
|
+
config: Record<string, PluginFieldSchema>;
|
|
61
|
+
|
|
62
|
+
/** Per-use params — set each time the plugin is used in a pipeline node */
|
|
63
|
+
params: Record<string, PluginFieldSchema>;
|
|
64
|
+
|
|
65
|
+
/** Named actions this plugin can perform */
|
|
66
|
+
actions: Record<string, PluginAction>;
|
|
67
|
+
|
|
68
|
+
/** Default action to run if none specified */
|
|
69
|
+
defaultAction?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Installed plugin instance (definition + user config values) */
|
|
73
|
+
export interface InstalledPlugin {
|
|
74
|
+
id: string; // instance ID (e.g., 'jenkins-backend')
|
|
75
|
+
definition: PluginDefinition;
|
|
76
|
+
config: Record<string, any>; // user-provided config values
|
|
77
|
+
installedAt: string;
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
instanceName?: string; // display name (e.g., 'Jenkins Backend')
|
|
80
|
+
source?: string; // source plugin ID if this is an instance (e.g., 'jenkins')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Result of executing a plugin action */
|
|
84
|
+
export interface PluginActionResult {
|
|
85
|
+
ok: boolean;
|
|
86
|
+
output: Record<string, any>;
|
|
87
|
+
rawResponse?: string;
|
|
88
|
+
error?: string;
|
|
89
|
+
duration?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Plugin source for marketplace */
|
|
93
|
+
export interface PluginSource {
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
icon: string;
|
|
97
|
+
version: string;
|
|
98
|
+
author: string;
|
|
99
|
+
description: string;
|
|
100
|
+
source: 'builtin' | 'local' | 'registry';
|
|
101
|
+
installed: boolean;
|
|
102
|
+
configCount: number; // number of config fields — 0 means no config needed
|
|
103
|
+
}
|