@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
package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts
CHANGED
|
@@ -1,63 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PTY Session Manager — manages named, project-scoped terminal sessions.
|
|
3
|
-
*
|
|
3
|
+
* Sessions are persisted in the database and survive server restarts.
|
|
4
|
+
* PTY processes are ephemeral — spawned on demand when a user connects.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* New sessions launch claude with `--session-id <uuid>` to set a known ID.
|
|
7
|
+
* Resumed sessions (after restart) launch claude with `--resume <uuid>`.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { IPty } from 'node-pty';
|
|
10
|
-
import { randomBytes } from 'node:crypto';
|
|
11
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
12
|
+
import { desc, eq } from 'drizzle-orm';
|
|
13
|
+
import { terminalSessions } from '@assistkick/shared/db/schema.js';
|
|
11
14
|
|
|
12
15
|
const OUTPUT_BUFFER_MAX = 50_000;
|
|
13
|
-
|
|
14
|
-
const ALLOWED_COMMAND_PREFIXES = [
|
|
15
|
-
'claude',
|
|
16
|
-
] as const;
|
|
17
|
-
|
|
18
|
-
const PROMPT = '\x1b[32m$\x1b[0m ';
|
|
19
|
-
const WELCOME_MSG = `\x1b[90mRestricted terminal — allowed commands: ${ALLOWED_COMMAND_PREFIXES.join(', ')}\x1b[0m\r\n`;
|
|
16
|
+
const IDLE_TIMEOUT_MS = 1_800_000; // 30 minutes
|
|
20
17
|
|
|
21
18
|
interface PtySessionManagerDeps {
|
|
22
19
|
spawn: (shell: string, args: string[], options: Record<string, unknown>) => IPty;
|
|
23
20
|
log: (tag: string, ...args: unknown[]) => void;
|
|
24
21
|
projectRoot: string;
|
|
22
|
+
getDb: () => any;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
projectId: string;
|
|
31
|
-
projectName: string;
|
|
32
|
-
pty: IPty | null;
|
|
33
|
-
state: 'idle' | 'running';
|
|
34
|
-
cols: number;
|
|
35
|
-
rows: number;
|
|
25
|
+
/** In-memory state for a live PTY process. */
|
|
26
|
+
interface LivePty {
|
|
27
|
+
pty: IPty;
|
|
36
28
|
outputBuffer: string;
|
|
37
|
-
inputBuffer: string;
|
|
38
29
|
listeners: Set<(data: string) => void>;
|
|
39
|
-
|
|
30
|
+
cols: number;
|
|
31
|
+
rows: number;
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
export interface SessionInfo {
|
|
43
35
|
id: string;
|
|
36
|
+
claudeSessionId: string;
|
|
44
37
|
name: string;
|
|
45
38
|
projectId: string;
|
|
46
39
|
projectName: string;
|
|
47
|
-
state: '
|
|
40
|
+
state: 'suspended' | 'running';
|
|
48
41
|
createdAt: string;
|
|
42
|
+
lastUsedAt: string;
|
|
49
43
|
}
|
|
50
44
|
|
|
51
45
|
export class PtySessionManager {
|
|
52
|
-
|
|
46
|
+
/** Live PTY processes — keyed by session ID. Only exists while PTY is running. */
|
|
47
|
+
private readonly live = new Map<string, LivePty>();
|
|
48
|
+
private readonly disconnectTimers = new Map<string, NodeJS.Timeout>();
|
|
53
49
|
private readonly spawn: PtySessionManagerDeps['spawn'];
|
|
54
50
|
private readonly log: PtySessionManagerDeps['log'];
|
|
55
51
|
private readonly projectRoot: string;
|
|
52
|
+
private readonly getDb: PtySessionManagerDeps['getDb'];
|
|
56
53
|
|
|
57
|
-
constructor({ spawn, log, projectRoot }: PtySessionManagerDeps) {
|
|
54
|
+
constructor({ spawn, log, projectRoot, getDb }: PtySessionManagerDeps) {
|
|
58
55
|
this.spawn = spawn;
|
|
59
56
|
this.log = log;
|
|
60
57
|
this.projectRoot = projectRoot;
|
|
58
|
+
this.getDb = getDb;
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
generateId = (): string => {
|
|
@@ -74,215 +72,256 @@ export class PtySessionManager {
|
|
|
74
72
|
return `${projectName} - ${dd}/${mm}/${yy} - ${hh}:${min}`;
|
|
75
73
|
};
|
|
76
74
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { valid: true };
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
listSessions = (): SessionInfo[] => {
|
|
97
|
-
return Array.from(this.sessions.values()).map(s => ({
|
|
98
|
-
id: s.id,
|
|
99
|
-
name: s.name,
|
|
100
|
-
projectId: s.projectId,
|
|
101
|
-
projectName: s.projectName,
|
|
102
|
-
state: s.state,
|
|
103
|
-
createdAt: s.createdAt.toISOString(),
|
|
75
|
+
/** List all sessions from the database, annotated with live/suspended state. */
|
|
76
|
+
listSessions = async (): Promise<SessionInfo[]> => {
|
|
77
|
+
const db = this.getDb();
|
|
78
|
+
const rows = await db.select().from(terminalSessions).orderBy(desc(terminalSessions.createdAt));
|
|
79
|
+
return rows.map((r: any) => ({
|
|
80
|
+
id: r.id,
|
|
81
|
+
claudeSessionId: r.claudeSessionId,
|
|
82
|
+
name: r.name,
|
|
83
|
+
projectId: r.projectId,
|
|
84
|
+
projectName: r.projectName,
|
|
85
|
+
state: this.live.has(r.id) ? 'running' as const : 'suspended' as const,
|
|
86
|
+
createdAt: r.createdAt,
|
|
87
|
+
lastUsedAt: r.lastUsedAt,
|
|
104
88
|
}));
|
|
105
89
|
};
|
|
106
90
|
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
/** Check whether a session exists (in DB). */
|
|
92
|
+
getSession = async (sessionId: string): Promise<SessionInfo | undefined> => {
|
|
93
|
+
const db = this.getDb();
|
|
94
|
+
const rows = await db.select().from(terminalSessions).where(eq(terminalSessions.id, sessionId));
|
|
95
|
+
if (rows.length === 0) return undefined;
|
|
96
|
+
const r = rows[0];
|
|
97
|
+
return {
|
|
98
|
+
id: r.id,
|
|
99
|
+
claudeSessionId: r.claudeSessionId,
|
|
100
|
+
name: r.name,
|
|
101
|
+
projectId: r.projectId,
|
|
102
|
+
projectName: r.projectName,
|
|
103
|
+
state: this.live.has(r.id) ? 'running' as const : 'suspended' as const,
|
|
104
|
+
createdAt: r.createdAt,
|
|
105
|
+
lastUsedAt: r.lastUsedAt,
|
|
106
|
+
};
|
|
109
107
|
};
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+
/** Create a new session — persists to DB and spawns claude with --session-id. */
|
|
110
|
+
createSession = async (projectId: string, projectName: string, cols: number, rows: number): Promise<SessionInfo> => {
|
|
112
111
|
const id = this.generateId();
|
|
112
|
+
const claudeSessionId = randomUUID();
|
|
113
113
|
const createdAt = new Date();
|
|
114
114
|
const name = this.formatSessionName(projectName, createdAt);
|
|
115
|
+
const now = createdAt.toISOString();
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
// Persist to DB
|
|
118
|
+
const db = this.getDb();
|
|
119
|
+
await db.insert(terminalSessions).values({
|
|
117
120
|
id,
|
|
118
|
-
|
|
121
|
+
claudeSessionId,
|
|
119
122
|
projectId,
|
|
120
123
|
projectName,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
outputBuffer: '',
|
|
126
|
-
inputBuffer: '',
|
|
127
|
-
listeners: new Set(),
|
|
128
|
-
createdAt,
|
|
129
|
-
};
|
|
124
|
+
name,
|
|
125
|
+
createdAt: now,
|
|
126
|
+
lastUsedAt: now,
|
|
127
|
+
});
|
|
130
128
|
|
|
131
|
-
this.
|
|
132
|
-
this.log('PTY', `Created session "${name}" (${id}) for project ${projectId}`);
|
|
129
|
+
this.log('PTY', `Created session "${name}" (${id}) for project ${projectId}, claude session ${claudeSessionId}`);
|
|
133
130
|
|
|
134
|
-
//
|
|
135
|
-
this.
|
|
136
|
-
this.autoLaunchClaude(session);
|
|
131
|
+
// Spawn claude with --session-id
|
|
132
|
+
this.spawnClaude(id, claudeSessionId, projectId, cols, rows, false);
|
|
137
133
|
|
|
138
|
-
return
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
claudeSessionId,
|
|
137
|
+
name,
|
|
138
|
+
projectId,
|
|
139
|
+
projectName,
|
|
140
|
+
state: 'running',
|
|
141
|
+
createdAt: now,
|
|
142
|
+
lastUsedAt: now,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Ensure the PTY is running for a session. If suspended (no PTY), resume claude.
|
|
148
|
+
* Returns true if PTY is (now) running; false if session not found.
|
|
149
|
+
*/
|
|
150
|
+
ensureRunning = async (sessionId: string, cols: number, rows: number): Promise<boolean> => {
|
|
151
|
+
if (this.live.has(sessionId)) return true;
|
|
152
|
+
|
|
153
|
+
const session = await this.getSession(sessionId);
|
|
154
|
+
if (!session) return false;
|
|
155
|
+
|
|
156
|
+
// Update last used timestamp
|
|
157
|
+
const db = this.getDb();
|
|
158
|
+
await db.update(terminalSessions)
|
|
159
|
+
.set({ lastUsedAt: new Date().toISOString() })
|
|
160
|
+
.where(eq(terminalSessions.id, sessionId));
|
|
161
|
+
|
|
162
|
+
// Resume claude
|
|
163
|
+
this.spawnClaude(sessionId, session.claudeSessionId, session.projectId, cols, rows, true);
|
|
164
|
+
return true;
|
|
139
165
|
};
|
|
140
166
|
|
|
141
167
|
addListener = (sessionId: string, listener: (data: string) => void): void => {
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
|
|
168
|
+
const entry = this.live.get(sessionId);
|
|
169
|
+
if (entry) {
|
|
170
|
+
const pendingTimer = this.disconnectTimers.get(sessionId);
|
|
171
|
+
if (pendingTimer) {
|
|
172
|
+
clearTimeout(pendingTimer);
|
|
173
|
+
this.disconnectTimers.delete(sessionId);
|
|
174
|
+
this.log('PTY', `Cancelled idle timer for session ${sessionId} — client reconnected`);
|
|
175
|
+
}
|
|
176
|
+
entry.listeners.add(listener);
|
|
145
177
|
}
|
|
146
178
|
};
|
|
147
179
|
|
|
148
180
|
removeListener = (sessionId: string, listener: (data: string) => void): void => {
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
|
|
181
|
+
const entry = this.live.get(sessionId);
|
|
182
|
+
if (entry) {
|
|
183
|
+
entry.listeners.delete(listener);
|
|
184
|
+
if (entry.listeners.size === 0 && !this.disconnectTimers.has(sessionId)) {
|
|
185
|
+
this.log('PTY', `All listeners disconnected from session ${sessionId} — starting ${IDLE_TIMEOUT_MS / 60_000}min idle timer`);
|
|
186
|
+
const timer = setTimeout(() => {
|
|
187
|
+
this.disconnectTimers.delete(sessionId);
|
|
188
|
+
const current = this.live.get(sessionId);
|
|
189
|
+
if (current && current.listeners.size === 0) {
|
|
190
|
+
this.log('PTY', `Idle timer fired for session ${sessionId} — killing orphan PTY`);
|
|
191
|
+
current.pty.kill();
|
|
192
|
+
this.live.delete(sessionId);
|
|
193
|
+
}
|
|
194
|
+
}, IDLE_TIMEOUT_MS);
|
|
195
|
+
this.disconnectTimers.set(sessionId, timer);
|
|
196
|
+
}
|
|
152
197
|
}
|
|
153
198
|
};
|
|
154
199
|
|
|
155
200
|
writeToSession = (sessionId: string, data: string): void => {
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
if (session.state === 'running' && session.pty) {
|
|
160
|
-
// Forward raw input to the running command
|
|
161
|
-
session.pty.write(data);
|
|
162
|
-
} else if (session.state === 'idle') {
|
|
163
|
-
// Handle command-line input
|
|
164
|
-
this.handleIdleInput(session, data);
|
|
201
|
+
const entry = this.live.get(sessionId);
|
|
202
|
+
if (entry) {
|
|
203
|
+
entry.pty.write(data);
|
|
165
204
|
}
|
|
166
205
|
};
|
|
167
206
|
|
|
168
207
|
resizeSession = (sessionId: string, cols: number, rows: number): void => {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
session.pty.resize(cols, rows);
|
|
175
|
-
}
|
|
208
|
+
const entry = this.live.get(sessionId);
|
|
209
|
+
if (entry) {
|
|
210
|
+
entry.cols = cols;
|
|
211
|
+
entry.rows = rows;
|
|
212
|
+
entry.pty.resize(cols, rows);
|
|
176
213
|
}
|
|
177
214
|
};
|
|
178
215
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
this.
|
|
186
|
-
this.log('PTY', `Destroyed session "${session.name}" (${sessionId})`);
|
|
216
|
+
/** Destroy a session — kills PTY and removes from DB permanently. */
|
|
217
|
+
destroySession = async (sessionId: string): Promise<void> => {
|
|
218
|
+
// Clear any pending idle timer
|
|
219
|
+
const pendingTimer = this.disconnectTimers.get(sessionId);
|
|
220
|
+
if (pendingTimer) {
|
|
221
|
+
clearTimeout(pendingTimer);
|
|
222
|
+
this.disconnectTimers.delete(sessionId);
|
|
187
223
|
}
|
|
224
|
+
|
|
225
|
+
// Kill live PTY if running
|
|
226
|
+
const entry = this.live.get(sessionId);
|
|
227
|
+
if (entry) {
|
|
228
|
+
entry.pty.kill();
|
|
229
|
+
this.live.delete(sessionId);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Remove from DB
|
|
233
|
+
const db = this.getDb();
|
|
234
|
+
await db.delete(terminalSessions).where(eq(terminalSessions.id, sessionId));
|
|
235
|
+
this.log('PTY', `Destroyed session ${sessionId}`);
|
|
188
236
|
};
|
|
189
237
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
238
|
+
/** Kill all live PTY processes (on server shutdown). Does NOT remove DB records. */
|
|
239
|
+
destroyAllPty = (): void => {
|
|
240
|
+
for (const timer of this.disconnectTimers.values()) {
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
}
|
|
243
|
+
this.disconnectTimers.clear();
|
|
244
|
+
for (const [id, entry] of this.live) {
|
|
245
|
+
entry.pty.kill();
|
|
246
|
+
this.log('PTY', `Killed PTY for session ${id} (shutdown)`);
|
|
193
247
|
}
|
|
248
|
+
this.live.clear();
|
|
194
249
|
};
|
|
195
250
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return session?.outputBuffer ?? '';
|
|
251
|
+
getDisconnectTimerCount = (): number => {
|
|
252
|
+
return this.disconnectTimers.size;
|
|
199
253
|
};
|
|
200
254
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (char === '\r' || char === '\n') {
|
|
204
|
-
// Enter pressed — validate and execute
|
|
205
|
-
this.emitOutput(session, '\r\n');
|
|
206
|
-
const command = session.inputBuffer.trim();
|
|
207
|
-
session.inputBuffer = '';
|
|
208
|
-
|
|
209
|
-
if (!command) {
|
|
210
|
-
this.emitOutput(session, PROMPT);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const validation = this.validateCommand(command);
|
|
215
|
-
if (!validation.valid) {
|
|
216
|
-
this.emitOutput(session, `\x1b[31m${validation.error}\x1b[0m\r\n`);
|
|
217
|
-
this.emitOutput(session, PROMPT);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.spawnCommand(session, command);
|
|
222
|
-
} else if (char === '\x7f' || char === '\b') {
|
|
223
|
-
// Backspace
|
|
224
|
-
if (session.inputBuffer.length > 0) {
|
|
225
|
-
session.inputBuffer = session.inputBuffer.slice(0, -1);
|
|
226
|
-
this.emitOutput(session, '\b \b');
|
|
227
|
-
}
|
|
228
|
-
} else if (char === '\x03') {
|
|
229
|
-
// Ctrl+C — clear input
|
|
230
|
-
session.inputBuffer = '';
|
|
231
|
-
this.emitOutput(session, '^C\r\n');
|
|
232
|
-
this.emitOutput(session, PROMPT);
|
|
233
|
-
} else if (char >= ' ') {
|
|
234
|
-
// Printable character — echo and buffer
|
|
235
|
-
session.inputBuffer += char;
|
|
236
|
-
this.emitOutput(session, char);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
255
|
+
hasDisconnectTimer = (sessionId: string): boolean => {
|
|
256
|
+
return this.disconnectTimers.has(sessionId);
|
|
239
257
|
};
|
|
240
258
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
259
|
+
getBufferedOutput = (sessionId: string): string => {
|
|
260
|
+
return this.live.get(sessionId)?.outputBuffer ?? '';
|
|
261
|
+
};
|
|
244
262
|
|
|
245
|
-
|
|
263
|
+
isLive = (sessionId: string): boolean => {
|
|
264
|
+
return this.live.has(sessionId);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// --- Private helpers ---
|
|
268
|
+
|
|
269
|
+
private spawnClaude = (
|
|
270
|
+
sessionId: string,
|
|
271
|
+
claudeSessionId: string,
|
|
272
|
+
projectId: string,
|
|
273
|
+
cols: number,
|
|
274
|
+
rows: number,
|
|
275
|
+
resume: boolean,
|
|
276
|
+
): void => {
|
|
277
|
+
const projectContext = `We are working on project-id ${projectId}`;
|
|
278
|
+
const args: string[] = ['--dangerously-skip-permissions', '--append-system-prompt', projectContext];
|
|
279
|
+
|
|
280
|
+
if (resume) {
|
|
281
|
+
args.push('--resume', claudeSessionId);
|
|
282
|
+
this.log('PTY', `Resuming claude session ${claudeSessionId} for ${sessionId}`);
|
|
283
|
+
} else {
|
|
284
|
+
args.push('--session-id', claudeSessionId);
|
|
285
|
+
this.log('PTY', `Launching new claude session ${claudeSessionId} for ${sessionId}`);
|
|
286
|
+
}
|
|
246
287
|
|
|
247
288
|
let spawnedPty: IPty;
|
|
248
289
|
try {
|
|
249
290
|
spawnedPty = this.spawn('claude', args, {
|
|
250
291
|
name: 'xterm-256color',
|
|
251
|
-
cols
|
|
252
|
-
rows
|
|
292
|
+
cols,
|
|
293
|
+
rows,
|
|
253
294
|
cwd: this.projectRoot,
|
|
254
295
|
env: this.buildEnv(),
|
|
255
296
|
});
|
|
256
297
|
} catch (err) {
|
|
257
298
|
const msg = err instanceof Error ? err.message : String(err);
|
|
258
|
-
this.
|
|
259
|
-
this.log('PTY', `Auto-launch failed for session ${session.id}: ${msg}`);
|
|
260
|
-
this.emitOutput(session, PROMPT);
|
|
299
|
+
this.log('PTY', `Failed to spawn claude for session ${sessionId}: ${msg}`);
|
|
261
300
|
return;
|
|
262
301
|
}
|
|
263
302
|
|
|
264
|
-
|
|
265
|
-
|
|
303
|
+
const entry: LivePty = {
|
|
304
|
+
pty: spawnedPty,
|
|
305
|
+
outputBuffer: '',
|
|
306
|
+
listeners: new Set(),
|
|
307
|
+
cols,
|
|
308
|
+
rows,
|
|
309
|
+
};
|
|
310
|
+
this.live.set(sessionId, entry);
|
|
266
311
|
|
|
267
312
|
spawnedPty.onData((data: string) => {
|
|
268
|
-
this.emitOutput(
|
|
313
|
+
this.emitOutput(entry, data);
|
|
269
314
|
});
|
|
270
315
|
|
|
271
316
|
spawnedPty.onExit(({ exitCode }) => {
|
|
272
|
-
this.log('PTY', `
|
|
273
|
-
|
|
274
|
-
session.state = 'idle';
|
|
275
|
-
this.emitOutput(session, '\r\n');
|
|
276
|
-
this.emitOutput(session, PROMPT);
|
|
317
|
+
this.log('PTY', `Claude exited with code ${exitCode} for session ${sessionId}`);
|
|
318
|
+
this.live.delete(sessionId);
|
|
277
319
|
});
|
|
278
320
|
};
|
|
279
321
|
|
|
280
322
|
private buildEnv = (): Record<string, string> => {
|
|
281
323
|
const env = { ...process.env } as Record<string, string>;
|
|
282
324
|
const home = env.HOME || '/root';
|
|
283
|
-
// Ensure common user-local binary directories are on PATH so that
|
|
284
|
-
// tools installed under the user profile (e.g. claude via npm -g)
|
|
285
|
-
// are found even when the server wasn't started from a login shell.
|
|
286
325
|
const extraPaths = [
|
|
287
326
|
`${home}/.local/bin`,
|
|
288
327
|
`${home}/.npm-global/bin`,
|
|
@@ -298,56 +337,12 @@ export class PtySessionManager {
|
|
|
298
337
|
return env;
|
|
299
338
|
};
|
|
300
339
|
|
|
301
|
-
private
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.log('PTY', `Executing validated command "${command}" for session ${session.id}`);
|
|
307
|
-
|
|
308
|
-
let spawnedPty: IPty;
|
|
309
|
-
try {
|
|
310
|
-
spawnedPty = this.spawn(cmd, args, {
|
|
311
|
-
name: 'xterm-256color',
|
|
312
|
-
cols: session.cols,
|
|
313
|
-
rows: session.rows,
|
|
314
|
-
cwd: this.projectRoot,
|
|
315
|
-
env: this.buildEnv(),
|
|
316
|
-
});
|
|
317
|
-
} catch (err) {
|
|
318
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
319
|
-
const env = this.buildEnv();
|
|
320
|
-
this.emitOutput(session, `\x1b[31mFailed to execute: ${msg}\x1b[0m\r\n`);
|
|
321
|
-
this.emitOutput(session, `\x1b[90mPATH: ${env.PATH}\x1b[0m\r\n`);
|
|
322
|
-
this.emitOutput(session, `\x1b[90mHOME: ${env.HOME || '(unset)'}\x1b[0m\r\n`);
|
|
323
|
-
this.emitOutput(session, `\x1b[90mSHELL: ${env.SHELL || '(unset)'}\x1b[0m\r\n`);
|
|
324
|
-
this.log('PTY', `Spawn failed for "${command}": ${msg} | PATH=${env.PATH}`);
|
|
325
|
-
this.emitOutput(session, PROMPT);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
session.pty = spawnedPty;
|
|
330
|
-
session.state = 'running';
|
|
331
|
-
|
|
332
|
-
spawnedPty.onData((data: string) => {
|
|
333
|
-
this.emitOutput(session, data);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
spawnedPty.onExit(({ exitCode }) => {
|
|
337
|
-
this.log('PTY', `Command exited with code ${exitCode} for session ${session.id}`);
|
|
338
|
-
session.pty = null;
|
|
339
|
-
session.state = 'idle';
|
|
340
|
-
this.emitOutput(session, '\r\n');
|
|
341
|
-
this.emitOutput(session, PROMPT);
|
|
342
|
-
});
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
private emitOutput = (session: PtySession, data: string): void => {
|
|
346
|
-
session.outputBuffer += data;
|
|
347
|
-
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {
|
|
348
|
-
session.outputBuffer = session.outputBuffer.slice(-OUTPUT_BUFFER_MAX);
|
|
340
|
+
private emitOutput = (entry: LivePty, data: string): void => {
|
|
341
|
+
entry.outputBuffer += data;
|
|
342
|
+
if (entry.outputBuffer.length > OUTPUT_BUFFER_MAX) {
|
|
343
|
+
entry.outputBuffer = entry.outputBuffer.slice(-OUTPUT_BUFFER_MAX);
|
|
349
344
|
}
|
|
350
|
-
for (const listener of
|
|
345
|
+
for (const listener of entry.listeners) {
|
|
351
346
|
listener(data);
|
|
352
347
|
}
|
|
353
348
|
};
|