@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,275 @@
1
+ import { describe, it, mock, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { WorkflowService } from './workflow_service.ts';
4
+
5
+ const MOCK_WORKFLOW = {
6
+ id: 'wf-1',
7
+ name: 'Test Workflow',
8
+ description: 'A test workflow',
9
+ projectId: null,
10
+ featureType: null,
11
+ isDefault: 0,
12
+ graphData: '{"nodes":[],"edges":[]}',
13
+ createdAt: '2024-01-01T00:00:00Z',
14
+ updatedAt: '2024-01-01T00:00:00Z',
15
+ };
16
+
17
+ const MOCK_DEFAULT_WORKFLOW = {
18
+ id: 'wf-default',
19
+ name: 'Default Pipeline',
20
+ description: 'Default workflow',
21
+ projectId: null,
22
+ featureType: null,
23
+ isDefault: 1,
24
+ graphData: '{"nodes":[{"id":"start_1"}],"edges":[]}',
25
+ createdAt: '2024-01-01T00:00:00Z',
26
+ updatedAt: '2024-01-01T00:00:00Z',
27
+ };
28
+
29
+ const createMockDb = () => {
30
+ let selectResults: any[][] = [];
31
+ let selectCallIndex = 0;
32
+
33
+ const db = {
34
+ select: mock.fn(() => {
35
+ const selectObj = {
36
+ from: mock.fn(() => {
37
+ const fromObj = {
38
+ where: mock.fn(() => {
39
+ const result = selectResults[selectCallIndex] || [];
40
+ selectCallIndex++;
41
+ return result;
42
+ }),
43
+ };
44
+ return fromObj;
45
+ }),
46
+ };
47
+ return selectObj;
48
+ }),
49
+ insert: mock.fn(() => ({
50
+ values: mock.fn(() => Promise.resolve()),
51
+ })),
52
+ update: mock.fn(() => ({
53
+ set: mock.fn(() => ({
54
+ where: mock.fn(() => Promise.resolve()),
55
+ })),
56
+ })),
57
+ delete: mock.fn(() => ({
58
+ where: mock.fn(() => Promise.resolve()),
59
+ })),
60
+ _setSelectResults: (results: any[][]) => {
61
+ selectResults = results;
62
+ selectCallIndex = 0;
63
+ },
64
+ };
65
+
66
+ return db;
67
+ };
68
+
69
+ const createService = (db: any) => {
70
+ return new WorkflowService({
71
+ getDb: () => db,
72
+ log: mock.fn(),
73
+ });
74
+ };
75
+
76
+ describe('WorkflowService', () => {
77
+ let db: ReturnType<typeof createMockDb>;
78
+ let service: WorkflowService;
79
+
80
+ beforeEach(() => {
81
+ db = createMockDb();
82
+ service = createService(db);
83
+ });
84
+
85
+ describe('list', () => {
86
+ it('returns global workflows when no projectId', async () => {
87
+ db._setSelectResults([[MOCK_DEFAULT_WORKFLOW]]);
88
+
89
+ const result = await service.list();
90
+
91
+ assert.equal(result.length, 1);
92
+ assert.equal(db.select.mock.calls.length, 1);
93
+ });
94
+
95
+ it('returns global + project workflows when projectId provided', async () => {
96
+ const projectWorkflow = { ...MOCK_WORKFLOW, projectId: 'proj_001' };
97
+ db._setSelectResults([[MOCK_DEFAULT_WORKFLOW, projectWorkflow]]);
98
+
99
+ const result = await service.list('proj_001');
100
+
101
+ assert.equal(result.length, 2);
102
+ });
103
+ });
104
+
105
+ describe('getById', () => {
106
+ it('returns workflow when found', async () => {
107
+ db._setSelectResults([[MOCK_WORKFLOW]]);
108
+
109
+ const result = await service.getById('wf-1');
110
+
111
+ assert.ok(result);
112
+ assert.equal(result.name, 'Test Workflow');
113
+ assert.equal(result.graphData, '{"nodes":[],"edges":[]}');
114
+ });
115
+
116
+ it('returns null when not found', async () => {
117
+ db._setSelectResults([[]]);
118
+
119
+ const result = await service.getById('nonexistent');
120
+
121
+ assert.equal(result, null);
122
+ });
123
+ });
124
+
125
+ describe('create', () => {
126
+ it('creates a workflow with correct fields', async () => {
127
+ const result = await service.create({
128
+ name: 'New Workflow',
129
+ graphData: '{"nodes":[],"edges":[]}',
130
+ });
131
+
132
+ assert.ok(result.id);
133
+ assert.equal(result.name, 'New Workflow');
134
+ assert.equal(result.projectId, null);
135
+ assert.equal(result.isDefault, 0);
136
+ assert.equal(result.graphData, '{"nodes":[],"edges":[]}');
137
+ assert.ok(result.createdAt);
138
+ assert.equal(db.insert.mock.calls.length, 1);
139
+ });
140
+
141
+ it('creates a project-scoped workflow', async () => {
142
+ const result = await service.create({
143
+ name: 'Project Workflow',
144
+ graphData: '{"nodes":[],"edges":[]}',
145
+ projectId: 'proj_001',
146
+ });
147
+
148
+ assert.equal(result.projectId, 'proj_001');
149
+ });
150
+
151
+ it('sets description when provided', async () => {
152
+ const result = await service.create({
153
+ name: 'Described Workflow',
154
+ description: 'With description',
155
+ graphData: '{"nodes":[],"edges":[]}',
156
+ });
157
+
158
+ assert.equal(result.description, 'With description');
159
+ });
160
+
161
+ it('creates a workflow with featureType', async () => {
162
+ const result = await service.create({
163
+ name: 'Video Workflow',
164
+ graphData: '{"nodes":[],"edges":[]}',
165
+ featureType: 'video',
166
+ });
167
+
168
+ assert.equal(result.featureType, 'video');
169
+ });
170
+
171
+ it('defaults featureType to null when not provided', async () => {
172
+ const result = await service.create({
173
+ name: 'Generic Workflow',
174
+ graphData: '{"nodes":[],"edges":[]}',
175
+ });
176
+
177
+ assert.equal(result.featureType, null);
178
+ });
179
+ });
180
+
181
+ describe('update', () => {
182
+ it('updates workflow name', async () => {
183
+ db._setSelectResults([[MOCK_WORKFLOW]]);
184
+
185
+ const result = await service.update('wf-1', { name: 'Updated Name' });
186
+
187
+ assert.equal(result.name, 'Updated Name');
188
+ assert.equal(db.update.mock.calls.length, 1);
189
+ });
190
+
191
+ it('updates workflow graphData', async () => {
192
+ db._setSelectResults([[MOCK_WORKFLOW]]);
193
+
194
+ const result = await service.update('wf-1', { graphData: '{"nodes":[{"id":"new"}],"edges":[]}' });
195
+
196
+ assert.equal(result.graphData, '{"nodes":[{"id":"new"}],"edges":[]}');
197
+ });
198
+
199
+ it('updates workflow description', async () => {
200
+ db._setSelectResults([[MOCK_WORKFLOW]]);
201
+
202
+ const result = await service.update('wf-1', { description: 'New description' });
203
+
204
+ assert.equal(result.description, 'New description');
205
+ });
206
+
207
+ it('updates workflow featureType', async () => {
208
+ db._setSelectResults([[MOCK_WORKFLOW]]);
209
+
210
+ const result = await service.update('wf-1', { featureType: 'video' });
211
+
212
+ assert.equal(result.featureType, 'video');
213
+ });
214
+
215
+ it('throws when workflow not found', async () => {
216
+ db._setSelectResults([[]]);
217
+
218
+ await assert.rejects(
219
+ () => service.update('nonexistent', { name: 'X' }),
220
+ { message: 'Workflow not found' },
221
+ );
222
+ });
223
+ });
224
+
225
+ describe('delete', () => {
226
+ it('deletes a workflow with no active executions', async () => {
227
+ // First: getById, second: check active executions
228
+ db._setSelectResults([[MOCK_WORKFLOW], []]);
229
+
230
+ await service.delete('wf-1');
231
+
232
+ assert.equal(db.delete.mock.calls.length, 1);
233
+ });
234
+
235
+ it('throws when workflow not found', async () => {
236
+ db._setSelectResults([[]]);
237
+
238
+ await assert.rejects(
239
+ () => service.delete('nonexistent'),
240
+ { message: 'Workflow not found' },
241
+ );
242
+ });
243
+
244
+ it('throws when workflow has active executions', async () => {
245
+ const activeExecution = { id: 'exec-1', workflowId: 'wf-1', status: 'running' };
246
+ db._setSelectResults([[MOCK_WORKFLOW], [activeExecution]]);
247
+
248
+ await assert.rejects(
249
+ () => service.delete('wf-1'),
250
+ { message: 'Cannot delete a workflow with active executions' },
251
+ );
252
+ });
253
+ });
254
+
255
+ describe('setDefault', () => {
256
+ it('sets a workflow as default', async () => {
257
+ db._setSelectResults([[MOCK_WORKFLOW]]);
258
+
259
+ const result = await service.setDefault('wf-1');
260
+
261
+ assert.equal(result.isDefault, 1);
262
+ // Two update calls: one to unset previous default, one to set new
263
+ assert.equal(db.update.mock.calls.length, 2);
264
+ });
265
+
266
+ it('throws when workflow not found', async () => {
267
+ db._setSelectResults([[]]);
268
+
269
+ await assert.rejects(
270
+ () => service.setDefault('nonexistent'),
271
+ { message: 'Workflow not found' },
272
+ );
273
+ });
274
+ });
275
+ });
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Workflow service — CRUD operations for workflows.
3
+ * Handles listing, creating, updating, deleting workflows,
4
+ * and managing the is_default flag with scope-level uniqueness.
5
+ */
6
+
7
+ import { eq, and, isNull, or } from 'drizzle-orm';
8
+ import { workflows, workflowExecutions } from '@assistkick/shared/db/schema.js';
9
+ import { randomUUID } from 'node:crypto';
10
+
11
+ interface WorkflowServiceDeps {
12
+ getDb: () => any;
13
+ log: (tag: string, ...args: any[]) => void;
14
+ }
15
+
16
+ export interface WorkflowRecord {
17
+ id: string;
18
+ name: string;
19
+ description: string | null;
20
+ projectId: string | null;
21
+ featureType: string | null;
22
+ triggerColumn: string | null;
23
+ isDefault: number;
24
+ graphData: string;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ }
28
+
29
+ export interface WorkflowListItem {
30
+ id: string;
31
+ name: string;
32
+ description: string | null;
33
+ projectId: string | null;
34
+ featureType: string | null;
35
+ triggerColumn: string | null;
36
+ isDefault: number;
37
+ createdAt: string;
38
+ updatedAt: string;
39
+ }
40
+
41
+ export class WorkflowService {
42
+ private readonly getDb: () => any;
43
+ private readonly log: (tag: string, ...args: any[]) => void;
44
+
45
+ constructor({ getDb, log }: WorkflowServiceDeps) {
46
+ this.getDb = getDb;
47
+ this.log = log;
48
+ }
49
+
50
+ /** List workflows visible to the given scope (global + project-specific). Excludes graph_data.
51
+ * Optional filters: featureType and triggerColumn for column-gated video workflow selection. */
52
+ list = async (projectId?: string, featureType?: string, triggerColumn?: string): Promise<WorkflowListItem[]> => {
53
+ const db = this.getDb();
54
+
55
+ const conditions = [];
56
+
57
+ // Scope: global + project-specific
58
+ if (projectId) {
59
+ conditions.push(or(isNull(workflows.projectId), eq(workflows.projectId, projectId)));
60
+ } else {
61
+ conditions.push(isNull(workflows.projectId));
62
+ }
63
+
64
+ // Optional featureType filter
65
+ if (featureType) {
66
+ conditions.push(eq(workflows.featureType, featureType));
67
+ }
68
+
69
+ // Optional triggerColumn filter
70
+ if (triggerColumn) {
71
+ conditions.push(eq(workflows.triggerColumn, triggerColumn));
72
+ }
73
+
74
+ const rows = await db
75
+ .select({
76
+ id: workflows.id,
77
+ name: workflows.name,
78
+ description: workflows.description,
79
+ projectId: workflows.projectId,
80
+ featureType: workflows.featureType,
81
+ triggerColumn: workflows.triggerColumn,
82
+ isDefault: workflows.isDefault,
83
+ createdAt: workflows.createdAt,
84
+ updatedAt: workflows.updatedAt,
85
+ })
86
+ .from(workflows)
87
+ .where(and(...conditions));
88
+
89
+ this.log('WORKFLOWS', `Listed ${rows.length} workflows`);
90
+ return rows;
91
+ };
92
+
93
+ /** Get a single workflow by ID, including graph_data. */
94
+ getById = async (id: string): Promise<WorkflowRecord | null> => {
95
+ const db = this.getDb();
96
+ const [row] = await db.select().from(workflows).where(eq(workflows.id, id));
97
+ return row || null;
98
+ };
99
+
100
+ /** Create a new workflow. */
101
+ create = async (data: {
102
+ name: string;
103
+ description?: string | null;
104
+ projectId?: string | null;
105
+ featureType?: string | null;
106
+ triggerColumn?: string | null;
107
+ graphData: string;
108
+ }): Promise<WorkflowRecord> => {
109
+ const db = this.getDb();
110
+ const now = new Date().toISOString();
111
+ const id = randomUUID();
112
+
113
+ const record: WorkflowRecord = {
114
+ id,
115
+ name: data.name,
116
+ description: data.description || null,
117
+ projectId: data.projectId || null,
118
+ featureType: data.featureType || null,
119
+ triggerColumn: data.triggerColumn || null,
120
+ isDefault: 0,
121
+ graphData: data.graphData,
122
+ createdAt: now,
123
+ updatedAt: now,
124
+ };
125
+
126
+ await db.insert(workflows).values(record);
127
+ this.log('WORKFLOWS', `Created workflow: ${data.name} (${id})`);
128
+ return record;
129
+ };
130
+
131
+ /** Update a workflow's name, description, featureType, triggerColumn, and/or graph_data. */
132
+ update = async (id: string, data: {
133
+ name?: string;
134
+ description?: string;
135
+ featureType?: string | null;
136
+ triggerColumn?: string | null;
137
+ graphData?: string;
138
+ }): Promise<WorkflowRecord> => {
139
+ const db = this.getDb();
140
+ const existing = await this.getById(id);
141
+
142
+ if (!existing) {
143
+ throw new Error('Workflow not found');
144
+ }
145
+
146
+ const now = new Date().toISOString();
147
+ const updates: Record<string, any> = { updatedAt: now };
148
+
149
+ if (data.name !== undefined) updates.name = data.name;
150
+ if (data.description !== undefined) updates.description = data.description;
151
+ if (data.featureType !== undefined) updates.featureType = data.featureType;
152
+ if (data.triggerColumn !== undefined) updates.triggerColumn = data.triggerColumn;
153
+ if (data.graphData !== undefined) updates.graphData = data.graphData;
154
+
155
+ await db.update(workflows).set(updates).where(eq(workflows.id, id));
156
+
157
+ this.log('WORKFLOWS', `Updated workflow: ${existing.name} (${id})`);
158
+ return { ...existing, ...updates };
159
+ };
160
+
161
+ /** Delete a workflow. Blocked if it has active (running/pending) executions. */
162
+ delete = async (id: string): Promise<void> => {
163
+ const db = this.getDb();
164
+ const existing = await this.getById(id);
165
+
166
+ if (!existing) {
167
+ throw new Error('Workflow not found');
168
+ }
169
+
170
+ // Check for active executions
171
+ const activeExecutions = await db
172
+ .select()
173
+ .from(workflowExecutions)
174
+ .where(
175
+ and(
176
+ eq(workflowExecutions.workflowId, id),
177
+ or(
178
+ eq(workflowExecutions.status, 'running'),
179
+ eq(workflowExecutions.status, 'pending'),
180
+ ),
181
+ ),
182
+ );
183
+
184
+ if (activeExecutions.length > 0) {
185
+ throw new Error('Cannot delete a workflow with active executions');
186
+ }
187
+
188
+ await db.delete(workflows).where(eq(workflows.id, id));
189
+ this.log('WORKFLOWS', `Deleted workflow: ${existing.name} (${id})`);
190
+ };
191
+
192
+ /**
193
+ * Resolve the default workflow for a given project scope.
194
+ * Priority: project-specific default → global default.
195
+ * Returns null if no default workflow exists.
196
+ */
197
+ resolveDefault = async (projectId?: string): Promise<WorkflowRecord | null> => {
198
+ const db = this.getDb();
199
+
200
+ // Try project-specific default first
201
+ if (projectId) {
202
+ const [projectDefault] = await db.select().from(workflows).where(
203
+ and(eq(workflows.projectId, projectId), eq(workflows.isDefault, 1)),
204
+ );
205
+ if (projectDefault) return projectDefault;
206
+ }
207
+
208
+ // Fall back to global default
209
+ const [globalDefault] = await db.select().from(workflows).where(
210
+ and(isNull(workflows.projectId), eq(workflows.isDefault, 1)),
211
+ );
212
+ return globalDefault || null;
213
+ };
214
+
215
+ /** Set a workflow as the default for its scope, unsetting any previous default in the same scope. */
216
+ setDefault = async (id: string): Promise<WorkflowRecord> => {
217
+ const db = this.getDb();
218
+ const existing = await this.getById(id);
219
+
220
+ if (!existing) {
221
+ throw new Error('Workflow not found');
222
+ }
223
+
224
+ const now = new Date().toISOString();
225
+
226
+ // Unset previous default in the same scope (same project_id or both null)
227
+ const scopeCondition = existing.projectId
228
+ ? eq(workflows.projectId, existing.projectId)
229
+ : isNull(workflows.projectId);
230
+
231
+ await db
232
+ .update(workflows)
233
+ .set({ isDefault: 0, updatedAt: now })
234
+ .where(and(scopeCondition, eq(workflows.isDefault, 1)));
235
+
236
+ // Set this workflow as default
237
+ await db
238
+ .update(workflows)
239
+ .set({ isDefault: 1, updatedAt: now })
240
+ .where(eq(workflows.id, id));
241
+
242
+ this.log('WORKFLOWS', `Set default workflow: ${existing.name} (${id})`);
243
+ return { ...existing, isDefault: 1, updatedAt: now };
244
+ };
245
+ }