vibe-coding-master 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +226 -0
  3. package/dist/backend/adapters/claude-adapter.js +38 -0
  4. package/dist/backend/adapters/command-runner.js +33 -0
  5. package/dist/backend/adapters/filesystem.js +60 -0
  6. package/dist/backend/adapters/git-adapter.js +33 -0
  7. package/dist/backend/api/artifact-routes.js +109 -0
  8. package/dist/backend/api/message-routes.js +90 -0
  9. package/dist/backend/api/project-routes.js +17 -0
  10. package/dist/backend/api/session-routes.js +64 -0
  11. package/dist/backend/api/task-routes.js +30 -0
  12. package/dist/backend/errors.js +29 -0
  13. package/dist/backend/runtime/node-pty-runtime.js +162 -0
  14. package/dist/backend/runtime/session-registry.js +36 -0
  15. package/dist/backend/runtime/terminal-runtime.js +1 -0
  16. package/dist/backend/server.js +159 -0
  17. package/dist/backend/services/artifact-service.js +170 -0
  18. package/dist/backend/services/command-dispatcher.js +37 -0
  19. package/dist/backend/services/message-service.js +217 -0
  20. package/dist/backend/services/project-service.js +71 -0
  21. package/dist/backend/services/session-service.js +221 -0
  22. package/dist/backend/services/status-service.js +21 -0
  23. package/dist/backend/services/task-service.js +88 -0
  24. package/dist/backend/templates/handoff.js +76 -0
  25. package/dist/backend/templates/message-envelope.js +27 -0
  26. package/dist/backend/templates/role-command.js +21 -0
  27. package/dist/backend/templates/role-messaging-context.js +44 -0
  28. package/dist/backend/ws/terminal-ws.js +60 -0
  29. package/dist/cli/vcmctl.js +141 -0
  30. package/dist/main.js +63 -0
  31. package/dist/shared/constants.js +45 -0
  32. package/dist/shared/types/api.js +1 -0
  33. package/dist/shared/types/artifact.js +1 -0
  34. package/dist/shared/types/message.js +1 -0
  35. package/dist/shared/types/project.js +1 -0
  36. package/dist/shared/types/role.js +1 -0
  37. package/dist/shared/types/session.js +1 -0
  38. package/dist/shared/types/task.js +1 -0
  39. package/dist/shared/types/terminal.js +1 -0
  40. package/dist/shared/validation/artifact-check.js +64 -0
  41. package/dist/shared/validation/slug-check.js +22 -0
  42. package/dist-frontend/assets/index-Bah6k-Ix.css +32 -0
  43. package/dist-frontend/assets/index-EMaQuIB6.js +58 -0
  44. package/dist-frontend/index.html +13 -0
  45. package/docs/cc-best-practices.md +2142 -0
  46. package/docs/product-design.md +1597 -0
  47. package/docs/v1-architecture-design.md +1431 -0
  48. package/docs/v1-implementation-plan.md +1949 -0
  49. package/docs/v1-message-bus-orchestration-design.md +534 -0
  50. package/package.json +60 -0
  51. package/scripts/clean-build.mjs +12 -0
  52. package/scripts/fix-node-pty-spawn-helper.mjs +31 -0
