@assistkick/create 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/package.json +2 -2
  2. package/templates/assistkick-product-system/.env.example +1 -0
  3. package/templates/assistkick-product-system/local.db +0 -0
  4. package/templates/assistkick-product-system/package.json +4 -2
  5. package/templates/assistkick-product-system/packages/backend/package.json +2 -0
  6. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
  7. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
  9. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
  10. package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
  11. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +43 -4
  12. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
  13. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
  14. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
  15. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
  16. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
  17. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +154 -0
  18. package/templates/assistkick-product-system/packages/backend/src/server.ts +81 -9
  19. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
  20. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
  21. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
  22. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
  23. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +28 -78
  24. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
  25. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
  26. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
  27. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
  28. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
  29. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
  30. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
  31. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
  32. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
  33. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
  34. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
  35. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +222 -0
  40. package/templates/assistkick-product-system/packages/frontend/index.html +3 -0
  41. package/templates/assistkick-product-system/packages/frontend/package-lock.json +800 -11
  42. package/templates/assistkick-product-system/packages/frontend/package.json +11 -1
  43. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +24 -7
  44. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +456 -16
  45. package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
  46. package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
  47. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
  48. package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
  49. package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
  50. package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +383 -0
  51. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
  52. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
  53. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
  54. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
  55. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
  56. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +193 -64
  57. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
  58. package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
  59. package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
  60. package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
  61. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +226 -291
  62. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  63. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  64. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +40 -66
  65. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +55 -115
  66. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  67. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +155 -77
  68. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  69. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +270 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +37 -0
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +207 -0
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +335 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +634 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +208 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +107 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  105. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/hooks/useGraph.ts +6 -21
  107. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +15 -80
  108. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  109. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  110. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  111. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  112. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  113. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  114. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +19 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +54 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +6 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +93 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +30 -0
  120. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +9 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +6 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/stores/useGitModalStore.ts +14 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphStore.ts +36 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphUIStore.ts +25 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +90 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/stores/useQaSheetStore.ts +27 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +76 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +336 -3632
  131. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  132. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  133. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  134. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  135. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  136. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  137. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  138. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  139. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +7 -1
  140. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  151. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  152. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  153. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  154. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  155. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  156. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  157. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  158. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
  161. package/templates/assistkick-product-system/packages/shared/db/schema.ts +113 -0
  162. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  163. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  164. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  165. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +16 -5
  166. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1753 -0
  167. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1281 -0
  168. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  169. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  170. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  171. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  172. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  173. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  174. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  175. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  176. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  177. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  178. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  179. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  180. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  181. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  182. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  183. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  184. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  185. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  186. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  187. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  188. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  189. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  190. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  191. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  192. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  193. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  194. package/templates/assistkick-product-system/tests/video_render_service.test.ts +179 -0
  195. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  196. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  197. package/templates/skills/assistkick-bootstrap/SKILL.md +3 -3
  198. package/templates/skills/assistkick-code-reviewer/SKILL.md +2 -2
  199. package/templates/skills/assistkick-debugger/SKILL.md +2 -2
  200. package/templates/skills/assistkick-developer/SKILL.md +6 -3
  201. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  202. package/templates/skills/assistkick-interview/SKILL.md +2 -2
  203. package/templates/skills/product-system/graph.json +1890 -0
  204. package/templates/skills/product-system/kanban.json +304 -0
  205. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  206. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  207. package/templates/skills/product-system/nodes/data_001.md +51 -0
  208. package/templates/skills/product-system/nodes/data_002.md +40 -0
  209. package/templates/skills/product-system/nodes/data_004.md +38 -0
  210. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  211. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  212. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  213. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  214. package/templates/skills/video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,162 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { SshKeyService } from './ssh_key_service.ts';
