@assistkick/create 1.6.0 → 1.8.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 (214) hide show
  1. package/package.json +2 -2
  2. package/templates/assistkick-product-system/.env.example +1 -0
  3. package/templates/assistkick-product-system/local.db +0 -0
  4. package/templates/assistkick-product-system/package.json +4 -2
  5. package/templates/assistkick-product-system/packages/backend/package.json +2 -0
  6. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
  7. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
  9. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
  10. package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
  11. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +43 -4
  12. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
  13. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
  14. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
  15. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
  16. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
  17. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +154 -0
  18. package/templates/assistkick-product-system/packages/backend/src/server.ts +81 -9
  19. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
  20. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
  21. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
  22. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
  23. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +28 -78
  24. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
  25. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
  26. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
  27. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
  28. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
  29. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
  30. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
  31. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
  32. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
  33. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
  34. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
  35. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +222 -0
  40. package/templates/assistkick-product-system/packages/frontend/index.html +3 -0
  41. package/templates/assistkick-product-system/packages/frontend/package-lock.json +800 -11
  42. package/templates/assistkick-product-system/packages/frontend/package.json +11 -1
  43. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +24 -7
  44. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +456 -16
  45. package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
  46. package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
  47. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
  48. package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
  49. package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
  50. package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +383 -0
  51. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
  52. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
  53. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
  54. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
  55. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
  56. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +193 -64
  57. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
  58. package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
  59. package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
  60. package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
  61. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +226 -291
  62. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  63. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  64. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +40 -66
  65. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +55 -115
  66. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  67. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +155 -77
  68. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  69. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +270 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +37 -0
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +207 -0
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +335 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +634 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +208 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +107 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  105. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/hooks/useGraph.ts +6 -21
  107. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +15 -80
  108. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  109. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  110. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  111. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  112. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  113. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  114. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +19 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +54 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +6 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +93 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +30 -0
  120. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +9 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +6 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/stores/useGitModalStore.ts +14 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphStore.ts +36 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphUIStore.ts +25 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +90 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/stores/useQaSheetStore.ts +27 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +76 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +336 -3632
  131. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  132. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  133. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  134. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  135. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  136. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  137. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  138. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  139. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +7 -1
  140. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  151. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  152. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  153. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  154. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  155. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  156. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  157. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  158. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
  161. package/templates/assistkick-product-system/packages/shared/db/schema.ts +113 -0
  162. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  163. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  164. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  165. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +16 -5
  166. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1753 -0
  167. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1281 -0
  168. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  169. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  170. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  171. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  172. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  173. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  174. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  175. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  176. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  177. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  178. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  179. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  180. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  181. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  182. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  183. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  184. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  185. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  186. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  187. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  188. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  189. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  190. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  191. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  192. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  193. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  194. package/templates/assistkick-product-system/tests/video_render_service.test.ts +179 -0
  195. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  196. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  197. package/templates/skills/assistkick-bootstrap/SKILL.md +3 -3
  198. package/templates/skills/assistkick-code-reviewer/SKILL.md +2 -2
  199. package/templates/skills/assistkick-debugger/SKILL.md +2 -2
  200. package/templates/skills/assistkick-developer/SKILL.md +6 -3
  201. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  202. package/templates/skills/assistkick-interview/SKILL.md +2 -2
  203. package/templates/skills/product-system/graph.json +1890 -0
  204. package/templates/skills/product-system/kanban.json +304 -0
  205. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  206. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  207. package/templates/skills/product-system/nodes/data_001.md +51 -0
  208. package/templates/skills/product-system/nodes/data_002.md +40 -0
  209. package/templates/skills/product-system/nodes/data_004.md +38 -0
  210. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  211. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  212. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  213. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  214. package/templates/skills/video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,356 @@
