@assistkick/create 1.10.0 → 1.11.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.
Files changed (206) hide show
  1. package/dist/src/scaffolder.d.ts +12 -1
  2. package/dist/src/scaffolder.js +40 -3
  3. package/dist/src/scaffolder.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/assistkick-product-system/package.json +1 -1
  6. package/templates/assistkick-product-system/packages/backend/package.json +1 -0
  7. package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
  9. package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
  10. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
  11. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
  13. package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
  14. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
  15. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
  16. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
  17. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
  18. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +390 -22
  19. package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
  20. package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
  21. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
  22. package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
  23. package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
  24. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
  25. package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
  26. package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
  27. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
  28. package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
  29. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
  30. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
  31. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
  32. package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
  33. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
  34. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
  35. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
  41. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
  42. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
  43. package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
  44. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
  45. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
  46. package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
  47. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
  48. package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
  49. package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
  50. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
  51. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
  52. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
  53. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
  54. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
  55. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
  56. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
  57. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
  58. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
  59. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
  60. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
  61. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +129 -8
  62. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
  63. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
  64. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
  65. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
  66. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
  67. package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
  68. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
  69. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
  70. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
  71. package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
  82. package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
  85. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
  86. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
  87. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
  88. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
  89. package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
  92. package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
  97. package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
  99. package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
  102. package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
  106. package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
  109. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
  111. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
  112. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
  113. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
  114. package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
  120. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
  132. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
  133. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
  134. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
  135. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
  136. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
  137. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
  138. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
  139. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
  140. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
  141. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
  142. package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
  143. package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
  144. package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
  145. package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
  146. package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
  147. package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
  148. package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
  149. package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
  150. package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
  151. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
  152. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
  153. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
  154. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
  155. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
  156. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
  157. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
  158. package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
  161. package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
  162. package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
  163. package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
  164. package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
  165. package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
  166. package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
  167. package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
  168. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
  169. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
  170. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
  171. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
  172. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
  173. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
  174. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
  175. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
  176. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
  177. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
  178. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
  179. package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
  180. package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
  181. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
  182. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
  183. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
  184. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
  185. package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
  186. package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
  187. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
  188. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
  189. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
  190. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  191. package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
  192. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
  193. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
  194. package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
  195. package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
  196. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
  197. package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
  198. package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
  199. package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
  200. package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
  201. package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
  202. package/templates/skills/assistkick-debugger/SKILL.md +23 -23
  203. package/templates/skills/assistkick-developer/SKILL.md +59 -63
  204. package/templates/skills/assistkick-interview/SKILL.md +26 -26
  205. package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
  206. package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,306 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createGitRoutes } from './git.ts';
