@assistkick/create 1.10.0 → 1.12.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 (209) 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/migrate.ts +82 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/0000_outgoing_ultron.sql +277 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
  161. package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
  162. package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
  163. package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
  164. package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
  165. package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
  166. package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
  167. package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
  168. package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
  169. package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
  170. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0000_snapshot.json +972 -22
  171. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
  172. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
  173. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
  174. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
  175. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
  176. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
  177. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
  178. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
  179. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
  180. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
  181. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +2 -100
  182. package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
  183. package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
  184. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
  185. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
  186. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
  187. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
  188. package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
  189. package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
  190. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
  191. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
  192. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
  193. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  194. package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
  195. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
  196. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
  197. package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
  198. package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
  199. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
  200. package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
  201. package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
  202. package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
  203. package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
  204. package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
  205. package/templates/skills/assistkick-debugger/SKILL.md +23 -23
  206. package/templates/skills/assistkick-developer/SKILL.md +59 -63
  207. package/templates/skills/assistkick-interview/SKILL.md +26 -26
  208. package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
  209. package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Preview server routes — REST API for managing dev preview servers
3
+ * and reverse proxy middleware for forwarding requests to dev servers.
4
+ *
5
+ * API routes:
6
+ * POST /api/preview/start — start a preview server
7
+ * POST /api/preview/stop — stop a preview server
8
+ * GET /api/preview/list — list running preview servers
9
+ * POST /api/preview/toggle-access — toggle public/private access
10
+ *
11
+ * Proxy route:
12
+ * /apps/<app-name>/* — reverse proxy to the dev server
13
+ */
14
+
15
+ import { Router } from 'express';
16
+ import type { Request, Response } from 'express';
17
+ import { createProxyMiddleware } from 'http-proxy-middleware';
18
+ import type { PreviewServerManager } from '../services/preview_server_manager.js';
19
+ import type { AuthMiddleware } from '../middleware/auth_middleware.js';
20
+ import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
21
+
22
+ interface PreviewRoutesDeps {
23
+ previewManager: PreviewServerManager;
24
+ workspaceService: ProjectWorkspaceService;
25
+ projectService: { getById: (id: string) => Promise<{ previewCommand: string | null } | null> };
26
+ log: (tag: string, ...args: unknown[]) => void;
27
+ }
28
+
29
+ export const createPreviewApiRoutes = ({ previewManager, workspaceService, projectService, log }: PreviewRoutesDeps): Router => {
30
+ const router: Router = Router();
31
+
32
+ // POST /api/preview/start
33
+ router.post('/start', async (req: Request, res: Response) => {
34
+ const { appName, projectDir, projectId, command } = req.body;
35
+
36
+ if (!appName || typeof appName !== 'string') {
37
+ res.status(400).json({ error: 'appName is required' });
38
+ return;
39
+ }
40
+
41
+ // Resolve projectDir from projectId if not provided directly
42
+ const resolvedDir = projectDir || (projectId ? workspaceService.getWorkspacePath(projectId) : null);
43
+ if (!resolvedDir || typeof resolvedDir !== 'string') {
44
+ res.status(400).json({ error: 'projectDir or projectId is required' });
45
+ return;
46
+ }
47
+
48
+ // Validate appName: only alphanumeric, hyphens, underscores
49
+ if (!/^[a-zA-Z0-9_-]+$/.test(appName)) {
50
+ res.status(400).json({ error: 'appName must contain only alphanumeric characters, hyphens, and underscores' });
51
+ return;
52
+ }
53
+
54
+ // Resolve command: explicit > project previewCommand > auto-detect (in manager)
55
+ let resolvedCommand = command?.trim() || undefined;
56
+ if (!resolvedCommand && projectId) {
57
+ const project = await projectService.getById(projectId);
58
+ if (project?.previewCommand) {
59
+ resolvedCommand = project.previewCommand;
60
+ }
61
+ }
62
+
63
+ try {
64
+ const app = await previewManager.start(appName.trim(), resolvedDir.trim(), resolvedCommand);
65
+ log('PREVIEW', `Started preview app "${app.appName}" on port ${app.port}`);
66
+ res.status(201).json({ app });
67
+ } catch (err: any) {
68
+ log('PREVIEW', `Failed to start preview: ${err.message}`);
69
+ res.status(409).json({ error: err.message });
70
+ }
71
+ });
72
+
73
+ // POST /api/preview/stop
74
+ router.post('/stop', async (req: Request, res: Response) => {
75
+ const { appName } = req.body;
76
+
77
+ if (!appName || typeof appName !== 'string') {
78
+ res.status(400).json({ error: 'appName is required' });
79
+ return;
80
+ }
81
+
82
+ const stopped = previewManager.stop(appName.trim());
83
+ if (!stopped) {
84
+ res.status(404).json({ error: `Preview app "${appName}" not found` });
85
+ return;
86
+ }
87
+
88
+ res.json({ ok: true });
89
+ });
90
+
91
+ // GET /api/preview/list
92
+ router.get('/list', async (_req: Request, res: Response) => {
93
+ const apps = previewManager.list();
94
+ res.json({ apps });
95
+ });
96
+
97
+ // POST /api/preview/toggle-access
98
+ router.post('/toggle-access', async (req: Request, res: Response) => {
99
+ const { appName, isPublic } = req.body;
100
+
101
+ if (!appName || typeof appName !== 'string') {
102
+ res.status(400).json({ error: 'appName is required' });
103
+ return;
104
+ }
105
+ if (typeof isPublic !== 'boolean') {
106
+ res.status(400).json({ error: 'isPublic (boolean) is required' });
107
+ return;
108
+ }
109
+
110
+ const toggled = previewManager.toggleAccess(appName.trim(), isPublic);
111
+ if (!toggled) {
112
+ res.status(404).json({ error: `Preview app "${appName}" not found` });
113
+ return;
114
+ }
115
+
116
+ res.json({ ok: true, isPublic });
117
+ });
118
+
119
+ return router;
120
+ };
121
+
122
+ interface PreviewProxyDeps {
123
+ previewManager: PreviewServerManager;
124
+ authMiddleware: AuthMiddleware;
125
+ log: (tag: string, ...args: unknown[]) => void;
126
+ }
127
+
128
+ /**
129
+ * Creates the /apps/:appName proxy middleware.
130
+ * - Checks if the app exists and is running
131
+ * - Enforces auth for private apps
132
+ * - Resets idle timer on each request
133
+ * - Proxies HTTP and WebSocket to the dev server
134
+ */
135
+ export const createPreviewProxyMiddleware = ({ previewManager, authMiddleware, log }: PreviewProxyDeps): Router => {
136
+ const router: Router = Router();
137
+
138
+ // Dynamic proxy — resolves the target per-request based on the app name
139
+ const proxy = createProxyMiddleware({
140
+ router: (req) => {
141
+ const appName = (req as any).previewAppName as string;
142
+ const app = previewManager.get(appName);
143
+ if (!app) {
144
+ return 'http://127.0.0.1:1'; // Will fail, caught by error handler
145
+ }
146
+ return `http://127.0.0.1:${app.port}`;
147
+ },
148
+ pathRewrite: (path, req) => {
149
+ const appName = (req as any).previewAppName as string;
150
+ // Strip /apps/<appName> prefix from the path
151
+ const prefix = `/apps/${appName}`;
152
+ return path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;
153
+ },
154
+ changeOrigin: true,
155
+ ws: true,
156
+ on: {
157
+ error: (err, req, res) => {
158
+ log('PREVIEW', `Proxy error: ${err.message}`);
159
+ if ('writeHead' in res && typeof res.writeHead === 'function') {
160
+ (res as any).writeHead(502);
161
+ (res as any).end('Preview server unavailable');
162
+ }
163
+ },
164
+ },
165
+ });
166
+
167
+ // Middleware to extract app name, check access, reset idle timer
168
+ router.use('/apps/:appName', async (req: Request, res: Response, next) => {
169
+ const appName = req.params.appName as string;
170
+ const app = previewManager.get(appName);
171
+
172
+ if (!app) {
173
+ res.status(404).json({ error: `Preview app "${appName}" not found or not running` });
174
+ return;
175
+ }
176
+
177
+ // Enforce auth for private apps
178
+ if (!app.isPublic) {
179
+ // Use auth middleware inline — if it sends 401, we stop here
180
+ await new Promise<void>((resolve) => {
181
+ authMiddleware.requireAuth(req, res, (() => {
182
+ resolve();
183
+ }) as any);
184
+ });
185
+ // If response was already sent (401), don't proceed
186
+ if (res.headersSent) {
187
+ return;
188
+ }
189
+ }
190
+
191
+ // Reset idle timer on access
192
+ previewManager.resetIdleTimer(appName);
193
+
194
+ // Attach app name for the proxy router function
195
+ (req as any).previewAppName = appName;
196
+
197
+ next();
198
+ });
199
+
200
+ // Mount the proxy after the access-check middleware
201
+ router.use('/apps/:appName', proxy as any);
202
+
203
+ return router;
204
+ };
@@ -0,0 +1,205 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createProjectRoutes } from './projects.ts';
4
+ import express from 'express';
5
+ import type { Server } from 'node:http';
6
+
7
+ const createMockProjectService = () => ({
8
+ listActive: mock.fn(async () => []),
9
+ create: mock.fn(async (name: string, type: string) => ({
10
+ id: 'proj_test1234',
11
+ name,
12
+ type,
13
+ isDefault: 0,
14
+ archivedAt: null,
15
+ repoUrl: null,
16
+ githubInstallationId: null,
17
+ githubRepoFullName: null,
18
+ baseBranch: null,
19
+ gitAuthMethod: null,
20
+ sshPrivateKeyEncrypted: null,
21
+ sshPublicKey: null,
22
+ previewCommand: null,
23
+ createdAt: '2026-03-11T00:00:00.000Z',
24
+ updatedAt: '2026-03-11T00:00:00.000Z',
25
+ })),
26
+ rename: mock.fn(),
27
+ updatePreviewCommand: mock.fn(async (id: string, cmd: string | null) => ({
28
+ id,
29
+ name: 'Test',
30
+ type: 'software',
31
+ isDefault: 0,
32
+ archivedAt: null,
33
+ previewCommand: cmd,
34
+ createdAt: '2026-03-11T00:00:00.000Z',
35
+ updatedAt: '2026-03-17T00:00:00.000Z',
36
+ })),
37
+ archive: mock.fn(),
38
+ restore: mock.fn(),
39
+ });
40
+
41
+ const createMockWorkspaceService = (gitAvailable = true) => ({
42
+ verifyGitAvailable: mock.fn(async () => {
43
+ if (!gitAvailable) {
44
+ throw new Error('Git is required to create a project. Please install git and try again.');
45
+ }
46
+ }),
47
+ initWorkspace: mock.fn(async () => '/tmp/workspaces/proj_test1234'),
48
+ });
49
+
50
+ const createTestApp = (projectService: any, workspaceService: any) => {
51
+ const log = mock.fn();
52
+ const app = express();
53
+ app.use(express.json());
54
+ const routes = createProjectRoutes({ projectService, workspaceService, log });
55
+ app.use('/api/projects', routes);
56
+ return { app, log };
57
+ };
58
+
59
+ const request = (server: Server) => {
60
+ const addr = server.address() as { port: number };
61
+ const base = `http://127.0.0.1:${addr.port}`;
62
+ return {
63
+ post: async (path: string, body: any) => {
64
+ const res = await fetch(`${base}${path}`, {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify(body),
68
+ });
69
+ return { status: res.status, body: await res.json() };
70
+ },
71
+ patch: async (path: string, body: any) => {
72
+ const res = await fetch(`${base}${path}`, {
73
+ method: 'PATCH',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify(body),
76
+ });
77
+ return { status: res.status, body: await res.json() };
78
+ },
79
+ };
80
+ };
81
+
82
+ describe('Project Routes — POST /api/projects', () => {
83
+ let server: Server;
84
+
85
+ afterEach(() => {
86
+ if (server) server.close();
87
+ });
88
+
89
+ it('creates a project with git init when git is available', async () => {
90
+ const projectService = createMockProjectService();
91
+ const workspaceService = createMockWorkspaceService(true);
92
+ const { app } = createTestApp(projectService, workspaceService);
93
+ server = app.listen(0);
94
+ const req = request(server);
95
+
96
+ const res = await req.post('/api/projects', { name: 'My Project' });
97
+
98
+ assert.equal(res.status, 201);
99
+ assert.equal(res.body.project.name, 'My Project');
100
+ assert.equal(res.body.project.id, 'proj_test1234');
101
+
102
+ // Verify git was checked and workspace was initialized
103
+ assert.equal(workspaceService.verifyGitAvailable.mock.calls.length, 1);
104
+ assert.equal(workspaceService.initWorkspace.mock.calls.length, 1);
105
+ assert.equal(workspaceService.initWorkspace.mock.calls[0].arguments[0], 'proj_test1234');
106
+ });
107
+
108
+ it('returns 400 when git is not available', async () => {
109
+ const projectService = createMockProjectService();
110
+ const workspaceService = createMockWorkspaceService(false);
111
+ const { app } = createTestApp(projectService, workspaceService);
112
+ server = app.listen(0);
113
+ const req = request(server);
114
+
115
+ const res = await req.post('/api/projects', { name: 'My Project' });
116
+
117
+ assert.equal(res.status, 400);
118
+ assert.equal(res.body.error, 'Git is required to create a project. Please install git and try again.');
119
+
120
+ // Project should NOT have been created
121
+ assert.equal(projectService.create.mock.calls.length, 0);
122
+ assert.equal(workspaceService.initWorkspace.mock.calls.length, 0);
123
+ });
124
+
125
+ it('returns 400 when project name is missing', async () => {
126
+ const projectService = createMockProjectService();
127
+ const workspaceService = createMockWorkspaceService(true);
128
+ const { app } = createTestApp(projectService, workspaceService);
129
+ server = app.listen(0);
130
+ const req = request(server);
131
+
132
+ const res = await req.post('/api/projects', {});
133
+
134
+ assert.equal(res.status, 400);
135
+ assert.equal(res.body.error, 'Project name is required');
136
+
137
+ // Neither git check nor creation should have been called
138
+ assert.equal(workspaceService.verifyGitAvailable.mock.calls.length, 0);
139
+ assert.equal(projectService.create.mock.calls.length, 0);
140
+ });
141
+
142
+ it('passes project type through to service', async () => {
143
+ const projectService = createMockProjectService();
144
+ const workspaceService = createMockWorkspaceService(true);
145
+ const { app } = createTestApp(projectService, workspaceService);
146
+ server = app.listen(0);
147
+ const req = request(server);
148
+
149
+ await req.post('/api/projects', { name: 'Video Proj', type: 'video' });
150
+
151
+ assert.equal(projectService.create.mock.calls[0].arguments[0], 'Video Proj');
152
+ assert.equal(projectService.create.mock.calls[0].arguments[1], 'video');
153
+ });
154
+ });
155
+
156
+ describe('Project Routes — PATCH /api/projects/:id', () => {
157
+ let server: Server;
158
+
159
+ afterEach(() => {
160
+ if (server) server.close();
161
+ });
162
+
163
+ it('updates preview command via PATCH', async () => {
164
+ const projectService = createMockProjectService();
165
+ const workspaceService = createMockWorkspaceService(true);
166
+ const { app } = createTestApp(projectService, workspaceService);
167
+ server = app.listen(0);
168
+ const req = request(server);
169
+
170
+ const res = await req.patch('/api/projects/proj_test1234', { previewCommand: 'npm run dev' });
171
+
172
+ assert.equal(res.status, 200);
173
+ assert.equal(res.body.project.previewCommand, 'npm run dev');
174
+ assert.equal(projectService.updatePreviewCommand.mock.calls.length, 1);
175
+ assert.equal(projectService.updatePreviewCommand.mock.calls[0].arguments[0], 'proj_test1234');
176
+ assert.equal(projectService.updatePreviewCommand.mock.calls[0].arguments[1], 'npm run dev');
177
+ });
178
+
179
+ it('clears preview command when empty string is sent', async () => {
180
+ const projectService = createMockProjectService();
181
+ const workspaceService = createMockWorkspaceService(true);
182
+ const { app } = createTestApp(projectService, workspaceService);
183
+ server = app.listen(0);
184
+ const req = request(server);
185
+
186
+ const res = await req.patch('/api/projects/proj_test1234', { previewCommand: '' });
187
+
188
+ assert.equal(res.status, 200);
189
+ assert.equal(projectService.updatePreviewCommand.mock.calls.length, 1);
190
+ assert.equal(projectService.updatePreviewCommand.mock.calls[0].arguments[1], null);
191
+ });
192
+
193
+ it('returns 400 when neither name nor previewCommand provided', async () => {
194
+ const projectService = createMockProjectService();
195
+ const workspaceService = createMockWorkspaceService(true);
196
+ const { app } = createTestApp(projectService, workspaceService);
197
+ server = app.listen(0);
198
+ const req = request(server);
199
+
200
+ const res = await req.patch('/api/projects/proj_test1234', {});
201
+
202
+ assert.equal(res.status, 400);
203
+ assert.equal(res.body.error, 'At least one of name or previewCommand is required');
204
+ });
205
+ });
@@ -9,13 +9,15 @@
9
9
 