1
+ /**
2
+ * File system routes — CRUD operations on project workspace files.
3
+ * GET /api/projects/:id/files — recursive directory tree
4
+ * GET /api/projects/:id/files/content — read file content
5
+ * PUT /api/projects/:id/files/content — write file content
6
+ * POST /api/projects/:id/files — create file or folder
7
+ * DELETE /api/projects/:id/files — delete file or folder
8
+ * PATCH /api/projects/:id/files — rename/move file or folder
9
+ */
10
+
11
+ import { Router } from 'express';
12
+ import { resolve, join, extname, relative } from 'node:path';
13
+ import { readdir, readFile, writeFile, mkdir, rm, rename, stat } from 'node:fs/promises';
14
+ import { existsSync } from 'node:fs';
15
+ import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
16
+
17
+ interface FileRoutesDeps {
18
+ workspaceService: ProjectWorkspaceService;
19
+ log: (tag: string, ...args: any[]) => void;
20
+ }
21
+
22
+ interface TreeEntry {
23
+ name: string;
24
+ path: string;
25
+ type: 'file' | 'directory';
26
+ children?: TreeEntry[];
27
+ }
28
+
29
+ const BINARY_EXTENSIONS = new Set([
30
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.svg',
31
+ '.mp3', '.mp4', '.wav', '.ogg', '.webm', '.avi', '.mov',
32
+ '.pdf', '.zip', '.tar', '.gz', '.7z', '.rar',
33
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
34
+ '.exe', '.dll', '.so', '.dylib',
35
+ '.bin', '.dat', '.db', '.sqlite',
36
+ ]);
37
+
38
+ const EXTENSION_LANGUAGE_MAP: Record<string, string> = {
39
+ '.ts': 'typescript',
40
+ '.tsx': 'typescript',
41
+ '.js': 'javascript',
42
+ '.jsx': 'javascript',
43
+ '.json': 'json',
44
+ '.html': 'html',
45
+ '.htm': 'html',
46
+ '.css': 'css',
47
+ '.scss': 'scss',
48
+ '.less': 'less',
49
+ '.md': 'markdown',
50
+ '.yaml': 'yaml',
51
+ '.yml': 'yaml',
52
+ '.xml': 'xml',
53
+ '.sql': 'sql',
54
+ '.sh': 'shell',
55
+ '.bash': 'shell',
56
+ '.zsh': 'shell',
57
+ '.py': 'python',
58
+ '.rb': 'ruby',
59
+ '.go': 'go',
60
+ '.rs': 'rust',
61
+ '.java': 'java',
62
+ '.c': 'c',
63
+ '.cpp': 'cpp',
64
+ '.h': 'c',
65
+ '.hpp': 'cpp',
66
+ '.cs': 'csharp',
67
+ '.php': 'php',
68
+ '.swift': 'swift',
69
+ '.kt': 'kotlin',
70
+ '.lua': 'lua',
71
+ '.r': 'r',
72
+ '.toml': 'toml',
73
+ '.ini': 'ini',
74
+ '.env': 'plaintext',
75
+ '.txt': 'plaintext',
76
+ '.log': 'plaintext',
77
+ '.gitignore': 'plaintext',
78
+ '.dockerignore': 'plaintext',
79
+ '.editorconfig': 'plaintext',
80
+ '.svg': 'xml',
81
+ };
82
+
83
+ const getLanguage = (filePath: string): string => {
84
+ const ext = extname(filePath).toLowerCase();
85
+ return EXTENSION_LANGUAGE_MAP[ext] || 'plaintext';
86
+ };
87
+
88
+ const isBinaryFile = (filePath: string): boolean => {
89
+ const ext = extname(filePath).toLowerCase();
90
+ return BINARY_EXTENSIONS.has(ext);
91
+ };
92
+
93
+ /**
94
+ * Validate that a path is safe — no traversal outside workspace.
95
+ * Returns the resolved absolute path if valid, or null if invalid.
96
+ */
97
+ const resolveAndValidatePath = (workspacePath: string, relativePath: string): string | null => {
98
+ if (relativePath.includes('..')) {
99
+ return null;
100
+ }
101
+ const resolved = resolve(workspacePath, relativePath);
102
+ if (!resolved.startsWith(workspacePath)) {
103
+ return null;
104
+ }
105
+ return resolved;
106
+ };
107
+
108
+ /** Build a recursive directory tree. */
109
+ const buildTree = async (dirPath: string, basePath: string): Promise<TreeEntry[]> => {
110
+ const entries = await readdir(dirPath, { withFileTypes: true });
111
+ const tree: TreeEntry[] = [];
112
+
113
+ for (const entry of entries) {
114
+ // Skip hidden files/directories (like .git)
115
+ if (entry.name.startsWith('.')) continue;
116
+
117
+ const fullPath = join(dirPath, entry.name);
118
+ const relPath = relative(basePath, fullPath);
119
+
120
+ if (entry.isDirectory()) {
121
+ const children = await buildTree(fullPath, basePath);
122
+ tree.push({ name: entry.name, path: relPath, type: 'directory', children });
123
+ } else {
124
+ tree.push({ name: entry.name, path: relPath, type: 'file' });
125
+ }
126
+ }
127
+
128
+ // Sort: directories first, then alphabetical
129
+ tree.sort((a, b) => {
130
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
131
+ return a.name.localeCompare(b.name);
132
+ });
133
+
134
+ return tree;
135
+ };
136
+
137
+ export const createFileRoutes = ({ workspaceService, log }: FileRoutesDeps): Router => {
138
+ const router: Router = Router({ mergeParams: true });
139
+
140
+ // GET / — recursive directory tree
141
+ router.get('/', async (req, res) => {
142
+ const { id } = req.params;
143
+ log('FILES', `GET /api/projects/${id}/files`);
144
+
145
+ try {
146
+ const wsPath = workspaceService.getWorkspacePath(id);
147
+ if (!existsSync(wsPath)) {
148
+ res.json({ tree: [] });
149
+ return;
150
+ }
151
+
152
+ const tree = await buildTree(wsPath, wsPath);
153
+ res.json({ tree });
154
+ } catch (err: any) {
155
+ log('FILES', `List files failed: ${err.message}`);
156
+ res.status(500).json({ error: `Failed to list files: ${err.message}` });
157
+ }
158
+ });
159
+
160
+ // GET /content?path=relative/path — read file content
161
+ router.get('/content', async (req, res) => {
162
+ const { id } = req.params;
163
+ const filePath = req.query.path as string;
164
+ log('FILES', `GET /api/projects/${id}/files/content?path=${filePath}`);
165
+
166
+ if (!filePath) {
167
+ res.status(400).json({ error: 'path query parameter is required' });
168
+ return;
169
+ }
170
+
171
+ const wsPath = workspaceService.getWorkspacePath(id);
172
+ const resolved = resolveAndValidatePath(wsPath, filePath);
173
+ if (!resolved) {
174
+ res.status(400).json({ error: 'Invalid path' });
175
+ return;
176
+ }
177
+
178
+ try {
179
+ if (!existsSync(resolved)) {
180
+ res.status(404).json({ error: 'File not found' });
181
+ return;
182
+ }
183
+
184
+ const fileStat = await stat(resolved);
185
+ if (fileStat.isDirectory()) {
186
+ res.status(400).json({ error: 'Path is a directory, not a file' });
187
+ return;
188
+ }
189
+
190
+ if (isBinaryFile(resolved)) {
191
+ res.json({ binary: true, size: fileStat.size });
192
+ return;
193
+ }
194
+
195
+ const content = await readFile(resolved, 'utf-8');
196
+ const language = getLanguage(resolved);
197
+ res.json({ content, language });
198
+ } catch (err: any) {
199
+ log('FILES', `Read file failed: ${err.message}`);
200
+ res.status(500).json({ error: `Failed to read file: ${err.message}` });
201
+ }
202
+ });
203
+
204
+ // PUT /content — write file content
205
+ router.put('/content', async (req, res) => {
206
+ const { id } = req.params;
207
+ const { path: filePath, content } = req.body;
208
+ log('FILES', `PUT /api/projects/${id}/files/content path="${filePath}"`);
209
+
210
+ if (!filePath || content === undefined) {
211
+ res.status(400).json({ error: 'path and content are required' });
212
+ return;
213
+ }
214
+
215
+ const wsPath = workspaceService.getWorkspacePath(id);
216
+ const resolved = resolveAndValidatePath(wsPath, filePath);
217
+ if (!resolved) {
218
+ res.status(400).json({ error: 'Invalid path' });
219
+ return;
220
+ }
221
+
222
+ try {
223
+ if (!existsSync(resolved)) {
224
+ res.status(404).json({ error: 'File not found' });
225
+ return;
226
+ }
227
+
228
+ await writeFile(resolved, content, 'utf-8');
229
+ res.json({ success: true });
230
+ } catch (err: any) {
231
+ log('FILES', `Write file failed: ${err.message}`);
232
+ res.status(500).json({ error: `Failed to write file: ${err.message}` });
233
+ }
234
+ });
235
+
236
+ // POST / — create file or folder
237
+ router.post('/', async (req, res) => {
238
+ const { id } = req.params;
239
+ const { path: filePath, type } = req.body;
240
+ log('FILES', `POST /api/projects/${id}/files path="${filePath}" type="${type}"`);
241
+
242
+ if (!filePath || !type) {
243
+ res.status(400).json({ error: 'path and type are required' });
244
+ return;
245
+ }
246
+
247
+ if (type !== 'file' && type !== 'directory') {
248
+ res.status(400).json({ error: 'type must be "file" or "directory"' });
249
+ return;
250
+ }
251
+
252
+ const wsPath = workspaceService.getWorkspacePath(id);
253
+ const resolved = resolveAndValidatePath(wsPath, filePath);
254
+ if (!resolved) {
255
+ res.status(400).json({ error: 'Invalid path' });
256
+ return;
257
+ }
258
+
259
+ try {
260
+ if (existsSync(resolved)) {
261
+ res.status(400).json({ error: 'Path already exists' });
262
+ return;
263
+ }
264
+
265
+ if (type === 'directory') {
266
+ await mkdir(resolved, { recursive: true });
267
+ } else {
268
+ // Ensure parent directory exists
269
+ const parentDir = resolve(resolved, '..');
270
+ if (!existsSync(parentDir)) {
271
+ await mkdir(parentDir, { recursive: true });
272
+ }
273
+ await writeFile(resolved, '', 'utf-8');
274
+ }
275
+
276
+ res.json({ success: true });
277
+ } catch (err: any) {
278
+ log('FILES', `Create failed: ${err.message}`);
279
+ res.status(500).json({ error: `Failed to create: ${err.message}` });
280
+ }
281
+ });
282
+
283
+ // DELETE /?path=relative/path — delete file or folder
284
+ router.delete('/', async (req, res) => {
285
+ const { id } = req.params;
286
+ const filePath = req.query.path as string;
287
+ log('FILES', `DELETE /api/projects/${id}/files?path=${filePath}`);
288
+
289
+ if (!filePath) {
290
+ res.status(400).json({ error: 'path query parameter is required' });
291
+ return;
292
+ }
293
+
294
+ const wsPath = workspaceService.getWorkspacePath(id);
295
+ const resolved = resolveAndValidatePath(wsPath, filePath);
296
+ if (!resolved) {
297
+ res.status(400).json({ error: 'Invalid path' });
298
+ return;
299
+ }
300
+
301
+ try {
302
+ if (!existsSync(resolved)) {
303
+ res.status(404).json({ error: 'File not found' });
304
+ return;
305
+ }
306
+
307
+ await rm(resolved, { recursive: true });
308
+ res.json({ success: true });
309
+ } catch (err: any) {
310
+ log('FILES', `Delete failed: ${err.message}`);
311
+ res.status(500).json({ error: `Failed to delete: ${err.message}` });
312
+ }
313
+ });
314
+
315
+ // PATCH / — rename/move file or folder
316
+ router.patch('/', async (req, res) => {
317
+ const { id } = req.params;
318
+ const { oldPath, newPath } = req.body;
319
+ log('FILES', `PATCH /api/projects/${id}/files oldPath="${oldPath}" newPath="${newPath}"`);
320
+
321
+ if (!oldPath || !newPath) {
322
+ res.status(400).json({ error: 'oldPath and newPath are required' });
323
+ return;
324
+ }
325
+
326
+ const wsPath = workspaceService.getWorkspacePath(id);
327
+ const resolvedOld = resolveAndValidatePath(wsPath, oldPath);
328
+ const resolvedNew = resolveAndValidatePath(wsPath, newPath);
329
+
330
+ if (!resolvedOld || !resolvedNew) {
331
+ res.status(400).json({ error: 'Invalid path' });
332
+ return;
333
+ }
334
+
335
+ try {
336
+ if (!existsSync(resolvedOld)) {
337
+ res.status(404).json({ error: 'File not found' });
338
+ return;
339
+ }
340
+
341
+ // Ensure parent directory of new path exists
342
+ const newParentDir = resolve(resolvedNew, '..');
343
+ if (!existsSync(newParentDir)) {
344
+ await mkdir(newParentDir, { recursive: true });
345
+ }
346
+
347
+ await rename(resolvedOld, resolvedNew);
348
+ res.json({ success: true });
349
+ } catch (err: any) {
350
+ log('FILES', `Rename failed: ${err.message}`);
351
+ res.status(500).json({ error: `Failed to rename: ${err.message}` });
352
+ }
353
+ });
354
+
355
+ return router;
356
+ };
@@ -7,21 +7,25 @@
7
7
  * POST /api/projects/:id/git/test — test GitHub App connection