4
+ import express from 'express';
5
+ import type { Server } from 'node:http';
6
+
7
+ const createMockDeps = (overrides: Record<string, any> = {}) => ({
8
+ projectService: {
9
+ getById: mock.fn(async () => ({ id: 'proj_1', repoUrl: 'https://github.com/org/repo.git' })),
10
+ connectRepo: mock.fn(async () => ({ id: 'proj_1' })),
11
+ ...overrides.projectService,
12
+ },
13
+ githubAppService: {
14
+ isConfigured: mock.fn(() => false),
15
+ getInstallationToken: mock.fn(async () => 'token123'),
16
+ ...overrides.githubAppService,
17
+ },
18
+ workspaceService: {
19
+ hasWorkspace: mock.fn(() => true),
20
+ getWorkspacePath: mock.fn(() => '/tmp/ws/proj_1'),
21
+ listBranches: mock.fn(async () => ({
22
+ current: 'main',
23
+ local: ['main', 'develop'],
24
+ remote: ['origin/main', 'origin/develop', 'origin/feature-x'],
25
+ })),
26
+ hasUncommittedChanges: mock.fn(async () => false),
27
+ commitAll: mock.fn(async () => {}),
28
+ checkoutBranch: mock.fn(async () => {}),
29
+ createBranch: mock.fn(async () => {}),
30
+ listRemotes: mock.fn(async () => [{ name: 'origin', url: 'https://github.com/org/repo.git' }]),
31
+ addRemote: mock.fn(async () => {}),
32
+ fetchRemote: mock.fn(async () => {}),
33
+ fetchRemoteSsh: mock.fn(async () => {}),
34
+ pullBranch: mock.fn(async () => {}),
35
+ pullBranchSsh: mock.fn(async () => {}),
36
+ pushBranch: mock.fn(async () => {}),
37
+ pushBranchSsh: mock.fn(async () => {}),
38
+ getAheadBehind: mock.fn(async () => ({ ahead: 2, behind: 1 })),
39
+ hasOnlyInitialCommit: mock.fn(async () => false),
40
+ resetToRemoteBranch: mock.fn(async () => {}),
41
+ getDefaultBranch: mock.fn(async () => 'main'),
42
+ getCurrentBranch: mock.fn(async () => 'main'),
43
+ ...overrides.workspaceService,
44
+ },
45
+ sshKeyService: {
46
+ isConfigured: mock.fn(() => false),
47
+ ...overrides.sshKeyService,
48
+ },
49
+ log: mock.fn(),
50
+ });
51
+
52
+ const createTestApp = (deps: any) => {
53
+ const app = express();
54
+ app.use(express.json());
55
+ const gitRoutes = createGitRoutes(deps);
56
+ app.use('/api/projects/:id/git', gitRoutes);
57
+ return app;
58
+ };
59
+
60
+ const request = (server: Server) => {
61
+ const addr = server.address() as { port: number };
62
+ const base = `http://127.0.0.1:${addr.port}`;
63
+ return {
64
+ get: async (path: string) => {
65
+ const res = await fetch(`${base}${path}`);
66
+ return { status: res.status, body: await res.json() };
67
+ },
68
+ post: async (path: string, body?: any) => {
69
+ const res = await fetch(`${base}${path}`, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify(body || {}),
73
+ });
74
+ return { status: res.status, body: await res.json() };
75
+ },
76
+ };
77
+ };
78
+
79
+ describe('Git Branch Routes', () => {
80
+ let server: Server;
81
+ let http: ReturnType<typeof request>;
82
+ let deps: ReturnType<typeof createMockDeps>;
83
+
84
+ beforeEach(async () => {
85
+ deps = createMockDeps();
86
+ const app = createTestApp(deps);
87
+ server = await new Promise<Server>((resolve) => {
88
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
89
+ });
90
+ http = request(server);
91
+ });
92
+
93
+ afterEach(async () => {
94
+ await new Promise<void>((resolve) => server.close(() => resolve()));
95
+ });
96
+
97
+ describe('GET /branches', () => {
98
+ it('returns local and remote branches with current branch', async () => {
99
+ const { status, body } = await http.get('/api/projects/proj_1/git/branches');
100
+ assert.equal(status, 200);
101
+ assert.equal(body.current, 'main');
102
+ assert.deepEqual(body.local, ['main', 'develop']);
103
+ assert.deepEqual(body.remote, ['origin/main', 'origin/develop', 'origin/feature-x']);
104
+ });
105
+
106
+ it('returns 404 when no workspace exists', async () => {
107
+ deps.workspaceService.hasWorkspace = mock.fn(() => false);
108
+ const app = createTestApp(deps);
109
+ await new Promise<void>((resolve) => server.close(() => resolve()));
110
+ server = await new Promise<Server>((resolve) => {
111
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
112
+ });
113
+ http = request(server);
114
+
115
+ const { status, body } = await http.get('/api/projects/proj_1/git/branches');
116
+ assert.equal(status, 404);
117
+ assert.ok(body.error.includes('No git workspace'));
118
+ });
119
+ });
120
+
121
+ describe('POST /checkout', () => {
122
+ it('switches to the specified branch', async () => {
123
+ const { status, body } = await http.post('/api/projects/proj_1/git/checkout', {
124
+ branch: 'develop',
125
+ });
126
+ assert.equal(status, 200);
127
+ assert.equal(body.success, true);
128
+ assert.equal(deps.workspaceService.checkoutBranch.mock.calls.length, 1);
129
+ });
130
+
131
+ it('returns 400 when branch is missing', async () => {
132
+ const { status, body } = await http.post('/api/projects/proj_1/git/checkout', {});
133
+ assert.equal(status, 400);
134
+ assert.ok(body.error.includes('branch is required'));
135
+ });
136
+
137
+ it('returns 409 when dirty with no commit message', async () => {
138
+ deps.workspaceService.hasUncommittedChanges = mock.fn(async () => true);
139
+ const app = createTestApp(deps);
140
+ await new Promise<void>((resolve) => server.close(() => resolve()));
141
+ server = await new Promise<Server>((resolve) => {
142
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
143
+ });
144
+ http = request(server);
145
+
146
+ const { status, body } = await http.post('/api/projects/proj_1/git/checkout', {
147
+ branch: 'develop',
148
+ });
149
+ assert.equal(status, 409);
150
+ assert.equal(body.dirty, true);
151
+ });
152
+
153
+ it('commits and switches when dirty with commit message', async () => {
154
+ deps.workspaceService.hasUncommittedChanges = mock.fn(async () => true);
155
+ const app = createTestApp(deps);
156
+ await new Promise<void>((resolve) => server.close(() => resolve()));
157
+ server = await new Promise<Server>((resolve) => {
158
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
159
+ });
160
+ http = request(server);
161
+
162
+ const { status, body } = await http.post('/api/projects/proj_1/git/checkout', {
163
+ branch: 'develop',
164
+ commitMessage: 'Save work',
165
+ });
166
+ assert.equal(status, 200);
167
+ assert.equal(body.success, true);
168
+ assert.equal(deps.workspaceService.commitAll.mock.calls.length, 1);
169
+ assert.equal(deps.workspaceService.checkoutBranch.mock.calls.length, 1);
170
+ });
171
+ });
172
+
173
+ describe('POST /branches (create)', () => {
174
+ it('creates and checks out a new branch', async () => {
175
+ const { status, body } = await http.post('/api/projects/proj_1/git/branches', {
176
+ name: 'feature-new',
177
+ });
178
+ assert.equal(status, 200);
179
+ assert.equal(body.success, true);
180
+ assert.equal(deps.workspaceService.createBranch.mock.calls.length, 1);
181
+ });
182
+
183
+ it('returns 400 when name is missing', async () => {
184
+ const { status, body } = await http.post('/api/projects/proj_1/git/branches', {});
185
+ assert.equal(status, 400);
186
+ assert.ok(body.error.includes('name is required'));
187
+ });
188
+
189
+ it('returns 400 for invalid branch names', async () => {
190
+ const invalidNames = ['has space', 'has..dots', '-starts-with-dash', 'has~tilde', 'has:colon'];
191
+ for (const name of invalidNames) {
192
+ const { status, body } = await http.post('/api/projects/proj_1/git/branches', { name });
193
+ assert.equal(status, 400, `Expected 400 for "${name}"`);
194
+ assert.ok(body.error.includes('Invalid branch name'), `Expected invalid branch name error for "${name}"`);
195
+ }
196
+ });
197
+
198
+ it('returns 404 when no workspace exists', async () => {
199
+ deps.workspaceService.hasWorkspace = mock.fn(() => false);
200
+ const app = createTestApp(deps);
201
+ await new Promise<void>((resolve) => server.close(() => resolve()));
202
+ server = await new Promise<Server>((resolve) => {
203
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
204
+ });
205
+ http = request(server);
206
+
207
+ const { status, body } = await http.post('/api/projects/proj_1/git/branches', {
208
+ name: 'feature-new',
209
+ });
210
+ assert.equal(status, 404);
211
+ assert.ok(body.error.includes('No git workspace'));
212
+ });
213
+ });
214
+
215
+ describe('GET /remotes', () => {
216
+ it('returns configured remotes', async () => {
217
+ const { status, body } = await http.get('/api/projects/proj_1/git/remotes');
218
+ assert.equal(status, 200);
219
+ assert.equal(body.remotes.length, 1);
220
+ assert.equal(body.remotes[0].name, 'origin');
221
+ });
222
+
223
+ it('returns 404 when no workspace', async () => {
224
+ deps.workspaceService.hasWorkspace = mock.fn(() => false);
225
+ const app = createTestApp(deps);
226
+ await new Promise<void>((resolve) => server.close(() => resolve()));
227
+ server = await new Promise<Server>((resolve) => {
228
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
229
+ });
230
+ http = request(server);
231
+
232
+ const { status } = await http.get('/api/projects/proj_1/git/remotes');
233
+ assert.equal(status, 404);
234
+ });
235
+ });
236
+
237
+ describe('POST /remotes', () => {
238
+ it('adds a new remote', async () => {
239
+ const { status, body } = await http.post('/api/projects/proj_1/git/remotes', {
240
+ url: 'https://github.com/org/other.git',
241
+ });
242
+ assert.equal(status, 200);
243
+ assert.equal(body.success, true);
244
+ assert.equal(deps.workspaceService.addRemote.mock.calls.length, 1);
245
+ });
246
+
247
+ it('returns 400 when url is missing', async () => {
248
+ const { status, body } = await http.post('/api/projects/proj_1/git/remotes', {});
249
+ assert.equal(status, 400);
250
+ assert.ok(body.error.includes('url is required'));
251
+ });
252
+ });
253
+
254
+ describe('POST /fetch', () => {
255
+ it('fetches from remote and returns ahead/behind', async () => {
256
+ const { status, body } = await http.post('/api/projects/proj_1/git/fetch', {});
257
+ assert.equal(status, 200);
258
+ assert.equal(body.ahead, 2);
259
+ assert.equal(body.behind, 1);
260
+ assert.equal(deps.workspaceService.fetchRemote.mock.calls.length, 1);
261
+ });
262
+ });
263
+
264
+ describe('POST /pull', () => {
265
+ it('pulls and returns ahead/behind', async () => {
266
+ const { status, body } = await http.post('/api/projects/proj_1/git/pull', {});
267
+ assert.equal(status, 200);
268
+ assert.equal(body.ahead, 2);
269
+ assert.equal(body.behind, 1);
270
+ assert.equal(deps.workspaceService.pullBranch.mock.calls.length, 1);
271
+ });
272
+
273
+ it('does a force reset when force=true', async () => {
274
+ const { status, body } = await http.post('/api/projects/proj_1/git/pull', { force: true });
275
+ assert.equal(status, 200);
276
+ assert.equal(deps.workspaceService.resetToRemoteBranch.mock.calls.length, 1);
277
+ });
278
+ });
279
+
280
+ describe('POST /push', () => {
281
+ it('pushes and returns ahead/behind', async () => {
282
+ const { status, body } = await http.post('/api/projects/proj_1/git/push', {});
283
+ assert.equal(status, 200);
284
+ assert.equal(body.ahead, 2);
285
+ assert.equal(body.behind, 1);
286
+ assert.equal(deps.workspaceService.pushBranch.mock.calls.length, 1);
287
+ });
288
+ });
289
+
290
+ describe('GET /ahead-behind', () => {
291
+ it('returns ahead and behind counts', async () => {
292
+ const { status, body } = await http.get('/api/projects/proj_1/git/ahead-behind');
293
+ assert.equal(status, 200);
294
+ assert.equal(body.ahead, 2);
295
+ assert.equal(body.behind, 1);
296
+ });
297
+ });
298
+
299
+ describe('GET /initial-commit', () => {
300
+ it('returns onlyInitialCommit flag', async () => {
301
+ const { status, body } = await http.get('/api/projects/proj_1/git/initial-commit');
302
+ assert.equal(status, 200);
303
+ assert.equal(body.onlyInitialCommit, false);
304
+ });
305
+ });
306
+ });
@@ -0,0 +1,133 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createGitRoutes } from './git.ts';
4
+ import express from 'express';
5
+ import type { Server } from 'node:http';
6
+
7
+ const createMockDeps = (overrides: Record<string, any> = {}) => ({
8
+ projectService: {
9
+ getById: mock.fn(async () => ({ id: 'proj_1', repoUrl: null, gitAuthMethod: null })),
10
+ connectRepo: mock.fn(async (_id: string, opts: any) => ({
11
+ id: 'proj_1',
12
+ repoUrl: opts.repoUrl,
13
+ githubInstallationId: opts.githubInstallationId || null,
14
+ githubRepoFullName: opts.githubRepoFullName || null,
15
+ baseBranch: opts.baseBranch || null,
16
+ gitAuthMethod: null,
17
+ })),
18
+ setGitHubAppAuth: mock.fn(async () => ({
19
+ id: 'proj_1',
20
+ gitAuthMethod: 'github_app',
21
+ })),
22
+ ...overrides.projectService,
23
+ },
24
+ githubAppService: {
25
+ isConfigured: mock.fn(() => true),
26
+ getInstallationToken: mock.fn(async () => 'token123'),
27
+ buildAuthenticatedCloneUrl: mock.fn(async () => 'https://x-access-token:token123@github.com/org/repo.git'),
28
+ ...overrides.githubAppService,
29
+ },
30
+ workspaceService: {
31
+ hasWorkspace: mock.fn(() => true),
32
+ getWorkspacePath: mock.fn(() => '/tmp/ws/proj_1'),
33
+ cloneRepo: mock.fn(async () => {}),
34
+ getDefaultBranch: mock.fn(async () => 'main'),
35
+ hasOnlyInitialCommit: mock.fn(async () => false),
36
+ resetToRemoteBranch: mock.fn(async () => {}),
37
+ listBranches: mock.fn(async () => ({ current: 'main', local: ['main'], remote: ['origin/main'] })),
38
+ hasUncommittedChanges: mock.fn(async () => false),
39
+ commitAll: mock.fn(async () => {}),
40
+ checkoutBranch: mock.fn(async () => {}),
41
+ createBranch: mock.fn(async () => {}),
42
+ listRemotes: mock.fn(async () => []),
43
+ addRemote: mock.fn(async () => {}),
44
+ fetchRemote: mock.fn(async () => {}),
45
+ fetchRemoteSsh: mock.fn(async () => {}),
46
+ pullBranch: mock.fn(async () => {}),
47
+ pullBranchSsh: mock.fn(async () => {}),
48
+ pushBranch: mock.fn(async () => {}),
49
+ pushBranchSsh: mock.fn(async () => {}),
50
+ getAheadBehind: mock.fn(async () => ({ ahead: 0, behind: 0 })),
51
+ getCurrentBranch: mock.fn(async () => 'main'),
52
+ ...overrides.workspaceService,
53
+ },
54
+ sshKeyService: {
55
+ isConfigured: mock.fn(() => false),
56
+ ...overrides.sshKeyService,
57
+ },
58
+ log: mock.fn(),
59
+ });
60
+
61
+ const createTestApp = (deps: any) => {
62
+ const app = express();
63
+ app.use(express.json());
64
+ const gitRoutes = createGitRoutes(deps);
65
+ app.use('/api/projects/:id/git', gitRoutes);
66
+ return app;
67
+ };
68
+
69
+ const request = (server: Server) => {
70
+ const addr = server.address() as { port: number };
71
+ const base = `http://127.0.0.1:${addr.port}`;
72
+ return {
73
+ post: async (path: string, body?: any) => {
74
+ const res = await fetch(`${base}${path}`, {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify(body || {}),
78
+ });
79
+ return { status: res.status, body: await res.json() };
80
+ },
81
+ };
82
+ };
83
+
84
+ describe('Git Connect Routes', () => {
85
+ let server: Server;
86
+ let http: ReturnType<typeof request>;
87
+ let deps: ReturnType<typeof createMockDeps>;
88
+
89
+ beforeEach(async () => {
90
+ deps = createMockDeps();
91
+ const app = createTestApp(deps);
92
+ server = await new Promise<Server>((resolve) => {
93
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
94
+ });
95
+ http = request(server);
96
+ });
97
+
98
+ afterEach(async () => {
99
+ await new Promise<void>((resolve) => server.close(() => resolve()));
100
+ });
101
+
102
+ describe('POST /connect', () => {
103
+ it('calls setGitHubAppAuth when githubInstallationId is provided', async () => {
104
+ const { status } = await http.post('/api/projects/proj_1/git/connect', {
105
+ githubRepoFullName: 'org/repo',
106
+ githubInstallationId: 'inst_123',
107
+ });
108
+
109
+ assert.equal(status, 200);
110
+ assert.equal(deps.projectService.setGitHubAppAuth.mock.calls.length, 1);
111
+ assert.equal(deps.projectService.setGitHubAppAuth.mock.calls[0].arguments[0], 'proj_1');
112
+ });
113
+
114
+ it('does not call setGitHubAppAuth when no githubInstallationId', async () => {
115
+ const { status } = await http.post('/api/projects/proj_1/git/connect', {
116
+ repoUrl: 'https://github.com/org/repo.git',
117
+ });
118
+
119
+ assert.equal(status, 200);
120
+ assert.equal(deps.projectService.setGitHubAppAuth.mock.calls.length, 0);
121
+ });
122
+
123
+ it('returns the project with github_app auth method set', async () => {
124
+ const { status, body } = await http.post('/api/projects/proj_1/git/connect', {
125
+ githubRepoFullName: 'org/repo',
126
+ githubInstallationId: 'inst_123',
127
+ });
128
+
129
+ assert.equal(status, 200);
130
+ assert.equal(body.project.gitAuthMethod, 'github_app');
131
+ });
132
+ });
133
+ });
@@ -49,7 +49,7 @@ export const createWorkflowExecutionRoutes = ({
49
49
  const result = await workflowEngine.start(featureId, resolvedWorkflowId, projectId);
50
50
  res.json({ started: true, feature_id: featureId, executionId: result.executionId });
51
51
  } catch (err: any) {
52
- log('WORKFLOW', `START ERROR: ${err.message}`);
52
+ log('WORKFLOW', `START ERROR:`, err.stack || err.message);
53
53
  res.status(500).json({ error: err.message });
54
54
  }
55
55
  });
