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