@chatroomcp/chatroom 0.1.3
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 +202 -0
- package/README.md +50 -0
- package/assets/chatroom.svg +15 -0
- package/chatroom.config.example.json +24 -0
- package/dist/app/application.d.ts +18 -0
- package/dist/app/application.js +63 -0
- package/dist/app/event-bus.d.ts +23 -0
- package/dist/app/event-bus.js +12 -0
- package/dist/app/external-access-registry.d.ts +18 -0
- package/dist/app/external-access-registry.js +48 -0
- package/dist/app/lifecycle.d.ts +12 -0
- package/dist/app/lifecycle.js +41 -0
- package/dist/auth/auth-service.d.ts +88 -0
- package/dist/auth/auth-service.js +222 -0
- package/dist/auth/ingress-policy.d.ts +21 -0
- package/dist/auth/ingress-policy.js +57 -0
- package/dist/auth/passkey-service.d.ts +39 -0
- package/dist/auth/passkey-service.js +145 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/run-cli.d.ts +1 -0
- package/dist/cli/run-cli.js +42 -0
- package/dist/config/load-config.d.ts +7 -0
- package/dist/config/load-config.js +182 -0
- package/dist/config/platform-paths.d.ts +6 -0
- package/dist/config/platform-paths.js +34 -0
- package/dist/config/types.d.ts +25 -0
- package/dist/config/types.js +1 -0
- package/dist/core/auth/passkey-repository.d.ts +18 -0
- package/dist/core/auth/passkey-repository.js +1 -0
- package/dist/core/auth/repository.d.ts +59 -0
- package/dist/core/auth/repository.js +7 -0
- package/dist/core/auth/session-repository.d.ts +6 -0
- package/dist/core/auth/session-repository.js +1 -0
- package/dist/core/errors/chatroom-error.d.ts +23 -0
- package/dist/core/errors/chatroom-error.js +46 -0
- package/dist/core/operations/bounded-value.d.ts +6 -0
- package/dist/core/operations/bounded-value.js +41 -0
- package/dist/core/operations/redactor.d.ts +4 -0
- package/dist/core/operations/redactor.js +65 -0
- package/dist/core/operations/repository.d.ts +19 -0
- package/dist/core/operations/repository.js +1 -0
- package/dist/core/operations/types.d.ts +31 -0
- package/dist/core/operations/types.js +9 -0
- package/dist/core/runtime/child-environment.d.ts +1 -0
- package/dist/core/runtime/child-environment.js +39 -0
- package/dist/core/runtime/command-runner.d.ts +15 -0
- package/dist/core/runtime/command-runner.js +39 -0
- package/dist/core/runtime/identity.d.ts +2 -0
- package/dist/core/runtime/identity.js +9 -0
- package/dist/infrastructure/database/app-database.d.ts +6 -0
- package/dist/infrastructure/database/app-database.js +104 -0
- package/dist/infrastructure/database/oauth-repository.d.ts +20 -0
- package/dist/infrastructure/database/oauth-repository.js +140 -0
- package/dist/infrastructure/database/operation-repository.d.ts +16 -0
- package/dist/infrastructure/database/operation-repository.js +129 -0
- package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
- package/dist/infrastructure/database/passkey-repository.js +57 -0
- package/dist/infrastructure/database/web-session-repository.d.ts +10 -0
- package/dist/infrastructure/database/web-session-repository.js +26 -0
- package/dist/infrastructure/http/http-server.d.ts +26 -0
- package/dist/infrastructure/http/http-server.js +166 -0
- package/dist/mcp/server/create-mcp-server.d.ts +3 -0
- package/dist/mcp/server/create-mcp-server.js +16 -0
- package/dist/mcp/server/tool-support.d.ts +105 -0
- package/dist/mcp/server/tool-support.js +124 -0
- package/dist/operations/operation-log.d.ts +23 -0
- package/dist/operations/operation-log.js +103 -0
- package/dist/plugins/cloud/api-client.d.ts +56 -0
- package/dist/plugins/cloud/api-client.js +122 -0
- package/dist/plugins/cloud/controller.d.ts +48 -0
- package/dist/plugins/cloud/controller.js +282 -0
- package/dist/plugins/cloud/device-proof.d.ts +9 -0
- package/dist/plugins/cloud/device-proof.js +17 -0
- package/dist/plugins/cloud/plugin.d.ts +4 -0
- package/dist/plugins/cloud/plugin.js +17 -0
- package/dist/plugins/cloud/state-store.d.ts +7 -0
- package/dist/plugins/cloud/state-store.js +74 -0
- package/dist/plugins/cloud/tunnel-client.d.ts +36 -0
- package/dist/plugins/cloud/tunnel-client.js +293 -0
- package/dist/plugins/cloud/types.d.ts +70 -0
- package/dist/plugins/cloud/types.js +32 -0
- package/dist/plugins/plugin-manager.d.ts +11 -0
- package/dist/plugins/plugin-manager.js +24 -0
- package/dist/plugins/process/backend.d.ts +19 -0
- package/dist/plugins/process/backend.js +1 -0
- package/dist/plugins/process/head-tail-buffer.d.ts +18 -0
- package/dist/plugins/process/head-tail-buffer.js +49 -0
- package/dist/plugins/process/infrastructure/pipe-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pipe-process-backend.js +61 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.js +50 -0
- package/dist/plugins/process/mcp.d.ts +4 -0
- package/dist/plugins/process/mcp.js +69 -0
- package/dist/plugins/process/plugin.d.ts +4 -0
- package/dist/plugins/process/plugin.js +28 -0
- package/dist/plugins/process/process-supervisor.d.ts +36 -0
- package/dist/plugins/process/process-supervisor.js +232 -0
- package/dist/plugins/process/types.d.ts +34 -0
- package/dist/plugins/process/types.js +1 -0
- package/dist/plugins/types.d.ts +32 -0
- package/dist/plugins/types.js +17 -0
- package/dist/plugins/web/api-types.d.ts +10 -0
- package/dist/plugins/web/api-types.js +1 -0
- package/dist/plugins/web/http/api-router.d.ts +13 -0
- package/dist/plugins/web/http/api-router.js +146 -0
- package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
- package/dist/plugins/web/http/cloud-api-router.js +41 -0
- package/dist/plugins/web/http/process-api-router.d.ts +3 -0
- package/dist/plugins/web/http/process-api-router.js +16 -0
- package/dist/plugins/web/http/workspace-api-router.d.ts +3 -0
- package/dist/plugins/web/http/workspace-api-router.js +82 -0
- package/dist/plugins/web/plugin.d.ts +8 -0
- package/dist/plugins/web/plugin.js +17 -0
- package/dist/plugins/web/runtime.d.ts +36 -0
- package/dist/plugins/web/runtime.js +66 -0
- package/dist/plugins/workspace/domain/filesystem.d.ts +25 -0
- package/dist/plugins/workspace/domain/filesystem.js +1 -0
- package/dist/plugins/workspace/domain/git.d.ts +7 -0
- package/dist/plugins/workspace/domain/git.js +1 -0
- package/dist/plugins/workspace/domain/repository.d.ts +16 -0
- package/dist/plugins/workspace/domain/repository.js +1 -0
- package/dist/plugins/workspace/domain/review.d.ts +28 -0
- package/dist/plugins/workspace/domain/review.js +1 -0
- package/dist/plugins/workspace/domain/workspace.d.ts +23 -0
- package/dist/plugins/workspace/domain/workspace.js +1 -0
- package/dist/plugins/workspace/git/git-command-runner.d.ts +6 -0
- package/dist/plugins/workspace/git/git-command-runner.js +15 -0
- package/dist/plugins/workspace/git/git-service.d.ts +14 -0
- package/dist/plugins/workspace/git/git-service.js +107 -0
- package/dist/plugins/workspace/git/git-snapshot-service.d.ts +33 -0
- package/dist/plugins/workspace/git/git-snapshot-service.js +352 -0
- package/dist/plugins/workspace/infrastructure/workspace-fs.d.ts +38 -0
- package/dist/plugins/workspace/infrastructure/workspace-fs.js +340 -0
- package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +12 -0
- package/dist/plugins/workspace/infrastructure/workspace-repository.js +51 -0
- package/dist/plugins/workspace/mcp.d.ts +4 -0
- package/dist/plugins/workspace/mcp.js +168 -0
- package/dist/plugins/workspace/plugin.d.ts +10 -0
- package/dist/plugins/workspace/plugin.js +37 -0
- package/dist/plugins/workspace/workspace-helpers.d.ts +9 -0
- package/dist/plugins/workspace/workspace-helpers.js +67 -0
- package/dist/plugins/workspace/workspace-service.d.ts +46 -0
- package/dist/plugins/workspace/workspace-service.js +339 -0
- package/dist/plugins/workspace/worktree-review-service.d.ts +16 -0
- package/dist/plugins/workspace/worktree-review-service.js +51 -0
- package/dist/presentation/http/http-utils.d.ts +7 -0
- package/dist/presentation/http/http-utils.js +47 -0
- package/dist/presentation/http/oauth-router.d.ts +5 -0
- package/dist/presentation/http/oauth-router.js +163 -0
- package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
- package/dist/web/assets/index-BF_mWl1D.js +26 -0
- package/dist/web/assets/index-JPZut9tc.css +1 -0
- package/dist/web/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/dist/web/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/dist/web/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/dist/web/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/dist/web/index.html +19 -0
- package/package.json +77 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CLOUD_SERVICE_SCHEMA } from "../../cloud/types.js";
|
|
4
|
+
import { asyncRoute } from "../../../presentation/http/http-utils.js";
|
|
5
|
+
export function createCloudApiRouter(controller, operations) {
|
|
6
|
+
const router = Router();
|
|
7
|
+
router.get("/cloud/status", (_req, res) => res.json(controller.status()));
|
|
8
|
+
router.post("/cloud/sync", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "sync" }, () => controller.syncStatus()))));
|
|
9
|
+
router.post("/cloud/management", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "management" }, () => controller.managementUrl()))));
|
|
10
|
+
router.post("/cloud/restore", asyncRoute(async (req, res) => {
|
|
11
|
+
const body = z
|
|
12
|
+
.object({ recoveryKey: z.string().min(20) })
|
|
13
|
+
.strict()
|
|
14
|
+
.parse(req.body);
|
|
15
|
+
res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "restore", input: body }, () => controller.restore(body.recoveryKey)));
|
|
16
|
+
}));
|
|
17
|
+
router.post("/cloud/recovery-key", asyncRoute(async (_req, res) => res.json(await operations.run({ pluginId: "cloud", source: "gui", action: "recovery-key.replace" }, async () => ({ recoveryKey: await controller.replaceRecoveryKey() })))));
|
|
18
|
+
router.post("/cloud/prefix", asyncRoute(async (req, res) => {
|
|
19
|
+
const body = z
|
|
20
|
+
.object({ prefix: z.string().min(1).max(64) })
|
|
21
|
+
.strict()
|
|
22
|
+
.parse(req.body);
|
|
23
|
+
res.json(await operations.run({
|
|
24
|
+
pluginId: "cloud",
|
|
25
|
+
source: "gui",
|
|
26
|
+
action: "prefix.set",
|
|
27
|
+
input: body,
|
|
28
|
+
}, () => controller.setPrefix(body.prefix)));
|
|
29
|
+
}));
|
|
30
|
+
router.post("/cloud/services/:service", asyncRoute(async (req, res) => {
|
|
31
|
+
const service = CLOUD_SERVICE_SCHEMA.parse(req.params.service);
|
|
32
|
+
const body = z.object({ enabled: z.boolean() }).strict().parse(req.body);
|
|
33
|
+
res.json(await operations.run({
|
|
34
|
+
pluginId: "cloud",
|
|
35
|
+
source: "gui",
|
|
36
|
+
action: "service.set",
|
|
37
|
+
input: { service, enabled: body.enabled },
|
|
38
|
+
}, () => controller.setService(service, body.enabled)));
|
|
39
|
+
}));
|
|
40
|
+
return router;
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { asyncRoute, requireString, } from "../../../presentation/http/http-utils.js";
|
|
3
|
+
export function createProcessApiRouter(application) {
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.get("/processes", (_req, res) => res.json(application.listProcesses()));
|
|
6
|
+
router.get("/processes/:processId", (req, res) => {
|
|
7
|
+
res.json(application.getProcess(requireString(req.params.processId, "processId")));
|
|
8
|
+
});
|
|
9
|
+
router.post("/processes/:processId/terminate", asyncRoute(async (req, res) => {
|
|
10
|
+
res.json(await application.processKill(requireString(req.params.processId, "processId"), false));
|
|
11
|
+
}));
|
|
12
|
+
router.post("/processes/:processId/kill", asyncRoute(async (req, res) => {
|
|
13
|
+
res.json(await application.processKill(requireString(req.params.processId, "processId"), true));
|
|
14
|
+
}));
|
|
15
|
+
return router;
|
|
16
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { ChatRoomError } from "../../../core/errors/chatroom-error.js";
|
|
3
|
+
import { asyncRoute, requireString, } from "../../../presentation/http/http-utils.js";
|
|
4
|
+
export function createWorkspaceApiRouter(application) {
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.get("/workspaces", asyncRoute(async (_req, res) => {
|
|
7
|
+
res.json(await application.discoverWorkspaces());
|
|
8
|
+
}));
|
|
9
|
+
router.get("/workspaces/:workspaceId", asyncRoute(async (req, res) => {
|
|
10
|
+
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
11
|
+
const workspace = application.getWorkspace(workspaceId);
|
|
12
|
+
const git = workspace.capabilities.git
|
|
13
|
+
? await application.gitInfoQuery(workspace.id)
|
|
14
|
+
: null;
|
|
15
|
+
res.json({ ...workspace, git });
|
|
16
|
+
}));
|
|
17
|
+
router.get("/workspaces/:workspaceId/files", asyncRoute(async (req, res) => {
|
|
18
|
+
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
19
|
+
const filePath = typeof req.query.path === "string" ? req.query.path : ".";
|
|
20
|
+
const recursive = req.query.recursive === "1";
|
|
21
|
+
res.json(await application.listWorkspaceFiles(workspaceId, filePath, recursive));
|
|
22
|
+
}));
|
|
23
|
+
router.get("/workspaces/:workspaceId/file", asyncRoute(async (req, res) => {
|
|
24
|
+
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
25
|
+
const filePath = requireString(req.query.path, "path");
|
|
26
|
+
res.json(await application.readWorkspaceFile(workspaceId, filePath, 2 * 1024 * 1024));
|
|
27
|
+
}));
|
|
28
|
+
router.get("/workspaces/:workspaceId/file/image", asyncRoute(async (req, res) => {
|
|
29
|
+
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
30
|
+
const filePath = requireString(req.query.path, "path");
|
|
31
|
+
const mime = imageMime(filePath);
|
|
32
|
+
if (!mime)
|
|
33
|
+
throw new ChatRoomError("INVALID_INPUT", "File type is not supported for image preview");
|
|
34
|
+
const file = await application.readWorkspaceFileBytes(workspaceId, filePath, 10 * 1024 * 1024);
|
|
35
|
+
if (file.truncated)
|
|
36
|
+
throw new ChatRoomError("INVALID_INPUT", "Image is too large to preview");
|
|
37
|
+
res.setHeader("Content-Type", mime);
|
|
38
|
+
res.setHeader("Content-Length", String(file.data.byteLength));
|
|
39
|
+
res.setHeader("Cache-Control", "no-store");
|
|
40
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
41
|
+
res.send(file.data);
|
|
42
|
+
}));
|
|
43
|
+
router.get("/workspaces/:workspaceId/worktree/diff", asyncRoute(async (req, res) => {
|
|
44
|
+
res.json(await application.previewWorktreeApply(requireString(req.params.workspaceId, "workspaceId")));
|
|
45
|
+
}));
|
|
46
|
+
router.get("/workspaces/:workspaceId/worktree/diff/file", asyncRoute(async (req, res) => {
|
|
47
|
+
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
48
|
+
res.json(await application.previewWorktreeFileDiff(workspaceId, requireString(req.query.path, "path")));
|
|
49
|
+
}));
|
|
50
|
+
router.post("/workspaces/:workspaceId/worktree/apply", asyncRoute(async (req, res) => {
|
|
51
|
+
const workspaceId = requireString(req.params.workspaceId, "workspaceId");
|
|
52
|
+
const body = (req.body ?? {});
|
|
53
|
+
if (body.paths !== undefined &&
|
|
54
|
+
(!Array.isArray(body.paths) ||
|
|
55
|
+
body.paths.some((value) => typeof value !== "string")))
|
|
56
|
+
throw new ChatRoomError("INVALID_INPUT", "paths must be an array of file paths");
|
|
57
|
+
res.json(await application.applyWorktree(workspaceId, body.paths));
|
|
58
|
+
}));
|
|
59
|
+
return router;
|
|
60
|
+
}
|
|
61
|
+
function imageMime(filePath) {
|
|
62
|
+
const extension = filePath.split(".").pop()?.toLowerCase();
|
|
63
|
+
switch (extension) {
|
|
64
|
+
case "png":
|
|
65
|
+
return "image/png";
|
|
66
|
+
case "jpg":
|
|
67
|
+
case "jpeg":
|
|
68
|
+
return "image/jpeg";
|
|
69
|
+
case "gif":
|
|
70
|
+
return "image/gif";
|
|
71
|
+
case "webp":
|
|
72
|
+
return "image/webp";
|
|
73
|
+
case "avif":
|
|
74
|
+
return "image/avif";
|
|
75
|
+
case "bmp":
|
|
76
|
+
return "image/bmp";
|
|
77
|
+
case "ico":
|
|
78
|
+
return "image/x-icon";
|
|
79
|
+
default:
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InternalPlugin } from "../types.js";
|
|
2
|
+
import { WebRuntime } from "./runtime.js";
|
|
3
|
+
interface WebPluginService {
|
|
4
|
+
application: WebRuntime;
|
|
5
|
+
}
|
|
6
|
+
export declare const WebServiceToken: import("../types.js").ServiceToken<WebPluginService>;
|
|
7
|
+
export declare function createWebPlugin(): InternalPlugin;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createServiceToken } from "../types.js";
|
|
2
|
+
import { WorkspaceService } from "../workspace/plugin.js";
|
|
3
|
+
import { ProcessService } from "../process/plugin.js";
|
|
4
|
+
import { WebRuntime } from "./runtime.js";
|
|
5
|
+
export const WebServiceToken = createServiceToken("web");
|
|
6
|
+
export function createWebPlugin() {
|
|
7
|
+
return {
|
|
8
|
+
id: "web",
|
|
9
|
+
activate(context) {
|
|
10
|
+
const workspace = context.services.require(WorkspaceService);
|
|
11
|
+
const processes = context.services.require(ProcessService);
|
|
12
|
+
context.services.provide(WebServiceToken, {
|
|
13
|
+
application: new WebRuntime(workspace.workspaces, context.operations, processes, workspace.git),
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OperationLog } from "../../operations/operation-log.js";
|
|
2
|
+
import type { WorkspaceService } from "../workspace/workspace-service.js";
|
|
3
|
+
import type { ProcessSupervisor } from "../process/process-supervisor.js";
|
|
4
|
+
import type { GitService } from "../workspace/git/git-service.js";
|
|
5
|
+
export declare class WebRuntime {
|
|
6
|
+
readonly workspaces: WorkspaceService;
|
|
7
|
+
readonly operations: OperationLog;
|
|
8
|
+
readonly processes: ProcessSupervisor;
|
|
9
|
+
readonly git: GitService;
|
|
10
|
+
constructor(workspaces: WorkspaceService, operations: OperationLog, processes: ProcessSupervisor, git: GitService);
|
|
11
|
+
previewWorktreeApply(workspaceId: string): Promise<import("./api-types.js").WorktreeApplyPreview>;
|
|
12
|
+
previewWorktreeFileDiff(workspaceId: string, filePath: string): Promise<import("./api-types.js").WorktreeFileDiff>;
|
|
13
|
+
applyWorktree(workspaceId: string, paths?: string[]): Promise<{
|
|
14
|
+
workspaceId: string;
|
|
15
|
+
bytes: number;
|
|
16
|
+
paths: string[];
|
|
17
|
+
conflicts: string[];
|
|
18
|
+
}>;
|
|
19
|
+
processKill(processId: string, force?: boolean): Promise<import("./api-types.js").ProcessSnapshot>;
|
|
20
|
+
listProcesses(): import("./api-types.js").ProcessSnapshot[];
|
|
21
|
+
getProcess(processId: string): import("./api-types.js").ProcessSnapshot;
|
|
22
|
+
discoverWorkspaces(): Promise<import("../workspace/domain/workspace.js").Workspace[]>;
|
|
23
|
+
getWorkspace(workspaceId: string): import("../workspace/domain/workspace.js").Workspace;
|
|
24
|
+
readWorkspaceFile(workspaceId: string, filePath: string, maxBytes?: number): Promise<{
|
|
25
|
+
content: string;
|
|
26
|
+
bytes: number;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
readWorkspaceFileBytes(workspaceId: string, filePath: string, maxBytes: number): Promise<{
|
|
30
|
+
data: Buffer;
|
|
31
|
+
bytes: number;
|
|
32
|
+
truncated: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
listWorkspaceFiles(workspaceId: string, filePath?: string, recursive?: boolean): Promise<import("../workspace/domain/filesystem.js").FileInfo[]>;
|
|
35
|
+
gitInfoQuery(workspaceId: string): Promise<import("../workspace/domain/git.js").GitInfo>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export class WebRuntime {
|
|
2
|
+
workspaces;
|
|
3
|
+
operations;
|
|
4
|
+
processes;
|
|
5
|
+
git;
|
|
6
|
+
constructor(workspaces, operations, processes, git) {
|
|
7
|
+
this.workspaces = workspaces;
|
|
8
|
+
this.operations = operations;
|
|
9
|
+
this.processes = processes;
|
|
10
|
+
this.git = git;
|
|
11
|
+
}
|
|
12
|
+
previewWorktreeApply(workspaceId) {
|
|
13
|
+
return this.workspaces.previewWorktreeApply(workspaceId);
|
|
14
|
+
}
|
|
15
|
+
previewWorktreeFileDiff(workspaceId, filePath) {
|
|
16
|
+
return this.workspaces.previewWorktreeFileDiff(workspaceId, filePath);
|
|
17
|
+
}
|
|
18
|
+
applyWorktree(workspaceId, paths) {
|
|
19
|
+
return this.operations.run({
|
|
20
|
+
pluginId: "workspace",
|
|
21
|
+
source: "gui",
|
|
22
|
+
action: "worktree.apply",
|
|
23
|
+
workspaceId,
|
|
24
|
+
input: { workspaceId, paths: paths ?? null },
|
|
25
|
+
}, () => this.workspaces.applyWorktree(workspaceId, paths));
|
|
26
|
+
}
|
|
27
|
+
processKill(processId, force = false) {
|
|
28
|
+
return this.operations.run({
|
|
29
|
+
pluginId: "process",
|
|
30
|
+
source: "gui",
|
|
31
|
+
action: force ? "kill" : "terminate",
|
|
32
|
+
processId,
|
|
33
|
+
input: { processId, force },
|
|
34
|
+
}, async () => this.processes.kill(processId, force));
|
|
35
|
+
}
|
|
36
|
+
listProcesses() {
|
|
37
|
+
return this.processes.list();
|
|
38
|
+
}
|
|
39
|
+
getProcess(processId) {
|
|
40
|
+
return this.processes.read(processId);
|
|
41
|
+
}
|
|
42
|
+
discoverWorkspaces() {
|
|
43
|
+
return this.workspaces.discoverGitWorkspaces();
|
|
44
|
+
}
|
|
45
|
+
getWorkspace(workspaceId) {
|
|
46
|
+
return this.workspaces.get(workspaceId);
|
|
47
|
+
}
|
|
48
|
+
async readWorkspaceFile(workspaceId, filePath, maxBytes) {
|
|
49
|
+
const fs = await this.workspaces.fs(workspaceId);
|
|
50
|
+
return fs.read(filePath, {
|
|
51
|
+
...(maxBytes === undefined ? {} : { maxBytes }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async readWorkspaceFileBytes(workspaceId, filePath, maxBytes) {
|
|
55
|
+
const fs = await this.workspaces.fs(workspaceId);
|
|
56
|
+
return fs.readBytes(filePath, { maxBytes });
|
|
57
|
+
}
|
|
58
|
+
async listWorkspaceFiles(workspaceId, filePath = ".", recursive = false) {
|
|
59
|
+
const fs = await this.workspaces.fs(workspaceId);
|
|
60
|
+
return fs.list(filePath, { recursive });
|
|
61
|
+
}
|
|
62
|
+
gitInfoQuery(workspaceId) {
|
|
63
|
+
const workspace = this.workspaces.markUsed(workspaceId);
|
|
64
|
+
return this.git.info(workspace.root);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Workspace filesystem value types. */
|
|
2
|
+
export interface FileInfo {
|
|
3
|
+
path: string;
|
|
4
|
+
type: "file" | "directory" | "symlink";
|
|
5
|
+
size: number;
|
|
6
|
+
modifiedAt: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SearchMatch {
|
|
9
|
+
path: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
preview: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PatchReplacement {
|
|
15
|
+
path: string;
|
|
16
|
+
oldText: string;
|
|
17
|
+
newText: string;
|
|
18
|
+
occurrence?: number | "all";
|
|
19
|
+
}
|
|
20
|
+
export interface ChangeSet {
|
|
21
|
+
files: string[];
|
|
22
|
+
replacements: number;
|
|
23
|
+
bytesBefore: number;
|
|
24
|
+
bytesAfter: number;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkspaceMode } from "./workspace.js";
|
|
2
|
+
export interface WorkspaceRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
root: string;
|
|
5
|
+
sourceRoot: string;
|
|
6
|
+
mode: WorkspaceMode;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
lastUsedAt: string;
|
|
9
|
+
}
|
|
10
|
+
export interface WorkspaceStateRepository {
|
|
11
|
+
upsert(workspace: WorkspaceRecord): void;
|
|
12
|
+
get(workspaceId: string): WorkspaceRecord | null;
|
|
13
|
+
findByRoot(root: string, mode: WorkspaceMode): WorkspaceRecord | null;
|
|
14
|
+
list(): WorkspaceRecord[];
|
|
15
|
+
remove(workspaceId: string): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type WorktreeFileStatus = "added" | "modified" | "deleted" | "type-changed";
|
|
2
|
+
export interface WorktreeReviewFile {
|
|
3
|
+
path: string;
|
|
4
|
+
status: WorktreeFileStatus;
|
|
5
|
+
additions: number | null;
|
|
6
|
+
deletions: number | null;
|
|
7
|
+
binary: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface WorktreeApplyPreview {
|
|
10
|
+
files: Array<WorktreeReviewFile & {
|
|
11
|
+
applied: boolean;
|
|
12
|
+
conflict: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
totalFiles: number;
|
|
15
|
+
pendingFiles: number;
|
|
16
|
+
appliedFiles: number;
|
|
17
|
+
mergeableFiles: number;
|
|
18
|
+
conflictFiles: number;
|
|
19
|
+
canApply: boolean;
|
|
20
|
+
reason: "no-changes" | "merge-conflicts" | "head-mismatch" | null;
|
|
21
|
+
}
|
|
22
|
+
export interface WorktreeFileDiff {
|
|
23
|
+
path: string;
|
|
24
|
+
diff: string;
|
|
25
|
+
bytes: number;
|
|
26
|
+
truncated: boolean;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain model for a persistent ChatRoom workspace.
|
|
3
|
+
* Workspace identity is persistent and independent from transport connection lifetime.
|
|
4
|
+
*/
|
|
5
|
+
/** `checkout` uses sourceRoot directly; `worktree` uses a managed detached Git worktree. */
|
|
6
|
+
export type WorkspaceMode = "checkout" | "worktree";
|
|
7
|
+
interface WorkspaceCapabilities {
|
|
8
|
+
filesystem: "read-write";
|
|
9
|
+
git: boolean;
|
|
10
|
+
skills: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface Workspace {
|
|
13
|
+
id: string;
|
|
14
|
+
root: string;
|
|
15
|
+
sourceRoot: string;
|
|
16
|
+
mode: WorkspaceMode;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
lastUsedAt: string;
|
|
19
|
+
instructions: string[];
|
|
20
|
+
skills: string[];
|
|
21
|
+
capabilities: WorkspaceCapabilities;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CommandRunner } from "../../../core/runtime/command-runner.js";
|
|
2
|
+
export declare class GitCommandRunner {
|
|
3
|
+
private readonly commands;
|
|
4
|
+
constructor(commands: CommandRunner);
|
|
5
|
+
run(cwd: string, args: string[], env?: Record<string, string>): Promise<import("../../../core/runtime/command-runner.js").CommandResult>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class GitCommandRunner {
|
|
2
|
+
commands;
|
|
3
|
+
constructor(commands) {
|
|
4
|
+
this.commands = commands;
|
|
5
|
+
}
|
|
6
|
+
run(cwd, args, env) {
|
|
7
|
+
return this.commands.run({
|
|
8
|
+
cwd,
|
|
9
|
+
command: "git",
|
|
10
|
+
args,
|
|
11
|
+
timeoutMs: 120_000,
|
|
12
|
+
env: { LC_ALL: "C", LANG: "C", ...(env ?? {}) },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GitInfo } from "../domain/git.js";
|
|
2
|
+
import type { GitCommandRunner } from "./git-command-runner.js";
|
|
3
|
+
export declare class GitService {
|
|
4
|
+
private readonly runner;
|
|
5
|
+
constructor(runner: GitCommandRunner);
|
|
6
|
+
info(cwd: string): Promise<GitInfo>;
|
|
7
|
+
head(cwd: string): Promise<string | null>;
|
|
8
|
+
branch(cwd: string): Promise<string | null>;
|
|
9
|
+
status(cwd: string): Promise<string>;
|
|
10
|
+
diff(cwd: string): Promise<string>;
|
|
11
|
+
createWorktree(sourceRoot: string, target: string): Promise<void>;
|
|
12
|
+
removeWorktree(sourceRoot: string, target: string, force?: boolean): Promise<void>;
|
|
13
|
+
pruneWorktrees(sourceRoot: string): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ChatRoomError } from "../../../core/errors/chatroom-error.js";
|
|
3
|
+
export class GitService {
|
|
4
|
+
runner;
|
|
5
|
+
constructor(runner) {
|
|
6
|
+
this.runner = runner;
|
|
7
|
+
}
|
|
8
|
+
async info(cwd) {
|
|
9
|
+
try {
|
|
10
|
+
const root = (await this.runner.run(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11
|
+
const [head, branch, status] = await Promise.all([
|
|
12
|
+
this.head(cwd),
|
|
13
|
+
this.branch(cwd),
|
|
14
|
+
this.runner.run(cwd, ["status", "--porcelain"]),
|
|
15
|
+
]);
|
|
16
|
+
return {
|
|
17
|
+
isRepository: true,
|
|
18
|
+
branch,
|
|
19
|
+
head,
|
|
20
|
+
dirty: status.stdout.trim().length > 0,
|
|
21
|
+
root: path.resolve(root),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (isNotRepositoryError(error))
|
|
26
|
+
return {
|
|
27
|
+
isRepository: false,
|
|
28
|
+
branch: null,
|
|
29
|
+
head: null,
|
|
30
|
+
dirty: false,
|
|
31
|
+
root: null,
|
|
32
|
+
};
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async head(cwd) {
|
|
37
|
+
try {
|
|
38
|
+
return ((await this.runner.run(cwd, ["rev-parse", "--verify", "HEAD"])).stdout.trim() || null);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (isMissingHeadError(error))
|
|
42
|
+
return null;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async branch(cwd) {
|
|
47
|
+
const value = (await this.runner.run(cwd, ["branch", "--show-current"])).stdout.trim();
|
|
48
|
+
return value || null;
|
|
49
|
+
}
|
|
50
|
+
async status(cwd) {
|
|
51
|
+
return (await this.runner.run(cwd, ["status", "--short", "--branch"]))
|
|
52
|
+
.stdout;
|
|
53
|
+
}
|
|
54
|
+
async diff(cwd) {
|
|
55
|
+
const [unstaged, staged] = await Promise.all([
|
|
56
|
+
this.runner.run(cwd, ["diff", "--no-ext-diff", "--unified=3"]),
|
|
57
|
+
this.runner.run(cwd, [
|
|
58
|
+
"diff",
|
|
59
|
+
"--cached",
|
|
60
|
+
"--no-ext-diff",
|
|
61
|
+
"--unified=3",
|
|
62
|
+
]),
|
|
63
|
+
]);
|
|
64
|
+
return [unstaged.stdout, staged.stdout].filter(Boolean).join("\n");
|
|
65
|
+
}
|
|
66
|
+
async createWorktree(sourceRoot, target) {
|
|
67
|
+
const info = await this.info(sourceRoot);
|
|
68
|
+
if (!info.isRepository)
|
|
69
|
+
throw new ChatRoomError("INVALID_INPUT", "Worktree mode requires a Git repository");
|
|
70
|
+
if (!info.head)
|
|
71
|
+
throw new ChatRoomError("CONFLICT", "Worktree mode requires at least one Git commit; repository HEAD does not exist");
|
|
72
|
+
await this.runner.run(sourceRoot, [
|
|
73
|
+
"worktree",
|
|
74
|
+
"add",
|
|
75
|
+
"--detach",
|
|
76
|
+
target,
|
|
77
|
+
"HEAD",
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
async removeWorktree(sourceRoot, target, force = false) {
|
|
81
|
+
await this.runner.run(sourceRoot, [
|
|
82
|
+
"worktree",
|
|
83
|
+
"remove",
|
|
84
|
+
...(force ? ["--force"] : []),
|
|
85
|
+
target,
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
async pruneWorktrees(sourceRoot) {
|
|
89
|
+
await this.runner.run(sourceRoot, ["worktree", "prune"]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function gitErrorText(error) {
|
|
93
|
+
if (!(error instanceof ChatRoomError))
|
|
94
|
+
return "";
|
|
95
|
+
const details = error.details;
|
|
96
|
+
return `${String(details?.stdout ?? "")}\n${String(details?.stderr ?? "")}`.toLowerCase();
|
|
97
|
+
}
|
|
98
|
+
function isNotRepositoryError(error) {
|
|
99
|
+
return gitErrorText(error).includes("not a git repository");
|
|
100
|
+
}
|
|
101
|
+
function isMissingHeadError(error) {
|
|
102
|
+
const text = gitErrorText(error);
|
|
103
|
+
return (text.includes("needed a single revision") ||
|
|
104
|
+
text.includes("unknown revision") ||
|
|
105
|
+
text.includes("bad revision") ||
|
|
106
|
+
text.includes("ambiguous argument 'head'"));
|
|
107
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WorktreeFileDiff, WorktreeReviewFile } from "../domain/review.js";
|
|
2
|
+
import type { GitCommandRunner } from "./git-command-runner.js";
|
|
3
|
+
import type { GitService } from "./git-service.js";
|
|
4
|
+
interface WorktreeMergeFile extends WorktreeReviewFile {
|
|
5
|
+
applied: boolean;
|
|
6
|
+
conflict: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class GitSnapshotService {
|
|
9
|
+
private readonly runner;
|
|
10
|
+
private readonly git;
|
|
11
|
+
constructor(runner: GitCommandRunner, git: GitService);
|
|
12
|
+
fileDiff(cwd: string, filePath: string, maxPreviewBytes?: number): Promise<WorktreeFileDiff>;
|
|
13
|
+
apply(worktreeRoot: string, sourceRoot: string, requestedPaths?: string[]): Promise<{
|
|
14
|
+
bytes: number;
|
|
15
|
+
paths: string[];
|
|
16
|
+
conflicts: string[];
|
|
17
|
+
}>;
|
|
18
|
+
applyState(worktreeRoot: string, sourceRoot: string): Promise<{
|
|
19
|
+
files: WorktreeMergeFile[];
|
|
20
|
+
}>;
|
|
21
|
+
private analyze;
|
|
22
|
+
private mergeTrees;
|
|
23
|
+
private snapshot;
|
|
24
|
+
private treeForPaths;
|
|
25
|
+
private baseTree;
|
|
26
|
+
private writeTree;
|
|
27
|
+
private stageAll;
|
|
28
|
+
private stateFromIndex;
|
|
29
|
+
private writeHeadPatch;
|
|
30
|
+
private writeTreePatch;
|
|
31
|
+
private withIndex;
|
|
32
|
+
}
|
|
33
|
+
export {};
|