@@ -61,7 +61,7 @@ export const createWorkflowExecutionRoutes = ({
61
61
  const status = await workflowEngine.getStatus(featureId);
62
62
  res.json(status);
63
63
  } catch (err: any) {
64
- log('WORKFLOW', `STATUS ERROR: ${err.message}`);
64
+ log('WORKFLOW', `STATUS ERROR:`, err.stack || err.message);
65
65
  res.status(500).json({ error: err.message });
66
66
  }
67
67
  });
@@ -82,7 +82,7 @@ export const createWorkflowExecutionRoutes = ({
82
82
  await workflowEngine.resume(executionId);
83
83
  res.json({ resumed: true, feature_id: featureId, executionId });
84
84
  } catch (err: any) {
85
- log('WORKFLOW', `RESUME ERROR: ${err.message}`);
85
+ log('WORKFLOW', `RESUME ERROR:`, err.stack || err.message);
86
86
  res.status(500).json({ error: err.message });
87
87
  }
88
88
  });
@@ -102,7 +102,7 @@ export const createWorkflowExecutionRoutes = ({
102
102
  await workflowEngine.forceNextNode(featureId, nodeId);
103
103
  res.json({ success: true, feature_id: featureId, skippedNodeId: nodeId });
104
104
  } catch (err: any) {
105
- log('WORKFLOW', `FORCE-NEXT ERROR: ${err.message}`);
105
+ log('WORKFLOW', `FORCE-NEXT ERROR:`, err.stack || err.message);
106
106
  res.status(500).json({ error: err.message });
107
107
  }
108
108
  });
