circuschief 0.1.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/package.json +33 -0
- package/packages/server/bin/cli.js +4 -0
- package/packages/server/src/agents/AgentGateway.js +64 -0
- package/packages/server/src/agents/BaseAgent.js +41 -0
- package/packages/server/src/agents/LoggingAgentWrapper.js +73 -0
- package/packages/server/src/agents/adapters/ClaudeCodeAdapter.js +33 -0
- package/packages/server/src/agents/adapters/CodexAdapter.js +26 -0
- package/packages/server/src/agents/types.js +43 -0
- package/packages/server/src/agents/vcr/CassetteStore.js +111 -0
- package/packages/server/src/agents/vcr/VCRAgentAdapter.js +126 -0
- package/packages/server/src/agents/vcr/VCRSummaryWrapper.js +71 -0
- package/packages/server/src/api/canvas-helpers.js +249 -0
- package/packages/server/src/api/canvas-trash-routes.js +205 -0
- package/packages/server/src/api/canvas.js +331 -0
- package/packages/server/src/api/commandButtons.js +312 -0
- package/packages/server/src/api/commands.js +169 -0
- package/packages/server/src/api/filesystem.js +62 -0
- package/packages/server/src/api/git.js +85 -0
- package/packages/server/src/api/index.js +44 -0
- package/packages/server/src/api/kanban.js +342 -0
- package/packages/server/src/api/metrics.js +194 -0
- package/packages/server/src/api/projects-helpers.js +43 -0
- package/packages/server/src/api/projects-session-helpers.js +295 -0
- package/packages/server/src/api/projects.js +384 -0
- package/packages/server/src/api/providers.js +249 -0
- package/packages/server/src/api/quickResponses.js +129 -0
- package/packages/server/src/api/sessions-archive.js +69 -0
- package/packages/server/src/api/sessions-commands.js +220 -0
- package/packages/server/src/api/sessions-conversations.js +168 -0
- package/packages/server/src/api/sessions-draft.js +72 -0
- package/packages/server/src/api/sessions-lifecycle.js +190 -0
- package/packages/server/src/api/sessions-messages.js +141 -0
- package/packages/server/src/api/sessions-notes.js +51 -0
- package/packages/server/src/api/sessions-patch.js +252 -0
- package/packages/server/src/api/sessions-streaming.js +86 -0
- package/packages/server/src/api/sessions.js +269 -0
- package/packages/server/src/api/settings.js +194 -0
- package/packages/server/src/api/templates.js +63 -0
- package/packages/server/src/app.js +51 -0
- package/packages/server/src/database.js +58 -0
- package/packages/server/src/db/AgentCallLogRepository.js +322 -0
- package/packages/server/src/db/AttachmentRepository.js +191 -0
- package/packages/server/src/db/BaseRepository.js +39 -0
- package/packages/server/src/db/CanvasItemRepository.js +315 -0
- package/packages/server/src/db/CommandButtonRepository.js +75 -0
- package/packages/server/src/db/CommandRunRepository.js +219 -0
- package/packages/server/src/db/ConversationRepository.js +379 -0
- package/packages/server/src/db/DatabaseManager.js +91 -0
- package/packages/server/src/db/KanbanBoardRepository.js +92 -0
- package/packages/server/src/db/KanbanCardRepository.js +286 -0
- package/packages/server/src/db/KanbanLaneRepository.js +279 -0
- package/packages/server/src/db/MessageRepository.js +156 -0
- package/packages/server/src/db/ProjectDefaultsRepository.js +173 -0
- package/packages/server/src/db/ProjectRepository.js +110 -0
- package/packages/server/src/db/ProviderRepository.js +307 -0
- package/packages/server/src/db/QuickResponseRepository.js +186 -0
- package/packages/server/src/db/SessionNoteRepository.js +60 -0
- package/packages/server/src/db/SessionRepository.js +314 -0
- package/packages/server/src/db/SessionSummaryRepository.js +200 -0
- package/packages/server/src/db/SessionTemplateRepository.js +171 -0
- package/packages/server/src/db/SettingsRepository.js +211 -0
- package/packages/server/src/db/TodoRepository.js +132 -0
- package/packages/server/src/db/WorkLogRepository.js +122 -0
- package/packages/server/src/db/conversation-helpers.js +119 -0
- package/packages/server/src/db/index.js +100 -0
- package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
- package/packages/server/src/db/migrations/conversationsMigrations.js +183 -0
- package/packages/server/src/db/migrations/index.js +199 -0
- package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
- package/packages/server/src/db/migrations/migrationUtils.js +55 -0
- package/packages/server/src/db/migrations/miscMigrations.js +242 -0
- package/packages/server/src/db/migrations/projectsMigrations.js +95 -0
- package/packages/server/src/db/migrations/sessionsMigrations.js +282 -0
- package/packages/server/src/db/session-helpers.js +150 -0
- package/packages/server/src/index.js +106 -0
- package/packages/server/src/logger.js +22 -0
- package/packages/server/src/middleware/sessionLookup.js +57 -0
- package/packages/server/src/middleware/upload.js +94 -0
- package/packages/server/src/schema.sql +363 -0
- package/packages/server/src/services/agentCallLogger.js +116 -0
- package/packages/server/src/services/canvasStore.js +56 -0
- package/packages/server/src/services/childSessionContext.js +61 -0
- package/packages/server/src/services/commandRunner.js +422 -0
- package/packages/server/src/services/conversationContext.js +72 -0
- package/packages/server/src/services/diffService.js +172 -0
- package/packages/server/src/services/draftSessionService.js +181 -0
- package/packages/server/src/services/encryption.js +134 -0
- package/packages/server/src/services/ghService.js +169 -0
- package/packages/server/src/services/gitService.js +520 -0
- package/packages/server/src/services/gitSessionSetup.js +48 -0
- package/packages/server/src/services/hookService.js +60 -0
- package/packages/server/src/services/kanbanService.js +262 -0
- package/packages/server/src/services/kanbanTriggers.js +273 -0
- package/packages/server/src/services/nodeSpawnHelper.js +63 -0
- package/packages/server/src/services/prStatusService.js +204 -0
- package/packages/server/src/services/prUrlService.js +224 -0
- package/packages/server/src/services/providerTestService.js +81 -0
- package/packages/server/src/services/scheduleService.js +110 -0
- package/packages/server/src/services/schedulerService.js +281 -0
- package/packages/server/src/services/sessionDuplicator.js +63 -0
- package/packages/server/src/services/sessionErrors.js +173 -0
- package/packages/server/src/services/sessionExecution.js +378 -0
- package/packages/server/src/services/sessionManager.js +356 -0
- package/packages/server/src/services/sessionPrompts.js +427 -0
- package/packages/server/src/services/sessionProvider.js +107 -0
- package/packages/server/src/services/slashCommandDiscovery.js +258 -0
- package/packages/server/src/services/slashCommandPluginDiscovery.js +216 -0
- package/packages/server/src/services/slashCommandService.js +306 -0
- package/packages/server/src/services/streamEventCallbacks.js +170 -0
- package/packages/server/src/services/streamEventHandler.js +488 -0
- package/packages/server/src/services/streamUsageHandler.js +228 -0
- package/packages/server/src/services/summaryBroadcast.js +61 -0
- package/packages/server/src/services/summaryClaudeClient.js +180 -0
- package/packages/server/src/services/summaryPrompts.js +169 -0
- package/packages/server/src/services/summaryService.js +552 -0
- package/packages/server/src/services/summaryStaleCheck.js +35 -0
- package/packages/server/src/services/systemMonitor.js +281 -0
- package/packages/server/src/services/templateTriggerService.js +197 -0
- package/packages/server/src/services/terminalOutput.js +160 -0
- package/packages/server/src/services/todoStore.js +58 -0
- package/packages/server/src/services/usageTracker.js +69 -0
- package/packages/server/src/services/withConcurrencyGuard.js +110 -0
- package/packages/server/src/websocket.js +10 -0
- package/packages/server/src/ws/WebSocketManager.js +240 -0
- package/packages/server/src/ws/index.js +50 -0
- package/packages/shared/package.json +27 -0
- package/packages/shared/src/constants.js +44 -0
- package/packages/shared/src/contracts/canvas.js +25 -0
- package/packages/shared/src/contracts/commandButtons.js +36 -0
- package/packages/shared/src/contracts/kanban.js +142 -0
- package/packages/shared/src/contracts/projects.js +63 -0
- package/packages/shared/src/contracts/providers.js +81 -0
- package/packages/shared/src/contracts/quickResponses.js +44 -0
- package/packages/shared/src/contracts/sessions.js +112 -0
- package/packages/shared/src/contracts/templates.js +51 -0
- package/packages/shared/src/index.js +5 -0
- package/packages/shared/src/protocol.js +76 -0
- package/packages/shared/src/routeParams.js +36 -0
- package/packages/shared/src/types.js +167 -0
- package/packages/shared/src/utils.js +101 -0
- package/packages/web/dist/assets/ActiveSessionsView-BQc76Jc8.js +1 -0
- package/packages/web/dist/assets/ActiveSessionsView-ofSvx-K1.css +1 -0
- package/packages/web/dist/assets/AgentLogsView-CTCjHjsu.js +2 -0
- package/packages/web/dist/assets/AgentLogsView-D90PnQVk.css +1 -0
- package/packages/web/dist/assets/ApiClient-Dbs1H78V.js +1 -0
- package/packages/web/dist/assets/ArchiveConfirmModal-CCxSZ52u.js +1 -0
- package/packages/web/dist/assets/ArchiveConfirmModal-CQZeuYBz.css +1 -0
- package/packages/web/dist/assets/CommandButtonDetailView-CF_-LXpU.js +1 -0
- package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +1 -0
- package/packages/web/dist/assets/EffortLevelSelector-BQaQmU2d.css +1 -0
- package/packages/web/dist/assets/EffortLevelSelector-DPofLvm-.js +1 -0
- package/packages/web/dist/assets/GeneralSettingsView-BCf53fpC.css +1 -0
- package/packages/web/dist/assets/GeneralSettingsView-BY1G-Kv8.js +1 -0
- package/packages/web/dist/assets/InterpolationHelp-CgdbNcJB.js +1 -0
- package/packages/web/dist/assets/InterpolationHelp-iNxTxmhs.css +1 -0
- package/packages/web/dist/assets/MarkdownEditor-CqT1U8lo.js +2 -0
- package/packages/web/dist/assets/MarkdownEditor-enuH2yvP.css +1 -0
- package/packages/web/dist/assets/ModelSelector-BBn_Ve0D.js +1 -0
- package/packages/web/dist/assets/ModelSelector-DPPD-92R.css +1 -0
- package/packages/web/dist/assets/NewSessionView-Bo5l49nu.js +3 -0
- package/packages/web/dist/assets/NewSessionView-Byoi1XdQ.css +1 -0
- package/packages/web/dist/assets/PathChooser-BoMGzeg2.css +1 -0
- package/packages/web/dist/assets/PathChooser-Cx9gQ-Qt.js +1 -0
- package/packages/web/dist/assets/ProjectEditView-BFuscj-V.js +1 -0
- package/packages/web/dist/assets/ProjectEditView-DNwBUNRk.css +1 -0
- package/packages/web/dist/assets/ProjectListView-C55H1JHQ.css +1 -0
- package/packages/web/dist/assets/ProjectListView-Dj0jBZ46.js +1 -0
- package/packages/web/dist/assets/ProjectNewView-Brdp-xUu.js +1 -0
- package/packages/web/dist/assets/ProjectNewView-CpgE4R-l.css +1 -0
- package/packages/web/dist/assets/ProvidersView-B_QQF3RM.css +1 -0
- package/packages/web/dist/assets/ProvidersView-Cxc-1skq.js +1 -0
- package/packages/web/dist/assets/QuickResponseSettings-B2eVAtHW.js +1 -0
- package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +1 -0
- package/packages/web/dist/assets/QuickResponsesPanel-DIBQFj0W.css +1 -0
- package/packages/web/dist/assets/QuickResponsesPanel-lU8pW2B0.js +1 -0
- package/packages/web/dist/assets/ResizableTextarea-B5nAA0RV.css +1 -0
- package/packages/web/dist/assets/ResizableTextarea-DSy1mWGY.js +1 -0
- package/packages/web/dist/assets/SessionCard-BvjLwVYg.js +1 -0
- package/packages/web/dist/assets/SessionCard-D20G3bX8.css +1 -0
- package/packages/web/dist/assets/SessionDetailView-BQbPg-RJ.js +36 -0
- package/packages/web/dist/assets/SessionDetailView-BrMG4p2-.css +1 -0
- package/packages/web/dist/assets/SessionFormOptions-BgqFR-5f.js +1 -0
- package/packages/web/dist/assets/SessionFormOptions-BuLlDF-7.css +1 -0
- package/packages/web/dist/assets/SessionListView-BAIBtJF7.css +1 -0
- package/packages/web/dist/assets/SessionListView-CYIHI8qF.js +1 -0
- package/packages/web/dist/assets/SessionLogStream-B-FwUMJQ.js +18 -0
- package/packages/web/dist/assets/SessionLogStream-zPUTiGbe.css +1 -0
- package/packages/web/dist/assets/SettingsView-DC8-hTQ-.css +1 -0
- package/packages/web/dist/assets/SettingsView-fZxpiGp7.js +1 -0
- package/packages/web/dist/assets/SlashCommandWizard-BB30cSvo.css +1 -0
- package/packages/web/dist/assets/SlashCommandWizard-BgaOw9W3.js +1 -0
- package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +1 -0
- package/packages/web/dist/assets/SummarySettingsView-eeu1Xq86.js +1 -0
- package/packages/web/dist/assets/TemplateDetailView-DEPKSwDo.js +1 -0
- package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +1 -0
- package/packages/web/dist/assets/apl-B4CMkyY2.js +1 -0
- package/packages/web/dist/assets/asciiarmor-Df11BRmG.js +1 -0
- package/packages/web/dist/assets/asn1-EdZsLKOL.js +1 -0
- package/packages/web/dist/assets/asterisk-B-8jnY81.js +1 -0
- package/packages/web/dist/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/packages/web/dist/assets/clike-B9uivgTg.js +1 -0
- package/packages/web/dist/assets/clojure-BMjYHr_A.js +1 -0
- package/packages/web/dist/assets/cmake-BQqOBYOt.js +1 -0
- package/packages/web/dist/assets/cobol-CWcv1MsR.js +1 -0
- package/packages/web/dist/assets/coffeescript-S37ZYGWr.js +1 -0
- package/packages/web/dist/assets/commandButtons-DNSHH8IA.js +4 -0
- package/packages/web/dist/assets/commonlisp-DBKNyK5s.js +1 -0
- package/packages/web/dist/assets/crystal-SjHAIU92.js +1 -0
- package/packages/web/dist/assets/css-BnMrqG3P.js +1 -0
- package/packages/web/dist/assets/cypher-C_CwsFkJ.js +1 -0
- package/packages/web/dist/assets/d-pRatUO7H.js +1 -0
- package/packages/web/dist/assets/diff-DbItnlRl.js +1 -0
- package/packages/web/dist/assets/dockerfile-BKs6k2Af.js +1 -0
- package/packages/web/dist/assets/dtd-DF_7sFjM.js +1 -0
- package/packages/web/dist/assets/dylan-DwRh75JA.js +1 -0
- package/packages/web/dist/assets/ebnf-CDyGwa7X.js +1 -0
- package/packages/web/dist/assets/ecl-Cabwm37j.js +1 -0
- package/packages/web/dist/assets/eiffel-CnydiIhH.js +1 -0
- package/packages/web/dist/assets/elm-vLlmbW-K.js +1 -0
- package/packages/web/dist/assets/erlang-BNw1qcRV.js +1 -0
- package/packages/web/dist/assets/factor-kuTfRLto.js +1 -0
- package/packages/web/dist/assets/fcl-Kvtd6kyn.js +1 -0
- package/packages/web/dist/assets/forth-Ffai-XNe.js +1 -0
- package/packages/web/dist/assets/fortran-DYz_wnZ1.js +1 -0
- package/packages/web/dist/assets/gas-Bneqetm1.js +1 -0
- package/packages/web/dist/assets/gherkin-heZmZLOM.js +1 -0
- package/packages/web/dist/assets/groovy-D9Dt4D0W.js +1 -0
- package/packages/web/dist/assets/haskell-BWDZoCOh.js +1 -0
- package/packages/web/dist/assets/haxe-H-WmDvRZ.js +1 -0
- package/packages/web/dist/assets/http-DBlCnlav.js +1 -0
- package/packages/web/dist/assets/idl-BEugSyMb.js +1 -0
- package/packages/web/dist/assets/index-BZlHgDSz.js +1 -0
- package/packages/web/dist/assets/index-BhWX8AfE.js +2 -0
- package/packages/web/dist/assets/index-Bi3XvF_f.js +1 -0
- package/packages/web/dist/assets/index-BqXoPf_D.js +1 -0
- package/packages/web/dist/assets/index-CAuTOZSD.js +1 -0
- package/packages/web/dist/assets/index-CKYk-fkb.js +1 -0
- package/packages/web/dist/assets/index-CTumW_tV.js +318 -0
- package/packages/web/dist/assets/index-CVOJVSsC.js +82 -0
- package/packages/web/dist/assets/index-CXK2Z3_z.js +1 -0
- package/packages/web/dist/assets/index-CYllQ3Vd.js +1 -0
- package/packages/web/dist/assets/index-CpsfI08O.js +1 -0
- package/packages/web/dist/assets/index-DQkhDeTA.js +3 -0
- package/packages/web/dist/assets/index-DWP8iCBp.js +1 -0
- package/packages/web/dist/assets/index-DkVb9W_J.js +1 -0
- package/packages/web/dist/assets/index-DmKHPbIa.js +1 -0
- package/packages/web/dist/assets/index-DrlQi03X.js +1 -0
- package/packages/web/dist/assets/index-gmCCsCQ1.css +1 -0
- package/packages/web/dist/assets/index-prTEzzgO.js +1 -0
- package/packages/web/dist/assets/index-wqgejMCM.js +1 -0
- package/packages/web/dist/assets/index-yh0ZHIWw.js +7 -0
- package/packages/web/dist/assets/javascript-qCveANmP.js +1 -0
- package/packages/web/dist/assets/julia-DuME0IfC.js +1 -0
- package/packages/web/dist/assets/livescript-BwQOo05w.js +1 -0
- package/packages/web/dist/assets/lua-BgMRiT3U.js +1 -0
- package/packages/web/dist/assets/mathematica-DTrFuWx2.js +1 -0
- package/packages/web/dist/assets/mbox-CNhZ1qSd.js +1 -0
- package/packages/web/dist/assets/mirc-CjQqDB4T.js +1 -0
- package/packages/web/dist/assets/mllike-CXdrOF99.js +1 -0
- package/packages/web/dist/assets/modelica-Dc1JOy9r.js +1 -0
- package/packages/web/dist/assets/mscgen-BA5vi2Kp.js +1 -0
- package/packages/web/dist/assets/mumps-BT43cFF4.js +1 -0
- package/packages/web/dist/assets/nginx-DdIZxoE0.js +1 -0
- package/packages/web/dist/assets/nsis-LdVXkNf5.js +1 -0
- package/packages/web/dist/assets/ntriples-BfvgReVJ.js +1 -0
- package/packages/web/dist/assets/octave-Ck1zUtKM.js +1 -0
- package/packages/web/dist/assets/oz-BzwKVEFT.js +1 -0
- package/packages/web/dist/assets/pascal--L3eBynH.js +1 -0
- package/packages/web/dist/assets/perl-CdXCOZ3F.js +1 -0
- package/packages/web/dist/assets/pig-CevX1Tat.js +1 -0
- package/packages/web/dist/assets/powershell-CFHJl5sT.js +1 -0
- package/packages/web/dist/assets/projects-DbBQQH-V.js +1 -0
- package/packages/web/dist/assets/properties-C78fOPTZ.js +1 -0
- package/packages/web/dist/assets/protobuf-ChK-085T.js +1 -0
- package/packages/web/dist/assets/providers-ceCc4xRU.js +1 -0
- package/packages/web/dist/assets/pug-DukmZTjD.js +1 -0
- package/packages/web/dist/assets/puppet-DMA9R1ak.js +1 -0
- package/packages/web/dist/assets/python-BuPzkPfP.js +1 -0
- package/packages/web/dist/assets/q-pXgVlZs6.js +1 -0
- package/packages/web/dist/assets/r-DUYO_cvP.js +1 -0
- package/packages/web/dist/assets/rpm-CTu-6PCP.js +1 -0
- package/packages/web/dist/assets/ruby-B2Rjki9n.js +1 -0
- package/packages/web/dist/assets/sas-B4kiWyti.js +1 -0
- package/packages/web/dist/assets/scheme-C41bIUwD.js +1 -0
- package/packages/web/dist/assets/sessions-D681M81k.js +1 -0
- package/packages/web/dist/assets/settings-D0evez2V.js +1 -0
- package/packages/web/dist/assets/shell-CjFT_Tl9.js +1 -0
- package/packages/web/dist/assets/sieve-C3Gn_uJK.js +1 -0
- package/packages/web/dist/assets/simple-mode-GW_nhZxv.js +1 -0
- package/packages/web/dist/assets/smalltalk-CnHTOXQT.js +1 -0
- package/packages/web/dist/assets/solr-DehyRSwq.js +1 -0
- package/packages/web/dist/assets/sparql-DkYu6x3z.js +1 -0
- package/packages/web/dist/assets/spreadsheet-BCZA_wO0.js +1 -0
- package/packages/web/dist/assets/sql-D0XecflT.js +1 -0
- package/packages/web/dist/assets/stex-C3f8Ysf7.js +1 -0
- package/packages/web/dist/assets/style-BTin-zR_.css +1 -0
- package/packages/web/dist/assets/stylus-B533Al4x.js +1 -0
- package/packages/web/dist/assets/swift-BzpIVaGY.js +1 -0
- package/packages/web/dist/assets/tcl-DVfN8rqt.js +1 -0
- package/packages/web/dist/assets/textile-CnDTJFAw.js +1 -0
- package/packages/web/dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
- package/packages/web/dist/assets/tiki-DGYXhP31.js +1 -0
- package/packages/web/dist/assets/toml-Bm5Em-hy.js +1 -0
- package/packages/web/dist/assets/troff-wAsdV37c.js +1 -0
- package/packages/web/dist/assets/ttcn-CfJYG6tj.js +1 -0
- package/packages/web/dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- package/packages/web/dist/assets/turtle-B1tBg_DP.js +1 -0
- package/packages/web/dist/assets/vb-CmGdzxic.js +1 -0
- package/packages/web/dist/assets/vbscript-BuJXcnF6.js +1 -0
- package/packages/web/dist/assets/velocity-D8B20fx6.js +1 -0
- package/packages/web/dist/assets/verilog-C6RDOZhf.js +1 -0
- package/packages/web/dist/assets/vhdl-lSbBsy5d.js +1 -0
- package/packages/web/dist/assets/webidl-ZXfAyPTL.js +1 -0
- package/packages/web/dist/assets/xquery-CQfU5ijd.js +1 -0
- package/packages/web/dist/assets/yacas-BJ4BC0dw.js +1 -0
- package/packages/web/dist/assets/z80-Hz9HOZM7.js +1 -0
- package/packages/web/dist/favicon.png +0 -0
- package/packages/web/dist/index.html +17 -0
- package/packages/web/dist/logo.png +0 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
|
+
import { mkdir } from 'fs/promises';
|
|
4
|
+
import { extname, join, basename } from 'path';
|
|
5
|
+
import { sessions, canvasItems } from '../database.js';
|
|
6
|
+
import { broadcastToSession } from '../websocket.js';
|
|
7
|
+
import { WS_MESSAGE_TYPES, UpdateCanvasItemRequest } from '../../../shared/src/index.js';
|
|
8
|
+
import { upload, handleUploadError } from '../middleware/upload.js';
|
|
9
|
+
import {
|
|
10
|
+
isBinaryContent,
|
|
11
|
+
getTypeFromExtension,
|
|
12
|
+
processFileBuffer,
|
|
13
|
+
VALID_INLINE_TYPES,
|
|
14
|
+
validateInlineType,
|
|
15
|
+
buildInlineItemData,
|
|
16
|
+
broadcastCanvasUpdate,
|
|
17
|
+
writeCanvasItemToFile,
|
|
18
|
+
} from './canvas-helpers.js';
|
|
19
|
+
import trashRoutes from './canvas-trash-routes.js';
|
|
20
|
+
|
|
21
|
+
// Error message constants
|
|
22
|
+
const ERR_SESSION_NOT_FOUND = 'Session not found';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Process multipart file upload (Mode 1)
|
|
26
|
+
* @param {Express.Multer.File} file - The uploaded file from multer
|
|
27
|
+
* @returns {{ itemData: Object } | { error: string }}
|
|
28
|
+
*/
|
|
29
|
+
function handleMultipartUpload(file) {
|
|
30
|
+
const result = processFileBuffer(file.buffer, file.originalname);
|
|
31
|
+
if (result.error) {
|
|
32
|
+
return { error: result.error };
|
|
33
|
+
}
|
|
34
|
+
return { itemData: result.itemData };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Process file path upload (Mode 2) - reads file from disk
|
|
39
|
+
* @param {string} filePath - Path to the file on disk
|
|
40
|
+
* @returns {{ itemData: Object } | { error: string }}
|
|
41
|
+
*/
|
|
42
|
+
function handleFilePathUpload(filePath) {
|
|
43
|
+
if (!existsSync(filePath)) {
|
|
44
|
+
return { error: `File not found: ${filePath}` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const fileBuffer = readFileSync(filePath);
|
|
49
|
+
const result = processFileBuffer(fileBuffer, basename(filePath));
|
|
50
|
+
if (result.error) {
|
|
51
|
+
return { error: result.error };
|
|
52
|
+
}
|
|
53
|
+
return { itemData: result.itemData };
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return { error: `Failed to read file: ${err.message}` };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Process inline content upload (Mode 3)
|
|
61
|
+
* @param {string} type - The content type
|
|
62
|
+
* @param {string} content - The content data
|
|
63
|
+
* @param {string} filename - The filename
|
|
64
|
+
* @returns {{ itemData: Object } | { error: string }}
|
|
65
|
+
*/
|
|
66
|
+
function handleInlineUpload(type, content, filename) {
|
|
67
|
+
const validationError = validateInlineType(type);
|
|
68
|
+
if (validationError) {
|
|
69
|
+
return { error: validationError };
|
|
70
|
+
}
|
|
71
|
+
return { itemData: buildInlineItemData(type, content, filename) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Re-export for tests
|
|
75
|
+
export { isBinaryContent, getTypeFromExtension };
|
|
76
|
+
|
|
77
|
+
const router = Router();
|
|
78
|
+
|
|
79
|
+
// Mount trash and bulk operation routes
|
|
80
|
+
router.use('/', trashRoutes);
|
|
81
|
+
|
|
82
|
+
// POST /api/sessions/:id/canvas - Add canvas item
|
|
83
|
+
// Supports three modes:
|
|
84
|
+
// 1. Multipart mode: FormData with 'file' field - from browser file uploads
|
|
85
|
+
// 2. File mode: { filePath } - reads file from disk
|
|
86
|
+
// 3. Inline mode: { type, content, filename } - uses provided content directly
|
|
87
|
+
router.post('/:id/canvas', upload.single('file'), handleUploadError, (req, res) => {
|
|
88
|
+
const session = sessions.getById(req.params.id);
|
|
89
|
+
if (!session) {
|
|
90
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { filePath, type, content, filename } = req.body;
|
|
94
|
+
let result;
|
|
95
|
+
|
|
96
|
+
// Determine upload mode and process accordingly
|
|
97
|
+
if (req.file) {
|
|
98
|
+
result = handleMultipartUpload(req.file);
|
|
99
|
+
} else if (filePath) {
|
|
100
|
+
result = handleFilePathUpload(filePath);
|
|
101
|
+
} else if (content !== undefined && type && filename) {
|
|
102
|
+
result = handleInlineUpload(type, content, filename);
|
|
103
|
+
} else {
|
|
104
|
+
return res.status(400).json({
|
|
105
|
+
error: 'Either file upload, filePath, or (type + content + filename) is required'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result.error) {
|
|
110
|
+
return res.status(400).json({ error: result.error });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const item = canvasItems.create(req.params.id, result.itemData);
|
|
114
|
+
|
|
115
|
+
// Broadcast to session subscribers
|
|
116
|
+
broadcastCanvasUpdate(req.params.id, item);
|
|
117
|
+
|
|
118
|
+
res.status(201).json(item);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// PUT /api/sessions/:id/canvas/:itemId - Update canvas item content in-place
|
|
122
|
+
router.put('/:id/canvas/:itemId', (req, res) => {
|
|
123
|
+
const session = sessions.getById(req.params.id);
|
|
124
|
+
if (!session) {
|
|
125
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parsed = UpdateCanvasItemRequest.safeParse(req.body);
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return res.status(400).json({ error: 'Invalid request body', details: parsed.error.issues });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const item = canvasItems.getById(req.params.itemId);
|
|
134
|
+
if (!item) {
|
|
135
|
+
return res.status(404).json({ error: 'Canvas item not found' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (item.sessionId !== req.params.id) {
|
|
139
|
+
return res.status(400).json({ error: 'Canvas item does not belong to this session' });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!VALID_INLINE_TYPES.includes(item.type)) {
|
|
143
|
+
return res.status(400).json({ error: `Cannot update content for type: ${item.type}. Editable types: ${VALID_INLINE_TYPES.join(', ')}` });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const updatedItem = canvasItems.updateContent(req.params.itemId, parsed.data.content);
|
|
147
|
+
broadcastToSession(req.params.id, WS_MESSAGE_TYPES.CANVAS_UPDATE, { item: updatedItem });
|
|
148
|
+
res.json(updatedItem);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// GET /api/sessions/:id/canvas - List canvas items
|
|
152
|
+
// Returns only latest version of each file (no version metadata exposed)
|
|
153
|
+
router.get('/:id/canvas', (req, res) => {
|
|
154
|
+
const session = sessions.getById(req.params.id);
|
|
155
|
+
if (!session) {
|
|
156
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Get only latest versions (one per filename)
|
|
160
|
+
const items = canvasItems.getLatestVersionsBySessionId(req.params.id);
|
|
161
|
+
|
|
162
|
+
// Strip content/data from list responses to reduce payload size.
|
|
163
|
+
// Clients should use GET /canvas/file/:filename/content for inline content.
|
|
164
|
+
res.json(items.map(({ content: _content, data: _data, ...meta }) => meta));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// GET /api/sessions/:id/canvas/all - List ALL canvas items (including all versions)
|
|
168
|
+
// Returns all versions of each file for the frontend UI
|
|
169
|
+
router.get('/:id/canvas/all', (req, res) => {
|
|
170
|
+
const session = sessions.getById(req.params.id);
|
|
171
|
+
if (!session) {
|
|
172
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Get ALL versions (not just latest)
|
|
176
|
+
const items = canvasItems.getBySessionId(req.params.id);
|
|
177
|
+
|
|
178
|
+
// Strip content/data from list responses to reduce payload size.
|
|
179
|
+
res.json(items.map(({ content: _content, data: _data, ...meta }) => meta));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// GET /api/sessions/:id/canvas/file/:filename/history/:version - Get historical version of canvas file
|
|
183
|
+
// Uses standard versioning: version 1 = oldest, version N = latest (where N = totalVersions)
|
|
184
|
+
// NOTE: This route must be defined BEFORE the main file route to match correctly
|
|
185
|
+
router.get('/:id/canvas/file/:filename/history/:version', async (req, res) => {
|
|
186
|
+
const { id: sessionId, filename, version } = req.params;
|
|
187
|
+
const versionNum = parseInt(version, 10);
|
|
188
|
+
|
|
189
|
+
const session = sessions.getById(sessionId);
|
|
190
|
+
if (!session) {
|
|
191
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Get all versions of this file (newest first)
|
|
195
|
+
const allVersions = canvasItems.getAllVersionsByFilename(sessionId, filename);
|
|
196
|
+
if (allVersions.length === 0) {
|
|
197
|
+
return res.status(404).json({ error: 'File not found on canvas' });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Validate version number
|
|
201
|
+
if (isNaN(versionNum) || versionNum < 1 || versionNum > allVersions.length) {
|
|
202
|
+
return res.status(404).json({
|
|
203
|
+
error: `Version ${versionNum} not found. Available versions: 1-${allVersions.length} (1 = oldest, ${allVersions.length} = latest)`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Convert version number to array index
|
|
208
|
+
// allVersions is sorted DESC (newest first): [0] = newest, [length-1] = oldest
|
|
209
|
+
// Standard versioning: version 1 = oldest, version N = newest
|
|
210
|
+
// Mapping: version 1 → index (length-1), version N → index 0
|
|
211
|
+
// Formula: index = length - version
|
|
212
|
+
const index = allVersions.length - versionNum;
|
|
213
|
+
const item = allVersions[index];
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
// Create temp directory for this session
|
|
217
|
+
const tempDir = `/tmp/canvas-${sessionId}`;
|
|
218
|
+
await mkdir(tempDir, { recursive: true });
|
|
219
|
+
|
|
220
|
+
// Include version in filename to avoid collisions when viewing multiple versions
|
|
221
|
+
const ext = extname(filename);
|
|
222
|
+
const base = basename(filename, ext);
|
|
223
|
+
const tempFilename = `${base}.v${versionNum}${ext}`;
|
|
224
|
+
const filePath = join(tempDir, tempFilename);
|
|
225
|
+
|
|
226
|
+
// Write content based on type
|
|
227
|
+
await writeCanvasItemToFile(item, filePath);
|
|
228
|
+
|
|
229
|
+
// Get file size
|
|
230
|
+
const stats = statSync(filePath);
|
|
231
|
+
const fileSize = stats.size;
|
|
232
|
+
|
|
233
|
+
res.json({
|
|
234
|
+
filePath,
|
|
235
|
+
type: item.type,
|
|
236
|
+
mimeType: item.mimeType,
|
|
237
|
+
fileSize,
|
|
238
|
+
createdAt: item.createdAt,
|
|
239
|
+
version: versionNum,
|
|
240
|
+
totalVersions: allVersions.length
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
res.status(500).json({ error: `Failed to write file: ${err.message}` });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// GET /api/sessions/:id/canvas/file/:filename/content - Get canvas file content inline
|
|
248
|
+
// Returns content/data inline in JSON for browser consumption (unlike the temp-file endpoint below)
|
|
249
|
+
// Supports ?version=N query param (1-based, 1 = oldest)
|
|
250
|
+
router.get('/:id/canvas/file/:filename/content', (req, res) => {
|
|
251
|
+
const session = sessions.getById(req.params.id);
|
|
252
|
+
if (!session) return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
253
|
+
|
|
254
|
+
const allVersions = canvasItems.getAllVersionsByFilename(req.params.id, req.params.filename);
|
|
255
|
+
if (allVersions.length === 0) return res.status(404).json({ error: 'File not found' });
|
|
256
|
+
|
|
257
|
+
// Support ?version=N (1-based, 1 = oldest)
|
|
258
|
+
const versionParam = parseInt(req.query.version);
|
|
259
|
+
let item;
|
|
260
|
+
if (!isNaN(versionParam) && versionParam >= 1 && versionParam <= allVersions.length) {
|
|
261
|
+
// allVersions is sorted DESC (newest first): [0] = newest, [length-1] = oldest
|
|
262
|
+
// Standard versioning: version 1 = oldest, version N = newest
|
|
263
|
+
const index = allVersions.length - versionParam;
|
|
264
|
+
item = allVersions[index];
|
|
265
|
+
} else if (!isNaN(versionParam)) {
|
|
266
|
+
return res.status(404).json({
|
|
267
|
+
error: `Version ${versionParam} not found. Available versions: 1-${allVersions.length}`
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
item = allVersions[0]; // latest
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
res.json({
|
|
274
|
+
content: item.content ?? null,
|
|
275
|
+
data: item.data ?? null,
|
|
276
|
+
type: item.type,
|
|
277
|
+
mimeType: item.mimeType,
|
|
278
|
+
filename: item.filename,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// GET /api/sessions/:id/canvas/file/:filename - Get canvas file by filename
|
|
283
|
+
// Writes the file to /tmp and returns the file path for Claude's Read tool
|
|
284
|
+
// Always returns the latest version
|
|
285
|
+
router.get('/:id/canvas/file/:filename', async (req, res) => {
|
|
286
|
+
const { id: sessionId, filename } = req.params;
|
|
287
|
+
|
|
288
|
+
const session = sessions.getById(sessionId);
|
|
289
|
+
if (!session) {
|
|
290
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Get all versions of this file (newest first)
|
|
294
|
+
const allVersions = canvasItems.getAllVersionsByFilename(sessionId, filename);
|
|
295
|
+
if (allVersions.length === 0) {
|
|
296
|
+
return res.status(404).json({ error: 'File not found on canvas' });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Always use latest version (index 0)
|
|
300
|
+
const item = allVersions[0];
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Create temp directory for this session
|
|
304
|
+
const tempDir = `/tmp/canvas-${sessionId}`;
|
|
305
|
+
await mkdir(tempDir, { recursive: true });
|
|
306
|
+
|
|
307
|
+
// Use original filename without version suffix
|
|
308
|
+
const filePath = join(tempDir, filename);
|
|
309
|
+
|
|
310
|
+
// Write content based on type
|
|
311
|
+
await writeCanvasItemToFile(item, filePath);
|
|
312
|
+
|
|
313
|
+
// Get file size
|
|
314
|
+
const stats = statSync(filePath);
|
|
315
|
+
const fileSize = stats.size;
|
|
316
|
+
|
|
317
|
+
res.json({
|
|
318
|
+
filePath,
|
|
319
|
+
type: item.type,
|
|
320
|
+
mimeType: item.mimeType,
|
|
321
|
+
fileSize,
|
|
322
|
+
createdAt: item.createdAt,
|
|
323
|
+
version: allVersions.length, // Latest version number in standard versioning
|
|
324
|
+
totalVersions: allVersions.length
|
|
325
|
+
});
|
|
326
|
+
} catch (err) {
|
|
327
|
+
res.status(500).json({ error: `Failed to write file: ${err.message}` });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
export default router;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { commandButtons, sessions, commandRuns, projects } from '../database.js';
|
|
3
|
+
import { CreateCommandButtonRequest, UpdateCommandButtonRequest } from '../../../shared/src/contracts/commandButtons.js';
|
|
4
|
+
import { commandRunner } from '../services/commandRunner.js';
|
|
5
|
+
import { broadcastToSession, broadcastToProject } from '../websocket.js';
|
|
6
|
+
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
7
|
+
import { databaseManager } from '../db/DatabaseManager.js';
|
|
8
|
+
|
|
9
|
+
// Error message constants
|
|
10
|
+
const ERR_SESSION_NOT_FOUND = 'Session not found';
|
|
11
|
+
|
|
12
|
+
const router = Router({ mergeParams: true });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create WebSocket broadcast callbacks for a command run
|
|
16
|
+
* @param {string} sessionId
|
|
17
|
+
* @param {string} projectId
|
|
18
|
+
* @param {string} runId
|
|
19
|
+
* @param {string} buttonId
|
|
20
|
+
* @returns {Object} Callbacks for onOutput, onComplete, onError
|
|
21
|
+
*/
|
|
22
|
+
function createCommandRunCallbacks(sessionId, projectId, runId, buttonId) {
|
|
23
|
+
return {
|
|
24
|
+
onOutput: (text) => {
|
|
25
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_OUTPUT, {
|
|
26
|
+
sessionId, runId, buttonId, output: text,
|
|
27
|
+
});
|
|
28
|
+
broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_OUTPUT, {
|
|
29
|
+
projectId, sessionId, runId, buttonId, output: text,
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
onComplete: (exitCode, output) => {
|
|
33
|
+
const status = exitCode === 0 ? 'success' : 'error';
|
|
34
|
+
console.log(`[CommandButtons] Command completed: runId=${runId}, buttonId=${buttonId}, exitCode=${exitCode}, status=${status}`);
|
|
35
|
+
console.log(`[CommandButtons] Broadcasting to session ${sessionId}`);
|
|
36
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_COMPLETE, {
|
|
37
|
+
sessionId, runId, buttonId, status, exitCode, output,
|
|
38
|
+
});
|
|
39
|
+
console.log(`[CommandButtons] Broadcasting to project ${projectId}`);
|
|
40
|
+
broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_COMPLETE, {
|
|
41
|
+
projectId, sessionId, runId, buttonId, status, exitCode, output,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
onError: (message) => {
|
|
45
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, {
|
|
46
|
+
sessionId, runId, buttonId, error: message,
|
|
47
|
+
});
|
|
48
|
+
broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, {
|
|
49
|
+
projectId, sessionId, runId, buttonId, error: message,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Broadcast a command run error to session and project subscribers
|
|
57
|
+
*/
|
|
58
|
+
function broadcastCommandRunError({ sessionId, projectId, runId, buttonId, errorMessage }) {
|
|
59
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, {
|
|
60
|
+
sessionId, runId, buttonId, error: errorMessage,
|
|
61
|
+
});
|
|
62
|
+
broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, {
|
|
63
|
+
projectId, sessionId, runId, buttonId, error: errorMessage,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// GET /api/projects/:projectId/command-buttons - List all command buttons for project
|
|
68
|
+
router.get('/', (req, res) => {
|
|
69
|
+
const { projectId } = req.params;
|
|
70
|
+
const buttons = commandButtons.getByProjectId(projectId);
|
|
71
|
+
res.json(buttons);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// GET /api/projects/:projectId/command-buttons/latest-runs - Get latest run for each button per session in project
|
|
75
|
+
router.get('/latest-runs', (req, res) => {
|
|
76
|
+
const { projectId } = req.params;
|
|
77
|
+
|
|
78
|
+
// Verify project exists
|
|
79
|
+
const project = projects.getById(projectId);
|
|
80
|
+
if (!project) {
|
|
81
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const latestRuns = commandRuns.getLatestRunsForProject(projectId);
|
|
85
|
+
res.json(latestRuns);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// POST /api/projects/:projectId/command-buttons - Create new command button
|
|
89
|
+
router.post('/', (req, res) => {
|
|
90
|
+
const result = CreateCommandButtonRequest.safeParse(req.body);
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
return res.status(400).json({ error: result.error.issues[0].message });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const button = commandButtons.create({
|
|
96
|
+
projectId: req.params.projectId,
|
|
97
|
+
label: result.data.label,
|
|
98
|
+
command: result.data.command,
|
|
99
|
+
sortOrder: result.data.sortOrder,
|
|
100
|
+
showOnList: result.data.showOnList,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
res.status(201).json(button);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// GET /api/projects/:projectId/command-buttons/:id - Get single button
|
|
107
|
+
router.get('/:id', (req, res) => {
|
|
108
|
+
const button = commandButtons.getById(req.params.id);
|
|
109
|
+
if (!button) {
|
|
110
|
+
return res.status(404).json({ error: 'Command button not found' });
|
|
111
|
+
}
|
|
112
|
+
res.json(button);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// PATCH /api/projects/:projectId/command-buttons/:id - Update button
|
|
116
|
+
router.patch('/:id', (req, res) => {
|
|
117
|
+
const button = commandButtons.getById(req.params.id);
|
|
118
|
+
if (!button) {
|
|
119
|
+
return res.status(404).json({ error: 'Command button not found' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = UpdateCommandButtonRequest.safeParse(req.body);
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
return res.status(400).json({ error: result.error.issues[0].message });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Map validated data and only include fields that were provided
|
|
128
|
+
const updateData = {};
|
|
129
|
+
if (result.data.label !== undefined) updateData.label = result.data.label;
|
|
130
|
+
if (result.data.command !== undefined) updateData.command = result.data.command;
|
|
131
|
+
if (result.data.sortOrder !== undefined) updateData.sortOrder = result.data.sortOrder;
|
|
132
|
+
if (result.data.showOnList !== undefined) updateData.showOnList = result.data.showOnList;
|
|
133
|
+
|
|
134
|
+
const updated = commandButtons.update(req.params.id, updateData);
|
|
135
|
+
res.json(updated);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// DELETE /api/projects/:projectId/command-buttons/:id - Delete button
|
|
139
|
+
router.delete('/:id', (req, res) => {
|
|
140
|
+
const button = commandButtons.getById(req.params.id);
|
|
141
|
+
if (!button) {
|
|
142
|
+
return res.status(404).json({ error: 'Command button not found' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
commandButtons.delete(req.params.id);
|
|
146
|
+
res.status(204).send();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// POST /api/sessions/:sessionId/command-buttons/:buttonId/run - Execute button command
|
|
150
|
+
router.post('/run/:buttonId', (req, res) => {
|
|
151
|
+
const { sessionId, buttonId } = req.params;
|
|
152
|
+
|
|
153
|
+
const session = sessions.getById(sessionId);
|
|
154
|
+
if (!session) {
|
|
155
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const button = commandButtons.getById(buttonId);
|
|
159
|
+
if (!button) {
|
|
160
|
+
return res.status(404).json({ error: 'Command button not found' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const workingDirectory = session.gitWorktree || session.project?.workingDirectory || process.cwd();
|
|
164
|
+
const runId = databaseManager.generateId();
|
|
165
|
+
|
|
166
|
+
res.json({ runId, buttonId, status: 'running', output: '' });
|
|
167
|
+
|
|
168
|
+
const callbacks = createCommandRunCallbacks(sessionId, session.projectId, runId, buttonId);
|
|
169
|
+
|
|
170
|
+
(async () => {
|
|
171
|
+
try {
|
|
172
|
+
await commandRunner.run(
|
|
173
|
+
{ runId, command: button.command, workingDirectory },
|
|
174
|
+
callbacks,
|
|
175
|
+
{ sessionId, buttonId }
|
|
176
|
+
);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(`Error running command button ${buttonId}:`, error);
|
|
179
|
+
broadcastCommandRunError({ sessionId, projectId: session.projectId, runId, buttonId, errorMessage: error.message });
|
|
180
|
+
}
|
|
181
|
+
})();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// GET /api/sessions/:sessionId/command-buttons/runs - Get active runs for session
|
|
185
|
+
router.get('/runs', (req, res) => {
|
|
186
|
+
const { sessionId } = req.params;
|
|
187
|
+
|
|
188
|
+
const session = sessions.getById(sessionId);
|
|
189
|
+
if (!session) {
|
|
190
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Get running + recent (last hour) from commandRunner
|
|
194
|
+
const activeRuns = commandRunner.getRunsBySession(sessionId);
|
|
195
|
+
|
|
196
|
+
// Also get latest run per button (for historical context beyond 1 hour)
|
|
197
|
+
const latestRuns = commandRuns.getLatestRunsForSession(sessionId);
|
|
198
|
+
|
|
199
|
+
// Merge, preferring activeRuns data (more current output)
|
|
200
|
+
// Use buttonId as the key to avoid duplicates
|
|
201
|
+
const runMap = new Map();
|
|
202
|
+
|
|
203
|
+
// First add latest runs from database
|
|
204
|
+
for (const run of latestRuns) {
|
|
205
|
+
runMap.set(run.buttonId, {
|
|
206
|
+
runId: run.id,
|
|
207
|
+
buttonId: run.buttonId,
|
|
208
|
+
status: run.status,
|
|
209
|
+
output: run.output,
|
|
210
|
+
exitCode: run.exitCode,
|
|
211
|
+
startedAt: run.startedAt,
|
|
212
|
+
completedAt: run.completedAt,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Override with active/recent runs (they're more current)
|
|
217
|
+
for (const run of activeRuns) {
|
|
218
|
+
runMap.set(run.buttonId, run);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
res.json(Array.from(runMap.values()));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// GET /api/sessions/:sessionId/command-buttons/runs/:runId - Get single run by ID
|
|
225
|
+
router.get('/runs/:runId', (req, res) => {
|
|
226
|
+
const { sessionId, runId } = req.params;
|
|
227
|
+
|
|
228
|
+
const session = sessions.getById(sessionId);
|
|
229
|
+
if (!session) {
|
|
230
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check if run is currently running (in memory)
|
|
234
|
+
if (commandRunner.isRunning(runId)) {
|
|
235
|
+
const activeRuns = commandRunner.getRunsBySession(sessionId);
|
|
236
|
+
const run = activeRuns.find((r) => r.runId === runId);
|
|
237
|
+
if (run) {
|
|
238
|
+
return res.json(run);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Otherwise check database
|
|
243
|
+
const run = commandRuns.getById(runId);
|
|
244
|
+
if (!run || run.sessionId !== sessionId) {
|
|
245
|
+
return res.status(404).json({ error: 'Run not found' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
res.json({
|
|
249
|
+
runId: run.id,
|
|
250
|
+
buttonId: run.buttonId,
|
|
251
|
+
status: run.status,
|
|
252
|
+
output: run.output,
|
|
253
|
+
exitCode: run.exitCode,
|
|
254
|
+
startedAt: run.startedAt,
|
|
255
|
+
completedAt: run.completedAt,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// DELETE /api/sessions/:sessionId/command-buttons/runs/:runId - Delete a command run record
|
|
260
|
+
router.delete('/runs/:runId', (req, res) => {
|
|
261
|
+
const { sessionId, runId } = req.params;
|
|
262
|
+
|
|
263
|
+
const session = sessions.getById(sessionId);
|
|
264
|
+
if (!session) {
|
|
265
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const run = commandRuns.getById(runId);
|
|
269
|
+
if (!run || run.sessionId !== sessionId) {
|
|
270
|
+
return res.status(404).json({ error: 'Run not found' });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (commandRunner.isRunning(runId)) {
|
|
274
|
+
return res.status(409).json({ error: 'Cannot delete a running command. Kill it first.' });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
commandRuns.deleteById(runId);
|
|
278
|
+
|
|
279
|
+
// Broadcast deletion to session and project subscribers
|
|
280
|
+
broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_DELETED, {
|
|
281
|
+
runId,
|
|
282
|
+
buttonId: run.buttonId,
|
|
283
|
+
sessionId,
|
|
284
|
+
});
|
|
285
|
+
broadcastToProject(session.projectId, WS_MESSAGE_TYPES.COMMAND_RUN_DELETED, {
|
|
286
|
+
runId,
|
|
287
|
+
buttonId: run.buttonId,
|
|
288
|
+
sessionId,
|
|
289
|
+
projectId: session.projectId,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
res.status(204).send();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// POST /api/sessions/:sessionId/command-buttons/runs/:runId/kill - Kill running command
|
|
296
|
+
router.post('/runs/:runId/kill', (req, res) => {
|
|
297
|
+
const { sessionId, runId } = req.params;
|
|
298
|
+
|
|
299
|
+
const session = sessions.getById(sessionId);
|
|
300
|
+
if (!session) {
|
|
301
|
+
return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const killed = commandRunner.kill(runId);
|
|
305
|
+
if (!killed) {
|
|
306
|
+
return res.status(404).json({ error: 'Run not found or already completed' });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
res.json({ success: true, runId });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
export default router;
|