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.
- package/LICENSE +201 -0
- package/README.md +226 -0
- package/dist/backend/adapters/claude-adapter.js +38 -0
- package/dist/backend/adapters/command-runner.js +33 -0
- package/dist/backend/adapters/filesystem.js +60 -0
- package/dist/backend/adapters/git-adapter.js +33 -0
- package/dist/backend/api/artifact-routes.js +109 -0
- package/dist/backend/api/message-routes.js +90 -0
- package/dist/backend/api/project-routes.js +17 -0
- package/dist/backend/api/session-routes.js +64 -0
- package/dist/backend/api/task-routes.js +30 -0
- package/dist/backend/errors.js +29 -0
- package/dist/backend/runtime/node-pty-runtime.js +162 -0
- package/dist/backend/runtime/session-registry.js +36 -0
- package/dist/backend/runtime/terminal-runtime.js +1 -0
- package/dist/backend/server.js +159 -0
- package/dist/backend/services/artifact-service.js +170 -0
- package/dist/backend/services/command-dispatcher.js +37 -0
- package/dist/backend/services/message-service.js +217 -0
- package/dist/backend/services/project-service.js +71 -0
- package/dist/backend/services/session-service.js +221 -0
- package/dist/backend/services/status-service.js +21 -0
- package/dist/backend/services/task-service.js +88 -0
- package/dist/backend/templates/handoff.js +76 -0
- package/dist/backend/templates/message-envelope.js +27 -0
- package/dist/backend/templates/role-command.js +21 -0
- package/dist/backend/templates/role-messaging-context.js +44 -0
- package/dist/backend/ws/terminal-ws.js +60 -0
- package/dist/cli/vcmctl.js +141 -0
- package/dist/main.js +63 -0
- package/dist/shared/constants.js +45 -0
- package/dist/shared/types/api.js +1 -0
- package/dist/shared/types/artifact.js +1 -0
- package/dist/shared/types/message.js +1 -0
- package/dist/shared/types/project.js +1 -0
- package/dist/shared/types/role.js +1 -0
- package/dist/shared/types/session.js +1 -0
- package/dist/shared/types/task.js +1 -0
- package/dist/shared/types/terminal.js +1 -0
- package/dist/shared/validation/artifact-check.js +64 -0
- package/dist/shared/validation/slug-check.js +22 -0
- package/dist-frontend/assets/index-Bah6k-Ix.css +32 -0
- package/dist-frontend/assets/index-EMaQuIB6.js +58 -0
- package/dist-frontend/index.html +13 -0
- package/docs/cc-best-practices.md +2142 -0
- package/docs/product-design.md +1597 -0
- package/docs/v1-architecture-design.md +1431 -0
- package/docs/v1-implementation-plan.md +1949 -0
- package/docs/v1-message-bus-orchestration-design.md +534 -0
- package/package.json +60 -0
- package/scripts/clean-build.mjs +12 -0
- package/scripts/fix-node-pty-spawn-helper.mjs +31 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { isDispatchableRole, isRoleName } from "../../shared/constants.js";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
export function registerArtifactRoutes(app, deps) {
|
|
4
|
+
app.get("/api/tasks/:taskSlug/artifacts", async (request) => {
|
|
5
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
6
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
7
|
+
return deps.artifactService.listArtifacts({
|
|
8
|
+
repoRoot: project.repoRoot,
|
|
9
|
+
handoffDir: task.handoffDir
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
app.get("/api/tasks/:taskSlug/artifacts/:artifactName", async (request) => {
|
|
13
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
14
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
15
|
+
const paths = deps.artifactService.getHandoffPaths(project.repoRoot, task.handoffDir);
|
|
16
|
+
const artifactPath = artifactNameToPath(paths, request.params.artifactName);
|
|
17
|
+
return {
|
|
18
|
+
path: artifactPath,
|
|
19
|
+
content: await deps.artifactService.readArtifact({
|
|
20
|
+
repoRoot: project.repoRoot,
|
|
21
|
+
artifactPath
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
app.get("/api/tasks/:taskSlug/role-commands/:role", async (request) => {
|
|
26
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
27
|
+
const role = parseDispatchableRole(request.params.role);
|
|
28
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
29
|
+
return {
|
|
30
|
+
role,
|
|
31
|
+
content: await deps.artifactService.readRoleCommand({
|
|
32
|
+
repoRoot: project.repoRoot,
|
|
33
|
+
handoffDir: task.handoffDir,
|
|
34
|
+
role
|
|
35
|
+
})
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
app.put("/api/tasks/:taskSlug/role-commands/:role", async (request) => {
|
|
39
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
40
|
+
const role = parseDispatchableRole(request.params.role);
|
|
41
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
42
|
+
await deps.artifactService.saveRoleCommand({
|
|
43
|
+
repoRoot: project.repoRoot,
|
|
44
|
+
handoffDir: task.handoffDir,
|
|
45
|
+
role,
|
|
46
|
+
content: request.body.content
|
|
47
|
+
});
|
|
48
|
+
return { ok: true };
|
|
49
|
+
});
|
|
50
|
+
app.get("/api/tasks/:taskSlug/logs/:role", async (request) => {
|
|
51
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
52
|
+
if (!isRoleName(request.params.role)) {
|
|
53
|
+
throw new VcmError({
|
|
54
|
+
code: "UNKNOWN_ROLE",
|
|
55
|
+
message: `Unknown role: ${request.params.role}`,
|
|
56
|
+
statusCode: 400
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
60
|
+
const paths = deps.artifactService.getHandoffPaths(project.repoRoot, task.handoffDir);
|
|
61
|
+
return {
|
|
62
|
+
role: request.params.role,
|
|
63
|
+
content: await deps.artifactService.readArtifact({
|
|
64
|
+
repoRoot: project.repoRoot,
|
|
65
|
+
artifactPath: paths.roleLogPaths[request.params.role]
|
|
66
|
+
})
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function parseDispatchableRole(role) {
|
|
71
|
+
if (!isDispatchableRole(role)) {
|
|
72
|
+
throw new VcmError({
|
|
73
|
+
code: "ROLE_NOT_DISPATCHABLE",
|
|
74
|
+
message: `${role} cannot receive role commands.`,
|
|
75
|
+
statusCode: 400
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return role;
|
|
79
|
+
}
|
|
80
|
+
function artifactNameToPath(paths, artifactName) {
|
|
81
|
+
if (artifactName === "architecture-plan.md") {
|
|
82
|
+
return paths.architecturePlanPath;
|
|
83
|
+
}
|
|
84
|
+
if (artifactName === "implementation-log.md") {
|
|
85
|
+
return paths.implementationLogPath;
|
|
86
|
+
}
|
|
87
|
+
if (artifactName === "validation-log.md") {
|
|
88
|
+
return paths.validationLogPath;
|
|
89
|
+
}
|
|
90
|
+
if (artifactName === "review-report.md") {
|
|
91
|
+
return paths.reviewReportPath;
|
|
92
|
+
}
|
|
93
|
+
throw new VcmError({
|
|
94
|
+
code: "ARTIFACT_UNKNOWN",
|
|
95
|
+
message: `Unknown artifact: ${artifactName}`,
|
|
96
|
+
statusCode: 404
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function requireCurrentProject(projectService) {
|
|
100
|
+
const project = await projectService.getCurrentProject();
|
|
101
|
+
if (!project) {
|
|
102
|
+
throw new VcmError({
|
|
103
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
104
|
+
message: "Connect a repository first.",
|
|
105
|
+
statusCode: 409
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return project;
|
|
109
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { VcmError } from "../errors.js";
|
|
2
|
+
export function registerMessageRoutes(app, deps) {
|
|
3
|
+
app.get("/api/tasks/:taskSlug/messages", async (request) => {
|
|
4
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
5
|
+
return deps.messageService.listMessages(context);
|
|
6
|
+
});
|
|
7
|
+
app.post("/api/tasks/:taskSlug/messages", async (request) => {
|
|
8
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
9
|
+
return deps.messageService.sendMessage({
|
|
10
|
+
...context,
|
|
11
|
+
...request.body
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
app.post("/api/tasks/:taskSlug/messages/:messageId/stage", async (request) => {
|
|
15
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
16
|
+
return deps.messageService.stageMessage({
|
|
17
|
+
...context,
|
|
18
|
+
messageId: request.params.messageId
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
app.post("/api/tasks/:taskSlug/messages/:messageId/approve", async (request) => {
|
|
22
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
23
|
+
return deps.messageService.approveMessage({
|
|
24
|
+
...context,
|
|
25
|
+
messageId: request.params.messageId
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
app.post("/api/tasks/:taskSlug/messages/:messageId/reject", async (request) => {
|
|
29
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
30
|
+
return deps.messageService.rejectMessage({
|
|
31
|
+
...context,
|
|
32
|
+
messageId: request.params.messageId
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
app.get("/api/tasks/:taskSlug/orchestration", async (request) => {
|
|
36
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
37
|
+
return deps.messageService.getOrchestrationState(context);
|
|
38
|
+
});
|
|
39
|
+
app.put("/api/tasks/:taskSlug/orchestration", async (request) => {
|
|
40
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
41
|
+
if (request.body.mode && request.body.mode !== "manual" && request.body.mode !== "auto") {
|
|
42
|
+
throw new VcmError({
|
|
43
|
+
code: "ORCHESTRATION_MODE_INVALID",
|
|
44
|
+
message: `Invalid orchestration mode: ${request.body.mode}`,
|
|
45
|
+
statusCode: 400
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return deps.messageService.updateOrchestrationState({
|
|
49
|
+
...context,
|
|
50
|
+
mode: request.body.mode,
|
|
51
|
+
paused: request.body.paused
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
app.post("/api/tasks/:taskSlug/orchestration/pause", async (request) => {
|
|
55
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
56
|
+
return deps.messageService.updateOrchestrationState({
|
|
57
|
+
...context,
|
|
58
|
+
paused: true
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
app.post("/api/tasks/:taskSlug/orchestration/resume", async (request) => {
|
|
62
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
63
|
+
return deps.messageService.updateOrchestrationState({
|
|
64
|
+
...context,
|
|
65
|
+
paused: false
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function getRouteContext(deps, taskSlug) {
|
|
70
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
71
|
+
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
72
|
+
const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
|
|
73
|
+
return {
|
|
74
|
+
repoRoot: project.repoRoot,
|
|
75
|
+
stateRoot: config.stateRoot,
|
|
76
|
+
handoffDir: task.handoffDir,
|
|
77
|
+
taskSlug
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function requireCurrentProject(projectService) {
|
|
81
|
+
const project = await projectService.getCurrentProject();
|
|
82
|
+
if (!project) {
|
|
83
|
+
throw new VcmError({
|
|
84
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
85
|
+
message: "Connect a repository first.",
|
|
86
|
+
statusCode: 409
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return project;
|
|
90
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function registerProjectRoutes(app, deps) {
|
|
2
|
+
app.get("/api/health", async () => ({ ok: true }));
|
|
3
|
+
app.post("/api/projects/connect", async (request) => {
|
|
4
|
+
return deps.projectService.connectProject(request.body);
|
|
5
|
+
});
|
|
6
|
+
app.get("/api/projects/current", async () => {
|
|
7
|
+
return deps.projectService.getCurrentProject();
|
|
8
|
+
});
|
|
9
|
+
app.get("/api/projects/harness", async () => ({
|
|
10
|
+
checks: [
|
|
11
|
+
{
|
|
12
|
+
name: "local-gui",
|
|
13
|
+
status: "ok"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isDispatchableRole, isRoleName } from "../../shared/constants.js";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
export function registerSessionRoutes(app, deps) {
|
|
4
|
+
app.get("/api/tasks/:taskSlug/sessions", async (request) => {
|
|
5
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
6
|
+
return deps.sessionService.listRoleSessions(project.repoRoot, request.params.taskSlug);
|
|
7
|
+
});
|
|
8
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/start", async (request) => {
|
|
9
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
10
|
+
const role = parseRole(request.params.role);
|
|
11
|
+
return deps.sessionService.startRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
12
|
+
});
|
|
13
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/stop", async (request) => {
|
|
14
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
15
|
+
const role = parseRole(request.params.role);
|
|
16
|
+
return deps.sessionService.stopRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
17
|
+
});
|
|
18
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/restart", async (request) => {
|
|
19
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
20
|
+
const role = parseRole(request.params.role);
|
|
21
|
+
return deps.sessionService.restartRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
22
|
+
});
|
|
23
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/resume", async (request) => {
|
|
24
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
25
|
+
const role = parseRole(request.params.role);
|
|
26
|
+
return deps.sessionService.resumeRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
27
|
+
});
|
|
28
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/dispatch", async (request) => {
|
|
29
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
30
|
+
if (!isDispatchableRole(request.params.role)) {
|
|
31
|
+
throw new VcmError({
|
|
32
|
+
code: "ROLE_NOT_DISPATCHABLE",
|
|
33
|
+
message: `${request.params.role} cannot receive role commands.`,
|
|
34
|
+
statusCode: 400
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return deps.commandDispatcher.dispatchRoleCommand({
|
|
38
|
+
repoRoot: project.repoRoot,
|
|
39
|
+
taskSlug: request.params.taskSlug,
|
|
40
|
+
role: request.params.role
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function parseRole(role) {
|
|
45
|
+
if (!isRoleName(role)) {
|
|
46
|
+
throw new VcmError({
|
|
47
|
+
code: "UNKNOWN_ROLE",
|
|
48
|
+
message: `Unknown role: ${role}`,
|
|
49
|
+
statusCode: 400
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return role;
|
|
53
|
+
}
|
|
54
|
+
async function requireCurrentProject(projectService) {
|
|
55
|
+
const project = await projectService.getCurrentProject();
|
|
56
|
+
if (!project) {
|
|
57
|
+
throw new VcmError({
|
|
58
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
59
|
+
message: "Connect a repository first.",
|
|
60
|
+
statusCode: 409
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return project;
|
|
64
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { VcmError } from "../errors.js";
|
|
2
|
+
export function registerTaskRoutes(app, deps) {
|
|
3
|
+
app.get("/api/tasks", async () => {
|
|
4
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
5
|
+
return deps.taskService.listTasks(project.repoRoot);
|
|
6
|
+
});
|
|
7
|
+
app.post("/api/tasks", async (request) => {
|
|
8
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
9
|
+
return deps.taskService.createTask(project.repoRoot, request.body);
|
|
10
|
+
});
|
|
11
|
+
app.get("/api/tasks/:taskSlug", async (request) => {
|
|
12
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
13
|
+
return deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
14
|
+
});
|
|
15
|
+
app.get("/api/tasks/:taskSlug/status", async (request) => {
|
|
16
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
17
|
+
return deps.statusService.getTaskStatus(project.repoRoot, request.params.taskSlug);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function requireCurrentProject(projectService) {
|
|
21
|
+
const project = await projectService.getCurrentProject();
|
|
22
|
+
if (!project) {
|
|
23
|
+
throw new VcmError({
|
|
24
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
25
|
+
message: "Connect a repository first.",
|
|
26
|
+
statusCode: 409
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return project;
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class VcmError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
statusCode;
|
|
4
|
+
hint;
|
|
5
|
+
constructor(input) {
|
|
6
|
+
super(input.message);
|
|
7
|
+
this.name = "VcmError";
|
|
8
|
+
this.code = input.code;
|
|
9
|
+
this.statusCode = input.statusCode ?? 400;
|
|
10
|
+
this.hint = input.hint;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function toVcmError(error) {
|
|
14
|
+
if (error instanceof VcmError) {
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
return new VcmError({
|
|
19
|
+
code: "INTERNAL_ERROR",
|
|
20
|
+
message: error.message,
|
|
21
|
+
statusCode: 500
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return new VcmError({
|
|
25
|
+
code: "INTERNAL_ERROR",
|
|
26
|
+
message: "Unknown error",
|
|
27
|
+
statusCode: 500
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as pty from "node-pty";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
export function createNodePtyTerminalRuntime(deps) {
|
|
4
|
+
const entries = new Map();
|
|
5
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
6
|
+
const id = deps.id ?? (() => `session_${Date.now()}_${Math.random().toString(16).slice(2)}`);
|
|
7
|
+
const emit = (entry, event) => {
|
|
8
|
+
const terminalEvent = {
|
|
9
|
+
id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
|
10
|
+
timestamp: now(),
|
|
11
|
+
...event
|
|
12
|
+
};
|
|
13
|
+
for (const listener of entry.listeners) {
|
|
14
|
+
listener(terminalEvent);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const create = async (input, sessionId = id()) => {
|
|
18
|
+
await deps.fs.ensureDir(input.logPath.replace(/[/\\][^/\\]+$/, ""));
|
|
19
|
+
const child = pty.spawn(input.command, input.args, {
|
|
20
|
+
cwd: input.cwd,
|
|
21
|
+
env: buildPtyEnvironment(process.env, input.env),
|
|
22
|
+
cols: input.cols ?? 100,
|
|
23
|
+
rows: input.rows ?? 28,
|
|
24
|
+
name: "xterm-256color"
|
|
25
|
+
});
|
|
26
|
+
const session = {
|
|
27
|
+
id: sessionId,
|
|
28
|
+
taskSlug: input.taskSlug,
|
|
29
|
+
role: input.role,
|
|
30
|
+
status: "running",
|
|
31
|
+
pid: child.pid,
|
|
32
|
+
startedAt: now(),
|
|
33
|
+
exitCode: null
|
|
34
|
+
};
|
|
35
|
+
const entry = {
|
|
36
|
+
input,
|
|
37
|
+
session,
|
|
38
|
+
process: child,
|
|
39
|
+
listeners: new Set()
|
|
40
|
+
};
|
|
41
|
+
entries.set(session.id, entry);
|
|
42
|
+
child.onData((data) => {
|
|
43
|
+
entry.session.lastOutputAt = now();
|
|
44
|
+
void deps.fs.appendText(input.logPath, data);
|
|
45
|
+
emit(entry, {
|
|
46
|
+
sessionId: session.id,
|
|
47
|
+
taskSlug: input.taskSlug,
|
|
48
|
+
role: input.role,
|
|
49
|
+
type: "output",
|
|
50
|
+
data
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
child.onExit(({ exitCode }) => {
|
|
54
|
+
entry.session.status = exitCode === 0 ? "exited" : "crashed";
|
|
55
|
+
entry.session.exitCode = exitCode;
|
|
56
|
+
emit(entry, {
|
|
57
|
+
sessionId: session.id,
|
|
58
|
+
taskSlug: input.taskSlug,
|
|
59
|
+
role: input.role,
|
|
60
|
+
type: "exit",
|
|
61
|
+
exitCode
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
return { ...session };
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
createSession(input) {
|
|
68
|
+
return create(input);
|
|
69
|
+
},
|
|
70
|
+
getSession(sessionId) {
|
|
71
|
+
const entry = entries.get(sessionId);
|
|
72
|
+
return entry ? { ...entry.session } : undefined;
|
|
73
|
+
},
|
|
74
|
+
getSessionByRole(taskSlug, role) {
|
|
75
|
+
for (const entry of entries.values()) {
|
|
76
|
+
if (entry.session.taskSlug === taskSlug && entry.session.role === role) {
|
|
77
|
+
return { ...entry.session };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
},
|
|
82
|
+
listSessions(taskSlug) {
|
|
83
|
+
return [...entries.values()]
|
|
84
|
+
.filter((entry) => !taskSlug || entry.session.taskSlug === taskSlug)
|
|
85
|
+
.map((entry) => ({ ...entry.session }));
|
|
86
|
+
},
|
|
87
|
+
write(sessionId, data) {
|
|
88
|
+
const entry = getEntry(entries, sessionId);
|
|
89
|
+
entry.process.write(data);
|
|
90
|
+
emit(entry, {
|
|
91
|
+
sessionId,
|
|
92
|
+
taskSlug: entry.session.taskSlug,
|
|
93
|
+
role: entry.session.role,
|
|
94
|
+
type: "input",
|
|
95
|
+
data
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
resize(sessionId, cols, rows) {
|
|
99
|
+
const entry = getEntry(entries, sessionId);
|
|
100
|
+
entry.process.resize(cols, rows);
|
|
101
|
+
},
|
|
102
|
+
async stop(sessionId) {
|
|
103
|
+
const entry = getEntry(entries, sessionId);
|
|
104
|
+
entry.session.status = "exited";
|
|
105
|
+
entry.process.kill();
|
|
106
|
+
},
|
|
107
|
+
async restart(sessionId) {
|
|
108
|
+
const entry = getEntry(entries, sessionId);
|
|
109
|
+
entry.process.kill();
|
|
110
|
+
return create(entry.input, sessionId);
|
|
111
|
+
},
|
|
112
|
+
subscribe(sessionId, listener) {
|
|
113
|
+
const entry = getEntry(entries, sessionId);
|
|
114
|
+
entry.listeners.add(listener);
|
|
115
|
+
void deps.fs.readText(entry.input.logPath)
|
|
116
|
+
.then((data) => {
|
|
117
|
+
if (!data || !entry.listeners.has(listener)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
listener({
|
|
121
|
+
id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
|
122
|
+
sessionId,
|
|
123
|
+
taskSlug: entry.session.taskSlug,
|
|
124
|
+
role: entry.session.role,
|
|
125
|
+
type: "output",
|
|
126
|
+
timestamp: now(),
|
|
127
|
+
data
|
|
128
|
+
});
|
|
129
|
+
})
|
|
130
|
+
.catch(() => {
|
|
131
|
+
// The log file may not exist yet for a brand-new session.
|
|
132
|
+
});
|
|
133
|
+
return () => {
|
|
134
|
+
entry.listeners.delete(listener);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function buildPtyEnvironment(baseEnv, inputEnv = {}) {
|
|
140
|
+
const env = {
|
|
141
|
+
...baseEnv,
|
|
142
|
+
...inputEnv,
|
|
143
|
+
TERM: inputEnv.TERM ?? "xterm-256color",
|
|
144
|
+
COLORTERM: inputEnv.COLORTERM ?? baseEnv.COLORTERM ?? "truecolor",
|
|
145
|
+
FORCE_COLOR: inputEnv.FORCE_COLOR ?? baseEnv.FORCE_COLOR ?? "3",
|
|
146
|
+
CLICOLOR: inputEnv.CLICOLOR ?? baseEnv.CLICOLOR ?? "1",
|
|
147
|
+
TERM_PROGRAM: inputEnv.TERM_PROGRAM ?? "VibeCodingMaster"
|
|
148
|
+
};
|
|
149
|
+
delete env.NO_COLOR;
|
|
150
|
+
return env;
|
|
151
|
+
}
|
|
152
|
+
function getEntry(entries, sessionId) {
|
|
153
|
+
const entry = entries.get(sessionId);
|
|
154
|
+
if (!entry) {
|
|
155
|
+
throw new VcmError({
|
|
156
|
+
code: "SESSION_MISSING",
|
|
157
|
+
message: `Terminal session does not exist: ${sessionId}`,
|
|
158
|
+
statusCode: 404
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return entry;
|
|
162
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function createSessionRegistry() {
|
|
2
|
+
const sessions = new Map();
|
|
3
|
+
return {
|
|
4
|
+
upsert(session) {
|
|
5
|
+
sessions.set(session.id, session);
|
|
6
|
+
},
|
|
7
|
+
get(sessionId) {
|
|
8
|
+
const session = sessions.get(sessionId);
|
|
9
|
+
return session ? { ...session } : undefined;
|
|
10
|
+
},
|
|
11
|
+
getByRole(taskSlug, role) {
|
|
12
|
+
const session = [...sessions.values()].find((candidate) => (candidate.taskSlug === taskSlug && candidate.role === role));
|
|
13
|
+
return session ? { ...session } : undefined;
|
|
14
|
+
},
|
|
15
|
+
list(taskSlug) {
|
|
16
|
+
return [...sessions.values()]
|
|
17
|
+
.filter((session) => !taskSlug || session.taskSlug === taskSlug)
|
|
18
|
+
.map((session) => ({ ...session }));
|
|
19
|
+
},
|
|
20
|
+
updateStatus(sessionId, status, patch = {}) {
|
|
21
|
+
const current = sessions.get(sessionId);
|
|
22
|
+
if (!current) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
sessions.set(sessionId, {
|
|
26
|
+
...current,
|
|
27
|
+
...patch,
|
|
28
|
+
status,
|
|
29
|
+
updatedAt: patch.updatedAt ?? new Date().toISOString()
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
remove(sessionId) {
|
|
33
|
+
sessions.delete(sessionId);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|