@assistkick/create 1.9.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 +391 -23
  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 +149 -14
  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
@@ -55,6 +55,15 @@ export class ProjectWorkspaceService {
55
55
  }
56
56
  };
57
57
 
58
+ /** Verify that git is installed and available on the system. Throws if not. */
59
+ verifyGitAvailable = async (): Promise<void> => {
60
+ try {
61
+ await this.claudeService.spawnCommand('git', ['--version'], this.workspacesDir);
62
+ } catch {
63
+ throw new Error('Git is required to create a project. Please install git and try again.');
64
+ }
65
+ };
66
+
58
67
  /** Initialize a new empty local git repository for a project. */
59
68
  initWorkspace = async (projectId: string): Promise<string> => {
60
69
  const wsPath = this.getWorkspacePath(projectId);
@@ -74,20 +83,31 @@ export class ProjectWorkspaceService {
74
83
  return wsPath;
75
84
  };
76
85
 
77
- /** Clone a remote repository into the project workspace. */
86
+ /**
87
+ * Connect a remote repository using git remote add + git fetch.
88
+ * Replaces the old rm -rf + git clone pattern.
89
+ * If the workspace doesn't have a git repo yet, initializes one first.
90
+ */
78
91
  cloneRepo = async (projectId: string, cloneUrl: string): Promise<string> => {
79
92
  const wsPath = this.getWorkspacePath(projectId);
80
- await this.ensureDir(this.workspacesDir);
93
+ await this.ensureDir(wsPath);
81
94
 
82
- // If workspace already exists, remove it first
83
- if (existsSync(wsPath)) {
84
- this.log('WORKSPACE', `Removing existing workspace before clone: ${wsPath}`);
85
- await this.claudeService.spawnCommand('rm', ['-rf', wsPath], this.workspacesDir);
95
+ // Ensure there's a local git repo
96
+ if (!existsSync(join(wsPath, '.git'))) {
97
+ await this.initWorkspace(projectId);
86
98
  }
87
99
 
88
- this.log('WORKSPACE', `Cloning ${cloneUrl} into ${wsPath}`);
89
- await this.claudeService.spawnCommand('git', ['clone', cloneUrl, wsPath], this.workspacesDir);
90
- this.log('WORKSPACE', `Clone completed for project ${projectId}`);
100
+ // Add the remote (replace if it already exists)
101
+ try {
102
+ await this.claudeService.spawnCommand('git', ['remote', 'remove', 'origin'], wsPath);
103
+ } catch {
104
+ // No existing origin — that's fine
105
+ }
106
+
107
+ this.log('WORKSPACE', `Adding remote origin ${cloneUrl} for project ${projectId}`);
108
+ await this.claudeService.spawnCommand('git', ['remote', 'add', 'origin', cloneUrl], wsPath);
109
+ await this.claudeService.spawnCommand('git', ['fetch', 'origin'], wsPath);
110
+ this.log('WORKSPACE', `Remote added and fetched for project ${projectId}`);
91
111
  return wsPath;
92
112
  };
93
113
 
@@ -103,16 +123,16 @@ export class ProjectWorkspaceService {
103
123
  // Returns e.g. "origin/main" — strip the "origin/" prefix
104
124
  const branch = result.trim().replace(/^origin\//, '');
105
125
  if (branch) return branch;
106
- } catch {
107
- // Fallback: try to get current branch name
126
+ } catch (err: any) {
127
+ this.log('WORKSPACE', `symbolic-ref lookup failed (trying fallback): ${err.message}`);
108
128
  }
109
129
 
110
130
  try {
111
131
  const result = await this.claudeService.spawnCommand('git', ['branch', '--show-current'], wsPath);
112
132
  const branch = result.trim();
113
133
  if (branch) return branch;
114
- } catch {
115
- // Final fallback
134
+ } catch (err: any) {
135
+ this.log('WORKSPACE', `branch --show-current failed (using default 'main'): ${err.message}`);
116
136
  }
117
137
 
118
138
  return 'main';
@@ -135,7 +155,7 @@ export class ProjectWorkspaceService {
135
155
  this.log('WORKSPACE', `Pull completed for project ${projectId}`);
136
156
  } catch (err: any) {
137
157
  // Pull may fail if there's no remote or no upstream — that's OK for local-only repos
138
- this.log('WORKSPACE', `Pull failed (non-fatal): ${err.message}`);
158
+ this.log('WORKSPACE', `Pull failed (non-fatal):`, err.stack || err.message);
139
159
  }
140
160
 
141
161
  if (token && repoFullName) {
@@ -192,21 +212,32 @@ export class ProjectWorkspaceService {
192
212
  }
193
213
  };
194
214
 
195
- /** Clone a remote repository using SSH key authentication. */
215
+ /**
216
+ * Connect a remote repository using SSH key authentication.
217
+ * Uses git remote add + git fetch instead of rm -rf + git clone.
218
+ */
196
219
  cloneRepoSsh = async (projectId: string, sshCloneUrl: string, encryptedPrivateKey: string): Promise<string> => {
197
220
  const wsPath = this.getWorkspacePath(projectId);
198
- await this.ensureDir(this.workspacesDir);
221
+ await this.ensureDir(wsPath);
199
222
 
200
- if (existsSync(wsPath)) {
201
- this.log('WORKSPACE', `Removing existing workspace before SSH clone: ${wsPath}`);
202
- await this.claudeService.spawnCommand('rm', ['-rf', wsPath], this.workspacesDir);
223
+ // Ensure there's a local git repo
224
+ if (!existsSync(join(wsPath, '.git'))) {
225
+ await this.initWorkspace(projectId);
203
226
  }
204
227
 
205
- this.log('WORKSPACE', `SSH cloning into ${wsPath}`);
228
+ // Add the remote (replace if it already exists)
229
+ try {
230
+ await this.claudeService.spawnCommand('git', ['remote', 'remove', 'origin'], wsPath);
231
+ } catch {
232
+ // No existing origin — that's fine
233
+ }
234
+
235
+ this.log('WORKSPACE', `SSH adding remote origin ${sshCloneUrl} for project ${projectId}`);
236
+ await this.claudeService.spawnCommand('git', ['remote', 'add', 'origin', sshCloneUrl], wsPath);
206
237
  await this.withSshKey(encryptedPrivateKey, () =>
207
- this.claudeService.spawnCommand('git', ['clone', sshCloneUrl, wsPath], this.workspacesDir),
238
+ this.claudeService.spawnCommand('git', ['fetch', 'origin'], wsPath),
208
239
  );
209
- this.log('WORKSPACE', `SSH clone completed for project ${projectId}`);
240
+ this.log('WORKSPACE', `SSH remote added and fetched for project ${projectId}`);
210
241
  return wsPath;
211
242
  };
212
243
 
@@ -222,7 +253,7 @@ export class ProjectWorkspaceService {
222
253
  );
223
254
  this.log('WORKSPACE', `SSH pull completed for project ${projectId}`);
224
255
  } catch (err: any) {
225
- this.log('WORKSPACE', `SSH pull failed (non-fatal): ${err.message}`);
256
+ this.log('WORKSPACE', `SSH pull failed (non-fatal):`, err.stack || err.message);
226
257
  }
227
258
  };
228
259
 
@@ -255,7 +286,9 @@ export class ProjectWorkspaceService {
255
286
  try {
256
287
  const result = await this.claudeService.spawnCommand('git', ['branch', '--show-current'], wsPath);
257
288
  currentBranch = result.trim() || null;
258
- } catch {}
289
+ } catch (err: any) {
290
+ this.log('WORKSPACE', `Failed to get current branch for ${projectId}: ${err.message}`);
291
+ }
259
292
 
260
293
  let remoteUrl: string | null = null;
261
294
  let hasRemote = false;
@@ -263,8 +296,250 @@ export class ProjectWorkspaceService {
263
296
  const result = await this.claudeService.spawnCommand('git', ['remote', 'get-url', 'origin'], wsPath);
264
297
  remoteUrl = result.trim() || null;
265
298
  hasRemote = !!remoteUrl;
266
- } catch {}
299
+ } catch (err: any) {
300
+ this.log('WORKSPACE', `No remote origin for ${projectId}: ${err.message}`);
301
+ }
267
302
 
268
303
  return { hasWorkspace: true, hasRemote, currentBranch, remoteUrl };
269
304
  };
