@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.
- package/package.json +2 -2
- 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/index.html +3 -0
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +800 -11
- package/templates/assistkick-product-system/packages/frontend/package.json +11 -1
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +24 -7
- 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 +383 -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 +193 -64
- 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 +226 -291
- 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 +40 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +55 -115
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +155 -77
- 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 +270 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +37 -0
- 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 +207 -0
- 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/useGraph.ts +6 -21
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +15 -80
- 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 +19 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +54 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +93 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +9 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +6 -0
- 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/useGitModalStore.ts +14 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphStore.ts +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphUIStore.ts +25 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +90 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useQaSheetStore.ts +27 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +76 -0
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +336 -3632
- 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 +7 -1
- 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 +16 -5
- 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-bootstrap/SKILL.md +3 -3
- package/templates/skills/assistkick-code-reviewer/SKILL.md +2 -2
- package/templates/skills/assistkick-debugger/SKILL.md +2 -2
- package/templates/skills/assistkick-developer/SKILL.md +6 -3
- package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
- package/templates/skills/assistkick-interview/SKILL.md +2 -2
- 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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BundleService — manages the Remotion bundle for the video package.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Build the Remotion bundle from the video package (via @remotion/bundler)
|
|
6
|
+
* - Track bundle status (ready, building, last built timestamp)
|
|
7
|
+
* - Scan the compositions directory for available compositions and metadata
|
|
8
|
+
* - Serve bundle path for static file serving
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { execFile } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
interface BundleServiceDeps {
|
|
16
|
+
videoPackageDir: string;
|
|
17
|
+
bundleOutputDir: string;
|
|
18
|
+
log: (tag: string, ...args: any[]) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CompositionMeta {
|
|
22
|
+
id: string;
|
|
23
|
+
durationInFrames: number;
|
|
24
|
+
fps: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
folder?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BundleStatus {
|
|
31
|
+
ready: boolean;
|
|
32
|
+
building: boolean;
|
|
33
|
+
bundlePath: string | null;
|
|
34
|
+
lastBuiltAt: string | null;
|
|
35
|
+
error: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class BundleService {
|
|
39
|
+
private readonly videoPackageDir: string;
|
|
40
|
+
private readonly bundleOutputDir: string;
|
|
41
|
+
private readonly log: BundleServiceDeps['log'];
|
|
42
|
+
private building = false;
|
|
43
|
+
private lastBuiltAt: string | null = null;
|
|
44
|
+
private lastError: string | null = null;
|
|
45
|
+
|
|
46
|
+
constructor({ videoPackageDir, bundleOutputDir, log }: BundleServiceDeps) {
|
|
47
|
+
this.videoPackageDir = videoPackageDir;
|
|
48
|
+
this.bundleOutputDir = bundleOutputDir;
|
|
49
|
+
this.log = log;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Returns the directory path where the built bundle is stored. */
|
|
53
|
+
getBundleDir = (): string => {
|
|
54
|
+
return this.bundleOutputDir;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Check if a bundle has been built and exists on disk. */
|
|
58
|
+
isBundleReady = (): boolean => {
|
|
59
|
+
return existsSync(join(this.bundleOutputDir, 'index.html'));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Returns current bundle status. */
|
|
63
|
+
getStatus = (): BundleStatus => {
|
|
64
|
+
return {
|
|
65
|
+
ready: this.isBundleReady(),
|
|
66
|
+
building: this.building,
|
|
67
|
+
bundlePath: this.isBundleReady() ? this.bundleOutputDir : null,
|
|
68
|
+
lastBuiltAt: this.lastBuiltAt,
|
|
69
|
+
error: this.lastError,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the Remotion bundle from the video package.
|
|
75
|
+
* Uses the Remotion CLI to create a webpack bundle.
|
|
76
|
+
*/
|
|
77
|
+
buildBundle = async (): Promise<BundleStatus> => {
|
|
78
|
+
if (this.building) {
|
|
79
|
+
this.log('BUNDLE', 'Bundle build already in progress');
|
|
80
|
+
return this.getStatus();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.building = true;
|
|
84
|
+
this.lastError = null;
|
|
85
|
+
this.log('BUNDLE', `Building Remotion bundle from ${this.videoPackageDir}`);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await this.spawnBundler();
|
|
89
|
+
this.lastBuiltAt = new Date().toISOString();
|
|
90
|
+
this.log('BUNDLE', `Bundle built successfully at ${this.lastBuiltAt}`);
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
this.lastError = err.message;
|
|
93
|
+
this.log('BUNDLE', `Bundle build failed: ${err.message}`);
|
|
94
|
+
} finally {
|
|
95
|
+
this.building = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return this.getStatus();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Scan the video package's compositions/ directory and extract metadata
|
|
103
|
+
* from each composition's index.tsx file.
|
|
104
|
+
*/
|
|
105
|
+
listCompositions = (): CompositionMeta[] => {
|
|
106
|
+
const compositionsDir = join(this.videoPackageDir, 'compositions');
|
|
107
|
+
if (!existsSync(compositionsDir)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const entries = readdirSync(compositionsDir);
|
|
112
|
+
const compositions: CompositionMeta[] = [];
|
|
113
|
+
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const entryPath = join(compositionsDir, entry);
|
|
116
|
+
if (!statSync(entryPath).isDirectory()) continue;
|
|
117
|
+
|
|
118
|
+
const indexPath = join(entryPath, 'index.tsx');
|
|
119
|
+
if (!existsSync(indexPath)) continue;
|
|
120
|
+
|
|
121
|
+
const meta = this.parseCompositionMeta(indexPath);
|
|
122
|
+
if (meta) {
|
|
123
|
+
compositions.push(meta);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return compositions;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse a composition's index.tsx file to extract exported metadata.
|
|
132
|
+
* Uses regex to find exported const values for id, fps, width, height, etc.
|
|
133
|
+
*/
|
|
134
|
+
private parseCompositionMeta = (filePath: string): CompositionMeta | null => {
|
|
135
|
+
try {
|
|
136
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
137
|
+
|
|
138
|
+
const idMatch = content.match(/export\s+const\s+id\s*=\s*['"]([^'"]+)['"]/);
|
|
139
|
+
if (!idMatch) return null;
|
|
140
|
+
|
|
141
|
+
const durationMatch = content.match(/export\s+const\s+durationInFrames\s*=\s*(\d+)/);
|
|
142
|
+
const fpsMatch = content.match(/export\s+const\s+fps\s*=\s*(\d+)/);
|
|
143
|
+
const widthMatch = content.match(/export\s+const\s+width\s*=\s*(\d+)/);
|
|
144
|
+
const heightMatch = content.match(/export\s+const\s+height\s*=\s*(\d+)/);
|
|
145
|
+
const folderMatch = content.match(/export\s+const\s+folder\s*=\s*['"]([^'"]+)['"]/);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
id: idMatch[1],
|
|
149
|
+
durationInFrames: durationMatch ? parseInt(durationMatch[1], 10) : 300,
|
|
150
|
+
fps: fpsMatch ? parseInt(fpsMatch[1], 10) : 30,
|
|
151
|
+
width: widthMatch ? parseInt(widthMatch[1], 10) : 1920,
|
|
152
|
+
height: heightMatch ? parseInt(heightMatch[1], 10) : 1080,
|
|
153
|
+
folder: folderMatch ? folderMatch[1] : undefined,
|
|
154
|
+
};
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
private spawnBundler = (): Promise<void> => {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const entryPoint = join(this.videoPackageDir, 'index.ts');
|
|
163
|
+
execFile('npx', [
|
|
164
|
+
'remotion', 'bundle',
|
|
165
|
+
entryPoint,
|
|
166
|
+
'--out-dir', this.bundleOutputDir,
|
|
167
|
+
'--log', 'warn',
|
|
168
|
+
], {
|
|
169
|
+
cwd: this.videoPackageDir,
|
|
170
|
+
timeout: 300_000,
|
|
171
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
172
|
+
env: { ...process.env },
|
|
173
|
+
}, (error, _stdout, stderr) => {
|
|
174
|
+
if (error) {
|
|
175
|
+
reject(new Error(`Remotion bundle failed: ${error.message}${stderr ? `\n${stderr}` : ''}`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
resolve();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Service initialization — creates
|
|
2
|
+
* Service initialization — creates workflow engine, claude service, and related instances.
|
|
3
3
|
* Deferred until VERBOSE flag is set at startup.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { join, dirname } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
9
8
|
import { createClaudeService } from '@assistkick/shared/lib/claude-service.js';
|
|
10
|
-
import { Pipeline } from '@assistkick/shared/lib/pipeline.js';
|
|
11
|
-
import { PipelineStateStore } from '@assistkick/shared/lib/pipeline-state-store.js';
|
|
12
|
-
import { PromptBuilder } from '@assistkick/shared/lib/prompt_builder.js';
|
|
13
9
|
import { GitWorkflow } from '@assistkick/shared/lib/git_workflow.js';
|
|
14
10
|
import { getDb } from '@assistkick/shared/lib/db.js';
|
|
15
11
|
import { loadKanban, getKanbanEntry, saveKanbanEntry } from '@assistkick/shared/lib/kanban.js';
|
|
16
|
-
import { readGraph
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
12
|
+
import { readGraph } from '@assistkick/shared/lib/graph.js';
|
|
13
|
+
import { WorkflowEngine } from '@assistkick/shared/lib/workflow_engine.js';
|
|
14
|
+
import { WorkflowOrchestrator } from '@assistkick/shared/lib/workflow_orchestrator.js';
|
|
15
|
+
import { WorkflowService } from './workflow_service.js';
|
|
16
|
+
import { WorkflowGroupService } from './workflow_group_service.js';
|
|
19
17
|
import { GitHubAppService } from './github_app_service.js';
|
|
20
18
|
import { ProjectWorkspaceService } from './project_workspace_service.js';
|
|
21
|
-
import {
|
|
19
|
+
import { SshKeyService } from './ssh_key_service.js';
|
|
22
20
|
|
|
23
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
22
|
// Navigate from packages/backend/src/services/ up to assistkick-product-system/
|
|
@@ -32,6 +30,7 @@ const TOOLS_DIR = IS_DEV ? join(SHARED_DIR, 'tools') : join(__dirname, '..', '..
|
|
|
32
30
|
const DATA_DIR = SHARED_DIR;
|
|
33
31
|
const PROJECT_ROOT = join(SKILL_ROOT, '..');
|
|
34
32
|
const SKILLS_DIR = join(PROJECT_ROOT, '.claude', 'skills');
|
|
33
|
+
const WORKSPACES_DIR = process.env.WORKSPACES_DIR || join(PROJECT_ROOT, 'workspaces');
|
|
35
34
|
const WORKTREES_DIR = join(PROJECT_ROOT, '.worktrees');
|
|
36
35
|
const DEVELOPER_SKILL_PATH = join(SKILLS_DIR, 'product-developer', 'SKILL.md');
|
|
37
36
|
const REVIEWER_SKILL_PATH = join(SKILLS_DIR, 'product-code-reviewer', 'SKILL.md');
|
|
@@ -43,14 +42,17 @@ export const log = (tag: string, ...args: any[]) => {
|
|
|
43
42
|
};
|
|
44
43
|
|
|
45
44
|
export let claudeService: any;
|
|
46
|
-
export let
|
|
47
|
-
export let
|
|
48
|
-
export let
|
|
45
|
+
export let workflowEngine: WorkflowEngine;
|
|
46
|
+
export let orchestrator: WorkflowOrchestrator;
|
|
47
|
+
export let workflowService: WorkflowService;
|
|
48
|
+
export let workflowGroupService: WorkflowGroupService;
|
|
49
49
|
export let githubAppService: GitHubAppService;
|
|
50
|
+
export let sshKeyService: SshKeyService;
|
|
50
51
|
export let workspaceService: ProjectWorkspaceService;
|
|
51
52
|
|
|
52
53
|
export const paths = {
|
|
53
54
|
projectRoot: PROJECT_ROOT,
|
|
55
|
+
workspacesDir: WORKSPACES_DIR,
|
|
54
56
|
worktreesDir: WORKTREES_DIR,
|
|
55
57
|
skillsDir: SKILLS_DIR,
|
|
56
58
|
toolsDir: TOOLS_DIR,
|
|
@@ -64,84 +66,32 @@ export const paths = {
|
|
|
64
66
|
|
|
65
67
|
export const initServices = (verbose: boolean) => {
|
|
66
68
|
claudeService = createClaudeService({ verbose, log });
|
|
67
|
-
pipelineStateStore = new PipelineStateStore({ getDb });
|
|
68
|
-
const promptBuilder = new PromptBuilder({ paths, log });
|
|
69
69
|
const gitWorkflow = new GitWorkflow({ claudeService, projectRoot: PROJECT_ROOT, worktreesDir: WORKTREES_DIR, log });
|
|
70
|
-
const workSummaryParser = new WorkSummaryParser();
|
|
71
70
|
|
|
72
71
|
// New services for project git repo integration
|
|
73
72
|
githubAppService = new GitHubAppService({ log });
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
sshKeyService = new SshKeyService({ log });
|
|
74
|
+
workspaceService = new ProjectWorkspaceService({ claudeService, workspacesDir: WORKSPACES_DIR, log, sshKeyService });
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* that pushes to the remote. Returns null if no workspace is configured.
|
|
81
|
-
*/
|
|
82
|
-
const resolveProjectWorkspace = async (projectId: string) => {
|
|
83
|
-
const project = await projectService.getById(projectId);
|
|
84
|
-
if (!project) return null;
|
|
76
|
+
// Workflow service for CRUD + default resolution
|
|
77
|
+
workflowService = new WorkflowService({ getDb, log });
|
|
78
|
+
workflowGroupService = new WorkflowGroupService({ getDb, log });
|
|
85
79
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const wsPath = workspaceService.getWorkspacePath(projectId);
|
|
93
|
-
const wsWorktreesDir = workspaceService.getWorktreesDir(projectId);
|
|
94
|
-
|
|
95
|
-
const projectGitWorkflow = new GitWorkflow({
|
|
96
|
-
claudeService,
|
|
97
|
-
projectRoot: wsPath,
|
|
98
|
-
worktreesDir: wsWorktreesDir,
|
|
99
|
-
log,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Build afterMerge hook for pushing to remote
|
|
103
|
-
let afterMerge = null;
|
|
104
|
-
if (project.repoUrl && project.githubInstallationId && project.githubRepoFullName) {
|
|
105
|
-
afterMerge = async () => {
|
|
106
|
-
// Get a fresh token for push
|
|
107
|
-
const token = await githubAppService.getInstallationToken(project.githubInstallationId!);
|
|
108
|
-
const authUrl = `https://x-access-token:${token}@github.com/${project.githubRepoFullName}.git`;
|
|
109
|
-
await projectGitWorkflow.setRemoteUrl(authUrl);
|
|
110
|
-
try {
|
|
111
|
-
const branch = project.baseBranch || 'main';
|
|
112
|
-
await projectGitWorkflow.pushToRemote(branch);
|
|
113
|
-
} finally {
|
|
114
|
-
// Reset to non-authenticated URL
|
|
115
|
-
await projectGitWorkflow.setRemoteUrl(`https://github.com/${project.githubRepoFullName}.git`).catch(() => {});
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return { gitWorkflow: projectGitWorkflow, afterMerge };
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
pipeline = new Pipeline({
|
|
124
|
-
promptBuilder,
|
|
125
|
-
gitWorkflow,
|
|
126
|
-
claudeService,
|
|
80
|
+
// WorkflowEngine replaces Pipeline
|
|
81
|
+
workflowEngine = new WorkflowEngine({
|
|
82
|
+
db: getDb(),
|
|
127
83
|
kanban: { getKanbanEntry, saveKanbanEntry },
|
|
128
|
-
|
|
84
|
+
claudeService,
|
|
85
|
+
gitWorkflow,
|
|
129
86
|
log,
|
|
130
|
-
stateStore: pipelineStateStore,
|
|
131
|
-
workSummaryParser,
|
|
132
|
-
getNode,
|
|
133
|
-
resolveProjectWorkspace,
|
|
134
87
|
});
|
|
135
88
|
|
|
136
|
-
|
|
137
|
-
|
|
89
|
+
// WorkflowOrchestrator replaces PipelineOrchestrator
|
|
90
|
+
orchestrator = new WorkflowOrchestrator({
|
|
91
|
+
workflowEngine,
|
|
138
92
|
loadKanban,
|
|
139
93
|
readGraph,
|
|
94
|
+
resolveDefaultWorkflow: (projectId?: string) => workflowService.resolveDefault(projectId),
|
|
140
95
|
log,
|
|
141
96
|
});
|
|
142
|
-
|
|
143
|
-
// Mark any active pipeline states as interrupted (server restart recovery)
|
|
144
|
-
pipelineStateStore.markInterrupted().catch((err: any) => {
|
|
145
|
-
log('STARTUP', `Failed to mark interrupted pipelines: ${err.message}`);
|
|
146
|
-
});
|
|
147
97
|
};
|
package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts
CHANGED
|
@@ -118,12 +118,28 @@ describe('ProjectService', () => {
|
|
|
118
118
|
|
|
119
119
|
assert.ok(result.id.startsWith('proj_'));
|
|
120
120
|
assert.equal(result.name, 'New Project');
|
|
121
|
+
assert.equal(result.type, 'software');
|
|
121
122
|
assert.equal(result.isDefault, 0);
|
|
122
123
|
assert.equal(result.archivedAt, null);
|
|
123
124
|
assert.ok(result.createdAt);
|
|
124
125
|
assert.ok(result.updatedAt);
|
|
125
126
|
assert.equal(db.insert.mock.calls.length, 1);
|
|
126
127
|
});
|
|
128
|
+
|
|
129
|
+
it('creates a project with video type', async () => {
|
|
130
|
+
const result = await service.create('Video Project', 'video');
|
|
131
|
+
|
|
132
|
+
assert.ok(result.id.startsWith('proj_'));
|
|
133
|
+
assert.equal(result.name, 'Video Project');
|
|
134
|
+
assert.equal(result.type, 'video');
|
|
135
|
+
assert.equal(result.isDefault, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('defaults to software type when no type specified', async () => {
|
|
139
|
+
const result = await service.create('Default Type');
|
|
140
|
+
|
|
141
|
+
assert.equal(result.type, 'software');
|
|
142
|
+
});
|
|
127
143
|
});
|
|
128
144
|
|
|
129
145
|
describe('rename', () => {
|
package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts
CHANGED
|
@@ -15,12 +15,16 @@ interface ProjectServiceDeps {
|
|
|
15
15
|
export interface ProjectRecord {
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
18
|
+
type: string;
|
|
18
19
|
isDefault: number;
|
|
19
20
|
archivedAt: string | null;
|
|
20
21
|
repoUrl: string | null;
|
|
21
22
|
githubInstallationId: string | null;
|
|
22
23
|
githubRepoFullName: string | null;
|
|
23
24
|
baseBranch: string | null;
|
|
25
|
+
gitAuthMethod: string | null;
|
|
26
|
+
sshPrivateKeyEncrypted: string | null;
|
|
27
|
+
sshPublicKey: string | null;
|
|
24
28
|
createdAt: string;
|
|
25
29
|
updatedAt: string;
|
|
26
30
|
}
|
|
@@ -58,7 +62,7 @@ export class ProjectService {
|
|
|
58
62
|
return row || null;
|
|
59
63
|
};
|
|
60
64
|
|
|
61
|
-
create = async (name: string): Promise<ProjectRecord> => {
|
|
65
|
+
create = async (name: string, type: string = 'software'): Promise<ProjectRecord> => {
|
|
62
66
|
const db = this.getDb();
|
|
63
67
|
const now = new Date().toISOString();
|
|
64
68
|
const id = this.generateId();
|
|
@@ -66,18 +70,22 @@ export class ProjectService {
|
|
|
66
70
|
const record: ProjectRecord = {
|
|
67
71
|
id,
|
|
68
72
|
name,
|
|
73
|
+
type,
|
|
69
74
|
isDefault: 0,
|
|
70
75
|
archivedAt: null,
|
|
71
76
|
repoUrl: null,
|
|
72
77
|
githubInstallationId: null,
|
|
73
78
|
githubRepoFullName: null,
|
|
74
79
|
baseBranch: null,
|
|
80
|
+
gitAuthMethod: null,
|
|
81
|
+
sshPrivateKeyEncrypted: null,
|
|
82
|
+
sshPublicKey: null,
|
|
75
83
|
createdAt: now,
|
|
76
84
|
updatedAt: now,
|
|
77
85
|
};
|
|
78
86
|
|
|
79
87
|
await db.insert(projects).values(record);
|
|
80
|
-
this.log('PROJECTS', `Created project: ${name} (${id})`);
|
|
88
|
+
this.log('PROJECTS', `Created project: ${name} (${id}) type=${type}`);
|
|
81
89
|
return record;
|
|
82
90
|
};
|
|
83
91
|
|
|
@@ -195,6 +203,9 @@ export class ProjectService {
|
|
|
195
203
|
githubInstallationId: null,
|
|
196
204
|
githubRepoFullName: null,
|
|
197
205
|
baseBranch: null,
|
|
206
|
+
gitAuthMethod: null,
|
|
207
|
+
sshPrivateKeyEncrypted: null,
|
|
208
|
+
sshPublicKey: null,
|
|
198
209
|
updatedAt: now,
|
|
199
210
|
})
|
|
200
211
|
.where(eq(projects.id, id));
|
|
@@ -206,6 +217,66 @@ export class ProjectService {
|
|
|
206
217
|
githubInstallationId: null,
|
|
207
218
|
githubRepoFullName: null,
|
|
208
219
|
baseBranch: null,
|
|
220
|
+
gitAuthMethod: null,
|
|
221
|
+
sshPrivateKeyEncrypted: null,
|
|
222
|
+
sshPublicKey: null,
|
|
223
|
+
updatedAt: now,
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Store SSH key data and set auth method to 'ssh_key'. */
|
|
228
|
+
setSshKeys = async (id: string, opts: {
|
|
229
|
+
sshPrivateKeyEncrypted: string;
|
|
230
|
+
sshPublicKey: string;
|
|
231
|
+
}): Promise<ProjectRecord> => {
|
|
232
|
+
const db = this.getDb();
|
|
233
|
+
const existing = await this.getById(id);
|
|
234
|
+
if (!existing) throw new Error('Project not found');
|
|
235
|
+
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
await db
|
|
238
|
+
.update(projects)
|
|
239
|
+
.set({
|
|
240
|
+
gitAuthMethod: 'ssh_key',
|
|
241
|
+
sshPrivateKeyEncrypted: opts.sshPrivateKeyEncrypted,
|
|
242
|
+
sshPublicKey: opts.sshPublicKey,
|
|
243
|
+
updatedAt: now,
|
|
244
|
+
})
|
|
245
|
+
.where(eq(projects.id, id));
|
|
246
|
+
|
|
247
|
+
this.log('PROJECTS', `Set SSH keys for project ${id}`);
|
|
248
|
+
return {
|
|
249
|
+
...existing,
|
|
250
|
+
gitAuthMethod: 'ssh_key',
|
|
251
|
+
sshPrivateKeyEncrypted: opts.sshPrivateKeyEncrypted,
|
|
252
|
+
sshPublicKey: opts.sshPublicKey,
|
|
253
|
+
updatedAt: now,
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** Set auth method to 'github_app' and clear SSH keys. */
|
|
258
|
+
setGitHubAppAuth = async (id: string): Promise<ProjectRecord> => {
|
|
259
|
+
const db = this.getDb();
|
|
260
|
+
const existing = await this.getById(id);
|
|
261
|
+
if (!existing) throw new Error('Project not found');
|
|
262
|
+
|
|
263
|
+
const now = new Date().toISOString();
|
|
264
|
+
await db
|
|
265
|
+
.update(projects)
|
|
266
|
+
.set({
|
|
267
|
+
gitAuthMethod: 'github_app',
|
|
268
|
+
sshPrivateKeyEncrypted: null,
|
|
269
|
+
sshPublicKey: null,
|
|
270
|
+
updatedAt: now,
|
|
271
|
+
})
|
|
272
|
+
.where(eq(projects.id, id));
|
|
273
|
+
|
|
274
|
+
this.log('PROJECTS', `Set GitHub App auth for project ${id}`);
|
|
275
|
+
return {
|
|
276
|
+
...existing,
|
|
277
|
+
gitAuthMethod: 'github_app',
|
|
278
|
+
sshPrivateKeyEncrypted: null,
|
|
279
|
+
sshPublicKey: null,
|
|
209
280
|
updatedAt: now,
|
|
210
281
|
};
|
|
211
282
|
};
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import { ProjectWorkspaceService } from './project_workspace_service.ts';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const WORKSPACES_DIR = '/tmp/test-workspaces';
|
|
7
7
|
|
|
8
8
|
const createMockClaudeService = () => ({
|
|
9
9
|
spawnCommand: mock.fn(async () => ''),
|
|
@@ -12,7 +12,7 @@ const createMockClaudeService = () => ({
|
|
|
12
12
|
const createService = (claudeService: any) => {
|
|
13
13
|
return new ProjectWorkspaceService({
|
|
14
14
|
claudeService,
|
|
15
|
-
|
|
15
|
+
workspacesDir: WORKSPACES_DIR,
|
|
16
16
|
log: mock.fn(),
|
|
17
17
|
});
|
|
18
18
|
};
|
|
@@ -29,14 +29,14 @@ describe('ProjectWorkspaceService', () => {
|
|
|
29
29
|
describe('getWorkspacePath', () => {
|
|
30
30
|
it('returns correct workspace path for a project', () => {
|
|
31
31
|
const path = service.getWorkspacePath('proj_abc123');
|
|
32
|
-
assert.equal(path, join(
|
|
32
|
+
assert.equal(path, join(WORKSPACES_DIR, 'proj_abc123'));
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
describe('getWorktreesDir', () => {
|
|
37
37
|
it('returns correct worktrees dir inside workspace', () => {
|
|
38
38
|
const path = service.getWorktreesDir('proj_abc123');
|
|
39
|
-
assert.equal(path, join(
|
|
39
|
+
assert.equal(path, join(WORKSPACES_DIR, 'proj_abc123', '.worktrees'));
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ProjectWorkspaceService — manages per-project workspace directories.
|
|
3
|
-
* Each project gets a workspace at <
|
|
3
|
+
* Each project gets a workspace at <workspacesDir>/<projectId>/.
|
|
4
|
+
* workspacesDir is injected via DI — in production it resolves to a persistent
|
|
5
|
+
* volume (/data/workspaces), in development to <projectRoot>/workspaces.
|
|
4
6
|
* If a remote git repo is connected, it is cloned there.
|
|
5
7
|
* If not, git init creates a local repo so worktrees and branching always work.
|
|
6
8
|
*/
|
|
@@ -8,27 +10,31 @@
|
|
|
8
10
|
import { join } from 'node:path';
|
|
9
11
|
import { existsSync } from 'node:fs';
|
|
10
12
|
import { mkdir } from 'node:fs/promises';
|
|
13
|
+
import type { SshKeyService } from './ssh_key_service.js';
|
|
11
14
|
|
|
12
15
|
interface ProjectWorkspaceServiceDeps {
|
|
13
|
-
claudeService: { spawnCommand: (cmd: string, args: string[], cwd: string) => Promise<string> };
|
|
14
|
-
|
|
16
|
+
claudeService: { spawnCommand: (cmd: string, args: string[], cwd: string, env?: Record<string, string>) => Promise<string> };
|
|
17
|
+
workspacesDir: string;
|
|
15
18
|
log: (tag: string, ...args: any[]) => void;
|
|
19
|
+
sshKeyService?: SshKeyService;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export class ProjectWorkspaceService {
|
|
19
23
|
private readonly claudeService: ProjectWorkspaceServiceDeps['claudeService'];
|
|
20
|
-
private readonly
|
|
24
|
+
private readonly workspacesDir: string;
|
|
21
25
|
private readonly log: ProjectWorkspaceServiceDeps['log'];
|
|
26
|
+
private readonly sshKeyService?: SshKeyService;
|
|
22
27
|
|
|
23
|
-
constructor({ claudeService,
|
|
28
|
+
constructor({ claudeService, workspacesDir, log, sshKeyService }: ProjectWorkspaceServiceDeps) {
|
|
24
29
|
this.claudeService = claudeService;
|
|
25
|
-
this.
|
|
30
|
+
this.workspacesDir = workspacesDir;
|
|
26
31
|
this.log = log;
|
|
32
|
+
this.sshKeyService = sshKeyService;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/** Get the workspace directory path for a project. */
|
|
30
36
|
getWorkspacePath = (projectId: string): string => {
|
|
31
|
-
return join(this.
|
|
37
|
+
return join(this.workspacesDir, projectId);
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
/** Get the worktrees directory inside a project workspace. */
|
|
@@ -71,17 +77,16 @@ export class ProjectWorkspaceService {
|
|
|
71
77
|
/** Clone a remote repository into the project workspace. */
|
|
72
78
|
cloneRepo = async (projectId: string, cloneUrl: string): Promise<string> => {
|
|
73
79
|
const wsPath = this.getWorkspacePath(projectId);
|
|
74
|
-
|
|
75
|
-
await this.ensureDir(parentDir);
|
|
80
|
+
await this.ensureDir(this.workspacesDir);
|
|
76
81
|
|
|
77
82
|
// If workspace already exists, remove it first
|
|
78
83
|
if (existsSync(wsPath)) {
|
|
79
84
|
this.log('WORKSPACE', `Removing existing workspace before clone: ${wsPath}`);
|
|
80
|
-
await this.claudeService.spawnCommand('rm', ['-rf', wsPath],
|
|
85
|
+
await this.claudeService.spawnCommand('rm', ['-rf', wsPath], this.workspacesDir);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
this.log('WORKSPACE', `Cloning ${cloneUrl} into ${wsPath}`);
|
|
84
|
-
await this.claudeService.spawnCommand('git', ['clone', cloneUrl, wsPath],
|
|
89
|
+
await this.claudeService.spawnCommand('git', ['clone', cloneUrl, wsPath], this.workspacesDir);
|
|
85
90
|
this.log('WORKSPACE', `Clone completed for project ${projectId}`);
|
|
86
91
|
return wsPath;
|
|
87
92
|
};
|
|
@@ -162,6 +167,77 @@ export class ProjectWorkspaceService {
|
|
|
162
167
|
}
|
|
163
168
|
};
|
|
164
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Run a git command with GIT_SSH_COMMAND set for SSH key auth.
|
|
172
|
+
* Handles writing temp key, setting env, running the command, and cleanup.
|
|
173
|
+
*/
|
|
174
|
+
private withSshKey = async <T>(encryptedPrivateKey: string, fn: () => Promise<T>): Promise<T> => {
|
|
175
|
+
if (!this.sshKeyService) throw new Error('SshKeyService not available');
|
|
176
|
+
|
|
177
|
+
const privateKeyPem = this.sshKeyService.decrypt(encryptedPrivateKey);
|
|
178
|
+
const keyFilePath = await this.sshKeyService.writeTempKeyFile(privateKeyPem);
|
|
179
|
+
const gitSshCommand = this.sshKeyService.buildGitSshCommand(keyFilePath);
|
|
180
|
+
const previousValue = process.env.GIT_SSH_COMMAND;
|
|
181
|
+
|
|
182
|
+
process.env.GIT_SSH_COMMAND = gitSshCommand;
|
|
183
|
+
try {
|
|
184
|
+
return await fn();
|
|
185
|
+
} finally {
|
|
186
|
+
if (previousValue !== undefined) {
|
|
187
|
+
process.env.GIT_SSH_COMMAND = previousValue;
|
|
188
|
+
} else {
|
|
189
|
+
delete process.env.GIT_SSH_COMMAND;
|
|
190
|
+
}
|
|
191
|
+
await this.sshKeyService.removeTempKeyFile(keyFilePath);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Clone a remote repository using SSH key authentication. */
|
|
196
|
+
cloneRepoSsh = async (projectId: string, sshCloneUrl: string, encryptedPrivateKey: string): Promise<string> => {
|
|
197
|
+
const wsPath = this.getWorkspacePath(projectId);
|
|
198
|
+
await this.ensureDir(this.workspacesDir);
|
|
199
|
+
|
|
200
|
+
if (existsSync(wsPath)) {
|
|
201
|
+
this.log('WORKSPACE', `Removing existing workspace before SSH clone: ${wsPath}`);
|
|
202
|
+
await this.claudeService.spawnCommand('rm', ['-rf', wsPath], this.workspacesDir);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.log('WORKSPACE', `SSH cloning into ${wsPath}`);
|
|
206
|
+
await this.withSshKey(encryptedPrivateKey, () =>
|
|
207
|
+
this.claudeService.spawnCommand('git', ['clone', sshCloneUrl, wsPath], this.workspacesDir),
|
|
208
|
+
);
|
|
209
|
+
this.log('WORKSPACE', `SSH clone completed for project ${projectId}`);
|
|
210
|
+
return wsPath;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Pull latest changes using SSH key authentication. */
|
|
214
|
+
pullLatestSsh = async (projectId: string, encryptedPrivateKey: string): Promise<void> => {
|
|
215
|
+
const wsPath = this.getWorkspacePath(projectId);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const branch = await this.getDefaultBranch(projectId);
|
|
219
|
+
this.log('WORKSPACE', `SSH pulling latest on ${branch} for project ${projectId}`);
|
|
220
|
+
await this.withSshKey(encryptedPrivateKey, () =>
|
|
221
|
+
this.claudeService.spawnCommand('git', ['pull', 'origin', branch], wsPath),
|
|
222
|
+
);
|
|
223
|
+
this.log('WORKSPACE', `SSH pull completed for project ${projectId}`);
|
|
224
|
+
} catch (err: any) {
|
|
225
|
+
this.log('WORKSPACE', `SSH pull failed (non-fatal): ${err.message}`);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** Push the default branch to the remote using SSH key authentication. */
|
|
230
|
+
pushToRemoteSsh = async (projectId: string, encryptedPrivateKey: string): Promise<void> => {
|
|
231
|
+
const wsPath = this.getWorkspacePath(projectId);
|
|
232
|
+
const branch = await this.getDefaultBranch(projectId);
|
|
233
|
+
|
|
234
|
+
this.log('WORKSPACE', `SSH pushing ${branch} to remote for project ${projectId}`);
|
|
235
|
+
await this.withSshKey(encryptedPrivateKey, () =>
|
|
236
|
+
this.claudeService.spawnCommand('git', ['push', 'origin', branch], wsPath),
|
|
237
|
+
);
|
|
238
|
+
this.log('WORKSPACE', `SSH push completed for project ${projectId}`);
|
|
239
|
+
};
|
|
240
|
+
|
|
165
241
|
/** Get workspace git status info. */
|
|
166
242
|
getStatus = async (projectId: string): Promise<{
|
|
167
243
|
hasWorkspace: boolean;
|