4
+ import { existsSync } from 'node:fs';
5
+ import { readFile } from 'node:fs/promises';
6
+
7
+ // 32-byte key as 64-char hex string
8
+ const TEST_ENCRYPTION_KEY = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2';
9
+
10
+ const createService = () => {
11
+ return new SshKeyService({ log: mock.fn() });
12
+ };
13
+
14
+ describe('SshKeyService', () => {
15
+ let originalEncryptionKey: string | undefined;
16
+
17
+ beforeEach(() => {
18
+ originalEncryptionKey = process.env.ENCRYPTION_KEY;
19
+ process.env.ENCRYPTION_KEY = TEST_ENCRYPTION_KEY;
20
+ });
21
+
22
+ afterEach(() => {
23
+ if (originalEncryptionKey !== undefined) {
24
+ process.env.ENCRYPTION_KEY = originalEncryptionKey;
25
+ } else {
26
+ delete process.env.ENCRYPTION_KEY;
27
+ }
28
+ });
29
+
30
+ describe('generateKeyPair', () => {
31
+ it('generates a valid ED25519 keypair', () => {
32
+ const service = createService();
33
+ const keyPair = service.generateKeyPair();
34
+
35
+ // Public key should be in OpenSSH format
36
+ assert.ok(keyPair.publicKey.startsWith('ssh-ed25519 '));
37
+ // Private key should be in PEM format
38
+ assert.ok(keyPair.privateKey.includes('-----BEGIN PRIVATE KEY-----'));
39
+ assert.ok(keyPair.privateKey.includes('-----END PRIVATE KEY-----'));
40
+ });
41
+
42
+ it('generates different keys each time', () => {
43
+ const service = createService();
44
+ const keyPair1 = service.generateKeyPair();
45
+ const keyPair2 = service.generateKeyPair();
46
+
47
+ assert.notEqual(keyPair1.publicKey, keyPair2.publicKey);
48
+ assert.notEqual(keyPair1.privateKey, keyPair2.privateKey);
49
+ });
50
+ });
51
+
52
+ describe('encrypt / decrypt', () => {
53
+ it('round-trips plaintext through encrypt/decrypt', () => {
54
+ const service = createService();
55
+ const plaintext = '-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEH\n-----END PRIVATE KEY-----';
56
+
57
+ const encrypted = service.encrypt(plaintext);
58
+ const decrypted = service.decrypt(encrypted);
59
+
60
+ assert.equal(decrypted, plaintext);
61
+ });
62
+
63
+ it('encrypted output is valid JSON with expected fields', () => {
64
+ const service = createService();
65
+ const encrypted = service.encrypt('test data');
66
+ const parsed = JSON.parse(encrypted);
67
+
68
+ assert.ok(parsed.iv, 'should have iv');
69
+ assert.ok(parsed.authTag, 'should have authTag');
70
+ assert.ok(parsed.ciphertext, 'should have ciphertext');
71
+ });
72
+
73
+ it('produces different ciphertexts for same plaintext (random IV)', () => {
74
+ const service = createService();
75
+ const plaintext = 'same plaintext';
76
+
77
+ const encrypted1 = service.encrypt(plaintext);
78
+ const encrypted2 = service.encrypt(plaintext);
79
+
80
+ assert.notEqual(encrypted1, encrypted2);
81
+ });
82
+
83
+ it('throws when ENCRYPTION_KEY is not set', () => {
84
+ delete process.env.ENCRYPTION_KEY;
85
+ const service = createService();
86
+
87
+ assert.throws(() => service.encrypt('test'), /ENCRYPTION_KEY/);
88
+ });
89
+
90
+ it('throws when ENCRYPTION_KEY has wrong length', () => {
91
+ process.env.ENCRYPTION_KEY = 'tooshort';
92
+ const service = createService();
93
+
94
+ assert.throws(() => service.encrypt('test'), /64-character hex/);
95
+ });
96
+
97
+ it('fails to decrypt with tampered ciphertext', () => {
98
+ const service = createService();
99
+ const encrypted = service.encrypt('secret data');
100
+ const parsed = JSON.parse(encrypted);
101
+
102
+ // Tamper with ciphertext
103
+ parsed.ciphertext = 'AAAA' + parsed.ciphertext.slice(4);
104
+ const tampered = JSON.stringify(parsed);
105
+
106
+ assert.throws(() => service.decrypt(tampered));
107
+ });
108
+ });
109
+
110
+ describe('writeTempKeyFile / removeTempKeyFile', () => {
111
+ it('writes a temp file with correct content and permissions', async () => {
112
+ const service = createService();
113
+ const content = '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----';
114
+
115
+ const filepath = await service.writeTempKeyFile(content);
116
+
117
+ assert.ok(existsSync(filepath));
118
+ const written = await readFile(filepath, 'utf8');
119
+ assert.equal(written, content);
120
+
121
+ // Clean up
122
+ await service.removeTempKeyFile(filepath);
123
+ assert.ok(!existsSync(filepath));
124
+ });
125
+
126
+ it('removeTempKeyFile does not throw for non-existent file', async () => {
127
+ const service = createService();
128
+ // Should not throw
129
+ await service.removeTempKeyFile('/tmp/nonexistent_ssh_key_test_12345');
130
+ });
131
+ });
132
+
133
+ describe('buildGitSshCommand', () => {
134
+ it('returns a valid ssh command string', () => {
135
+ const service = createService();
136
+ const cmd = service.buildGitSshCommand('/tmp/key_file');
137
+
138
+ assert.ok(cmd.includes('-i /tmp/key_file'));
139
+ assert.ok(cmd.includes('StrictHostKeyChecking=no'));
140
+ assert.ok(cmd.includes('UserKnownHostsFile=/dev/null'));
141
+ });
142
+ });
143
+
144
+ describe('isConfigured', () => {
145
+ it('returns true when ENCRYPTION_KEY is valid', () => {
146
+ const service = createService();
147
+ assert.equal(service.isConfigured(), true);
148
+ });
149
+
150
+ it('returns false when ENCRYPTION_KEY is not set', () => {
151
+ delete process.env.ENCRYPTION_KEY;
152
+ const service = createService();
153
+ assert.equal(service.isConfigured(), false);
154
+ });
155
+
156
+ it('returns false when ENCRYPTION_KEY has wrong length', () => {
157
+ process.env.ENCRYPTION_KEY = 'abc123';
158
+ const service = createService();
159
+ assert.equal(service.isConfigured(), false);
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * SshKeyService — generates ED25519 SSH keypairs, encrypts/decrypts private keys
3
+ * with AES-256-GCM, and manages temporary key files for git operations.
4
+ */
5
+
6
+ import { generateKeyPairSync, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
7
+ import { writeFile, unlink, chmod } from 'node:fs/promises';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+
11
+ interface SshKeyServiceDeps {
12
+ log: (tag: string, ...args: any[]) => void;
13
+ }
14
+
15
+ export interface SshKeyPair {
16
+ publicKey: string;
17
+ privateKey: string;
18
+ }
19
+
20
+ export interface EncryptedKey {
21
+ iv: string;
22
+ authTag: string;
23
+ ciphertext: string;
24
+ }
25
+
26
+ export class SshKeyService {
27
+ private readonly log: SshKeyServiceDeps['log'];
28
+
29
+ constructor({ log }: SshKeyServiceDeps) {
30
+ this.log = log;
31
+ }
32
+
33
+ private getEncryptionKey = (): Buffer => {
34
+ const key = process.env.ENCRYPTION_KEY;
35
+ if (!key) throw new Error('ENCRYPTION_KEY environment variable is not set');
36
+ // Expect a 64-char hex string (32 bytes)
37
+ const buf = Buffer.from(key, 'hex');
38
+ if (buf.length !== 32) {
39
+ throw new Error('ENCRYPTION_KEY must be a 64-character hex string (32 bytes for AES-256)');
40
+ }
41
+ return buf;
42
+ };
43
+
44
+ /** Generate an ED25519 SSH keypair. */
45
+ generateKeyPair = (): SshKeyPair => {
46
+ this.log('SSH', 'Generating ED25519 keypair');
47
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
48
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
49
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
50
+ });
51
+
52
+ // Convert the PEM public key to OpenSSH format for use as a deploy key
53
+ const sshPublicKey = this.pemToSshPublicKey(publicKey);
54
+
55
+ return {
56
+ publicKey: sshPublicKey,
57
+ privateKey,
58
+ };
59
+ };
60
+
61
+ /** Convert PEM public key to OpenSSH format (ssh-ed25519 ...). */
62
+ private pemToSshPublicKey = (pemPublicKey: string): string => {
63
+ // Extract the base64 body from PEM
64
+ const lines = pemPublicKey.split('\n').filter(l => !l.startsWith('-----') && l.trim());
65
+ const derB64 = lines.join('');
66
+ const der = Buffer.from(derB64, 'base64');
67
+
68
+ // ED25519 SPKI DER: 30 2a 30 05 06 03 2b 65 70 03 21 00 <32 bytes key>
69
+ // The raw public key bytes start at offset 12
70
+ const rawKey = der.subarray(12);
71
+
72
+ // Build SSH wire format: string "ssh-ed25519" + string <raw key>
73
+ const keyType = Buffer.from('ssh-ed25519');
74
+ const typeLen = Buffer.alloc(4);
75
+ typeLen.writeUInt32BE(keyType.length);
76
+ const keyLen = Buffer.alloc(4);
77
+ keyLen.writeUInt32BE(rawKey.length);
78
+
79
+ const sshBlob = Buffer.concat([typeLen, keyType, keyLen, rawKey]);
80
+ return `ssh-ed25519 ${sshBlob.toString('base64')}`;
81
+ };
82
+
83
+ /** Encrypt a private key using AES-256-GCM. Returns a JSON string. */
84
+ encrypt = (plaintext: string): string => {
85
+ const key = this.getEncryptionKey();
86
+ const iv = randomBytes(12);
87
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
88
+
89
+ let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
90
+ ciphertext += cipher.final('base64');
91
+ const authTag = cipher.getAuthTag();
92
+
93
+ const encrypted: EncryptedKey = {
94
+ iv: iv.toString('base64'),
95
+ authTag: authTag.toString('base64'),
96
+ ciphertext,
97
+ };
98
+
99
+ return JSON.stringify(encrypted);
100
+ };
101
+
102
+ /** Decrypt a private key from its encrypted JSON string. */
103
+ decrypt = (encryptedJson: string): string => {
104
+ const key = this.getEncryptionKey();
105
+ const { iv, authTag, ciphertext }: EncryptedKey = JSON.parse(encryptedJson);
106
+
107
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'));
108
+ decipher.setAuthTag(Buffer.from(authTag, 'base64'));
109
+
110
+ let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
111
+ plaintext += decipher.final('utf8');
112
+
113
+ return plaintext;
114
+ };
115
+
116
+ /** Write an SSH private key to a temporary file with restricted permissions. Returns the file path. */
117
+ writeTempKeyFile = async (privateKeyPem: string): Promise<string> => {
118
+ const filename = `assistkick_ssh_${randomBytes(8).toString('hex')}`;
119
+ const filepath = join(tmpdir(), filename);
120
+
121
+ await writeFile(filepath, privateKeyPem, { mode: 0o600 });
122
+ await chmod(filepath, 0o600);
123
+ this.log('SSH', `Temporary key file created: ${filepath}`);
124
+ return filepath;
125
+ };
126
+
127
+ /** Remove a temporary key file. */
128
+ removeTempKeyFile = async (filepath: string): Promise<void> => {
129
+ try {
130
+ await unlink(filepath);
131
+ this.log('SSH', `Temporary key file removed: ${filepath}`);
132
+ } catch {
133
+ this.log('SSH', `Failed to remove temp key file (non-fatal): ${filepath}`);
134
+ }
135
+ };
136
+
137
+ /** Build the GIT_SSH_COMMAND value for using a specific key file. */
138
+ buildGitSshCommand = (keyFilePath: string): string => {
139
+ return `ssh -i ${keyFilePath} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
140
+ };
141
+
142
+ /** Check whether ENCRYPTION_KEY is configured. */
143
+ isConfigured = (): boolean => {
144
+ const key = process.env.ENCRYPTION_KEY;
145
+ if (!key) return false;
146
+ return Buffer.from(key, 'hex').length === 32;
147
+ };
148
+ }
@@ -2,6 +2,9 @@
2
2
  * WebSocket handler for terminal connections.
3
3
  * Authenticates via cookie, enforces admin-only access,
4
4
  * and bridges WebSocket ↔ PTY session by sessionId.
5
+ *
6
+ * If the session is suspended (no PTY running), it auto-resumes
7
+ * the claude session via --resume before bridging.
5
8
  */
6
9
 
7
10
  import type { IncomingMessage } from 'node:http';
@@ -88,6 +91,8 @@ export class TerminalWsHandler {
88
91
  // Parse sessionId from query params
89
92
  const url = new URL(req.url || '', 'http://localhost');
90
93
  const sessionId = url.searchParams.get('sessionId');
94
+ const cols = parseInt(url.searchParams.get('cols') || '80', 10);
95
+ const rows = parseInt(url.searchParams.get('rows') || '24', 10);
91
96
 
92
97
  if (!sessionId) {
93
98
  this.log('TERMINAL', 'Connection rejected — no sessionId provided');
@@ -95,14 +100,15 @@ export class TerminalWsHandler {
95
100
  return;
96
101
  }
97
102
 
98
- const session = this.ptyManager.getSession(sessionId);
99
- if (!session) {
103
+ // Ensure the PTY is running (auto-resumes suspended sessions)
104
+ const running = await this.ptyManager.ensureRunning(sessionId, cols, rows);
105
+ if (!running) {
100
106
  this.log('TERMINAL', `Connection rejected — session ${sessionId} not found`);
101
107
  ws.close(4004, 'Session not found');
102
108
  return;
103
109
  }
104
110
 
105
- this.log('TERMINAL', `Admin ${payload.email} connected to session ${session.name}`);
111
+ this.log('TERMINAL', `Admin ${payload.email} connected to session ${sessionId}`);
106
112
 
107
113
  // Send buffered output from previous connection
108
114
  const buffered = this.ptyManager.getBufferedOutput(sessionId);
@@ -134,9 +140,9 @@ export class TerminalWsHandler {
134
140
  });
135
141
 
136
142
  ws.on('close', () => {
137
- this.log('TERMINAL', `Admin ${payload.email} disconnected from session ${session.name}`);
143
+ this.log('TERMINAL', `Admin ${payload.email} disconnected from session ${sessionId}`);
138
144
  this.ptyManager.removeListener(sessionId, outputListener);
139
- // PTY session persists (dec_030) — not destroyed on disconnect
145
+ // PTY session persists — not destroyed on disconnect
140
146
  });
141
147
 
142
148
  ws.on('error', (err) => {
@@ -0,0 +1,64 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { join } from 'node:path';
4
+ import { TtsService } from './tts_service.js';
5
+
6
+ describe('TtsService', () => {
7
+ const makeTtsService = () => {
8
+ const logs: string[] = [];
9
+ return new TtsService({
10
+ workspacesDir: '/data/workspaces',
11
+ videoPackageDir: '/app/packages/video',
12
+ log: (tag: string, ...args: any[]) => { logs.push(`[${tag}] ${args.join(' ')}`); },
13
+ });
14
+ };
15
+
16
+ it('resolves media base path for a project', () => {
17
+ const svc = makeTtsService();
18
+ assert.equal(
19
+ svc.getMediaBasePath('proj_abc'),
20
+ join('/data/workspaces', 'proj_abc', 'media'),
21
+ );
22
+ });
23
+
24
+ it('resolves audio path for a project and feature', () => {
25
+ const svc = makeTtsService();
26
+ assert.equal(
27
+ svc.getAudioPath('proj_abc', 'feat_123'),
28
+ join('/data/workspaces', 'proj_abc', 'media', 'feat_123', 'audio'),
29
+ );
30
+ });
31
+
32
+ it('rejects when script file does not exist', async () => {
33
+ const svc = makeTtsService();
34
+ await assert.rejects(
35
+ () => svc.generate({
36
+ scriptPath: '/nonexistent/script.md',
37
+ projectId: 'proj_test',
38
+ featureId: 'feat_test',
39
+ }),
40
+ { message: /Script file not found/ },
41
+ );
42
+ });
43
+
44
+ it('rejects when ELEVENLABS_API_KEY is not set', async () => {
45
+ const originalKey = process.env.ELEVENLABS_API_KEY;
46
+ delete process.env.ELEVENLABS_API_KEY;
47
+
48
+ const svc = makeTtsService();
49
+ try {
50
+ await assert.rejects(
51
+ () => svc.generate({
52
+ scriptPath: import.meta.dirname + '/tts_service.ts', // any existing file
53
+ projectId: 'proj_test',
54
+ featureId: 'feat_test',
55
+ }),
56
+ { message: /ELEVENLABS_API_KEY/ },
57
+ );
58
+ } finally {
59
+ if (originalKey !== undefined) {
60
+ process.env.ELEVENLABS_API_KEY = originalKey;
61
+ }
62
+ }
63
+ });
64
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * TtsService — runs the process_script.ts CLI to generate TTS audio
3
+ * from video script narration directives.
4
+ *
5
+ * Spawns the script as a subprocess with --json output and parses the result.
6
+ * Audio files are written to /data/workspaces/<projectId>/media/<featureId>/audio/.
7
+ */
8
+
9
+ import { execFile } from 'node:child_process';
10
+ import { existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ interface TtsServiceDeps {
14
+ workspacesDir: string;
15
+ videoPackageDir: string;
16
+ log: (tag: string, ...args: any[]) => void;
17
+ }
18
+
19
+ export interface TtsGenerateOptions {
20
+ scriptPath: string;
21
+ projectId: string;
22
+ featureId: string;
23
+ force?: boolean;
24
+ voiceId?: string;
25
+ }
26
+
27
+ export interface TtsResult {
28
+ processed: number;
29
+ generated: number;
30
+ skipped: number;
31
+ errors: string[];
32
+ durations: Record<string, number>;
33
+ audioBasePath: string;
34
+ }
35
+
36
+ export class TtsService {
37
+ private readonly workspacesDir: string;
38
+ private readonly videoPackageDir: string;
39
+ private readonly log: TtsServiceDeps['log'];
40
+
41
+ constructor({ workspacesDir, videoPackageDir, log }: TtsServiceDeps) {
42
+ this.workspacesDir = workspacesDir;
43
+ this.videoPackageDir = videoPackageDir;
44
+ this.log = log;
45
+ }
46
+
47
+ /** Resolve the media base directory for a project. */
48
+ getMediaBasePath = (projectId: string): string => {
49
+ return join(this.workspacesDir, projectId, 'media');
50
+ };
51
+
52
+ /** Resolve the audio directory for a specific feature within a project. */
53
+ getAudioPath = (projectId: string, featureId: string): string => {
54
+ return join(this.getMediaBasePath(projectId), featureId, 'audio');
55
+ };
56
+
57
+ /**
58
+ * Generate TTS audio by running process_script.ts against a script file.
59
+ * Returns a structured summary of what was processed.
60
+ */
61
+ generate = async (opts: TtsGenerateOptions): Promise<TtsResult> => {
62
+ const { scriptPath, projectId, featureId, force, voiceId } = opts;
63
+
64
+ if (!existsSync(scriptPath)) {
65
+ throw new Error(`Script file not found: ${scriptPath}`);
66
+ }
67
+
68
+ const apiKey = process.env.ELEVENLABS_API_KEY;
69
+ if (!apiKey) {
70
+ throw new Error('ELEVENLABS_API_KEY environment variable is not configured.');
71
+ }
72
+
73
+ const scriptFile = join(this.videoPackageDir, 'scripts', 'process_script.ts');
74
+ const mediaBase = join(this.workspacesDir, projectId, 'media');
75
+
76
+ const args = [
77
+ scriptFile,
78
+ scriptPath,
79
+ '--project-id', projectId,
80
+ '--feature-id', featureId,
81
+ '--media-base', mediaBase,
82
+ '--json',
83
+ ];
84
+
85
+ if (force) args.push('--force');
86
+ if (voiceId) args.push('--voice-id', voiceId);
87
+
88
+ this.log('TTS', `Generating TTS for project=${projectId} feature=${featureId}`);
89
+
90
+ const result = await this.spawnTsx(args);
91
+ return result;
92
+ };
93
+
94
+ private spawnTsx = (args: string[]): Promise<TtsResult> => {
95
+ return new Promise((resolve, reject) => {
96
+ execFile('npx', ['tsx', ...args], {
97
+ env: { ...process.env },
98
+ cwd: this.videoPackageDir,
99
+ timeout: 300_000, // 5 minutes max
100
+ maxBuffer: 10 * 1024 * 1024,
101
+ }, (error, stdout, stderr) => {
102
+ if (stderr) {
103
+ this.log('TTS', `stderr: ${stderr}`);
104
+ }
105
+
106
+ if (error) {
107
+ // Try to parse JSON error from stdout
108
+ try {
109
+ const parsed = JSON.parse(stdout);
110
+ if (parsed.error) {
111
+ reject(new Error(parsed.error));
112
+ return;
113
+ }
114
+ } catch {
115
+ // Not JSON, use the error message
116
+ }
117
+ reject(new Error(`TTS process failed: ${error.message}`));
118
+ return;
119
+ }
120
+
121
+ try {
122
+ const parsed = JSON.parse(stdout.trim());
123
+ if (parsed.error) {
124
+ reject(new Error(parsed.error));
125
+ return;
126
+ }
127
+ resolve(parsed as TtsResult);
128
+ } catch {
129
+ reject(new Error(`Failed to parse TTS output: ${stdout}`));
130
+ }
131
+ });
132
+ });
133
+ };
134
+ }