@@ -0,0 +1,221 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import { ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
4
+ import { VcmError } from "../errors.js";
5
+ import { resolveRepoPath } from "../adapters/filesystem.js";
6
+ import { renderRoleMessagingContext } from "../templates/role-messaging-context.js";
7
+ export function createSessionService(deps) {
8
+ const now = deps.now ?? (() => new Date().toISOString());
9
+ async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
10
+ const live = deps.registry.getByRole(taskSlug, role);
11
+ if (live && live.status === "running") {
12
+ return live;
13
+ }
14
+ const config = await deps.projectService.loadConfig(repoRoot);
15
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
16
+ const paths = deps.artifactService.getHandoffPaths(repoRoot, task.handoffDir);
17
+ const persisted = await loadPersistedRoleRecord(deps.fs, repoRoot, config.stateRoot, taskSlug, role);
18
+ const permissionMode = input.permissionMode ?? persisted?.permissionMode ?? "default";
19
+ const claudeSessionId = launchMode === "resume"
20
+ ? persisted?.claudeSessionId
21
+ : randomUUID();
22
+ if (!claudeSessionId) {
23
+ throw new VcmError({
24
+ code: "CLAUDE_SESSION_MISSING",
25
+ message: `${role} does not have a Claude session id to resume.`,
26
+ statusCode: 409,
27
+ hint: "Start the role once before using Resume."
28
+ });
29
+ }
30
+ const startCommand = deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume");
31
+ const runtimeSession = await deps.runtime.createSession({
32
+ taskSlug,
33
+ role,
34
+ command: startCommand.command,
35
+ args: startCommand.args,
36
+ cwd: repoRoot,
37
+ env: {
38
+ VCM_API_URL: deps.apiUrl,
39
+ VCM_CTL_COMMAND: deps.vcmctlCommand,
40
+ VCM_TASK_SLUG: taskSlug,
41
+ VCM_ROLE: role
42
+ },
43
+ cols: input.cols,
44
+ rows: input.rows,
45
+ logPath: resolveRepoPath(repoRoot, paths.roleLogPaths[role])
46
+ });
47
+ const timestamp = now();
48
+ const record = {
49
+ id: runtimeSession.id,
50
+ claudeSessionId,
51
+ taskSlug,
52
+ role,
53
+ status: runtimeSession.status,
54
+ command: startCommand.display,
55
+ permissionMode,
56
+ cwd: repoRoot,
57
+ terminalBackend: "node-pty",
58
+ pid: runtimeSession.pid,
59
+ logPath: paths.roleLogPaths[role],
60
+ roleCommandPath: isDispatchableRole(role)
61
+ ? paths.roleCommandPaths[role]
62
+ : undefined,
63
+ handoffArtifactPath: getHandoffArtifactPath(paths, role),
64
+ startedAt: runtimeSession.startedAt,
65
+ updatedAt: timestamp,
66
+ lastOutputAt: runtimeSession.lastOutputAt,
67
+ exitCode: runtimeSession.exitCode
68
+ };
69
+ deps.registry.upsert(record);
70
+ await persistTaskSession(deps.fs, repoRoot, config.stateRoot, record);
71
+ await deps.taskService.updateTaskStatus(repoRoot, taskSlug, "running");
72
+ deps.runtime.write(runtimeSession.id, `${renderRoleMessagingContext(task, paths, role, deps.vcmctlCommand)}\r`);
73
+ return record;
74
+ }
75
+ return {
76
+ startRoleSession(repoRoot, taskSlug, role, input = {}) {
77
+ return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
78
+ },
79
+ resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
80
+ return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
81
+ },
82
+ async stopRoleSession(repoRoot, taskSlug, role) {
83
+ const existing = await this.getRoleSession(repoRoot, taskSlug, role);
84
+ if (!existing) {
85
+ throw new VcmError({
86
+ code: "SESSION_MISSING",
87
+ message: `${role} session has not been started.`,
88
+ statusCode: 404
89
+ });
90
+ }
91
+ if (deps.runtime.getSession(existing.id)) {
92
+ await deps.runtime.stop(existing.id);
93
+ }
94
+ const updated = {
95
+ ...existing,
96
+ status: "exited",
97
+ updatedAt: now()
98
+ };
99
+ deps.registry.upsert(updated);
100
+ const config = await deps.projectService.loadConfig(repoRoot);
101
+ await persistTaskSession(deps.fs, repoRoot, config.stateRoot, updated);
102
+ return updated;
103
+ },
104
+ async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
105
+ const existing = await this.getRoleSession(repoRoot, taskSlug, role);
106
+ if (!existing) {
107
+ return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
108
+ }
109
+ if (deps.runtime.getSession(existing.id)) {
110
+ await deps.runtime.stop(existing.id);
111
+ }
112
+ deps.registry.remove(existing.id);
113
+ return existing.claudeSessionId
114
+ ? launchRoleSession(repoRoot, taskSlug, role, input, "resume")
115
+ : launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
116
+ },
117
+ async getRoleSession(repoRoot, taskSlug, role) {
118
+ const config = await deps.projectService.loadConfig(repoRoot);
119
+ const record = deps.registry.getByRole(taskSlug, role)
120
+ ?? await loadPersistedRoleRecord(deps.fs, repoRoot, config.stateRoot, taskSlug, role);
121
+ if (!record) {
122
+ return undefined;
123
+ }
124
+ const runtimeSession = deps.runtime.getSession(record.id);
125
+ if (!runtimeSession) {
126
+ return {
127
+ ...record,
128
+ status: getRecoverableStatus(record),
129
+ pid: undefined,
130
+ exitCode: record.exitCode ?? null
131
+ };
132
+ }
133
+ return {
134
+ ...record,
135
+ status: runtimeSession.status,
136
+ pid: runtimeSession.pid,
137
+ lastOutputAt: runtimeSession.lastOutputAt,
138
+ exitCode: runtimeSession.exitCode
139
+ };
140
+ },
141
+ async listRoleSessions(repoRoot, taskSlug) {
142
+ const sessions = [];
143
+ for (const role of ROLE_NAMES) {
144
+ const session = await this.getRoleSession(repoRoot, taskSlug, role);
145
+ if (session) {
146
+ sessions.push(session);
147
+ }
148
+ }
149
+ return sessions;
150
+ }
151
+ };
152
+ }
153
+ function getRecoverableStatus(record) {
154
+ if (!record.claudeSessionId) {
155
+ return record.status === "running" ? "missing" : record.status;
156
+ }
157
+ if (record.status === "done") {
158
+ return "done";
159
+ }
160
+ return "resumable";
161
+ }
162
+ function getHandoffArtifactPath(paths, role) {
163
+ if (role === "architect") {
164
+ return paths.architecturePlanPath;
165
+ }
166
+ if (role === "coder") {
167
+ return paths.implementationLogPath;
168
+ }
169
+ if (role === "reviewer") {
170
+ return paths.reviewReportPath;
171
+ }
172
+ return undefined;
173
+ }
174
+ async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role) {
175
+ const sessionPath = getTaskSessionPath(repoRoot, stateRoot, taskSlug);
176
+ if (!(await fs.pathExists(sessionPath))) {
177
+ return undefined;
178
+ }
179
+ const current = await fs.readJson(sessionPath);
180
+ return current.roles[role]?.record;
181
+ }
182
+ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
183
+ const sessionPath = getTaskSessionPath(repoRoot, stateRoot, session.taskSlug);
184
+ const empty = createEmptyTaskSessionRecord(session.taskSlug, session.updatedAt);
185
+ const current = await fs.pathExists(sessionPath)
186
+ ? await fs.readJson(sessionPath)
187
+ : empty;
188
+ const record = {
189
+ ...session,
190
+ updatedAt: session.updatedAt
191
+ };
192
+ await fs.writeJsonAtomic(sessionPath, {
193
+ ...current,
194
+ updatedAt: session.updatedAt,
195
+ roles: {
196
+ ...current.roles,
197
+ [session.role]: {
198
+ id: session.id,
199
+ claudeSessionId: session.claudeSessionId,
200
+ status: session.status,
201
+ record
202
+ }
203
+ }
204
+ });
205
+ }
206
+ function createEmptyTaskSessionRecord(taskSlug, updatedAt) {
207
+ return {
208
+ version: 1,
209
+ taskSlug,
210
+ updatedAt,
211
+ roles: {
212
+ "project-manager": { id: null, status: "not_started" },
213
+ architect: { id: null, status: "not_started" },
214
+ coder: { id: null, status: "not_started" },
215
+ reviewer: { id: null, status: "not_started" }
216
+ }
217
+ };
218
+ }
219
+ function getTaskSessionPath(repoRoot, stateRoot, taskSlug) {
220
+ return path.join(repoRoot, stateRoot, "sessions", `${taskSlug}.json`);
221
+ }
@@ -0,0 +1,21 @@
1
+ export function createStatusService(deps) {
2
+ return {
3
+ async getTaskStatus(repoRoot, taskSlug) {
4
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
5
+ const artifacts = await deps.artifactService.listArtifacts({
6
+ repoRoot,
7
+ handoffDir: task.handoffDir
8
+ });
9
+ const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
10
+ const warnings = artifacts.checks
11
+ .filter((check) => check.status !== "ok")
12
+ .map((check) => `${check.path}: ${check.status}`);
13
+ return {
14
+ task,
15
+ sessions,
16
+ artifacts,
17
+ warnings
18
+ };
19
+ }
20
+ };
21
+ }
@@ -0,0 +1,88 @@
1
+ import path from "node:path";
2
+ import { assertValidTaskSlug } from "../../shared/validation/slug-check.js";
3
+ import { VcmError } from "../errors.js";
4
+ export function createTaskService(deps) {
5
+ const now = deps.now ?? (() => new Date().toISOString());
6
+ return {
7
+ async createTask(repoRoot, input) {
8
+ assertValidTaskSlug(input.taskSlug);
9
+ const config = await deps.projectService.loadConfig(repoRoot);
10
+ const taskPath = getTaskPath(repoRoot, config.stateRoot, input.taskSlug);
11
+ if (await deps.fs.pathExists(taskPath)) {
12
+ throw new VcmError({
13
+ code: "TASK_EXISTS",
14
+ message: `Task already exists: ${input.taskSlug}`,
15
+ statusCode: 409
16
+ });
17
+ }
18
+ const timestamp = now();
19
+ const task = {
20
+ version: 1,
21
+ taskSlug: input.taskSlug,
22
+ title: input.title,
23
+ createdAt: timestamp,
24
+ updatedAt: timestamp,
25
+ repoRoot,
26
+ branch: await deps.git.getCurrentBranch(repoRoot),
27
+ handoffDir: path.posix.join(config.handoffRoot, input.taskSlug),
28
+ status: "created",
29
+ specPath: input.specPath
30
+ };
31
+ await deps.artifactService.ensureHandoffStructure({
32
+ repoRoot,
33
+ taskSlug: input.taskSlug,
34
+ handoffDir: task.handoffDir
35
+ });
36
+ await deps.artifactService.createArtifactTemplates({
37
+ repoRoot,
38
+ taskSlug: input.taskSlug,
39
+ handoffDir: task.handoffDir
40
+ });
41
+ await this.saveTask(repoRoot, task);
42
+ return task;
43
+ },
44
+ async listTasks(repoRoot) {
45
+ const config = await deps.projectService.loadConfig(repoRoot);
46
+ const tasksDir = path.join(repoRoot, config.stateRoot, "tasks");
47
+ if (!(await deps.fs.pathExists(tasksDir))) {
48
+ return [];
49
+ }
50
+ const entries = await deps.fs.readDir(tasksDir);
51
+ const tasks = [];
52
+ for (const entry of entries.filter((candidate) => candidate.endsWith(".json"))) {
53
+ tasks.push(await deps.fs.readJson(path.join(tasksDir, entry)));
54
+ }
55
+ return tasks.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
56
+ },
57
+ async loadTask(repoRoot, taskSlug) {
58
+ assertValidTaskSlug(taskSlug);
59
+ const config = await deps.projectService.loadConfig(repoRoot);
60
+ const taskPath = getTaskPath(repoRoot, config.stateRoot, taskSlug);
61
+ if (!(await deps.fs.pathExists(taskPath))) {
62
+ throw new VcmError({
63
+ code: "TASK_MISSING",
64
+ message: `Task does not exist: ${taskSlug}`,
65
+ statusCode: 404
66
+ });
67
+ }
68
+ return deps.fs.readJson(taskPath);
69
+ },
70
+ async saveTask(repoRoot, task) {
71
+ const config = await deps.projectService.loadConfig(repoRoot);
72
+ await deps.fs.writeJsonAtomic(getTaskPath(repoRoot, config.stateRoot, task.taskSlug), task);
73
+ },
74
+ async updateTaskStatus(repoRoot, taskSlug, status) {
75
+ const task = await this.loadTask(repoRoot, taskSlug);
76
+ const updated = {
77
+ ...task,
78
+ status,
79
+ updatedAt: now()
80
+ };
81
+ await this.saveTask(repoRoot, updated);
82
+ return updated;
83
+ }
84
+ };
85
+ }
86
+ function getTaskPath(repoRoot, stateRoot, taskSlug) {
87
+ return path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`);
88
+ }
@@ -0,0 +1,76 @@
1
+ export function renderArchitecturePlanTemplate(taskSlug) {
2
+ return `# Architecture Plan: ${taskSlug}
3
+
4
+ ## Context
5
+
6
+ TBD
7
+
8
+ ## Architecture Decision
9
+
10
+ TBD
11
+
12
+ ## Implementation Plan
13
+
14
+ TBD
15
+
16
+ ## Risks
17
+
18
+ TBD
19
+
20
+ ## Stop Conditions
21
+
22
+ TBD
23
+ `;
24
+ }
25
+ export function renderImplementationLogTemplate(taskSlug) {
26
+ return `# Implementation Log: ${taskSlug}
27
+
28
+ ## Summary
29
+
30
+ TBD
31
+
32
+ ## Files Changed
33
+
34
+ TBD
35
+
36
+ ## Validation
37
+
38
+ TBD
39
+
40
+ ## Deviations From Architecture Plan
41
+
42
+ TBD
43
+
44
+ ## Follow-ups
45
+
46
+ TBD
47
+ `;
48
+ }
49
+ export function renderValidationLogTemplate(taskSlug) {
50
+ return `# Validation Log: ${taskSlug}
51
+
52
+ ## Validation
53
+
54
+ Not run yet.
55
+ `;
56
+ }
57
+ export function renderReviewReportTemplate(taskSlug) {
58
+ return `# Review Report: ${taskSlug}
59
+
60
+ ## Summary
61
+
62
+ TBD
63
+
64
+ ## Findings
65
+
66
+ TBD
67
+
68
+ ## Validation
69
+
70
+ TBD
71
+
72
+ ## Decision
73
+
74
+ TBD
75
+ `;
76
+ }
@@ -0,0 +1,27 @@
1
+ export function renderMessageEnvelope(message) {
2
+ const artifactRefs = message.artifactRefs.length > 0
3
+ ? message.artifactRefs.map((artifact) => `- ${artifact}`).join("\n")
4
+ : "- none";
5
+ return `
6
+ [VCM MESSAGE]
7
+ id: ${message.id}
8
+ task: ${message.taskSlug}
9
+ from: ${message.fromRole}
10
+ to: ${message.toRole}
11
+ type: ${message.type}
12
+
13
+ ${message.body}
14
+
15
+ Artifact refs:
16
+ ${artifactRefs}
17
+
18
+ Instructions:
19
+ - Read the message and execute only within this VCM task.
20
+ - Reply to project-manager with vcmctl reply when complete, blocked, or unclear.
21
+ [/VCM MESSAGE]
22
+ `;
23
+ }
24
+ export function renderManualStagePrompt(message) {
25
+ const target = message.bodyPath ?? `VCM message ${message.id}`;
26
+ return `Read and handle VCM message ${message.id} at ${target}`;
27
+ }
@@ -0,0 +1,21 @@
1
+ export function renderRoleCommandTemplate(taskSlug, role) {
2
+ return `# ${role} command for ${taskSlug}
3
+
4
+ ## Objective
5
+
6
+ TBD
7
+
8
+ ## Inputs
9
+
10
+ - Task slug: ${taskSlug}
11
+
12
+ ## Expected Output Artifact
13
+
14
+ TBD
15
+
16
+ ## Stop Conditions
17
+
18
+ - Stop and ask the user if the task scope is unclear.
19
+ - Stop before making high-risk changes without explicit user approval.
20
+ `;
21
+ }
@@ -0,0 +1,44 @@
1
+ export function renderRoleMessagingContext(task, paths, role, vcmctlCommand = "vcmctl") {
2
+ if (role === "project-manager") {
3
+ return `VCM messaging context
4
+
5
+ Task slug: ${task.taskSlug}
6
+ Canonical handoff directory: ${task.handoffDir}
7
+
8
+ You are the orchestration hub for this task.
9
+
10
+ Use VCM messaging instead of asking the user to copy commands:
11
+ - Send work to architect/coder/reviewer with: ${vcmctlCommand} send --to <role> --type task --body-file <file>
12
+ - Ask a question with: ${vcmctlCommand} send --to <role> --type question --body "..."
13
+ - Check pending messages with: ${vcmctlCommand} inbox
14
+
15
+ Canonical role command files still exist for durable handoff:
16
+ - architect: ${paths.roleCommandPaths.architect}
17
+ - coder: ${paths.roleCommandPaths.coder}
18
+ - reviewer: ${paths.roleCommandPaths.reviewer}
19
+
20
+ Hard rules:
21
+ - Use only ${task.handoffDir} for this task.
22
+ - Do not create or write .ai/handoffs/<other-task>/ for this task.
23
+ - Non-trivial blockers or high-risk decisions must be escalated to the user.
24
+ - In manual orchestration mode, sent messages wait for user approval before the target role executes them.
25
+ `;
26
+ }
27
+ return `VCM messaging context
28
+
29
+ Task slug: ${task.taskSlug}
30
+ Canonical handoff directory: ${task.handoffDir}
31
+ Current role: ${role}
32
+
33
+ When complete, blocked, or unclear, reply to project-manager through VCM:
34
+ - ${vcmctlCommand} reply --type result --body-file <file>
35
+ - ${vcmctlCommand} reply --type blocked --body "..."
36
+ - ${vcmctlCommand} reply --type question --body "..."
37
+
38
+ Hard rules:
39
+ - Only reply to project-manager. Do not message other roles directly.
40
+ - Use only ${task.handoffDir} for this task.
41
+ - Do not create or write .ai/handoffs/<other-task>/ for this task.
42
+ - In manual orchestration mode, your replies wait for user approval before project-manager receives them.
43
+ `;
44
+ }
@@ -0,0 +1,60 @@
1
+ import { WebSocketServer } from "ws";
2
+ import { toVcmError } from "../errors.js";
3
+ export function registerTerminalWs(app, deps) {
4
+ const wss = new WebSocketServer({ noServer: true });
5
+ app.server.on("upgrade", (request, socket, head) => {
6
+ const url = new URL(request.url ?? "/", "http://localhost");
7
+ const match = /^\/ws\/terminal\/([^/]+)$/.exec(url.pathname);
8
+ if (!match) {
9
+ return;
10
+ }
11
+ wss.handleUpgrade(request, socket, head, (ws) => {
12
+ bindTerminalSocket(ws, decodeURIComponent(match[1] ?? ""), deps.runtime);
13
+ });
14
+ });
15
+ }
16
+ function bindTerminalSocket(ws, sessionId, runtime) {
17
+ let unsubscribe = () => { };
18
+ try {
19
+ unsubscribe = runtime.subscribe(sessionId, (event) => {
20
+ if (event.type === "output") {
21
+ send(ws, { type: "output", data: event.data ?? "" });
22
+ }
23
+ else if (event.type === "exit") {
24
+ send(ws, { type: "exit", exitCode: event.exitCode ?? null });
25
+ }
26
+ else if (event.type === "status" && event.status) {
27
+ send(ws, { type: "status", status: event.status });
28
+ }
29
+ });
30
+ }
31
+ catch (error) {
32
+ const vcmError = toVcmError(error);
33
+ send(ws, { type: "error", message: vcmError.message });
34
+ ws.close();
35
+ return;
36
+ }
37
+ ws.on("message", (raw) => {
38
+ try {
39
+ const message = JSON.parse(raw.toString());
40
+ if (message.type === "input") {
41
+ runtime.write(sessionId, message.data);
42
+ }
43
+ else if (message.type === "resize") {
44
+ runtime.resize(sessionId, message.cols, message.rows);
45
+ }
46
+ }
47
+ catch (error) {
48
+ const vcmError = toVcmError(error);
49
+ send(ws, { type: "error", message: vcmError.message });
50
+ }
51
+ });
52
+ ws.on("close", () => {
53
+ unsubscribe();
54
+ });
55
+ }
56
+ function send(ws, message) {
57
+ if (ws.readyState === ws.OPEN) {
58
+ ws.send(JSON.stringify(message));
59
+ }
60
+ }