@@ -122,11 +122,68 @@ export const createWorkflowExecutionRoutes = ({
122
122
  await workflowEngine.restartNode(featureId, nodeId);
123
123
  res.json({ success: true, feature_id: featureId, restartedNodeId: nodeId });
124
124
  } catch (err: any) {
125
- log('WORKFLOW', `RESTART-NODE ERROR: ${err.message}`);
125
+ log('WORKFLOW', `RESTART-NODE ERROR:`, err.stack || err.message);
126
126
  res.status(500).json({ error: err.message });
127
127
  }
128
128
  });
129
129
 
130
+ // POST /api/kanban/:id/workflow/stop-node — stop the currently executing node
131
+ router.post('/:id/workflow/stop-node', async (req, res) => {
132
+ const featureId = req.params.id;
133
+ const { nodeId } = req.body || {};
134
+ log('WORKFLOW', `POST /api/kanban/${featureId}/workflow/stop-node nodeId=${nodeId}`);
135
+
136
+ if (!nodeId) {
137
+ res.status(400).json({ error: 'nodeId is required' });
138
+ return;
139
+ }
140
+
141
+ try {
142
+ await workflowEngine.stopNode(featureId, nodeId);
143
+ // If orchestrator Play All is active, abort it
144
+ if (orchestrator.getStatus().active) {
145
+ orchestrator.stopPlayAll();
146
+ log('WORKFLOW', 'Play All aborted due to user stop');
147
+ }
148
+ res.json({ success: true, feature_id: featureId, stoppedNodeId: nodeId });
149
+ } catch (err: any) {
150
+ log('WORKFLOW', `STOP-NODE ERROR: ${err.message}`);
151
+ if (err.message.includes('No running execution')) {
152
+ res.status(404).json({ error: err.message });
153
+ } else if (err.message.includes('not the current')) {
154
+ res.status(400).json({ error: err.message });
155
+ } else {
156
+ res.status(500).json({ error: err.message });
157
+ }
158
+ }
159
+ });
160
+
161
+ // POST /api/kanban/:id/workflow/continue-node — resume a stuck/failed agent node
162
+ router.post('/:id/workflow/continue-node', async (req, res) => {
163
+ const featureId = req.params.id;
164
+ const { nodeId } = req.body || {};
165
+ log('WORKFLOW', `POST /api/kanban/${featureId}/workflow/continue-node nodeId=${nodeId}`);
166
+
167
+ if (!nodeId) {
168
+ res.status(400).json({ error: 'nodeId is required' });
169
+ return;
170
+ }
171
+
172
+ try {
173
+ await workflowEngine.continueNode(featureId, nodeId);
174
+ res.json({ success: true, feature_id: featureId, continuedNodeId: nodeId });
175
+ } catch (err: any) {
176
+ log('WORKFLOW', `CONTINUE-NODE ERROR: ${err.message}`);
177
+ if (err.message.includes('No execution found')) {
178
+ res.status(404).json({ error: err.message });
179
+ } else if (err.message.includes('no Claude session ID')) {
180
+ res.status(400).json({ error: err.message });
181
+ } else {
182
+ res.status(500).json({ error: err.message });
183
+ }
184
+ }
185
+ });
186
+
130
187
  // GET /api/kanban/:id/workflow-monitor — get full execution monitor data for a feature
