@assistkick/create 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +61 -6
- 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 +158 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +60 -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 +43 -77
- 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 +245 -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 +458 -18
- 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/IterationCommentModal.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +263 -167
- 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/GenerateTTSNode.tsx +52 -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/RebuildBundleNode.tsx +20 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -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 +341 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +643 -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 +246 -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 +136 -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/0014_nifty_punisher.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/0014_snapshot.json +1545 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +77 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +114 -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 +1999 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1437 -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 +181 -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
|
@@ -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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, mock, beforeEach } from 'node:test';
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { PtySessionManager } from './pty_session_manager.ts';
|
|
4
4
|
|
|
@@ -14,11 +14,50 @@ const createMockPty = () => {
|
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
/** In-memory mock DB that mimics Drizzle's API surface for terminal_sessions. */
|
|
18
|
+
const createMockDb = () => {
|
|
19
|
+
const rows: any[] = [];
|
|
20
|
+
|
|
21
|
+
const where = (pred: any) => ({
|
|
22
|
+
// select().from().where() returns matching rows
|
|
23
|
+
then: (resolve: Function) => resolve(rows),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
_rows: rows,
|
|
28
|
+
select: () => ({
|
|
29
|
+
from: () => ({
|
|
30
|
+
where: (_pred: any) => Promise.resolve(rows.filter(() => true)),
|
|
31
|
+
then: (resolve: Function) => resolve(rows),
|
|
32
|
+
}),
|
|
33
|
+
}),
|
|
34
|
+
insert: () => ({
|
|
35
|
+
values: (val: any) => {
|
|
36
|
+
rows.push(val);
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
update: () => ({
|
|
41
|
+
set: () => ({
|
|
42
|
+
where: () => Promise.resolve(),
|
|
43
|
+
}),
|
|
44
|
+
}),
|
|
45
|
+
delete: () => ({
|
|
46
|
+
where: () => {
|
|
47
|
+
// Remove all rows (simplified for tests)
|
|
48
|
+
rows.length = 0;
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
17
55
|
describe('PtySessionManager', () => {
|
|
18
56
|
let manager: PtySessionManager;
|
|
19
57
|
let spawnMock: ReturnType<typeof mock.fn>;
|
|
20
58
|
let logMock: ReturnType<typeof mock.fn>;
|
|
21
59
|
let mockPty: ReturnType<typeof createMockPty>;
|
|
60
|
+
let mockDb: ReturnType<typeof createMockDb>;
|
|
22
61
|
const PROJECT_ROOT = '/test/project/root';
|
|
23
62
|
const PROJECT_ID = 'proj_test';
|
|
24
63
|
const PROJECT_NAME = 'Test Project';
|
|
@@ -27,125 +66,94 @@ describe('PtySessionManager', () => {
|
|
|
27
66
|
mockPty = createMockPty();
|
|
28
67
|
spawnMock = mock.fn(() => mockPty);
|
|
29
68
|
logMock = mock.fn();
|
|
30
|
-
|
|
69
|
+
mockDb = createMockDb();
|
|
70
|
+
manager = new PtySessionManager({
|
|
71
|
+
spawn: spawnMock as any,
|
|
72
|
+
log: logMock,
|
|
73
|
+
projectRoot: PROJECT_ROOT,
|
|
74
|
+
getDb: () => mockDb,
|
|
75
|
+
});
|
|
31
76
|
});
|
|
32
77
|
|
|
33
78
|
describe('spawnCommand uses projectRoot as cwd', () => {
|
|
34
|
-
it('passes projectRoot as cwd when auto-launching claude', () => {
|
|
35
|
-
|
|
79
|
+
it('passes projectRoot as cwd when auto-launching claude', async () => {
|
|
80
|
+
await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
36
81
|
|
|
37
|
-
// Auto-launch is triggered on session creation
|
|
38
82
|
assert.equal(spawnMock.mock.calls.length, 1);
|
|
39
83
|
const spawnOptions = spawnMock.mock.calls[0].arguments[2] as Record<string, unknown>;
|
|
40
84
|
assert.equal(spawnOptions.cwd, PROJECT_ROOT);
|
|
41
85
|
});
|
|
42
86
|
});
|
|
43
87
|
|
|
44
|
-
describe('validateCommand', () => {
|
|
45
|
-
it('allows "claude" command', () => {
|
|
46
|
-
const result = manager.validateCommand('claude');
|
|
47
|
-
assert.equal(result.valid, true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('allows "claude" with arguments', () => {
|
|
51
|
-
const result = manager.validateCommand('claude --help');
|
|
52
|
-
assert.equal(result.valid, true);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('rejects disallowed commands', () => {
|
|
56
|
-
const result = manager.validateCommand('rm -rf /');
|
|
57
|
-
assert.equal(result.valid, false);
|
|
58
|
-
assert.ok(result.error?.includes('not allowed'));
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('rejects empty command', () => {
|
|
62
|
-
const result = manager.validateCommand('');
|
|
63
|
-
assert.equal(result.valid, false);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
88
|
describe('session lifecycle', () => {
|
|
68
|
-
it('creates a new named session with project binding', () => {
|
|
69
|
-
const session = manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
89
|
+
it('creates a new named session with project binding', async () => {
|
|
90
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
70
91
|
assert.equal(session.projectId, PROJECT_ID);
|
|
71
92
|
assert.equal(session.projectName, PROJECT_NAME);
|
|
72
|
-
assert.equal(session.state, 'running');
|
|
93
|
+
assert.equal(session.state, 'running');
|
|
73
94
|
assert.ok(session.id.startsWith('term_'));
|
|
74
95
|
assert.ok(session.name.startsWith(PROJECT_NAME));
|
|
75
96
|
});
|
|
76
97
|
|
|
77
|
-
it('creates separate sessions for the same project', () => {
|
|
78
|
-
const s1 = manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
79
|
-
const s2 = manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
98
|
+
it('creates separate sessions for the same project', async () => {
|
|
99
|
+
const s1 = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
100
|
+
const s2 = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
80
101
|
assert.notEqual(s1.id, s2.id);
|
|
81
102
|
});
|
|
82
103
|
|
|
83
|
-
it('destroys session and kills pty', () => {
|
|
84
|
-
const session = manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
85
|
-
|
|
86
|
-
manager.destroySession(session.id);
|
|
104
|
+
it('destroys session and kills pty', async () => {
|
|
105
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
106
|
+
await manager.destroySession(session.id);
|
|
87
107
|
assert.equal(mockPty.kill.mock.calls.length, 1);
|
|
88
|
-
assert.equal(manager.getSession(session.id), undefined);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('lists all active sessions', () => {
|
|
92
|
-
const s1 = manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
93
|
-
const s2 = manager.createSession('proj_other', 'Other', 80, 24);
|
|
94
|
-
const list = manager.listSessions();
|
|
95
|
-
assert.equal(list.length, 2);
|
|
96
|
-
assert.ok(list.some(s => s.id === s1.id));
|
|
97
|
-
assert.ok(list.some(s => s.id === s2.id));
|
|
98
108
|
});
|
|
99
109
|
|
|
100
|
-
it('session
|
|
101
|
-
const session = manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
102
|
-
|
|
103
|
-
const list = manager.listSessions();
|
|
104
|
-
assert.equal(list.length, 0);
|
|
110
|
+
it('session state is running immediately after creation', async () => {
|
|
111
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
112
|
+
assert.equal(session.state, 'running');
|
|
105
113
|
});
|
|
106
114
|
});
|
|
107
115
|
|
|
108
116
|
describe('auto-launch claude on session creation', () => {
|
|
109
|
-
it('auto-launches claude with --dangerously-skip-permissions
|
|
110
|
-
const session = manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
117
|
+
it('auto-launches claude with --dangerously-skip-permissions, --append-system-prompt, and --session-id', async () => {
|
|
118
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
111
119
|
|
|
112
120
|
assert.equal(spawnMock.mock.calls.length, 1);
|
|
113
121
|
const [cmd, args] = spawnMock.mock.calls[0].arguments as [string, string[]];
|
|
114
122
|
assert.equal(cmd, 'claude');
|
|
115
123
|
assert.ok(args.includes('--dangerously-skip-permissions'));
|
|
116
124
|
assert.ok(args.includes('--append-system-prompt'));
|
|
125
|
+
assert.ok(args.includes('--session-id'));
|
|
126
|
+
assert.ok(args.includes(session.claudeSessionId));
|
|
117
127
|
assert.ok(args.includes(`We are working on project-id ${PROJECT_ID}`));
|
|
118
128
|
});
|
|
119
129
|
|
|
120
|
-
it('uses --append-system-prompt (not --system-prompt) to preserve default prompt', () => {
|
|
121
|
-
manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
130
|
+
it('uses --append-system-prompt (not --system-prompt) to preserve default prompt', async () => {
|
|
131
|
+
await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
122
132
|
|
|
123
133
|
const [, args] = spawnMock.mock.calls[0].arguments as [string, string[]];
|
|
124
134
|
assert.ok(args.includes('--append-system-prompt'));
|
|
125
|
-
assert.ok(!args.includes('--system-prompt') || args.indexOf('--system-prompt') === args.indexOf('--append-system-prompt'));
|
|
126
135
|
});
|
|
127
136
|
|
|
128
|
-
it('includes the session projectId in the system prompt', () => {
|
|
137
|
+
it('includes the session projectId in the system prompt', async () => {
|
|
129
138
|
const customProjectId = 'proj_custom_123';
|
|
130
|
-
manager.createSession(customProjectId, PROJECT_NAME, 80, 24);
|
|
139
|
+
await manager.createSession(customProjectId, PROJECT_NAME, 80, 24);
|
|
131
140
|
|
|
132
141
|
const [, args] = spawnMock.mock.calls[0].arguments as [string, string[]];
|
|
133
142
|
const promptIndex = args.indexOf('--append-system-prompt');
|
|
134
143
|
assert.ok(promptIndex !== -1);
|
|
135
144
|
assert.ok(args[promptIndex + 1].includes(customProjectId));
|
|
136
145
|
});
|
|
146
|
+
});
|
|
137
147
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const existing = manager.getSession(session.id);
|
|
142
|
-
assert.ok(existing);
|
|
148
|
+
describe('ensureRunning resumes suspended sessions', () => {
|
|
149
|
+
it('returns true and does not re-spawn for already live session', async () => {
|
|
150
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
143
151
|
assert.equal(spawnMock.mock.calls.length, 1);
|
|
144
|
-
});
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
const result = await manager.ensureRunning(session.id, 80, 24);
|
|
154
|
+
assert.equal(result, true);
|
|
155
|
+
// No extra spawn
|
|
156
|
+
assert.equal(spawnMock.mock.calls.length, 1);
|
|
149
157
|
});
|
|
150
158
|
});
|
|
151
159
|
|
|
@@ -156,4 +164,137 @@ describe('PtySessionManager', () => {
|
|
|
156
164
|
assert.equal(name, 'My Project - 06/03/26 - 14:30');
|
|
157
165
|
});
|
|
158
166
|
});
|
|
167
|
+
|
|
168
|
+
describe('orphan session cleanup', () => {
|
|
169
|
+
afterEach(() => {
|
|
170
|
+
mock.timers.reset();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('starts idle timer when last listener disconnects', async () => {
|
|
174
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
175
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
176
|
+
const listener = mock.fn();
|
|
177
|
+
|
|
178
|
+
manager.addListener(session.id, listener);
|
|
179
|
+
manager.removeListener(session.id, listener);
|
|
180
|
+
|
|
181
|
+
assert.equal(manager.hasDisconnectTimer(session.id), true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('does not start timer when other listeners remain', async () => {
|
|
185
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
186
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
187
|
+
const listener1 = mock.fn();
|
|
188
|
+
const listener2 = mock.fn();
|
|
189
|
+
|
|
190
|
+
manager.addListener(session.id, listener1);
|
|
191
|
+
manager.addListener(session.id, listener2);
|
|
192
|
+
manager.removeListener(session.id, listener1);
|
|
193
|
+
|
|
194
|
+
assert.equal(manager.hasDisconnectTimer(session.id), false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('cancels timer when client reconnects before timeout', async () => {
|
|
198
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
199
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
200
|
+
const listener1 = mock.fn();
|
|
201
|
+
const listener2 = mock.fn();
|
|
202
|
+
|
|
203
|
+
manager.addListener(session.id, listener1);
|
|
204
|
+
manager.removeListener(session.id, listener1);
|
|
205
|
+
assert.equal(manager.hasDisconnectTimer(session.id), true);
|
|
206
|
+
|
|
207
|
+
// Reconnect before timer fires
|
|
208
|
+
manager.addListener(session.id, listener2);
|
|
209
|
+
assert.equal(manager.hasDisconnectTimer(session.id), false);
|
|
210
|
+
|
|
211
|
+
// PTY should still be live
|
|
212
|
+
assert.equal(manager.isLive(session.id), true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('kills PTY when idle timer fires with zero listeners', async () => {
|
|
216
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
217
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
218
|
+
const listener = mock.fn();
|
|
219
|
+
|
|
220
|
+
manager.addListener(session.id, listener);
|
|
221
|
+
manager.removeListener(session.id, listener);
|
|
222
|
+
|
|
223
|
+
// Advance time past the 30-minute timeout
|
|
224
|
+
mock.timers.tick(1_800_000);
|
|
225
|
+
|
|
226
|
+
// PTY should be killed (removed from live map), but DB record persists
|
|
227
|
+
assert.equal(manager.isLive(session.id), false);
|
|
228
|
+
assert.equal(manager.hasDisconnectTimer(session.id), false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('does not kill PTY if listener reconnects before timer fires', async () => {
|
|
232
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
233
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
234
|
+
const listener1 = mock.fn();
|
|
235
|
+
const listener2 = mock.fn();
|
|
236
|
+
|
|
237
|
+
manager.addListener(session.id, listener1);
|
|
238
|
+
manager.removeListener(session.id, listener1);
|
|
239
|
+
|
|
240
|
+
// Advance time partially
|
|
241
|
+
mock.timers.tick(900_000); // 15 minutes
|
|
242
|
+
|
|
243
|
+
// Reconnect
|
|
244
|
+
manager.addListener(session.id, listener2);
|
|
245
|
+
|
|
246
|
+
// Advance past original timeout
|
|
247
|
+
mock.timers.tick(900_001);
|
|
248
|
+
|
|
249
|
+
// PTY should still be live
|
|
250
|
+
assert.equal(manager.isLive(session.id), true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('clears pending timer when session is manually destroyed', async () => {
|
|
254
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
255
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
256
|
+
const listener = mock.fn();
|
|
257
|
+
|
|
258
|
+
manager.addListener(session.id, listener);
|
|
259
|
+
manager.removeListener(session.id, listener);
|
|
260
|
+
assert.equal(manager.hasDisconnectTimer(session.id), true);
|
|
261
|
+
|
|
262
|
+
await manager.destroySession(session.id);
|
|
263
|
+
assert.equal(manager.hasDisconnectTimer(session.id), false);
|
|
264
|
+
assert.equal(manager.getDisconnectTimerCount(), 0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('clears all pending timers on destroyAllPty', async () => {
|
|
268
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
269
|
+
const s1 = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
270
|
+
const s2 = await manager.createSession('proj_other', 'Other', 80, 24);
|
|
271
|
+
const l1 = mock.fn();
|
|
272
|
+
const l2 = mock.fn();
|
|
273
|
+
|
|
274
|
+
manager.addListener(s1.id, l1);
|
|
275
|
+
manager.addListener(s2.id, l2);
|
|
276
|
+
manager.removeListener(s1.id, l1);
|
|
277
|
+
manager.removeListener(s2.id, l2);
|
|
278
|
+
|
|
279
|
+
assert.equal(manager.getDisconnectTimerCount(), 2);
|
|
280
|
+
|
|
281
|
+
manager.destroyAllPty();
|
|
282
|
+
assert.equal(manager.getDisconnectTimerCount(), 0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('only starts one timer per session even with multiple removeListener calls', async () => {
|
|
286
|
+
mock.timers.enable({ apis: ['setTimeout'] });
|
|
287
|
+
const session = await manager.createSession(PROJECT_ID, PROJECT_NAME, 80, 24);
|
|
288
|
+
const listener = mock.fn();
|
|
289
|
+
|
|
290
|
+
manager.addListener(session.id, listener);
|
|
291
|
+
manager.removeListener(session.id, listener);
|
|
292
|
+
|
|
293
|
+
// Try removing again (no-op since listener already removed)
|
|
294
|
+
manager.removeListener(session.id, listener);
|
|
295
|
+
|
|
296
|
+
// Should still have exactly one timer
|
|
297
|
+
assert.equal(manager.hasDisconnectTimer(session.id), true);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
159
300
|
});
|