305
+
306
+ /** List all local and remote branches. */
307
+ listBranches = async (projectId: string): Promise<{
308
+ current: string;
309
+ local: string[];
310
+ remote: string[];
311
+ }> => {
312
+ const wsPath = this.getWorkspacePath(projectId);
313
+
314
+ // Get current branch
315
+ let current = 'main';
316
+ try {
317
+ const result = await this.claudeService.spawnCommand('git', ['branch', '--show-current'], wsPath);
318
+ current = result.trim() || 'main';
319
+ } catch {}
320
+
321
+ // Get local branches
322
+ const local: string[] = [];
323
+ try {
324
+ const result = await this.claudeService.spawnCommand('git', ['branch', '--format=%(refname:short)'], wsPath);
325
+ for (const line of result.trim().split('\n')) {
326
+ const name = line.trim();
327
+ if (name) local.push(name);
328
+ }
329
+ } catch {}
330
+
331
+ // Get remote branches
332
+ const remote: string[] = [];
333
+ try {
334
+ const result = await this.claudeService.spawnCommand('git', ['branch', '-r', '--format=%(refname:short)'], wsPath);
335
+ for (const line of result.trim().split('\n')) {
336
+ const name = line.trim();
337
+ // Skip HEAD pointer and empty lines
338
+ if (name && !name.endsWith('/HEAD')) remote.push(name);
339
+ }
340
+ } catch {}
341
+
342
+ return { current, local, remote };
343
+ };
344
+
345
+ /** Check if there are uncommitted changes in the workspace. */
346
+ hasUncommittedChanges = async (projectId: string): Promise<boolean> => {
347
+ const wsPath = this.getWorkspacePath(projectId);
348
+ try {
349
+ const result = await this.claudeService.spawnCommand('git', ['status', '--porcelain'], wsPath);
350
+ return result.trim().length > 0;
351
+ } catch {
352
+ return false;
353
+ }
354
+ };
355
+
356
+ /** Commit all current changes with a message. */
357
+ commitAll = async (projectId: string, message: string): Promise<void> => {
358
+ const wsPath = this.getWorkspacePath(projectId);
359
+ await this.claudeService.spawnCommand('git', ['add', '-A'], wsPath);
360
+ await this.claudeService.spawnCommand('git', ['commit', '-m', message], wsPath);
361
+ this.log('WORKSPACE', `Committed all changes for project ${projectId}`);
362
+ };
363
+
364
+ /** Checkout an existing branch. */
365
+ checkoutBranch = async (projectId: string, branchName: string): Promise<void> => {
366
+ const wsPath = this.getWorkspacePath(projectId);
367
+ // If it's a remote branch (e.g. origin/feature), check it out locally
368
+ if (branchName.includes('/')) {
369
+ const localName = branchName.replace(/^[^/]+\//, '');
370
+ await this.claudeService.spawnCommand('git', ['checkout', '-b', localName, branchName], wsPath);
371
+ this.log('WORKSPACE', `Checked out remote branch ${branchName} as ${localName} for project ${projectId}`);
372
+ } else {
373
+ await this.claudeService.spawnCommand('git', ['checkout', branchName], wsPath);
374
+ this.log('WORKSPACE', `Checked out branch ${branchName} for project ${projectId}`);
375
+ }
376
+ };
377
+
378
+ /** Create and checkout a new branch. */
379
+ createBranch = async (projectId: string, branchName: string): Promise<void> => {
380
+ const wsPath = this.getWorkspacePath(projectId);
381
+ await this.claudeService.spawnCommand('git', ['checkout', '-b', branchName], wsPath);
382
+ this.log('WORKSPACE', `Created and checked out new branch ${branchName} for project ${projectId}`);
383
+ };
384
+
385
+ /** List all configured remotes with their URLs. */
386
+ listRemotes = async (projectId: string): Promise<Array<{ name: string; url: string }>> => {
387
+ const wsPath = this.getWorkspacePath(projectId);
388
+ const remotes: Array<{ name: string; url: string }> = [];
389
+ try {
390
+ const result = await this.claudeService.spawnCommand('git', ['remote', '-v'], wsPath);
391
+ const seen = new Set<string>();
392
+ for (const line of result.trim().split('\n')) {
393
+ const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/);
394
+ if (match && !seen.has(match[1])) {
395
+ seen.add(match[1]);
396
+ remotes.push({ name: match[1], url: match[2] });
397
+ }
398
+ }
399
+ } catch {
400
+ // No remotes
401
+ }
402
+ return remotes;
403
+ };
404
+
405
+ /** Add a new remote. */
406
+ addRemote = async (projectId: string, name: string, url: string): Promise<void> => {
407
+ const wsPath = this.getWorkspacePath(projectId);
408
+ await this.claudeService.spawnCommand('git', ['remote', 'add', name, url], wsPath);
409
+ this.log('WORKSPACE', `Added remote ${name} → ${url} for project ${projectId}`);
410
+ };
411
+
412
+ /** Fetch from a remote to update tracking branches. */
413
+ fetchRemote = async (projectId: string, remote: string = 'origin'): Promise<void> => {
414
+ const wsPath = this.getWorkspacePath(projectId);
415
+ this.log('WORKSPACE', `Fetching ${remote} for project ${projectId}`);
416
+ await this.claudeService.spawnCommand('git', ['fetch', remote], wsPath);
417
+ };
418
+
419
+ /** Fetch from a remote using SSH key authentication. */
420
+ fetchRemoteSsh = async (projectId: string, encryptedPrivateKey: string, remote: string = 'origin'): Promise<void> => {
421
+ const wsPath = this.getWorkspacePath(projectId);
422
+ this.log('WORKSPACE', `SSH fetching ${remote} for project ${projectId}`);
423
+ await this.withSshKey(encryptedPrivateKey, () =>
424
+ this.claudeService.spawnCommand('git', ['fetch', remote], wsPath),
425
+ );
426
+ };
427
+
428
+ /** Pull current branch from remote. */
429
+ pullBranch = async (projectId: string, token?: string, repoFullName?: string): Promise<void> => {
430
+ const wsPath = this.getWorkspacePath(projectId);
431
+ const branch = await this.getCurrentBranch(projectId);
432
+
433
+ if (token && repoFullName) {
434
+ const authUrl = `https://x-access-token:${token}@github.com/${repoFullName}.git`;
435
+ await this.claudeService.spawnCommand('git', ['remote', 'set-url', 'origin', authUrl], wsPath);
436
+ }
437
+
438
+ try {
439
+ this.log('WORKSPACE', `Pulling ${branch} for project ${projectId}`);
440
+ await this.claudeService.spawnCommand('git', ['pull', 'origin', branch], wsPath);
441
+ } finally {
442
+ if (token && repoFullName) {
443
+ await this.claudeService.spawnCommand(
444
+ 'git', ['remote', 'set-url', 'origin', `https://github.com/${repoFullName}.git`], wsPath,
445
+ ).catch(() => {});
446
+ }
447
+ }
448
+ };
449
+
450
+ /** Pull current branch from remote using SSH. */
451
+ pullBranchSsh = async (projectId: string, encryptedPrivateKey: string): Promise<void> => {
452
+ const wsPath = this.getWorkspacePath(projectId);
453
+ const branch = await this.getCurrentBranch(projectId);
454
+
455
+ this.log('WORKSPACE', `SSH pulling ${branch} for project ${projectId}`);
456
+ await this.withSshKey(encryptedPrivateKey, () =>
457
+ this.claudeService.spawnCommand('git', ['pull', 'origin', branch], wsPath),
458
+ );
459
+ };
460
+
461
+ /** Push current branch to remote. */
462
+ pushBranch = async (projectId: string, token?: string, repoFullName?: string): Promise<void> => {
463
+ const wsPath = this.getWorkspacePath(projectId);
464
+ const branch = await this.getCurrentBranch(projectId);
465
+
466
+ if (token && repoFullName) {
467
+ const authUrl = `https://x-access-token:${token}@github.com/${repoFullName}.git`;
468
+ await this.claudeService.spawnCommand('git', ['remote', 'set-url', 'origin', authUrl], wsPath);
469
+ }
470
+
471
+ try {
472
+ this.log('WORKSPACE', `Pushing ${branch} for project ${projectId}`);
473
+ await this.claudeService.spawnCommand('git', ['push', '-u', 'origin', branch], wsPath);
474
+ } finally {
475
+ if (token && repoFullName) {
476
+ await this.claudeService.spawnCommand(
477
+ 'git', ['remote', 'set-url', 'origin', `https://github.com/${repoFullName}.git`], wsPath,
478
+ ).catch(() => {});
479
+ }
480
+ }
481
+ };
482
+
483
+ /** Push current branch to remote using SSH. */
484
+ pushBranchSsh = async (projectId: string, encryptedPrivateKey: string): Promise<void> => {
485
+ const wsPath = this.getWorkspacePath(projectId);
486
+ const branch = await this.getCurrentBranch(projectId);
487
+
488
+ this.log('WORKSPACE', `SSH pushing ${branch} for project ${projectId}`);
489
+ await this.withSshKey(encryptedPrivateKey, () =>
490
+ this.claudeService.spawnCommand('git', ['push', '-u', 'origin', branch], wsPath),
491
+ );
492
+ };
493
+
494
+ /** Get the current branch name. */
495
+ getCurrentBranch = async (projectId: string): Promise<string> => {
496
+ const wsPath = this.getWorkspacePath(projectId);
497
+ try {
498
+ const result = await this.claudeService.spawnCommand('git', ['branch', '--show-current'], wsPath);
499
+ return result.trim() || 'main';
500
+ } catch {
501
+ return 'main';
502
+ }
503
+ };
504
+
505
+ /** Get ahead/behind counts relative to the remote tracking branch. */
506
+ getAheadBehind = async (projectId: string): Promise<{ ahead: number; behind: number }> => {
507
+ const wsPath = this.getWorkspacePath(projectId);
508
+ try {
509
+ const branch = await this.getCurrentBranch(projectId);
510
+ const result = await this.claudeService.spawnCommand(
511
+ 'git', ['rev-list', '--left-right', '--count', `${branch}...origin/${branch}`], wsPath,
512
+ );
513
+ const parts = result.trim().split(/\s+/);
514
+ return { ahead: parseInt(parts[0], 10) || 0, behind: parseInt(parts[1], 10) || 0 };
515
+ } catch {
516
+ return { ahead: 0, behind: 0 };
517
+ }
518
+ };
519
+
520
+ /** Check if the workspace has only the initial empty commit (auto-init). */
521
+ hasOnlyInitialCommit = async (projectId: string): Promise<boolean> => {
522
+ const wsPath = this.getWorkspacePath(projectId);
523
+ try {
524
+ const result = await this.claudeService.spawnCommand('git', ['rev-list', '--count', 'HEAD'], wsPath);
525
+ const count = parseInt(result.trim(), 10);
526
+ return count <= 1;
527
+ } catch {
528
+ return true;
529
+ }
530
+ };
531
+
532
+ /** Reset current branch to match a remote branch. Used when pulling from a populated remote. */
533
+ resetToRemoteBranch = async (projectId: string, remoteBranch: string = 'origin/main'): Promise<void> => {
534
+ const wsPath = this.getWorkspacePath(projectId);
535
+ this.log('WORKSPACE', `Resetting to ${remoteBranch} for project ${projectId}`);
536
+ await this.claudeService.spawnCommand('git', ['reset', '--hard', remoteBranch], wsPath);
537
+ };
538
+
539
+ /** Delete a local branch. */
540
+ deleteBranch = async (projectId: string, branchName: string): Promise<void> => {
541
+ const wsPath = this.getWorkspacePath(projectId);
542
+ await this.claudeService.spawnCommand('git', ['branch', '-D', branchName], wsPath);
543
+ this.log('WORKSPACE', `Deleted branch ${branchName} for project ${projectId}`);
544
+ };
270
545
  }
