@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
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Agent service — CRUD operations for agents.
3
+ * Handles listing, creating, updating, deleting agents with deletion protection,
4
+ * and resetting default agents to original prompts.
5
+ */
6
+
7
+ import { eq, isNull } from 'drizzle-orm';
8
+ import { agents, workflows } from '@assistkick/shared/db/schema.js';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { readFile } from 'node:fs/promises';
11
+
12
+ interface AgentServiceDeps {
13
+ getDb: () => any;
14
+ log: (tag: string, ...args: any[]) => void;
15
+ skillPaths: {
16
+ developer: string;
17
+ reviewer: string;
18
+ debugger: string;
19
+ videoScriptWriter?: string;
20
+ videoCompositionAgent?: string;
21
+ };
22
+ }
23
+
24
+ export interface AgentRecord {
25
+ id: string;
26
+ name: string;
27
+ promptTemplate: string;
28
+ projectId: string | null;
29
+ isDefault: number;
30
+ createdAt: string;
31
+ updatedAt: string;
32
+ }
33
+
34
+ /** Maps stage identifiers to default agent display names. */
35
+ const DEFAULT_AGENT_NAMES: Record<string, string> = {
36
+ developer: 'Default Developer',
37
+ reviewer: 'Default Reviewer',
38
+ debugger: 'Default Debugger',
39
+ videoScriptWriter: 'Default Video Script Writer',
40
+ videoCompositionAgent: 'Default Video Composition Agent',
41
+ };
42
+
43
+ export class AgentService {
44
+ private readonly getDb: () => any;
45
+ private readonly log: (tag: string, ...args: any[]) => void;
46
+ private readonly skillPaths: AgentServiceDeps['skillPaths'];
47
+
48
+ constructor({ getDb, log, skillPaths }: AgentServiceDeps) {
49
+ this.getDb = getDb;
50
+ this.log = log;
51
+ this.skillPaths = skillPaths;
52
+ }
53
+
54
+ /** List agents filtered by scope. */
55
+ listAgents = async (scope: 'global' | 'project', projectId?: string): Promise<AgentRecord[]> => {
56
+ const db = this.getDb();
57
+
58
+ if (scope === 'global') {
59
+ return db.select().from(agents).where(isNull(agents.projectId));
60
+ }
61
+
62
+ if (!projectId) {
63
+ throw new Error('projectId is required for project scope');
64
+ }
65
+
66
+ return db.select().from(agents).where(eq(agents.projectId, projectId));
67
+ };
68
+
69
+ /** Get a single agent by ID. */
70
+ getById = async (id: string): Promise<AgentRecord | null> => {
71
+ const db = this.getDb();
72
+ const [row] = await db.select().from(agents).where(eq(agents.id, id));
73
+ return row || null;
74
+ };
75
+
76
+ /** Create a new agent. */
77
+ create = async (data: { name: string; promptTemplate: string; projectId?: string | null }): Promise<AgentRecord> => {
78
+ const db = this.getDb();
79
+ const now = new Date().toISOString();
80
+ const id = randomUUID();
81
+
82
+ const record: AgentRecord = {
83
+ id,
84
+ name: data.name,
85
+ promptTemplate: data.promptTemplate,
86
+ projectId: data.projectId || null,
87
+ isDefault: 0,
88
+ createdAt: now,
89
+ updatedAt: now,
90
+ };
91
+
92
+ await db.insert(agents).values(record);
93
+ this.log('AGENTS', `Created agent: ${data.name} (${id})`);
94
+ return record;
95
+ };
96
+
97
+ /** Update an agent's name and/or prompt_template. */
98
+ update = async (id: string, data: { name?: string; promptTemplate?: string }): Promise<AgentRecord> => {
99
+ const db = this.getDb();
100
+ const existing = await this.getById(id);
101
+
102
+ if (!existing) {
103
+ throw new Error('Agent not found');
104
+ }
105
+
106
+ const now = new Date().toISOString();
107
+ const updates: Record<string, any> = { updatedAt: now };
108
+
109
+ if (data.name !== undefined) updates.name = data.name;
110
+ if (data.promptTemplate !== undefined) updates.promptTemplate = data.promptTemplate;
111
+
112
+ await db.update(agents).set(updates).where(eq(agents.id, id));
113
+
114
+ this.log('AGENTS', `Updated agent: ${existing.name} (${id})`);
115
+ return { ...existing, ...updates };
116
+ };
117
+
118
+ /** Delete an agent. Blocked if referenced in any workflow graph_data. */
119
+ delete = async (id: string): Promise<void> => {
120
+ const db = this.getDb();
121
+ const existing = await this.getById(id);
122
+
123
+ if (!existing) {
124
+ throw new Error('Agent not found');
125
+ }
126
+
127
+ if (existing.isDefault) {
128
+ throw new Error('Cannot delete a default agent');
129
+ }
130
+
131
+ // Check if agent is referenced in any workflow graph_data (RunAgent node configs)
132
+ const allWorkflows = await db.select().from(workflows);
133
+ const referencingWorkflow = allWorkflows.find((w: { graphData: string }) => w.graphData.includes(id));
134
+ if (referencingWorkflow) {
135
+ throw new Error('Cannot delete an agent that is referenced in a workflow');
136
+ }
137
+
138
+ await db.delete(agents).where(eq(agents.id, id));
139
+ this.log('AGENTS', `Deleted agent: ${existing.name} (${id})`);
140
+ };
141
+
142
+ /** Reset a default agent's prompt_template to the original system prompt. */
143
+ resetToDefault = async (id: string): Promise<AgentRecord> => {
144
+ const db = this.getDb();
145
+ const existing = await this.getById(id);
146
+
147
+ if (!existing) {
148
+ throw new Error('Agent not found');
149
+ }
150
+
151
+ if (!existing.isDefault) {
152
+ throw new Error('Only default agents can be reset');
153
+ }
154
+
155
+ // Determine the stage from the agent's name (e.g. "Default Developer" → "developer")
156
+ const stage = this.inferStageFromName(existing.name);
157
+ if (!stage) {
158
+ throw new Error('Cannot determine stage for this default agent');
159
+ }
160
+
161
+ const template = await this.buildDefaultTemplate(stage);
162
+ const now = new Date().toISOString();
163
+
164
+ await db
165
+ .update(agents)
166
+ .set({ promptTemplate: template, updatedAt: now })
167
+ .where(eq(agents.id, id));
168
+
169
+ this.log('AGENTS', `Reset default agent to original prompt: ${existing.name} (${id})`);
170
+ return { ...existing, promptTemplate: template, updatedAt: now };
171
+ };
172
+
173
+ /** Refresh all default agent prompt templates from SKILL.md + template sections.
174
+ * Called on server startup to keep stored prompts in sync with code changes.
175
+ * Also creates any missing default agents that have a skill path configured. */
176
+ syncDefaults = async (): Promise<void> => {
177
+ const db = this.getDb();
178
+ const allAgents = await db.select().from(agents).where(eq(agents.isDefault, 1));
179
+ const now = new Date().toISOString();
180
+
181
+ // Update existing default agents
182
+ for (const agent of allAgents) {
183
+ const stage = this.inferStageFromName(agent.name);
184
+ if (!stage) continue;
185
+ try {
186
+ const template = await this.buildDefaultTemplate(stage);
187
+ if (template !== agent.promptTemplate) {
188
+ await db.update(agents).set({ promptTemplate: template, updatedAt: now }).where(eq(agents.id, agent.id));
189
+ this.log('AGENTS', `Synced default agent prompt: ${agent.name} (${agent.id})`);
190
+ }
191
+ } catch { /* skip if skill file missing */ }
192
+ }
193
+
194
+ // Create missing default agents
195
+ for (const [stage, name] of Object.entries(DEFAULT_AGENT_NAMES)) {
196
+ const skillPath = this.skillPaths[stage as keyof typeof this.skillPaths];
197
+ if (!skillPath) continue;
198
+ const exists = allAgents.some((a: AgentRecord) => this.inferStageFromName(a.name) === stage);
199
+ if (exists) continue;
200
+ try {
201
+ const template = await this.buildDefaultTemplate(stage);
202
+ const id = randomUUID();
203
+ await db.insert(agents).values({
204
+ id,
205
+ name,
206
+ promptTemplate: template,
207
+ projectId: null,
208
+ isDefault: 1,
209
+ createdAt: now,
210
+ updatedAt: now,
211
+ });
212
+ this.log('AGENTS', `Created missing default agent: ${name} (${id})`);
213
+ } catch { /* skip if skill file missing or read error */ }
214
+ }
215
+ };
216
+
217
+ /** Infer the stage from a default agent's name. */
218
+ private inferStageFromName = (name: string): string | null => {
219
+ const lower = name.toLowerCase();
220
+ if (lower.includes('developer')) return 'developer';
221
+ if (lower.includes('reviewer')) return 'reviewer';
222
+ if (lower.includes('debugger')) return 'debugger';
223
+ if (lower.includes('video script')) return 'videoScriptWriter';
224
+ if (lower.includes('video composition')) return 'videoCompositionAgent';
225
+ return null;
226
+ };
227
+
228
+ /** Build the default prompt template for a stage by reading the SKILL.md file. */
229
+ private buildDefaultTemplate = async (stage: string): Promise<string> => {
230
+ const pathMap: Record<string, string | undefined> = {
231
+ developer: this.skillPaths.developer,
232
+ reviewer: this.skillPaths.reviewer,
233
+ debugger: this.skillPaths.debugger,
234
+ videoScriptWriter: this.skillPaths.videoScriptWriter,
235
+ videoCompositionAgent: this.skillPaths.videoCompositionAgent,
236
+ };
237
+
238
+ const skillPath = pathMap[stage];
239
+ if (!skillPath) {
240
+ throw new Error(`Unknown stage: ${stage}`);
241
+ }
242
+
243
+ const skillContent = await readFile(skillPath, 'utf-8');
244
+
245
+ // Append the dynamic template sections with {{placeholder}} syntax
246
+ // These match the format used in the seed data
247
+ let template = skillContent + '\n\n';
248
+
249
+ if (stage === 'developer') {
250
+ template += this.buildDeveloperTemplateSections();
251
+ } else if (stage === 'reviewer') {
252
+ template += this.buildReviewerTemplateSections();
253
+ } else if (stage === 'debugger') {
254
+ template += this.buildDebuggerTemplateSections();
255
+ } else if (stage === 'videoScriptWriter') {
256
+ template += this.buildVideoScriptWriterTemplateSections();
257
+ } else if (stage === 'videoCompositionAgent') {
258
+ template += this.buildVideoCompositionAgentTemplateSections();
259
+ }
260
+
261
+ return template;
262
+ };
263
+
264
+ private buildDeveloperTemplateSections = (): string => {
265
+ let s = '';
266
+ s += `## CRITICAL: Data Isolation\n`;
267
+ s += `You are working in a git worktree. The worktree is ONLY for code changes.\n`;
268
+ s += `All kanban, graph, and spec data lives in the MAIN REPO — never use the worktree copy.\n\n`;
269
+ s += `**All tool commands MUST use absolute paths to the main repo's tools:**\n`;
270
+ s += `- Tools directory: {{mainToolsDir}}\n`;
271
+ s += `- All tool commands MUST be run from: {{mainSkillDir}}\n`;
272
+ s += `- Example: cd {{mainSkillDir}} && npx tsx packages/shared/tools/get_node.ts {{featureId}}{{pidFlag}}\n`;
273
+ s += `- Example: cd {{mainSkillDir}} && npx tsx packages/shared/tools/move_card.ts {{featureId}} in_review{{pidFlag}}\n\n`;
274
+ s += `NEVER run tools from the worktree directory. NEVER use relative paths like \`assistkick-product-system/tools/\`.\n`;
275
+ s += `ALWAYS \`cd {{mainSkillDir}}\` before running any tool command.\n`;
276
+ s += `ALWAYS include --project-id {{projectId}} when running any tool command.\n\n`;
277
+ s += `## Development Guidelines\n`;
278
+ s += `Before writing any code, load the development guidelines:\n`;
279
+ s += ` cd {{mainSkillDir}} && npx tsx packages/shared/tools/get_node.ts nfr_001{{pidFlag}}\n`;
280
+ s += `These guidelines define mandatory coding standards (class-based architecture, arrow functions, constructor DI, naming conventions). All code you write MUST follow them.\n\n`;
281
+ s += `## Current Task\n`;
282
+ s += `Implement feature {{featureId}}. Read the feature spec using get_node, understand all related nodes, and implement it.\n\n`;
283
+ s += `{{debuggerFindings}}\n\n`;
284
+ s += `{{previousReviewNotes}}\n\n`;
285
+ s += `When done implementing, do NOT move the card yourself — the pipeline will handle card transitions.\n\n`;
286
+ s += `## REQUIRED: Work Summary Output\n`;
287
+ s += `After completing your implementation, you MUST output a structured work summary block as your final text output.\n`;
288
+ s += `The pipeline will parse this block to record what was done. Use this exact format:\n\n`;
289
+ s += '```\n';
290
+ s += `## Work Summary\n`;
291
+ s += `### Approach\n`;
292
+ s += `<A short paragraph explaining what you implemented and why you chose this approach.>\n`;
293
+ s += `### Decisions\n`;
294
+ s += `- <Key decision 1 you made during implementation>\n`;
295
+ s += `- <Key decision 2>\n`;
296
+ s += '```\n\n';
297
+ s += `This block MUST appear in your final output. Do not skip it.\n`;
298
+ return s;
299
+ };
300
+
301
+ private buildReviewerTemplateSections = (): string => {
302
+ let s = '';
303
+ s += `## CRITICAL: Data Isolation\n`;
304
+ s += `You are reviewing code in a git worktree. The worktree is ONLY for code changes.\n`;
305
+ s += `All kanban, graph, and spec data lives in the MAIN REPO — never use the worktree copy.\n\n`;
306
+ s += `**All tool commands MUST use absolute paths to the main repo's tools:**\n`;
307
+ s += `- Tools directory: {{mainToolsDir}}\n`;
308
+ s += `- All tool commands MUST be run from: {{mainSkillDir}}\n`;
309
+ s += `- Example: cd {{mainSkillDir}} && npx tsx packages/shared/tools/get_node.ts {{featureId}}{{pidFlag}}\n`;
310
+ s += `- Example: cd {{mainSkillDir}} && npx tsx packages/shared/tools/move_card.ts {{featureId}} qa{{pidFlag}}\n\n`;
311
+ s += `NEVER run tools from the worktree directory. NEVER use relative paths like \`assistkick-product-system/tools/\`.\n`;
312
+ s += `ALWAYS \`cd {{mainSkillDir}}\` before running any tool command.\n`;
313
+ s += `ALWAYS include --project-id {{projectId}} when running any tool command.\n\n`;
314
+ s += `## Current Task\n`;
315
+ s += `Review feature {{featureId}}. Read the feature spec using get_node, understand all related nodes, and review the implementation.\n`;
316
+ s += `If the implementation passes review, approve it by running: cd {{mainSkillDir}} && npx tsx packages/shared/tools/move_card.ts {{featureId}} qa{{pidFlag}}\n`;
317
+ s += `If it fails, reject it with a note: cd {{mainSkillDir}} && npx tsx packages/shared/tools/move_card.ts {{featureId}} todo{{pidFlag}} --note "description of issues"\n`;
318
+ return s;
319
+ };
320
+
321
+ private buildVideoScriptWriterTemplateSections = (): string => {
322
+ let s = '';
323
+ s += `## CRITICAL: Data Isolation\n`;
324
+ s += `You are working in a git worktree. The worktree is ONLY for code changes.\n`;
325
+ s += `All kanban, graph, and spec data lives in the MAIN REPO — never use the worktree copy.\n\n`;
326
+ s += `## Current Task\n`;
327
+ s += `Write a video script for the following feature brief:\n\n`;
328
+ s += `{{featureDescription}}\n\n`;
329
+ s += `Write the script to: {{worktreePath}}/assistkick-product-system/packages/video/scripts/{{compositionName}}-script.md\n\n`;
330
+ s += `The composition name is: {{compositionName}}\n\n`;
331
+ s += `## Iteration Handling\n`;
332
+ s += `When re-running on a feature that was moved back to TODO, you receive iteration\n`;
333
+ s += `comments from the previous review. Address all feedback in your revised script.\n\n`;
334
+ s += `{{iterationComments}}\n\n`;
335
+ s += `When done, do NOT move the card yourself — the pipeline will handle card transitions.\n\n`;
336
+ s += `## REQUIRED: Work Summary Output\n`;
337
+ s += `After completing your script, you MUST output a structured work summary block as your final text output.\n`;
338
+ s += `The pipeline will parse this block to record what was done. Use this exact format:\n\n`;
339
+ s += '```\n';
340
+ s += `## Work Summary\n`;
341
+ s += `### Approach\n`;
342
+ s += `<A short paragraph explaining the script structure and creative choices.>\n`;
343
+ s += `### Decisions\n`;
344
+ s += `- <Key decision 1 about scene structure or content>\n`;
345
+ s += `- <Key decision 2>\n`;
346
+ s += '```\n\n';
347
+ s += `This block MUST appear in your final output. Do not skip it.\n`;
348
+ return s;
349
+ };
350
+
351
+ private buildVideoCompositionAgentTemplateSections = (): string => {
352
+ let s = '';
353
+ s += `## CRITICAL: Data Isolation\n`;
354
+ s += `You are working in a git worktree. The worktree is ONLY for code changes.\n`;
355
+ s += `All kanban, graph, and spec data lives in the MAIN REPO — never use the worktree copy.\n\n`;
356
+ s += `## Current Task\n`;
357
+ s += `Read the video script and generate the Remotion composition code for the following feature:\n\n`;
358
+ s += `{{featureDescription}}\n\n`;
359
+ s += `Script file: {{worktreePath}}/assistkick-product-system/packages/video/scripts/{{compositionName}}-script.md\n\n`;
360
+ s += `The composition name is: {{compositionName}}\n`;
361
+ s += `Write the composition to: {{worktreePath}}/assistkick-product-system/packages/video/compositions/{{compositionName}}/index.tsx\n`;
362
+ s += `Register it in: {{worktreePath}}/assistkick-product-system/packages/video/Root.tsx\n\n`;
363
+ s += `## Component Reuse\n`;
364
+ s += `Before creating new components, list existing files in packages/video/components/ within the worktree.\n`;
365
+ s += `Import and reuse shared components (TitleScene, Scene, OutroScene, PartDivider, EmailScene, VideoSplitLayout, theme) rather than duplicating.\n`;
366
+ s += `Only create new components in packages/video/components/ when the script describes a scene type not covered by existing components.\n\n`;
367
+ s += `## Iteration Handling\n`;
368
+ s += `When re-running on a feature that was moved back to TODO, you receive iteration\n`;
369
+ s += `comments from the previous review. Address all feedback in your revised composition.\n\n`;
370
+ s += `{{iterationComments}}\n\n`;
371
+ s += `When done, do NOT move the card yourself — the pipeline will handle card transitions.\n\n`;
372
+ s += `## REQUIRED: Work Summary Output\n`;
373
+ s += `After completing your composition, you MUST output a structured work summary block as your final text output.\n`;
374
+ s += `The pipeline will parse this block to record what was done. Use this exact format:\n\n`;
375
+ s += '```\n';
376
+ s += `## Work Summary\n`;
377
+ s += `### Approach\n`;
378
+ s += `<A short paragraph explaining the composition structure and component choices.>\n`;
379
+ s += `### Decisions\n`;
380
+ s += `- <Key decision 1 about component reuse or scene implementation>\n`;
381
+ s += `- <Key decision 2>\n`;
382
+ s += '```\n\n';
383
+ s += `This block MUST appear in your final output. Do not skip it.\n`;
384
+ return s;
385
+ };
386
+
387
+ private buildDebuggerTemplateSections = (): string => {
388
+ let s = '';
389
+ s += `## CRITICAL: Data Isolation\n`;
390
+ s += `You are investigating bugs in a git worktree. The worktree is ONLY for code changes.\n`;
391
+ s += `All kanban, graph, and spec data lives in the MAIN REPO — never use the worktree copy.\n\n`;
392
+ s += `**All tool commands MUST use absolute paths to the main repo's tools:**\n`;
393
+ s += `- Tools directory: {{mainToolsDir}}\n`;
394
+ s += `- All tool commands MUST be run from: {{mainSkillDir}}\n`;
395
+ s += `- Example: cd {{mainSkillDir}} && npx tsx packages/shared/tools/get_node.ts {{featureId}}{{pidFlag}}\n\n`;
396
+ s += `NEVER run tools from the worktree directory. NEVER use relative paths.\n`;
397
+ s += `ALWAYS \`cd {{mainSkillDir}}\` before running any tool command.\n`;
398
+ s += `ALWAYS include --project-id {{projectId}} when running any tool command.\n\n`;
399
+ s += `## Current Task\n`;
400
+ s += `You are running as part of an automated pipeline. Do NOT create bugfix features or move kanban cards.\n`;
401
+ s += `Instead, investigate the QA rejection notes below and output your findings as a structured report.\n\n`;
402
+ s += `Feature: {{featureId}}\n\n`;
403
+ s += `{{unaddressedNotes}}\n\n`;
404
+ s += `## Instructions\n`;
405
+ s += `1. Read the feature spec using get_node to understand what the feature should do\n`;
406
+ s += `2. Read all related nodes to understand the full context\n`;
407
+ s += `3. Trace through the code to find the root cause of each reported issue\n`;
408
+ s += `4. Output a structured report with your findings. For each issue:\n`;
409
+ s += ` - The original QA note text\n`;
410
+ s += ` - Root cause analysis with specific file paths and line numbers\n`;
411
+ s += ` - Why the current code fails\n`;
412
+ s += ` - Specific guidance on what needs to change to fix it\n`;
413
+ s += `5. Do NOT create bugfix features, do NOT move kanban cards, do NOT write code fixes\n`;
414
+ return s;
415
+ };
416
+ }
@@ -0,0 +1,189 @@
1
+ import { describe, it, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { BundleService } from './bundle_service.js';
7
+
8
+ describe('BundleService', () => {
9
+ let tempDir: string;
10
+ let videoPackageDir: string;
11
+ let bundleOutputDir: string;
12
+ let logs: string[];
13
+
14
+ const makeService = () => {
15
+ logs = [];
16
+ return new BundleService({
17
+ videoPackageDir,
18
+ bundleOutputDir,
19
+ log: (tag: string, ...args: any[]) => { logs.push(`[${tag}] ${args.join(' ')}`); },
20
+ });
21
+ };
22
+
23
+ beforeEach(() => {
24
+ tempDir = join(tmpdir(), `bundle-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
25
+ videoPackageDir = join(tempDir, 'video');
26
+ bundleOutputDir = join(tempDir, 'bundle-output');
27
+ mkdirSync(join(videoPackageDir, 'compositions'), { recursive: true });
28
+ mkdirSync(bundleOutputDir, { recursive: true });
29
+ });
30
+
31
+ afterEach(() => {
32
+ rmSync(tempDir, { recursive: true, force: true });
33
+ });
34
+
35
+ describe('getStatus', () => {
36
+ it('reports not ready when no bundle exists', () => {
37
+ const svc = makeService();
38
+ const status = svc.getStatus();
39
+ assert.equal(status.ready, false);
40
+ assert.equal(status.building, false);
41
+ assert.equal(status.bundlePath, null);
42
+ assert.equal(status.lastBuiltAt, null);
43
+ assert.equal(status.error, null);
44
+ });
45
+
46
+ it('reports ready when bundle index.html exists', () => {
47
+ writeFileSync(join(bundleOutputDir, 'index.html'), '<html></html>');
48
+ const svc = makeService();
49
+ const status = svc.getStatus();
50
+ assert.equal(status.ready, true);
51
+ assert.equal(status.bundlePath, bundleOutputDir);
52
+ });
53
+ });
54
+
55
+ describe('isBundleReady', () => {
56
+ it('returns false when bundle directory is empty', () => {
57
+ const svc = makeService();
58
+ assert.equal(svc.isBundleReady(), false);
59
+ });
60
+
61
+ it('returns true when index.html exists in bundle dir', () => {
62
+ writeFileSync(join(bundleOutputDir, 'index.html'), '<html></html>');
63
+ const svc = makeService();
64
+ assert.equal(svc.isBundleReady(), true);
65
+ });
66
+ });
67
+
68
+ describe('getBundleDir', () => {
69
+ it('returns the configured bundle output directory', () => {
70
+ const svc = makeService();
71
+ assert.equal(svc.getBundleDir(), bundleOutputDir);
72
+ });
73
+ });
74
+
75
+ describe('listCompositions', () => {
76
+ it('returns empty array when compositions directory is empty', () => {
77
+ const svc = makeService();
78
+ const result = svc.listCompositions();
79
+ assert.deepEqual(result, []);
80
+ });
81
+
82
+ it('returns empty array when compositions directory does not exist', () => {
83
+ rmSync(join(videoPackageDir, 'compositions'), { recursive: true });
84
+ const svc = makeService();
85
+ const result = svc.listCompositions();
86
+ assert.deepEqual(result, []);
87
+ });
88
+
89
+ it('parses composition metadata from index.tsx', () => {
90
+ const compDir = join(videoPackageDir, 'compositions', 'my_video');
91
+ mkdirSync(compDir, { recursive: true });
92
+ writeFileSync(join(compDir, 'index.tsx'), `
93
+ import { MyComponent } from '../../components/my_component';
94
+
95
+ export const component = MyComponent;
96
+ export const id = 'my-video';
97
+ export const durationInFrames = 450;
98
+ export const fps = 30;
99
+ export const width = 1920;
100
+ export const height = 1080;
101
+ export const folder = 'Demos';
102
+ `);
103
+
104
+ const svc = makeService();
105
+ const result = svc.listCompositions();
106
+ assert.equal(result.length, 1);
107
+ assert.deepEqual(result[0], {
108
+ id: 'my-video',
109
+ durationInFrames: 450,
110
+ fps: 30,
111
+ width: 1920,
112
+ height: 1080,
113
+ folder: 'Demos',
114
+ });
115
+ });
116
+
117
+ it('uses defaults for missing optional fields', () => {
118
+ const compDir = join(videoPackageDir, 'compositions', 'simple');
119
+ mkdirSync(compDir, { recursive: true });
120
+ writeFileSync(join(compDir, 'index.tsx'), `
121
+ export const component = () => null;
122
+ export const id = 'simple-comp';
123
+ export const durationInFrames = 600;
124
+ `);
125
+
126
+ const svc = makeService();
127
+ const result = svc.listCompositions();
128
+ assert.equal(result.length, 1);
129
+ assert.equal(result[0].id, 'simple-comp');
130
+ assert.equal(result[0].durationInFrames, 600);
131
+ assert.equal(result[0].fps, 30);
132
+ assert.equal(result[0].width, 1920);
133
+ assert.equal(result[0].height, 1080);
134
+ assert.equal(result[0].folder, undefined);
135
+ });
136
+
137
+ it('skips directories without index.tsx', () => {
138
+ const compDir = join(videoPackageDir, 'compositions', 'incomplete');
139
+ mkdirSync(compDir, { recursive: true });
140
+ writeFileSync(join(compDir, 'some_other_file.ts'), 'export const x = 1;');
141
+
142
+ const svc = makeService();
143
+ const result = svc.listCompositions();
144
+ assert.deepEqual(result, []);
145
+ });
146
+
147
+ it('skips files without exported id', () => {
148
+ const compDir = join(videoPackageDir, 'compositions', 'no_id');
149
+ mkdirSync(compDir, { recursive: true });
150
+ writeFileSync(join(compDir, 'index.tsx'), `
151
+ export const component = () => null;
152
+ export const durationInFrames = 300;
153
+ `);
154
+
155
+ const svc = makeService();
156
+ const result = svc.listCompositions();
157
+ assert.deepEqual(result, []);
158
+ });
159
+
160
+ it('handles multiple compositions', () => {
161
+ for (const name of ['alpha', 'beta', 'gamma']) {
162
+ const compDir = join(videoPackageDir, 'compositions', name);
163
+ mkdirSync(compDir, { recursive: true });
164
+ writeFileSync(join(compDir, 'index.tsx'), `
165
+ export const component = () => null;
166
+ export const id = '${name}';
167
+ export const durationInFrames = 300;
168
+ export const fps = 24;
169
+ export const width = 1280;
170
+ export const height = 720;
171
+ `);
172
+ }
173
+
174
+ const svc = makeService();
175
+ const result = svc.listCompositions();
176
+ assert.equal(result.length, 3);
177
+ const ids = result.map(c => c.id).sort();
178
+ assert.deepEqual(ids, ['alpha', 'beta', 'gamma']);
179
+ });
180
+
181
+ it('ignores non-directory entries in compositions folder', () => {
182
+ writeFileSync(join(videoPackageDir, 'compositions', '.gitkeep'), '');
183
+
184
+ const svc = makeService();
185
+ const result = svc.listCompositions();
186
+ assert.deepEqual(result, []);
187
+ });
188
+ });
189
+ });