@assistkick/create 1.7.0 → 1.9.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/bin/create.js +0 -0
  2. package/package.json +9 -7
  3. package/templates/assistkick-product-system/.env.example +1 -0
  4. package/templates/assistkick-product-system/local.db +0 -0
  5. package/templates/assistkick-product-system/package.json +4 -2
  6. package/templates/assistkick-product-system/packages/backend/package.json +2 -0
  7. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
  9. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
  10. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
  11. package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +61 -6
  13. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
  14. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
  15. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
  16. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
  17. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
  18. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +158 -0
  19. package/templates/assistkick-product-system/packages/backend/src/server.ts +60 -9
  20. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
  21. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
  22. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
  23. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
  24. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +43 -77
  25. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
  26. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
  27. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
  28. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
  29. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
  30. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
  31. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
  32. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
  33. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
  34. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
  35. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +245 -0
  41. package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
  42. package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
  43. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
  44. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +458 -18
  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 +20 -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 +187 -56
  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/IterationCommentModal.tsx +80 -0
  62. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +263 -167
  63. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  64. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  65. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
  66. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
  67. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  68. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
  69. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GenerateTTSNode.tsx +52 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RebuildBundleNode.tsx +20 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +341 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +643 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +246 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +136 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  109. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  111. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  112. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  113. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  114. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
  118. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
  120. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
  123. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
  124. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
  125. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  132. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  133. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
  134. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  135. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  136. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  137. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  138. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  139. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  140. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/0014_nifty_punisher.sql +15 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  151. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  152. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  153. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  154. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  155. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0014_snapshot.json +1545 -0
  156. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +77 -0
  157. package/templates/assistkick-product-system/packages/shared/db/schema.ts +114 -0
  158. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  159. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  160. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  161. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
  162. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1999 -0
  163. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1437 -0
  164. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  165. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  166. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  167. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  168. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  169. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  170. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  171. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  172. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  173. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  174. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  175. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  176. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  177. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  178. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  179. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  180. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  181. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  182. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  183. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  184. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  185. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  186. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  187. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  188. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  189. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  190. package/templates/assistkick-product-system/tests/video_render_service.test.ts +181 -0
  191. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  192. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  193. package/templates/skills/assistkick-developer/SKILL.md +3 -0
  194. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  195. package/templates/skills/product-system/graph.json +1890 -0
  196. package/templates/skills/product-system/kanban.json +304 -0
  197. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  198. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  199. package/templates/skills/product-system/nodes/data_001.md +51 -0
  200. package/templates/skills/product-system/nodes/data_002.md +40 -0
  201. package/templates/skills/product-system/nodes/data_004.md +38 -0
  202. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  203. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  204. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  205. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  206. package/templates/skills/video-script-writer/SKILL.md +136 -0
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistkick/create",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Scaffold assistkick-product-system into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,14 +10,16 @@
10
10
  "dist",
11
11
  "templates"
12
12
  ],
13
- "devDependencies": {
14
- "@types/node": "^25.3.3",
15
- "tsx": "^4.21.0",
16
- "typescript": "^5.9.3"
17
- },
18
13
  "scripts": {
19
14
  "build": "tsc",
20
15
  "prepare_templates": "bash scripts/prepare_templates.sh",
16
+ "prepublishOnly": "bash scripts/prepare_templates.sh && pnpm build",
17
+ "npm-publish": "npm publish",
21
18
  "test": "tsx --test tests/**/*.test.ts"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^25.3.3",
22
+ "tsx": "^4.21.0",
23
+ "typescript": "^5.9.3"
22
24
  }
23
- }
25
+ }
@@ -6,3 +6,4 @@ NODE_ENV=development
6
6
  RESEND_API_KEY=your-resend-api-key
7
7
  APP_BASE_URL=http://localhost:3000
8
8
  EMAIL_FROM=noreply@yourdomain.com