10
10
  import { Router } from 'express';
11
11
  import type { ProjectService } from '../services/project_service.js';
12
+ import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
12
13
 
13
14
  interface ProjectRoutesDeps {
14
15
  projectService: ProjectService;
16
+ workspaceService: ProjectWorkspaceService;
15
17
  log: (tag: string, ...args: any[]) => void;
16
18
  }
17
19
 
18
- export const createProjectRoutes = ({ projectService, log }: ProjectRoutesDeps): Router => {
20
+ export const createProjectRoutes = ({ projectService, workspaceService, log }: ProjectRoutesDeps): Router => {
19
21
  const router: Router = Router();
20
22
 
21
23
  // GET /api/projects — list active projects
@@ -44,8 +46,21 @@ export const createProjectRoutes = ({ projectService, log }: ProjectRoutesDeps):
44
46
  const validTypes = ['software', 'video'];
45
47
  const projectType = type && validTypes.includes(type) ? type : 'software';
46
48
 
49
+ try {
50
+ // Verify git is available before creating the project
51
+ await workspaceService.verifyGitAvailable();
52
+ } catch (err: any) {
53
+ log('PROJECTS', `Git verification failed: ${err.message}`);
54
+ res.status(400).json({ error: err.message });
55
+ return;
56
+ }
57
+
47
58
  try {
48
59
  const project = await projectService.create(name.trim(), projectType);
60
+
61
+ // Auto-initialize local git repo with an empty commit
62
+ await workspaceService.initWorkspace(project.id);
63
+
49
64
  res.status(201).json({ project });
50
65
  } catch (err: any) {
51
66
  log('PROJECTS', `Create project failed: ${err.message}`);
@@ -53,27 +68,40 @@ export const createProjectRoutes = ({ projectService, log }: ProjectRoutesDeps):
53
68
  }
54
69
  });