@@ -157,6 +157,50 @@ describe('PtySessionManager', () => {
157
157
  });
158
158
  });
159
159
 
160
+ describe('dual session types', () => {
161
+ it('creates a claude session by default', async () => {
162
+ const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
163
+ assert.equal(session.sessionType, 'claude');
164
+
165
+ const [cmd] = spawnMock.mock.calls[0].arguments as [string, string[]];
166
+ assert.equal(cmd, 'claude');
167
+ });
168
+
169
+ it('creates a claude session when explicitly requested', async () => {
170
+ const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24, 'claude');
171
+ assert.equal(session.sessionType, 'claude');
172
+
173
+ const [cmd, args] = spawnMock.mock.calls[0].arguments as [string, string[]];
174
+ assert.equal(cmd, 'claude');
175
+ assert.ok(args.includes('--session-id'));
176
+ });
177
+
178
+ it('creates a terminal session that spawns /bin/bash', async () => {
179
+ const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24, 'terminal');
180
+ assert.equal(session.sessionType, 'terminal');
181
+
182
+ const [cmd, args] = spawnMock.mock.calls[0].arguments as [string, string[]];
183
+ assert.equal(cmd, '/bin/bash');
184
+ assert.deepEqual(args, []);
185
+ });
186
+
187
+ it('terminal session does not include claude flags', async () => {
188
+ await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24, 'terminal');
189
+
190
+ const [, args] = spawnMock.mock.calls[0].arguments as [string, string[]];
191
+ assert.ok(!args.includes('--dangerously-skip-permissions'));
192
+ assert.ok(!args.includes('--append-system-prompt'));
193
+ assert.ok(!args.includes('--session-id'));
194
+ });
195
+
196
+ it('persists sessionType in the DB record', async () => {
197
+ await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24, 'terminal');
198
+
199
+ const dbRow = mockDb._rows[0];
200
+ assert.equal(dbRow.sessionType, 'terminal');
201
+ });
202
+ });
203
+
160
204
  describe('formatSessionName', () => {
161
205
  it('formats session name as ProjectName - DD/MM/YY - HH:MM', () => {
162
206
  const date = new Date(2026, 2, 6, 14, 30); // 2026-03-06 14:30
@@ -31,12 +31,15 @@ interface LivePty {
31
31
  rows: number;
32
32
  }
33
33
 
34
+ export type SessionType = 'claude' | 'terminal';
35
+
34
36
  export interface SessionInfo {
35
37
  id: string;
36
38
  claudeSessionId: string;
37
39
  name: string;
38
40
  projectId: string;
39
41
  projectName: string;
42
+ sessionType: SessionType;
40
43
  state: 'suspended' | 'running';
41
44
  createdAt: string;
42
45
  lastUsedAt: string;
@@ -82,6 +85,7 @@ export class PtySessionManager {
82
85
  name: r.name,
83
86
  projectId: r.projectId,
84
87
  projectName: r.projectName,
88
+ sessionType: (r.sessionType || 'claude') as SessionType,
85
89
  state: this.live.has(r.id) ? 'running' as const : 'suspended' as const,
86
90
  createdAt: r.createdAt,
87
91
  lastUsedAt: r.lastUsedAt,
@@ -100,14 +104,15 @@ export class PtySessionManager {
100
104
  name: r.name,
101
105
  projectId: r.projectId,
102
106
  projectName: r.projectName,
107
+ sessionType: (r.sessionType || 'claude') as SessionType,
103
108
  state: this.live.has(r.id) ? 'running' as const : 'suspended' as const,
104
109
  createdAt: r.createdAt,
105
110
  lastUsedAt: r.lastUsedAt,
106
111
  };
107
112
  };
108
113
 
109
- /** Create a new session — persists to DB and spawns claude with --session-id. */
110
- createSession = async (projectId: string, projectName: string, cols: number, rows: number): Promise<SessionInfo> => {
114
+ /** Create a new session — persists to DB and spawns the appropriate process. */
115
+ createSession = async (projectId: string, projectName: string, cols: number, rows: number, sessionType: SessionType = 'claude'): Promise<SessionInfo> => {
111
116
  const id = this.generateId();
112
117
  const claudeSessionId = randomUUID();
113
118
  const createdAt = new Date();
@@ -122,14 +127,18 @@ export class PtySessionManager {
122
127
  projectId,
123
128
  projectName,
124
129
  name,
130
+ sessionType,
125
131
  createdAt: now,
126
132
  lastUsedAt: now,
127
133
  });
128
134
 
129
- this.log('PTY', `Created session "${name}" (${id}) for project ${projectId}, claude session ${claudeSessionId}`);
135
+ this.log('PTY', `Created ${sessionType} session "${name}" (${id}) for project ${projectId}`);
130
136
 
131
- // Spawn claude with --session-id
132
- this.spawnClaude(id, claudeSessionId, projectId, cols, rows, false);
137
+ if (sessionType === 'claude') {
138
+ this.spawnClaude(id, claudeSessionId, projectId, cols, rows, false);
139
+ } else {
140
+ this.spawnShell(id, cols, rows);
141
+ }
133
142
 
134
143
  return {
135
144
  id,
@@ -137,6 +146,7 @@ export class PtySessionManager {
137
146
  name,
138
147
  projectId,
139
148
  projectName,
149
+ sessionType,
140
150
  state: 'running',
141
151
  createdAt: now,
142
152
  lastUsedAt: now,
@@ -159,8 +169,12 @@ export class PtySessionManager {
159
169
  .set({ lastUsedAt: new Date().toISOString() })
160
170
  .where(eq(terminalSessions.id, sessionId));
161
171
 
162
- // Resume claude
163
- this.spawnClaude(sessionId, session.claudeSessionId, session.projectId, cols, rows, true);
172
+ // Resume based on session type
173
+ if (session.sessionType === 'terminal') {
174
+ this.spawnShell(sessionId, cols, rows);
175
+ } else {
176
+ this.spawnClaude(sessionId, session.claudeSessionId, session.projectId, cols, rows, true);
177
+ }
164
178
  return true;
165
179
  };
166
180
 
@@ -319,6 +333,47 @@ export class PtySessionManager {
319
333
  });
320
334
  };
321
335
 
336
+ private spawnShell = (
337
+ sessionId: string,
338
+ cols: number,
339
+ rows: number,
340
+ ): void => {
341
+ this.log('PTY', `Launching plain shell for session ${sessionId}`);
342
+
343
+ let spawnedPty: IPty;
344
+ try {
345
+ spawnedPty = this.spawn('/bin/bash', [], {
346
+ name: 'xterm-256color',
347
+ cols,
348
+ rows,
349
+ cwd: this.projectRoot,
350
+ env: this.buildEnv(),
351
+ });
352
+ } catch (err) {
353
+ const msg = err instanceof Error ? err.message : String(err);
354
+ this.log('PTY', `Failed to spawn shell for session ${sessionId}: ${msg}`);
355
+ return;
356
+ }
357
+
358
+ const entry: LivePty = {
359
+ pty: spawnedPty,
360
+ outputBuffer: '',
361
+ listeners: new Set(),
362
+ cols,
363
+ rows,
364
+ };
365
+ this.live.set(sessionId, entry);
366
+
367
+ spawnedPty.onData((data: string) => {
368
+ this.emitOutput(entry, data);
369
+ });
370
+
371
+ spawnedPty.onExit(({ exitCode }) => {
372
+ this.log('PTY', `Shell exited with code ${exitCode} for session ${sessionId}`);
373
+ this.live.delete(sessionId);
374
+ });
375
+ };
376
+
322
377
  private buildEnv = (): Record<string, string> => {
323
378
  const env = { ...process.env } as Record<string, string>;
324
379
  const home = env.HOME || '/root';