8
8
  * GET /api/projects/:id/git/installations — list GitHub App installations
9
9
  * GET /api/projects/:id/git/repos — list repos for an installation
10
+ * POST /api/projects/:id/git/ssh/generate — generate SSH keypair for project
11
+ * POST /api/projects/:id/git/ssh/connect — connect repo via SSH clone URL
10
12
  */
11
13
 
12
14
  import { Router } from 'express';
13
15
  import type { ProjectService } from '../services/project_service.js';
14
16
  import type { GitHubAppService } from '../services/github_app_service.js';
15
17
  import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
18
+ import type { SshKeyService } from '../services/ssh_key_service.js';
16
19
 
17
20
  interface GitRoutesDeps {
18
21
  projectService: ProjectService;
19
22
  githubAppService: GitHubAppService;
20
23
  workspaceService: ProjectWorkspaceService;
24
+ sshKeyService: SshKeyService;
21
25
  log: (tag: string, ...args: any[]) => void;
22
26
  }
23
27
 
24
- export const createGitRoutes = ({ projectService, githubAppService, workspaceService, log }: GitRoutesDeps): Router => {
28
+ export const createGitRoutes = ({ projectService, githubAppService, workspaceService, sshKeyService, log }: GitRoutesDeps): Router => {
25
29
  const router: Router = Router({ mergeParams: true });
26
30
 
27
31
  // POST /api/projects/:id/git/connect — connect a git repo
@@ -138,7 +142,10 @@ export const createGitRoutes = ({ projectService, githubAppService, workspaceSer
138
142
  githubInstallationId: project.githubInstallationId,
139
143
  githubRepoFullName: project.githubRepoFullName,
140
144
  baseBranch: project.baseBranch,
145
+ gitAuthMethod: project.gitAuthMethod,
146
+ sshPublicKey: project.sshPublicKey,
141
147
  githubAppConfigured: githubAppService.isConfigured(),
148
+ sshConfigured: sshKeyService.isConfigured(),
142
149
  });
143
150
  } catch (err: any) {
144
151
  log('GIT', `Get git status failed: ${err.message}`);
@@ -227,5 +234,93 @@ export const createGitRoutes = ({ projectService, githubAppService, workspaceSer
227
234
  }
228
235
  });
229
236
 
237
+ // POST /api/projects/:id/git/ssh/generate — generate (or regenerate) SSH keypair
238
+ router.post('/ssh/generate', async (req, res) => {
239
+ const { id } = req.params;
240
+ log('GIT', `POST /api/projects/${id}/git/ssh/generate`);
241
+
242
+ try {
243
+ if (!sshKeyService.isConfigured()) {
244
+ res.status(400).json({ error: 'ENCRYPTION_KEY environment variable is not configured' });
245
+ return;
246
+ }
247
+
248
+ const project = await projectService.getById(id);
249
+ if (!project) {
250
+ res.status(404).json({ error: 'Project not found' });
251
+ return;
252
+ }
253
+
254
+ const keyPair = sshKeyService.generateKeyPair();
255
+ const encryptedPrivateKey = sshKeyService.encrypt(keyPair.privateKey);
256
+
257
+ const updated = await projectService.setSshKeys(id, {
258
+ sshPrivateKeyEncrypted: encryptedPrivateKey,
259
+ sshPublicKey: keyPair.publicKey,
260
+ });
261
+
262
+ res.json({
263
+ project: {
264
+ ...updated,
265
+ sshPrivateKeyEncrypted: undefined, // Never expose encrypted key to frontend
266
+ },
267
+ publicKey: keyPair.publicKey,
268
+ });
269
+ } catch (err: any) {
270
+ log('GIT', `Generate SSH key failed: ${err.message}`);
271
+ res.status(500).json({ error: `Failed to generate SSH key: ${err.message}` });
272
+ }
273
+ });
274
+
275
+ // POST /api/projects/:id/git/ssh/connect — connect repo using SSH clone URL
276
+ router.post('/ssh/connect', async (req, res) => {
277
+ const { id } = req.params;
278
+ const { repoUrl, baseBranch } = req.body;
279
+ log('GIT', `POST /api/projects/${id}/git/ssh/connect repoUrl="${repoUrl}"`);
280
+
281
+ if (!repoUrl) {
282
+ res.status(400).json({ error: 'repoUrl is required (SSH clone URL, e.g. git@github.com:org/repo.git)' });
283
+ return;
284
+ }
285
+
286
+ try {
287
+ const project = await projectService.getById(id);
288
+ if (!project) {
289
+ res.status(404).json({ error: 'Project not found' });
290
+ return;
291
+ }
292
+
293
+ if (!project.sshPrivateKeyEncrypted) {
294
+ res.status(400).json({ error: 'No SSH key configured. Generate an SSH key first.' });
295
+ return;
296
+ }
297
+
298
+ // Clone using SSH
299
+ await workspaceService.cloneRepoSsh(id, repoUrl, project.sshPrivateKeyEncrypted);
300
+
301
+ // Detect default branch
302
+ const detectedBranch = await workspaceService.getDefaultBranch(id);
303
+ const effectiveBranch = baseBranch || detectedBranch;
304
+
305
+ // Save repo metadata
306
+ const updated = await projectService.connectRepo(id, {
307
+ repoUrl,
308
+ baseBranch: effectiveBranch,
309
+ });
310
+
311
+ res.json({
312
+ project: {
313
+ ...updated,
314
+ sshPrivateKeyEncrypted: undefined,
315
+ gitAuthMethod: project.gitAuthMethod,
316
+ sshPublicKey: project.sshPublicKey,
317
+ },
318
+ });
319
+ } catch (err: any) {
320
+ log('GIT', `SSH connect repo failed: ${err.message}`);
321
+ res.status(500).json({ error: `Failed to connect repo via SSH: ${err.message}` });
322
+ }
323
+ });
324
+
230
325
  return router;
231
326
  };