55
70
 
56
- // PATCH /api/projects/:id — rename a project
71
+ // PATCH /api/projects/:id — update a project (name and/or previewCommand)
57
72
  router.patch('/:id', async (req, res) => {
58
73
  const { id } = req.params;
59
- const { name } = req.body;
60
- log('PROJECTS', `PATCH /api/projects/${id} name="${name}"`);
74
+ const { name, previewCommand } = req.body;
75
+ log('PROJECTS', `PATCH /api/projects/${id} name="${name}" previewCommand="${previewCommand}"`);
61
76
 
62
- if (!name || typeof name !== 'string' || !name.trim()) {
63
- res.status(400).json({ error: 'Project name is required' });
77
+ // At least one field must be provided
78
+ const hasName = name && typeof name === 'string' && name.trim();
79
+ const hasPreviewCommand = previewCommand !== undefined;
80
+
81
+ if (!hasName && !hasPreviewCommand) {
82
+ res.status(400).json({ error: 'At least one of name or previewCommand is required' });
64
83
  return;
65
84
  }
66
85
 
67
86
  try {
68
- const project = await projectService.rename(id, name.trim());
87
+ let project;
88
+ if (hasName) {
89
+ project = await projectService.rename(id, name.trim());
90
+ }
91
+ if (hasPreviewCommand) {
92
+ const cmd = typeof previewCommand === 'string' && previewCommand.trim()
93
+ ? previewCommand.trim()
94
+ : null;
95
+ project = await projectService.updatePreviewCommand(id, cmd);
96
+ }
69
97
  res.json({ project });
70
98
  } catch (err: any) {
71
- log('PROJECTS', `Rename project failed: ${err.message}`);
99
+ log('PROJECTS', `Update project failed: ${err.message}`);
72
100
  if (err.message === 'Project not found') {
73
101
  res.status(404).json({ error: err.message });
74
102
  return;
75
103
  }
76
- res.status(500).json({ error: 'Failed to rename project' });
104
+ res.status(500).json({ error: 'Failed to update project' });
77
105
  }
78
106
  });
79
107
 
@@ -0,0 +1,139 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { createSkillRoutes, parseSkillMd } from './skills.ts';
7
+ import express from 'express';
8
+ import type { Server } from 'node:http';
9
+
10
+ /** Create a test app with skill routes mounted at /api/projects/:id/skills */
11
+ const createTestApp = (workspacePath: string) => {
12
+ const workspaceService = {
13
+ getWorkspacePath: (_projectId: string) => workspacePath,
14
+ } as any;
15
+ const log = mock.fn();
16
+ const app = express();
17
+ app.use(express.json());
18
+ const skillRoutes = createSkillRoutes({ workspaceService, log });
19
+ app.use('/api/projects/:id/skills', skillRoutes);
20
+ return { app, log };
21
+ };
22
+
23
+ /** Simple HTTP request helper using built-in fetch against an Express server */
24
+ const request = (server: Server) => {
25
+ const addr = server.address() as { port: number };
26
+ const base = `http://127.0.0.1:${addr.port}`;
27
+ return {
28
+ get: async (path: string) => {
29
+ const res = await fetch(`${base}${path}`);
30
+ return { status: res.status, body: await res.json() };
31
+ },
32
+ };
33
+ };
34
+
35
+ describe('Skills Routes', () => {
36
+ let tmpDir: string;
37
+ let server: Server;
38
+ let http: ReturnType<typeof request>;
39
+
40
+ beforeEach(async () => {
41
+ tmpDir = await mkdtemp(join(tmpdir(), 'skills-routes-test-'));
42
+ const { app } = createTestApp(tmpDir);
43
+ server = await new Promise<Server>((resolve) => {
44
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
45
+ });
46
+ http = request(server);
47
+ });
48
+
49
+ afterEach(async () => {
50
+ await new Promise<void>((resolve) => server.close(() => resolve()));
51
+ await rm(tmpDir, { recursive: true, force: true });
52
+ });
53
+
54
+ describe('GET / — list skills', () => {
55
+ it('returns empty skills when .claude/skills does not exist', async () => {
56
+ const { status, body } = await http.get('/api/projects/proj_1/skills');
57
+ assert.equal(status, 200);
58
+ assert.deepEqual(body.skills, []);
59
+ });
60
+
61
+ it('returns skill folders with parsed SKILL.md metadata', async () => {
62
+ const skillsDir = join(tmpDir, '.claude', 'skills', 'my-skill');
63
+ await mkdir(skillsDir, { recursive: true });
64
+ await writeFile(join(skillsDir, 'SKILL.md'), '# My Awesome Skill\nDoes amazing things.\n\n## Usage\n...');
65
+
66
+ const { status, body } = await http.get('/api/projects/proj_1/skills');
67
+ assert.equal(status, 200);
68
+ assert.equal(body.skills.length, 1);
69
+ assert.equal(body.skills[0].folderName, 'my-skill');
70
+ assert.equal(body.skills[0].name, 'My Awesome Skill');
71
+ assert.equal(body.skills[0].description, 'Does amazing things.');
72
+ });
73
+
74
+ it('uses folder name as fallback when SKILL.md is missing', async () => {
75
+ const skillsDir = join(tmpDir, '.claude', 'skills', 'bare-skill');
76
+ await mkdir(skillsDir, { recursive: true });
77
+
78
+ const { status, body } = await http.get('/api/projects/proj_1/skills');
79
+ assert.equal(status, 200);
80
+ assert.equal(body.skills.length, 1);
81
+ assert.equal(body.skills[0].folderName, 'bare-skill');
82
+ assert.equal(body.skills[0].name, 'bare-skill');
83
+ assert.equal(body.skills[0].description, '');
84
+ });
85
+
86
+ it('lists multiple skills sorted by discovery order', async () => {
87
+ const baseDir = join(tmpDir, '.claude', 'skills');
88
+ await mkdir(join(baseDir, 'alpha-skill'), { recursive: true });
89
+ await mkdir(join(baseDir, 'beta-skill'), { recursive: true });
90
+ await writeFile(join(baseDir, 'alpha-skill', 'SKILL.md'), '# Alpha\nFirst skill.');
91
+ await writeFile(join(baseDir, 'beta-skill', 'SKILL.md'), '# Beta\nSecond skill.');
92
+
93
+ const { status, body } = await http.get('/api/projects/proj_1/skills');
94
+ assert.equal(status, 200);
95
+ assert.equal(body.skills.length, 2);
96
+ const names = body.skills.map((s: any) => s.folderName);
97
+ assert.ok(names.includes('alpha-skill'));
98
+ assert.ok(names.includes('beta-skill'));
99
+ });
100
+
101
+ it('ignores non-directory entries in skills folder', async () => {
102
+ const baseDir = join(tmpDir, '.claude', 'skills');
103
+ await mkdir(baseDir, { recursive: true });
104
+ await writeFile(join(baseDir, 'not-a-skill.txt'), 'just a file');
105
+ await mkdir(join(baseDir, 'real-skill'));
106
+
107
+ const { status, body } = await http.get('/api/projects/proj_1/skills');
108
+ assert.equal(status, 200);
109
+ assert.equal(body.skills.length, 1);
110
+ assert.equal(body.skills[0].folderName, 'real-skill');
111
+ });
112
+ });
113
+ });
114
+
115
+ describe('parseSkillMd', () => {
116
+ it('parses name and description from markdown', () => {
117
+ const result = parseSkillMd('# My Skill\nThis is the description.\n\n## Details\nMore info.');
118
+ assert.equal(result.name, 'My Skill');
119
+ assert.equal(result.description, 'This is the description.');
120
+ });
121
+
122
+ it('returns empty strings for empty content', () => {
123
+ const result = parseSkillMd('');
124
+ assert.equal(result.name, '');
125
+ assert.equal(result.description, '');
126
+ });
127
+
128
+ it('handles content with only a heading', () => {
129
+ const result = parseSkillMd('# Just A Heading');
130
+ assert.equal(result.name, 'Just A Heading');
131
+ assert.equal(result.description, '');
132
+ });
133
+
134
+ it('skips sub-headings when looking for description', () => {
135
+ const result = parseSkillMd('# Name\n## Sub Heading\nActual description here.');
136
+ assert.equal(result.name, 'Name');
137
+ assert.equal(result.description, 'Actual description here.');
138
+ });
139
+ });