9
+ WORKSPACES_DIR=./worktrees
File without changes
@@ -7,12 +7,14 @@
7
7
  "scripts": {
8
8
  "dev": "concurrently -n backend,frontend -c blue,green \"pnpm --filter @assistkick/backend dev\" \"pnpm --filter @assistkick/frontend dev\"",
9
9
  "build": "pnpm -r build",
10
- "start": "tsx packages/backend/src/server.ts",
10
+ "start": "concurrently -n backend,frontend -c blue,green \"pnpm --filter @assistkick/backend start\" \"pnpm --filter @assistkick/frontend dev\"",
11
11
  "test": "tsx --test tests/**/*.test.ts",
12
12
  "clean": "pnpm -r clean",
13
13
  "db:migrate": "pnpm --filter @assistkick/shared db:migrate",
14
14
  "db:rollback": "pnpm --filter @assistkick/shared db:rollback",
15
- "db:generate": "pnpm --filter @assistkick/shared db:generate"
15
+ "db:generate": "pnpm --filter @assistkick/shared db:generate",
16
+ "remotion:studio": "pnpm --filter @assistkick/video studio",
17
+ "remotion:render": "pnpm --filter @assistkick/video render"
16
18
  },
17
19
  "pnpm": {
18
20
  "onlyBuiltDependencies": ["esbuild", "node-pty"]
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@assistkick/shared": "workspace:*",
15
+ "@remotion/renderer": "^4.0.434",
15
16
  "bcryptjs": "^3.0.3",
16
17
  "cookie-parser": "^1.4.7",
17
18
  "cors": "^2.8.5",
@@ -21,6 +22,7 @@
21
22
  "gray-matter": "^4.0.3",
22
23
  "jose": "^6.1.3",
23
24
  "node-pty": "^1.1.0",
25
+ "remotion": "^4.0.434",
24
26
  "resend": "^6.9.3",
25
27
  "ws": "^8.19.0"
26
28
  },
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Agent routes — CRUD endpoints for agent management.
3
+ * GET /api/agents — list agents (scope=global|project, projectId)
4
+ * GET /api/agents/:id — get single agent
5
+ * POST /api/agents — create agent
6
+ * PUT /api/agents/:id — update agent
7
+ * DELETE /api/agents/:id — delete agent (409 if assigned or default)
8
+ * POST /api/agents/:id/reset — reset default agent to original prompt
9
+ */
10
+
11
+ import { Router } from 'express';
12
+ import type { AgentService } from '../services/agent_service.js';
13
+
14
+ interface AgentRoutesDeps {
15
+ agentService: AgentService;
16
+ log: (tag: string, ...args: any[]) => void;
17
+ }
18
+
19
+ export const createAgentRoutes = ({ agentService, log }: AgentRoutesDeps): Router => {
20
+ const router: Router = Router();
21
+
22
+ // GET /api/agents?scope=global|project&projectId=...
23
+ router.get('/', async (req, res) => {
24
+ const scope = (req.query.scope as string) || 'global';
25
+ const projectId = req.query.projectId as string | undefined;
26
+ log('AGENTS', `GET /api/agents scope=${scope} projectId=${projectId || 'none'}`);
27
+
28
+ if (scope !== 'global' && scope !== 'project') {
29
+ res.status(400).json({ error: 'scope must be "global" or "project"' });
30
+ return;
31
+ }
32
+
33
+ if (scope === 'project' && !projectId) {
34
+ res.status(400).json({ error: 'projectId is required for project scope' });
35
+ return;
36
+ }
37
+
38
+ try {
39
+ const agentList = await agentService.listAgents(scope, projectId);
40
+ res.json({ agents: agentList });
41
+ } catch (err: any) {
42
+ log('AGENTS', `List agents failed: ${err.message}`);
43
+ res.status(500).json({ error: 'Failed to list agents' });
44
+ }
45
+ });
46
+
47
+ // GET /api/agents/:id
48
+ router.get('/:id', async (req, res) => {
49
+ const { id } = req.params;
50
+ log('AGENTS', `GET /api/agents/${id}`);
51
+
52
+ try {
53
+ const agent = await agentService.getById(id);
54
+ if (!agent) {
55
+ res.status(404).json({ error: 'Agent not found' });
56
+ return;
57
+ }
58
+ res.json({ agent });
59
+ } catch (err: any) {
60
+ log('AGENTS', `Get agent failed: ${err.message}`);
61
+ res.status(500).json({ error: 'Failed to get agent' });
62
+ }
63
+ });
64
+
65
+ // POST /api/agents
66
+ router.post('/', async (req, res) => {
67
+ const { name, promptTemplate, projectId } = req.body;
68
+ log('AGENTS', `POST /api/agents name="${name}"`);
69
+
70
+ if (!name || typeof name !== 'string' || !name.trim()) {
71
+ res.status(400).json({ error: 'Agent name is required' });
72
+ return;
73
+ }
74
+
75
+ if (!promptTemplate || typeof promptTemplate !== 'string') {
76
+ res.status(400).json({ error: 'promptTemplate is required' });
77
+ return;
78
+ }
79
+
80
+ try {
81
+ const agent = await agentService.create({
82
+ name: name.trim(),
83
+ promptTemplate,
84
+ projectId: projectId || null,
85
+ });
86
+ res.status(201).json({ agent });
87
+ } catch (err: any) {
88
+ log('AGENTS', `Create agent failed: ${err.message}`);
89
+ res.status(500).json({ error: 'Failed to create agent' });
90
+ }
91
+ });
92
+
93
+ // PUT /api/agents/:id
94
+ router.put('/:id', async (req, res) => {
95
+ const { id } = req.params;
96
+ const { name, promptTemplate } = req.body;
97
+ log('AGENTS', `PUT /api/agents/${id}`);
98
+
99
+ if (name !== undefined && (typeof name !== 'string' || !name.trim())) {
100
+ res.status(400).json({ error: 'Agent name must be a non-empty string' });
101
+ return;
102
+ }
103
+
104
+ try {
105
+ const agent = await agentService.update(id, {
106
+ name: name?.trim(),
107
+ promptTemplate,
108
+ });
109
+ res.json({ agent });
110
+ } catch (err: any) {
111
+ log('AGENTS', `Update agent failed: ${err.message}`);
112
+ if (err.message === 'Agent not found') {
113
+ res.status(404).json({ error: err.message });
114
+ return;
115
+ }
116
+ res.status(500).json({ error: 'Failed to update agent' });
117
+ }
118
+ });
119
+
120
+ // DELETE /api/agents/:id
121
+ router.delete('/:id', async (req, res) => {
122
+ const { id } = req.params;
123
+ log('AGENTS', `DELETE /api/agents/${id}`);
124
+
125
+ try {
126
+ await agentService.delete(id);
127
+ res.json({ success: true });
128
+ } catch (err: any) {
129
+ log('AGENTS', `Delete agent failed: ${err.message}`);
130
+ if (err.message === 'Agent not found') {
131
+ res.status(404).json({ error: err.message });
132
+ return;
133
+ }
134
+ if (err.message === 'Cannot delete a default agent' || err.message === 'Cannot delete an agent that is assigned to a stage') {
135
+ res.status(409).json({ error: err.message });
136
+ return;
137
+ }
138
+ res.status(500).json({ error: 'Failed to delete agent' });
139
+ }
140
+ });
141
+
142
+ // POST /api/agents/:id/reset
143
+ router.post('/:id/reset', async (req, res) => {
144
+ const { id } = req.params;
145
+ log('AGENTS', `POST /api/agents/${id}/reset`);
146
+
147
+ try {
148
+ const agent = await agentService.resetToDefault(id);
149
+ res.json({ agent });
150
+ } catch (err: any) {
151
+ log('AGENTS', `Reset agent failed: ${err.message}`);
152
+ if (err.message === 'Agent not found') {
153
+ res.status(404).json({ error: err.message });
154
+ return;
155
+ }
156
+ if (err.message === 'Only default agents can be reset') {
157
+ res.status(400).json({ error: err.message });
158
+ return;
159
+ }
160
+ res.status(500).json({ error: 'Failed to reset agent' });
161
+ }
162
+ });
163
+
164
+ return router;
165
+ };
@@ -0,0 +1,358 @@
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 { existsSync } from 'node:fs';
7
+ import { createFileRoutes } from './files.ts';
8
+ import express from 'express';
9
+ import type { Server } from 'node:http';
10
+
11
+ /** Create a test app with file routes mounted at /api/projects/:id/files */
12
+ const createTestApp = (workspacePath: string) => {
13
+ const workspaceService = {
14
+ getWorkspacePath: (_projectId: string) => workspacePath,
15
+ } as any;
16
+ const log = mock.fn();
17
+ const app = express();
18
+ app.use(express.json());
19
+ const fileRoutes = createFileRoutes({ workspaceService, log });
20
+ app.use('/api/projects/:id/files', fileRoutes);
21
+ return { app, log };
22
+ };
23
+
24
+ /** Simple HTTP request helper using built-in fetch against an Express server */
25
+ const request = (server: Server) => {
26
+ const addr = server.address() as { port: number };
27
+ const base = `http://127.0.0.1:${addr.port}`;
28
+ return {
29
+ get: async (path: string) => {
30
+ const res = await fetch(`${base}${path}`);
31
+ return { status: res.status, body: await res.json() };
32
+ },
33
+ post: async (path: string, body: any) => {
34
+ const res = await fetch(`${base}${path}`, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(body),
38
+ });
39
+ return { status: res.status, body: await res.json() };
40
+ },
41
+ put: async (path: string, body: any) => {
42
+ const res = await fetch(`${base}${path}`, {
43
+ method: 'PUT',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify(body),
46
+ });
47
+ return { status: res.status, body: await res.json() };
48
+ },
49
+ delete: async (path: string) => {
50
+ const res = await fetch(`${base}${path}`, { method: 'DELETE' });
51
+ return { status: res.status, body: await res.json() };
52
+ },
53
+ patch: async (path: string, body: any) => {
54
+ const res = await fetch(`${base}${path}`, {
55
+ method: 'PATCH',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify(body),
58
+ });
59
+ return { status: res.status, body: await res.json() };
60
+ },
61
+ };
62
+ };
63
+
64
+ describe('File Routes', () => {
65
+ let tmpDir: string;
66
+ let server: Server;
67
+ let http: ReturnType<typeof request>;
68
+
69
+ beforeEach(async () => {
70
+ tmpDir = await mkdtemp(join(tmpdir(), 'file-routes-test-'));
71
+ const { app } = createTestApp(tmpDir);
72
+ server = await new Promise<Server>((resolve) => {
73
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
74
+ });
75
+ http = request(server);
76
+ });
77
+
78
+ afterEach(async () => {
79
+ await new Promise<void>((resolve) => server.close(() => resolve()));
80
+ await rm(tmpDir, { recursive: true, force: true });
81
+ });
82
+
83
+ describe('GET / — directory tree', () => {
84
+ it('returns empty tree for empty workspace', async () => {
85
+ const { status, body } = await http.get('/api/projects/proj_1/files');
86
+ assert.equal(status, 200);
87
+ assert.deepEqual(body.tree, []);
88
+ });
89
+
90
+ it('returns recursive tree with files and directories', async () => {
91
+ await mkdir(join(tmpDir, 'src'));
92
+ await writeFile(join(tmpDir, 'src', 'index.ts'), 'console.log("hello")');
93
+ await writeFile(join(tmpDir, 'README.md'), '# Hello');
94
+
95
+ const { status, body } = await http.get('/api/projects/proj_1/files');
96
+ assert.equal(status, 200);
97
+ assert.equal(body.tree.length, 2);
98
+
99
+ // Directories come first
100
+ const srcEntry = body.tree.find((e: any) => e.name === 'src');
101
+ assert.ok(srcEntry);
102
+ assert.equal(srcEntry.type, 'directory');
103
+ assert.equal(srcEntry.children.length, 1);
104
+ assert.equal(srcEntry.children[0].name, 'index.ts');
105
+ assert.equal(srcEntry.children[0].type, 'file');
106
+
107
+ const readmeEntry = body.tree.find((e: any) => e.name === 'README.md');
108
+ assert.ok(readmeEntry);
109
+ assert.equal(readmeEntry.type, 'file');
110
+ });
111
+
112
+ it('skips hidden files and directories', async () => {
113
+ await mkdir(join(tmpDir, '.git'));
114
+ await writeFile(join(tmpDir, '.gitignore'), 'node_modules');
115
+ await writeFile(join(tmpDir, 'visible.ts'), 'export {}');
116
+
117
+ const { status, body } = await http.get('/api/projects/proj_1/files');
118
+ assert.equal(status, 200);
119
+ assert.equal(body.tree.length, 1);
120
+ assert.equal(body.tree[0].name, 'visible.ts');
121
+ });
122
+ });
123
+
124
+ describe('GET /content — read file', () => {
125
+ it('returns file content and language', async () => {
126
+ await writeFile(join(tmpDir, 'hello.ts'), 'const x = 1;');
127
+ const { status, body } = await http.get('/api/projects/proj_1/files/content?path=hello.ts');
128
+ assert.equal(status, 200);
129
+ assert.equal(body.content, 'const x = 1;');
130
+ assert.equal(body.language, 'typescript');
131
+ });
132
+
133
+ it('returns 404 for non-existent file', async () => {
134
+ const { status, body } = await http.get('/api/projects/proj_1/files/content?path=missing.ts');
135
+ assert.equal(status, 404);
136
+ assert.equal(body.error, 'File not found');
137
+ });
138
+
139
+ it('returns 400 for path traversal', async () => {
140
+ const { status, body } = await http.get('/api/projects/proj_1/files/content?path=../../../etc/passwd');
141
+ assert.equal(status, 400);
142
+ assert.equal(body.error, 'Invalid path');
143
+ });
144
+
145
+ it('returns 400 when path is missing', async () => {
146
+ const { status } = await http.get('/api/projects/proj_1/files/content');
147
+ assert.equal(status, 400);
148
+ });
149
+
150
+ it('returns binary flag for binary files', async () => {
151
+ await writeFile(join(tmpDir, 'image.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47]));
152
+ const { status, body } = await http.get('/api/projects/proj_1/files/content?path=image.png');
153
+ assert.equal(status, 200);
154
+ assert.equal(body.binary, true);
155
+ assert.equal(typeof body.size, 'number');
156
+ assert.equal(body.content, undefined);
157
+ });
158
+
159
+ it('infers language from extension', async () => {
160
+ await writeFile(join(tmpDir, 'style.css'), 'body {}');
161
+ const { status, body } = await http.get('/api/projects/proj_1/files/content?path=style.css');
162
+ assert.equal(status, 200);
163
+ assert.equal(body.language, 'css');
164
+ });
165
+ });
166
+
167
+ describe('PUT /content — write file', () => {
168
+ it('writes content to an existing file', async () => {
169
+ await writeFile(join(tmpDir, 'test.ts'), 'old content');
170
+ const { status, body } = await http.put('/api/projects/proj_1/files/content', {
171
+ path: 'test.ts',
172
+ content: 'new content',
173
+ });
174
+ assert.equal(status, 200);
175
+ assert.equal(body.success, true);
176
+
177
+ // Verify content was written
178
+ const readRes = await http.get('/api/projects/proj_1/files/content?path=test.ts');
179
+ assert.equal(readRes.body.content, 'new content');
180
+ });
181
+
182
+ it('returns 404 for non-existent file', async () => {
183
+ const { status, body } = await http.put('/api/projects/proj_1/files/content', {
184
+ path: 'nonexistent.ts',
185
+ content: 'hello',
186
+ });
187
+ assert.equal(status, 404);
188
+ assert.equal(body.error, 'File not found');
189
+ });
190
+
191
+ it('returns 400 for path traversal', async () => {
192
+ const { status, body } = await http.put('/api/projects/proj_1/files/content', {
193
+ path: '../evil.ts',
194
+ content: 'hack',
195
+ });
196
+ assert.equal(status, 400);
197
+ assert.equal(body.error, 'Invalid path');
198
+ });
199
+
200
+ it('returns 400 when path or content is missing', async () => {
201
+ const { status } = await http.put('/api/projects/proj_1/files/content', { path: 'test.ts' });
202
+ assert.equal(status, 400);
203
+ });
204
+ });
205
+
206
+ describe('POST / — create file or folder', () => {
207
+ it('creates a new file', async () => {
208
+ const { status, body } = await http.post('/api/projects/proj_1/files', {
209
+ path: 'new_file.ts',
210
+ type: 'file',
211
+ });
212
+ assert.equal(status, 200);
213
+ assert.equal(body.success, true);
214
+ assert.ok(existsSync(join(tmpDir, 'new_file.ts')));
215
+ });
216
+
217
+ it('creates a new directory', async () => {
218
+ const { status, body } = await http.post('/api/projects/proj_1/files', {
219
+ path: 'new_dir',
220
+ type: 'directory',
221
+ });
222
+ assert.equal(status, 200);
223
+ assert.equal(body.success, true);
224
+ assert.ok(existsSync(join(tmpDir, 'new_dir')));
225
+ });
226
+
227
+ it('creates nested file with parent directories', async () => {
228
+ const { status, body } = await http.post('/api/projects/proj_1/files', {
229
+ path: 'deep/nested/file.ts',
230
+ type: 'file',
231
+ });
232
+ assert.equal(status, 200);
233
+ assert.equal(body.success, true);
234
+ assert.ok(existsSync(join(tmpDir, 'deep', 'nested', 'file.ts')));
235
+ });
236
+
237
+ it('returns 400 when path already exists', async () => {
238
+ await writeFile(join(tmpDir, 'existing.ts'), 'content');
239
+ const { status, body } = await http.post('/api/projects/proj_1/files', {
240
+ path: 'existing.ts',
241
+ type: 'file',
242
+ });
243
+ assert.equal(status, 400);
244
+ assert.equal(body.error, 'Path already exists');
245
+ });
246
+
247
+ it('returns 400 for path traversal', async () => {
248
+ const { status, body } = await http.post('/api/projects/proj_1/files', {
249
+ path: '../evil.ts',
250
+ type: 'file',
251
+ });
252
+ assert.equal(status, 400);
253
+ assert.equal(body.error, 'Invalid path');
254
+ });
255
+
256
+ it('returns 400 for invalid type', async () => {
257
+ const { status } = await http.post('/api/projects/proj_1/files', {
258
+ path: 'test.ts',
259
+ type: 'symlink',
260
+ });
261
+ assert.equal(status, 400);
262
+ });
263
+ });
264
+
265
+ describe('DELETE / — delete file or folder', () => {
266
+ it('deletes a file', async () => {
267
+ await writeFile(join(tmpDir, 'to_delete.ts'), 'bye');
268
+ const { status, body } = await http.delete('/api/projects/proj_1/files?path=to_delete.ts');
269
+ assert.equal(status, 200);
270
+ assert.equal(body.success, true);
271
+ assert.ok(!existsSync(join(tmpDir, 'to_delete.ts')));
272
+ });
273
+
274
+ it('deletes a directory recursively', async () => {
275
+ await mkdir(join(tmpDir, 'dir_to_delete', 'sub'), { recursive: true });
276
+ await writeFile(join(tmpDir, 'dir_to_delete', 'sub', 'file.ts'), 'nested');
277
+ const { status, body } = await http.delete('/api/projects/proj_1/files?path=dir_to_delete');
278
+ assert.equal(status, 200);
279
+ assert.equal(body.success, true);
280
+ assert.ok(!existsSync(join(tmpDir, 'dir_to_delete')));
281
+ });
282
+
283
+ it('returns 404 for non-existent file', async () => {
284
+ const { status, body } = await http.delete('/api/projects/proj_1/files?path=ghost.ts');
285
+ assert.equal(status, 404);
286
+ assert.equal(body.error, 'File not found');
287
+ });
288
+
289
+ it('returns 400 for path traversal', async () => {
290
+ const { status, body } = await http.delete('/api/projects/proj_1/files?path=../evil');
291
+ assert.equal(status, 400);
292
+ assert.equal(body.error, 'Invalid path');
293
+ });
294
+
295
+ it('returns 400 when path is missing', async () => {
296
+ const { status } = await http.delete('/api/projects/proj_1/files');
297
+ assert.equal(status, 400);
298
+ });
299
+ });
300
+
301
+ describe('PATCH / — rename/move', () => {
302
+ it('renames a file', async () => {
303
+ await writeFile(join(tmpDir, 'old_name.ts'), 'content');
304
+ const { status, body } = await http.patch('/api/projects/proj_1/files', {
305
+ oldPath: 'old_name.ts',
306
+ newPath: 'new_name.ts',
307
+ });
308
+ assert.equal(status, 200);
309
+ assert.equal(body.success, true);
310
+ assert.ok(!existsSync(join(tmpDir, 'old_name.ts')));
311
+ assert.ok(existsSync(join(tmpDir, 'new_name.ts')));
312
+ });
313
+
314
+ it('moves a file to a new directory', async () => {
315
+ await writeFile(join(tmpDir, 'movable.ts'), 'content');
316
+ const { status, body } = await http.patch('/api/projects/proj_1/files', {
317
+ oldPath: 'movable.ts',
318
+ newPath: 'subdir/movable.ts',
319
+ });
320
+ assert.equal(status, 200);
321
+ assert.equal(body.success, true);
322
+ assert.ok(existsSync(join(tmpDir, 'subdir', 'movable.ts')));
323
+ });
324
+
325
+ it('returns 404 when source does not exist', async () => {
326
+ const { status, body } = await http.patch('/api/projects/proj_1/files', {
327
+ oldPath: 'nonexistent.ts',
328
+ newPath: 'target.ts',
329
+ });
330
+ assert.equal(status, 404);
331
+ assert.equal(body.error, 'File not found');
332
+ });
333
+
334
+ it('returns 400 for path traversal in oldPath', async () => {
335
+ const { status, body } = await http.patch('/api/projects/proj_1/files', {
336
+ oldPath: '../evil.ts',
337
+ newPath: 'safe.ts',
338
+ });
339
+ assert.equal(status, 400);
340
+ assert.equal(body.error, 'Invalid path');
341
+ });
342
+
343
+ it('returns 400 for path traversal in newPath', async () => {
344
+ await writeFile(join(tmpDir, 'safe.ts'), 'content');
345
+ const { status, body } = await http.patch('/api/projects/proj_1/files', {
346
+ oldPath: 'safe.ts',
347
+ newPath: '../evil.ts',
348
+ });
349
+ assert.equal(status, 400);
350
+ assert.equal(body.error, 'Invalid path');
351
+ });
352
+
353
+ it('returns 400 when oldPath or newPath is missing', async () => {
354
+ const { status } = await http.patch('/api/projects/proj_1/files', { oldPath: 'test.ts' });
355
+ assert.equal(status, 400);
356
+ });
357
+ });
358
+ });