@@ -49,6 +49,7 @@ router.get('/node/:id', async (req, res) => {
49
49
  status: row.status,
50
50
  priority: row.priority,
51
51
  ...(row.kind ? {kind: row.kind} : {}),
52
+ ...(row.featureType ? {feature_type: row.featureType} : {}),
52
53
  created_at: row.createdAt,
53
54
  updated_at: row.updatedAt,
54
55
  };
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Kanban API routes — CRUD for kanban board, notes, pipeline.
2
+ * Kanban API routes — CRUD for kanban board, notes.
3
3
  */
4
4
 
5
5
  import { Router } from 'express';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { readGraph } from '@assistkick/shared/lib/graph.js';
8
8
  import { loadKanban, getKanbanEntry, saveKanbanEntry } from '@assistkick/shared/lib/kanban.js';
9
- import { log, pipeline } from '../services/init.js';
9
+ import { log } from '../services/init.js';
10
10
 
11
11
  const router: Router = Router();
12
12
 
@@ -19,9 +19,9 @@ router.get('/', async (req, res) => {
19
19
  try {
20
20
  const kanban = await loadKanban(projectId);
21
21
 
22
- // Auto-add missing feature nodes to the Backlog column
22
+ // Auto-add missing feature and epic nodes to the Backlog column
23
23
  const graph = await readGraph(projectId);
24
- const featureNodes = graph.nodes.filter((n: any) => n.type === 'feature');
24
+ const featureNodes = graph.nodes.filter((n: any) => n.type === 'feature' || n.type === 'epic');
25
25
  for (const node of featureNodes) {
26
26
  if (!kanban[node.id]) {
27
27
  const newEntry = {
@@ -36,6 +36,22 @@ router.get('/', async (req, res) => {
36
36
  }
37
37
  }
38
38
 
39
+ // Enrich kanban entries with epic_parent_id from contains edges
40
+ const childToEpic = new Map<string, string>();
41
+ for (const edge of graph.edges) {
42
+ if (edge.relation !== 'contains') continue;
43
+ const parentNode = graph.nodes.find((n: any) => n.id === edge.from);
44
+ if (parentNode?.type === 'epic') {
45
+ childToEpic.set(edge.to, edge.from);
46
+ }
47
+ }
48
+ for (const [nodeId, entry] of Object.entries(kanban) as [string, any][]) {
49
+ const epicParentId = childToEpic.get(nodeId);
50
+ if (epicParentId) {
51
+ entry.epic_parent_id = epicParentId;
52
+ }
53
+ }
54
+
39
55
  res.json(kanban);
40
56
  } catch (err: any) {
41
57
  log('API', `GET /api/kanban FAILED: ${err.message}`);
@@ -88,11 +104,34 @@ router.post('/:id/move', async (req, res) => {
88
104
  await saveKanbanEntry(featureId, entry);
89
105
  log('MOVE', `OK: ${featureId} ${currentColumn} → ${column}`);
90
106
 
107
+ // If this is an epic, cascade move to all child features
108
+ const movedChildren: string[] = [];
109
+ const projectId = req.query.project_id as string | undefined;
110
+ const graph = await readGraph(projectId);
111
+ const node = graph.nodes.find((n: any) => n.id === featureId);
112
+ if (node?.type === 'epic') {
113
+ const childIds = graph.edges
114
+ .filter((e: any) => e.from === featureId && e.relation === 'contains')
115
+ .map((e: any) => e.to);
116
+ for (const childId of childIds) {
117
+ const childEntry = await getKanbanEntry(childId);
118
+ if (childEntry && childEntry.column !== column) {
119
+ const childPrev = childEntry.column;
120
+ childEntry.column = column;
121
+ childEntry.moved_at = new Date().toISOString();
122
+ await saveKanbanEntry(childId, childEntry);
123
+ movedChildren.push(childId);
124
+ log('MOVE', `CASCADE: ${childId} ${childPrev} → ${column} (child of ${featureId})`);
125
+ }
126
+ }
127
+ }
128
+
91
129
  res.json({
92
130
  feature_id: featureId,
93
131
  previous_column: currentColumn,
94
132
  new_column: column,
95
133
  rejection_count: entry.rejection_count || 0,
134
+ moved_children: movedChildren,
96
135
  });
97
136
  } catch (err: any) {
98
137
  log('MOVE', `ERROR: ${err.message}`);