@chatroomcp/chatroom 0.2.1 → 0.2.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/README.md +5 -3
- package/dist/app/application.d.ts +0 -1
- package/dist/app/application.js +2 -0
- package/dist/app/event-bus.d.ts +0 -2
- package/dist/app/event-bus.js +0 -1
- package/dist/app/external-access-registry.d.ts +0 -1
- package/dist/app/external-access-registry.js +0 -1
- package/dist/app/lifecycle.d.ts +0 -1
- package/dist/auth/auth-service.js +0 -4
- package/dist/auth/ingress-policy.d.ts +0 -1
- package/dist/auth/ingress-policy.js +0 -1
- package/dist/cli/run-cli.js +0 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/core/auth/repository.d.ts +0 -23
- package/dist/core/auth/repository.js +0 -6
- package/dist/core/errors/chatroom-error.d.ts +0 -15
- package/dist/core/errors/chatroom-error.js +0 -15
- package/dist/core/operations/bounded-value.d.ts +0 -1
- package/dist/core/operations/bounded-value.js +26 -10
- package/dist/core/operations/redactor.js +0 -1
- package/dist/core/operations/repository.d.ts +0 -1
- package/dist/core/operations/types.d.ts +0 -4
- package/dist/core/operations/types.js +0 -2
- package/dist/infrastructure/database/app-database.js +4 -97
- package/dist/infrastructure/database/migrations.d.ts +2 -0
- package/dist/infrastructure/database/migrations.js +44 -0
- package/dist/infrastructure/database/operation-repository.js +3 -9
- package/dist/infrastructure/database/schema.d.ts +2 -0
- package/dist/infrastructure/database/schema.js +85 -0
- package/dist/infrastructure/http/http-server.js +0 -1
- package/dist/mcp/server/create-mcp-server.js +3 -3
- package/dist/mcp/server/plugin-mcp-registrar.d.ts +0 -2
- package/dist/mcp/server/plugin-mcp-registrar.js +0 -4
- package/dist/mcp/server/tool-support.d.ts +0 -61
- package/dist/mcp/server/tool-support.js +0 -54
- package/dist/native/linux/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +1 -1
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/operations/operation-log.d.ts +0 -3
- package/dist/operations/operation-log.js +0 -15
- package/dist/plugins/cloud/api-client.js +1 -0
- package/dist/plugins/cloud/controller.d.ts +2 -0
- package/dist/plugins/cloud/controller.js +28 -5
- package/dist/plugins/cloud/tunnel-client.js +0 -4
- package/dist/plugins/git/git-service.d.ts +33 -0
- package/dist/plugins/git/git-service.js +414 -0
- package/dist/plugins/git/plugin.d.ts +4 -0
- package/dist/plugins/git/plugin.js +12 -0
- package/dist/plugins/git/types.d.ts +32 -0
- package/dist/plugins/process/backend.d.ts +0 -1
- package/dist/plugins/process/head-tail-buffer.d.ts +0 -1
- package/dist/plugins/process/infrastructure/pipe-process-backend.js +0 -1
- package/dist/plugins/process/infrastructure/pty-process-backend.js +1 -4
- package/dist/plugins/process/process-supervisor.js +10 -23
- package/dist/plugins/process/types.d.ts +0 -5
- package/dist/plugins/types.d.ts +0 -1
- package/dist/plugins/web/api-types.d.ts +3 -7
- package/dist/plugins/web/http/api-router.d.ts +0 -1
- package/dist/plugins/web/http/api-router.js +2 -4
- package/dist/plugins/web/http/git-api-router.d.ts +3 -0
- package/dist/plugins/web/http/git-api-router.js +98 -0
- package/dist/plugins/web/http/workspace-api-router.js +51 -37
- package/dist/plugins/web/plugin.js +4 -6
- package/dist/plugins/web/runtime.d.ts +5 -27
- package/dist/plugins/web/runtime.js +3 -42
- package/dist/plugins/workspace/mcp.js +20 -135
- package/dist/plugins/workspace/metadata.d.ts +6 -0
- package/dist/plugins/workspace/metadata.js +115 -0
- package/dist/plugins/workspace/plugin.d.ts +2 -8
- package/dist/plugins/workspace/plugin.js +8 -20
- package/dist/plugins/workspace/types.d.ts +23 -0
- package/dist/plugins/workspace/{infrastructure/workspace-fs.d.ts → workspace-fs.d.ts} +4 -14
- package/dist/plugins/workspace/workspace-fs.js +200 -0
- package/dist/plugins/workspace/workspace-service.d.ts +11 -42
- package/dist/plugins/workspace/workspace-service.js +103 -302
- package/dist/presentation/http/http-utils.d.ts +0 -1
- package/dist/presentation/http/oauth-router.d.ts +0 -1
- package/dist/presentation/http/oauth-router.js +0 -1
- package/dist/web/assets/index-CEzdY5mz.js +27 -0
- package/dist/web/assets/index-D_mpjwCG.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -3
- package/dist/plugins/workspace/domain/filesystem.d.ts +0 -25
- package/dist/plugins/workspace/domain/git.d.ts +0 -7
- package/dist/plugins/workspace/domain/repository.d.ts +0 -16
- package/dist/plugins/workspace/domain/repository.js +0 -1
- package/dist/plugins/workspace/domain/review.d.ts +0 -28
- package/dist/plugins/workspace/domain/review.js +0 -1
- package/dist/plugins/workspace/domain/workspace.d.ts +0 -23
- package/dist/plugins/workspace/domain/workspace.js +0 -1
- package/dist/plugins/workspace/git/git-command-runner.d.ts +0 -6
- package/dist/plugins/workspace/git/git-command-runner.js +0 -15
- package/dist/plugins/workspace/git/git-service.d.ts +0 -14
- package/dist/plugins/workspace/git/git-service.js +0 -107
- package/dist/plugins/workspace/git/git-snapshot-service.d.ts +0 -33
- package/dist/plugins/workspace/git/git-snapshot-service.js +0 -352
- package/dist/plugins/workspace/infrastructure/workspace-fs.js +0 -340
- package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +0 -12
- package/dist/plugins/workspace/infrastructure/workspace-repository.js +0 -51
- package/dist/plugins/workspace/workspace-helpers.d.ts +0 -9
- package/dist/plugins/workspace/workspace-helpers.js +0 -67
- package/dist/plugins/workspace/worktree-review-service.d.ts +0 -16
- package/dist/plugins/workspace/worktree-review-service.js +0 -51
- package/dist/web/assets/index-B4vO7wVS.css +0 -1
- package/dist/web/assets/index-BU7LCgKB.js +0 -26
- /package/dist/plugins/{workspace/domain/filesystem.js → git/types.js} +0 -0
- /package/dist/plugins/workspace/{domain/git.js → types.js} +0 -0
|
@@ -1,339 +1,140 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Workspace lifetime is deliberately independent from MCP or HTTP connection lifetime.
|
|
4
|
-
*/
|
|
5
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
6
|
-
import { mkdir, readdir, realpath, rm } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
7
3
|
import path from "node:path";
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
4
|
import { ChatRoomError } from "../../core/errors/chatroom-error.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
5
|
+
import { readInstructions, readPresetPrompt, readSkills, readSummary, } from "./metadata.js";
|
|
6
|
+
import { WorkspaceFs } from "./workspace-fs.js";
|
|
12
7
|
export class WorkspaceService {
|
|
13
|
-
repository;
|
|
14
|
-
git;
|
|
15
|
-
review;
|
|
16
8
|
allowedRoots;
|
|
17
|
-
|
|
18
|
-
pending = new Map();
|
|
19
|
-
filesystems = new Map();
|
|
20
|
-
workspaces = new Map();
|
|
21
|
-
constructor(repository, git, review, allowedRoots, dataDir) {
|
|
22
|
-
this.repository = repository;
|
|
23
|
-
this.git = git;
|
|
24
|
-
this.review = review;
|
|
9
|
+
constructor(allowedRoots) {
|
|
25
10
|
this.allowedRoots = allowedRoots;
|
|
26
|
-
this.dataDir = dataDir;
|
|
27
11
|
}
|
|
28
|
-
static async create(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for (const root of allowedRoots) {
|
|
32
|
-
const expanded = expandHome(root);
|
|
33
|
-
const canonical = await realpath(expanded).catch(() => {
|
|
12
|
+
static async create(allowedRoots) {
|
|
13
|
+
const canonicalRoots = await Promise.all(allowedRoots.map(async (root) => {
|
|
14
|
+
const canonical = await realpath(expandHome(root)).catch(() => {
|
|
34
15
|
throw new ChatRoomError("NOT_FOUND", `Allowed root does not exist: ${root}`);
|
|
35
16
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const mode = request.mode ?? "checkout";
|
|
49
|
-
const sourceRoot = await this.validateSourceRoot(request.path);
|
|
50
|
-
if (mode === "worktree")
|
|
51
|
-
return this.openLocked(sourceRoot, mode);
|
|
52
|
-
const key = `checkout:${sourceRoot}`;
|
|
53
|
-
// Coalesce concurrent opens of the same logical workspace instead of racing worktree creation or persistence.
|
|
54
|
-
const existingPending = this.pending.get(key);
|
|
55
|
-
if (existingPending)
|
|
56
|
-
return existingPending;
|
|
57
|
-
const operation = this.openLocked(sourceRoot, mode).finally(() => this.pending.delete(key));
|
|
58
|
-
this.pending.set(key, operation);
|
|
59
|
-
return operation;
|
|
60
|
-
}
|
|
61
|
-
get(workspaceId) {
|
|
62
|
-
const workspace = this.workspaces.get(workspaceId);
|
|
63
|
-
if (!workspace)
|
|
64
|
-
throw new ChatRoomError("NOT_FOUND", `Unknown WorkspaceId: ${workspaceId}`);
|
|
65
|
-
if (!this.isRecordValid(workspace)) {
|
|
66
|
-
this.filesystems.delete(workspace.id);
|
|
67
|
-
this.workspaces.delete(workspace.id);
|
|
68
|
-
this.repository.remove(workspace.id);
|
|
69
|
-
throw new ChatRoomError("NOT_FOUND", `Workspace no longer exists: ${workspaceId}`);
|
|
70
|
-
}
|
|
71
|
-
return workspace;
|
|
72
|
-
}
|
|
73
|
-
list() {
|
|
74
|
-
const valid = [];
|
|
75
|
-
for (const workspace of this.workspaces.values()) {
|
|
76
|
-
if (this.isRecordValid(workspace)) {
|
|
77
|
-
valid.push(workspace);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
this.filesystems.delete(workspace.id);
|
|
81
|
-
this.workspaces.delete(workspace.id);
|
|
82
|
-
this.repository.remove(workspace.id);
|
|
83
|
-
}
|
|
84
|
-
return valid.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
85
|
-
}
|
|
86
|
-
async discoverGitWorkspaces() {
|
|
87
|
-
for (const root of this.allowedRoots) {
|
|
88
|
-
await this.discoverGitWorkspace(root);
|
|
17
|
+
if (!(await stat(canonical)).isDirectory())
|
|
18
|
+
throw new ChatRoomError("INVALID_INPUT", `Allowed root is not a directory: ${root}`);
|
|
19
|
+
return canonical;
|
|
20
|
+
}));
|
|
21
|
+
return new WorkspaceService([...new Set(canonicalRoots)]);
|
|
22
|
+
}
|
|
23
|
+
roots() {
|
|
24
|
+
return [...this.allowedRoots];
|
|
25
|
+
}
|
|
26
|
+
async list() {
|
|
27
|
+
const roots = new Set();
|
|
28
|
+
for (const allowedRoot of this.allowedRoots) {
|
|
89
29
|
let entries;
|
|
90
30
|
try {
|
|
91
|
-
entries = await readdir(
|
|
31
|
+
entries = await readdir(allowedRoot, { withFileTypes: true });
|
|
92
32
|
}
|
|
93
33
|
catch {
|
|
94
34
|
continue;
|
|
95
35
|
}
|
|
96
36
|
for (const entry of entries) {
|
|
97
|
-
if (!entry.isDirectory() ||
|
|
37
|
+
if (!entry.isDirectory() ||
|
|
38
|
+
entry.isSymbolicLink() ||
|
|
39
|
+
entry.name.startsWith("."))
|
|
98
40
|
continue;
|
|
99
|
-
await
|
|
41
|
+
const candidate = await realpath(path.join(allowedRoot, entry.name)).catch(() => null);
|
|
42
|
+
if (candidate && this.isWorkspaceRoot(candidate))
|
|
43
|
+
roots.add(candidate);
|
|
100
44
|
}
|
|
101
45
|
}
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const existing = this.filesystems.get(workspaceId);
|
|
125
|
-
if (existing)
|
|
126
|
-
return existing;
|
|
127
|
-
const filesystem = await WorkspaceFs.create(workspace.root);
|
|
128
|
-
this.filesystems.set(workspaceId, filesystem);
|
|
129
|
-
return filesystem;
|
|
130
|
-
}
|
|
131
|
-
async refresh(workspaceId) {
|
|
132
|
-
const workspace = this.get(workspaceId);
|
|
133
|
-
const fs = await this.fs(workspaceId);
|
|
134
|
-
const gitInfo = await this.git.info(workspace.root);
|
|
135
|
-
workspace.instructions = await discoverInstructions(fs);
|
|
136
|
-
workspace.skills = await discoverSkills(fs);
|
|
137
|
-
workspace.capabilities = {
|
|
138
|
-
filesystem: "read-write",
|
|
139
|
-
git: gitInfo.isRepository,
|
|
140
|
-
skills: workspace.skills.length > 0,
|
|
141
|
-
};
|
|
142
|
-
return workspace;
|
|
143
|
-
}
|
|
144
|
-
async remove(workspaceId, force = false) {
|
|
145
|
-
const workspace = this.get(workspaceId);
|
|
146
|
-
if (workspace.mode === "checkout") {
|
|
147
|
-
this.filesystems.delete(workspace.id);
|
|
148
|
-
this.workspaces.delete(workspace.id);
|
|
149
|
-
this.repository.remove(workspace.id);
|
|
150
|
-
return workspace;
|
|
151
|
-
}
|
|
152
|
-
const gitInfo = await this.git.info(workspace.root);
|
|
153
|
-
if (gitInfo.dirty && !force)
|
|
154
|
-
throw new ChatRoomError("CONFLICT", "Worktree contains uncommitted changes");
|
|
155
|
-
await this.git.removeWorktree(workspace.sourceRoot, workspace.root, force);
|
|
156
|
-
await this.git.pruneWorktrees(workspace.sourceRoot).catch(() => undefined);
|
|
157
|
-
this.filesystems.delete(workspace.id);
|
|
158
|
-
this.workspaces.delete(workspace.id);
|
|
159
|
-
this.repository.remove(workspace.id);
|
|
160
|
-
await this.removeManagedContainer(workspace.root);
|
|
161
|
-
return workspace;
|
|
162
|
-
}
|
|
163
|
-
async previewWorktreeApply(workspaceId) {
|
|
164
|
-
return this.review.preview(this.get(workspaceId));
|
|
165
|
-
}
|
|
166
|
-
async previewWorktreeFileDiff(workspaceId, filePath) {
|
|
167
|
-
return this.review.fileDiff(this.get(workspaceId), filePath);
|
|
168
|
-
}
|
|
169
|
-
async applyWorktree(workspaceId, paths) {
|
|
170
|
-
const workspace = this.get(workspaceId);
|
|
171
|
-
const result = await this.review.apply(workspace, paths);
|
|
172
|
-
this.markUsed(workspace.id);
|
|
173
|
-
return { workspaceId: workspace.id, ...result };
|
|
46
|
+
return Promise.all([...roots]
|
|
47
|
+
.sort((a, b) => a.localeCompare(b))
|
|
48
|
+
.map(async (root) => {
|
|
49
|
+
const fs = await WorkspaceFs.create(root);
|
|
50
|
+
return {
|
|
51
|
+
root,
|
|
52
|
+
name: path.basename(root),
|
|
53
|
+
summary: await readSummary(fs),
|
|
54
|
+
};
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
async resolve(input) {
|
|
58
|
+
if (typeof input !== "string" || !input.trim())
|
|
59
|
+
throw new ChatRoomError("INVALID_INPUT", "Workspace root is required");
|
|
60
|
+
const canonical = await realpath(expandHome(input)).catch(() => {
|
|
61
|
+
throw new ChatRoomError("NOT_FOUND", `Workspace root does not exist: ${input}`);
|
|
62
|
+
});
|
|
63
|
+
if (!(await stat(canonical)).isDirectory())
|
|
64
|
+
throw new ChatRoomError("INVALID_INPUT", `Workspace root is not a directory: ${input}`);
|
|
65
|
+
if (!this.isWorkspaceRoot(canonical))
|
|
66
|
+
throw new ChatRoomError("FORBIDDEN", "Workspace must be a direct child of a configured allowed root", { root: canonical });
|
|
67
|
+
return canonical;
|
|
174
68
|
}
|
|
175
|
-
async
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.filesystems.set(existing.id, await WorkspaceFs.create(existing.root));
|
|
180
|
-
return this.refresh(existing.id);
|
|
181
|
-
}
|
|
182
|
-
return this.createWorkspace(sourceRoot, sourceRoot, mode);
|
|
183
|
-
}
|
|
184
|
-
const uuid = randomUUID();
|
|
185
|
-
const id = `ws_${uuid}`;
|
|
186
|
-
const container = path.join(this.dataDir, "worktrees", uuid.replaceAll("-", "").slice(0, 8));
|
|
187
|
-
const target = path.join(container, path.basename(sourceRoot) || "workspace");
|
|
188
|
-
await mkdir(container, { recursive: true, mode: 0o700 });
|
|
189
|
-
let worktreeCreated = false;
|
|
69
|
+
async createProject(parentInput, nameInput) {
|
|
70
|
+
const parent = await this.resolveAllowedRoot(parentInput);
|
|
71
|
+
const name = validateProjectName(nameInput);
|
|
72
|
+
const target = path.join(parent, name);
|
|
190
73
|
try {
|
|
191
|
-
await
|
|
192
|
-
worktreeCreated = true;
|
|
193
|
-
return await this.createWorkspace(target, sourceRoot, mode, id);
|
|
74
|
+
await mkdir(target, { recursive: false });
|
|
194
75
|
}
|
|
195
76
|
catch (error) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.repository.remove(id);
|
|
199
|
-
if (worktreeCreated)
|
|
200
|
-
await this.git
|
|
201
|
-
.removeWorktree(sourceRoot, target, true)
|
|
202
|
-
.catch(() => undefined);
|
|
203
|
-
await this.git.pruneWorktrees(sourceRoot).catch(() => undefined);
|
|
204
|
-
await rm(container, { recursive: true, force: true }).catch(() => undefined);
|
|
77
|
+
if (error.code === "EEXIST")
|
|
78
|
+
throw new ChatRoomError("CONFLICT", `Workspace already exists: ${target}`);
|
|
205
79
|
throw error;
|
|
206
80
|
}
|
|
81
|
+
await mkdir(path.join(target, ".chatroom"));
|
|
82
|
+
const root = await realpath(target);
|
|
83
|
+
return { root, name: path.basename(root), summary: null };
|
|
207
84
|
}
|
|
208
|
-
async
|
|
85
|
+
async info(input) {
|
|
86
|
+
const root = await this.resolve(input);
|
|
209
87
|
const fs = await WorkspaceFs.create(root);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
88
|
+
const [summary, presetPrompt, instructions, skills] = await Promise.all([
|
|
89
|
+
readSummary(fs),
|
|
90
|
+
readPresetPrompt(fs),
|
|
91
|
+
readInstructions(fs),
|
|
92
|
+
readSkills(fs),
|
|
93
|
+
]);
|
|
94
|
+
return {
|
|
216
95
|
root,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
instructions: await discoverInstructions(fs),
|
|
96
|
+
name: path.basename(root),
|
|
97
|
+
summary,
|
|
98
|
+
presetPrompt,
|
|
99
|
+
instructions,
|
|
222
100
|
skills,
|
|
223
|
-
capabilities: {
|
|
224
|
-
filesystem: "read-write",
|
|
225
|
-
git: gitInfo.isRepository,
|
|
226
|
-
skills: skills.length > 0,
|
|
227
|
-
},
|
|
228
101
|
};
|
|
229
|
-
this.workspaces.set(workspace.id, workspace);
|
|
230
|
-
this.repository.upsert(toRecord(workspace));
|
|
231
|
-
return workspace;
|
|
232
102
|
}
|
|
233
|
-
async
|
|
234
|
-
|
|
235
|
-
if (!this.isRecordValid(record)) {
|
|
236
|
-
this.repository.remove(record.id);
|
|
237
|
-
if (record.mode === "worktree")
|
|
238
|
-
await this.removeManagedContainer(record.root);
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
const fs = await WorkspaceFs.create(record.root);
|
|
243
|
-
const gitInfo = await this.git.info(record.root);
|
|
244
|
-
const skills = await discoverSkills(fs);
|
|
245
|
-
const workspace = {
|
|
246
|
-
...record,
|
|
247
|
-
instructions: await discoverInstructions(fs),
|
|
248
|
-
skills,
|
|
249
|
-
capabilities: {
|
|
250
|
-
filesystem: "read-write",
|
|
251
|
-
git: gitInfo.isRepository,
|
|
252
|
-
skills: skills.length > 0,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
this.filesystems.set(record.id, fs);
|
|
256
|
-
this.workspaces.set(record.id, workspace);
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
this.repository.remove(record.id);
|
|
260
|
-
if (record.mode === "worktree")
|
|
261
|
-
await this.removeManagedContainer(record.root);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
103
|
+
async fs(input) {
|
|
104
|
+
return WorkspaceFs.create(await this.resolve(input));
|
|
264
105
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return false;
|
|
268
|
-
try {
|
|
269
|
-
const sourceRoot = realpathSync(record.sourceRoot);
|
|
270
|
-
if (!this.allowedRoots.some((root) => inside(root, sourceRoot)))
|
|
271
|
-
return false;
|
|
272
|
-
if (record.mode === "checkout")
|
|
273
|
-
return realpathSync(record.root) === sourceRoot;
|
|
274
|
-
const managedRoot = realpathSync(path.join(this.dataDir, "worktrees"));
|
|
275
|
-
return inside(managedRoot, realpathSync(record.root));
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
106
|
+
isWorkspaceRoot(candidate) {
|
|
107
|
+
return this.allowedRoots.some((root) => path.dirname(candidate) === root);
|
|
280
108
|
}
|
|
281
|
-
async
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (workspace.mode !== "worktree")
|
|
285
|
-
continue;
|
|
286
|
-
if (await exists(workspace.sourceRoot)) {
|
|
287
|
-
if (!pruned.has(workspace.sourceRoot)) {
|
|
288
|
-
await this.git
|
|
289
|
-
.pruneWorktrees(workspace.sourceRoot)
|
|
290
|
-
.catch(() => undefined);
|
|
291
|
-
pruned.add(workspace.sourceRoot);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (await exists(workspace.root))
|
|
295
|
-
continue;
|
|
296
|
-
this.filesystems.delete(workspace.id);
|
|
297
|
-
this.workspaces.delete(workspace.id);
|
|
298
|
-
this.repository.remove(workspace.id);
|
|
299
|
-
await this.removeManagedContainer(workspace.root);
|
|
300
|
-
}
|
|
301
|
-
const managedRoot = path.join(this.dataDir, "worktrees");
|
|
302
|
-
const knownContainers = new Set([...this.workspaces.values()]
|
|
303
|
-
.filter((workspace) => workspace.mode === "worktree")
|
|
304
|
-
.map((workspace) => path.resolve(path.dirname(workspace.root))));
|
|
305
|
-
for (const entry of await readdir(managedRoot, { withFileTypes: true })) {
|
|
306
|
-
const container = path.resolve(managedRoot, entry.name);
|
|
307
|
-
if (knownContainers.has(container))
|
|
308
|
-
continue;
|
|
309
|
-
if (entry.isSymbolicLink()) {
|
|
310
|
-
await rm(container, { force: true }).catch(() => undefined);
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
if (entry.isDirectory())
|
|
314
|
-
await rm(container, { recursive: true, force: true });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
async removeManagedContainer(worktreeRoot) {
|
|
318
|
-
const managedRoot = path.join(this.dataDir, "worktrees");
|
|
319
|
-
if (!inside(managedRoot, worktreeRoot))
|
|
320
|
-
return;
|
|
321
|
-
const container = path.dirname(worktreeRoot);
|
|
322
|
-
if (container === managedRoot)
|
|
323
|
-
return;
|
|
324
|
-
await rm(container, { recursive: true, force: true }).catch(() => undefined);
|
|
325
|
-
}
|
|
326
|
-
async validateSourceRoot(input) {
|
|
327
|
-
if (!input || typeof input !== "string")
|
|
328
|
-
throw new ChatRoomError("INVALID_INPUT", "Workspace path is required");
|
|
109
|
+
async resolveAllowedRoot(input) {
|
|
110
|
+
if (typeof input !== "string" || !input.trim())
|
|
111
|
+
throw new ChatRoomError("INVALID_INPUT", "Allowed root is required");
|
|
329
112
|
const canonical = await realpath(expandHome(input)).catch(() => {
|
|
330
|
-
throw new ChatRoomError("NOT_FOUND", `
|
|
113
|
+
throw new ChatRoomError("NOT_FOUND", `Allowed root does not exist: ${input}`);
|
|
331
114
|
});
|
|
332
|
-
if (!this.allowedRoots.
|
|
333
|
-
throw new ChatRoomError("FORBIDDEN", "
|
|
334
|
-
path: canonical,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
115
|
+
if (!this.allowedRoots.includes(canonical))
|
|
116
|
+
throw new ChatRoomError("FORBIDDEN", "Project can only be created directly under a configured allowed root");
|
|
337
117
|
return canonical;
|
|
338
118
|
}
|
|
339
119
|
}
|
|
120
|
+
function validateProjectName(input) {
|
|
121
|
+
if (typeof input !== "string")
|
|
122
|
+
throw new ChatRoomError("INVALID_INPUT", "Project name is required");
|
|
123
|
+
const name = input.trim();
|
|
124
|
+
if (!name ||
|
|
125
|
+
name === "." ||
|
|
126
|
+
name === ".." ||
|
|
127
|
+
name.startsWith(".") ||
|
|
128
|
+
name.includes("/") ||
|
|
129
|
+
name.includes("\\") ||
|
|
130
|
+
name.includes("\0"))
|
|
131
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid project name");
|
|
132
|
+
return name;
|
|
133
|
+
}
|
|
134
|
+
function expandHome(input) {
|
|
135
|
+
if (input === "~")
|
|
136
|
+
return os.homedir();
|
|
137
|
+
if (input.startsWith("~/") || input.startsWith("~\\"))
|
|
138
|
+
return path.join(os.homedir(), input.slice(2));
|
|
139
|
+
return path.resolve(input);
|
|
140
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Shared Express helpers for async error propagation and typed ChatRoom error responses. */
|
|
2
1
|
import type { Request, Response, NextFunction, RequestHandler } from "express";
|
|
3
2
|
export declare function asyncRoute(handler: (req: Request, res: Response, next: NextFunction) => Promise<unknown>): RequestHandler;
|
|
4
3
|
export declare function errorMiddleware(error: unknown, _req: Request, res: Response, _next: NextFunction): void;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** OAuth and owner-session HTTP routes backed by AuthService rather than protocol-specific persistence logic. */
|
|
2
1
|
import { Router } from "express";
|
|
3
2
|
import type { AuthService } from "../../auth/auth-service.js";
|
|
4
3
|
import type { IngressPolicy } from "../../auth/ingress-policy.js";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** OAuth and owner-session HTTP routes backed by AuthService rather than protocol-specific persistence logic. */
|
|
2
1
|
import { Router } from "express";
|
|
3
2
|
import { asChatRoomError, ChatRoomError, } from "../../core/errors/chatroom-error.js";
|
|
4
3
|
import { escapeHtml, requireString } from "./http-utils.js";
|