131
188
  router.get('/:id/workflow-monitor', async (req, res) => {
132
189
  const featureId = req.params.id;
@@ -138,7 +195,7 @@ export const createWorkflowExecutionRoutes = ({
138
195
  }
139
196
  res.json({ monitor: data });
140
197
  } catch (err: any) {
141
- log('WORKFLOW', `MONITOR ERROR: ${err.message}`);
198
+ log('WORKFLOW', `MONITOR ERROR:`, err.stack || err.message);
142
199
  res.status(500).json({ error: err.message });
143
200
  }
144
201
  });
@@ -156,7 +213,7 @@ export const createWorkflowExecutionRoutes = ({
156
213
  const metrics = await workflowEngine.getExecutionMetrics(executionId);
157
214
  res.json({ metrics });
158
215
  } catch (err: any) {
159
- log('WORKFLOW', `METRICS ERROR: ${err.message}`);
216
+ log('WORKFLOW', `METRICS ERROR:`, err.stack || err.message);
160
217
  res.status(500).json({ error: err.message });
161
218
  }
162
219
  });
@@ -177,7 +234,7 @@ export const createWorkflowExecutionRoutes = ({
177
234
  result.error ? { error: result.error } : { started: result.started },
178
235
  );
179
236
  } catch (err: any) {
180
- log('ORCHESTRATOR', `UNEXPECTED ERROR: ${err.message}`);
237
+ log('ORCHESTRATOR', `UNEXPECTED ERROR:`, err.stack || err.message);
181
238
  res.status(500).json({ error: err.message });
182
239
  }
183
240
  });
@@ -190,7 +247,7 @@ export const createWorkflowExecutionRoutes = ({
190
247
  result.error ? { error: result.error } : { stopped: result.stopped },
191
248
  );
192
249
  } catch (err: any) {
193
- log('ORCHESTRATOR', `UNEXPECTED ERROR: ${err.message}`);
250
+ log('ORCHESTRATOR', `UNEXPECTED ERROR:`, err.stack || err.message);
194
251
  res.status(500).json({ error: err.message });
195
252
  }
196
253
  });