collabmd 0.1.19 → 0.1.21
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 +13 -11
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.js +22 -0
- package/public/assets/js/chunks/{preview-render-compiler-UZ4SZDQQ.js → preview-render-compiler-6Y7TKXFJ.js} +1 -1
- package/public/assets/js/chunks/{quick-switcher-controller-J7I3CUET.js → quick-switcher-controller-A6SKH5HI.js} +1 -1
- package/public/assets/js/{excalidraw-editor-ZDYKMZOL.js → excalidraw-editor-TCIH6GJL.js} +1 -1
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +102 -75
- package/public/assets/js/preview-render-worker.js +15 -15
- package/public/index.html +14 -4
- package/src/client/application/app-shell/git-feature.js +86 -19
- package/src/client/application/app-shell/ui-feature.js +4 -0
- package/src/client/application/app-shell-elements.js +1 -0
- package/src/client/bootstrap/collabmd-app-shell.js +16 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +2 -1
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +245 -7
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/backlinks-panel.js +157 -101
- package/src/client/presentation/comment-ui-controller.js +233 -10
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/presentation/git-panel-controller.js +73 -0
- package/src/client/presentation/outline-controller.js +25 -0
- package/src/client/styles/style.css +184 -9
- package/src/domain/wiki-link-resolver.js +16 -5
- package/src/domain/workspace-change.js +68 -0
- package/src/domain/workspace-room.js +3 -0
- package/src/server/create-app-server.js +41 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +191 -22
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/infrastructure/git/errors.js +4 -1
- package/src/server/infrastructure/git/git-service.js +215 -1
- package/src/server/infrastructure/git/responses.js +12 -19
- package/src/server/infrastructure/http/create-git-api-command-handler.js +58 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +16 -1
- package/src/server/infrastructure/http/create-request-handler.js +7 -0
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +58 -14
- package/src/server/infrastructure/http/create-vault-api-handler.js +3 -0
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +3 -1
- package/src/server/infrastructure/http/http-response.js +65 -28
- package/src/server/infrastructure/persistence/pull-backup-store.js +283 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +179 -52
- package/src/server/infrastructure/workspace/file-system-sync-service.js +488 -0
- package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +551 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +0 -1
- package/public/assets/js/chunks/editor-session-AH6Z3MXW.js +0 -22
|
@@ -6,6 +6,7 @@ import { parseNameStatusOutput } from './parsers.js';
|
|
|
6
6
|
import { createEmptyWorkspaceChange, createWorkspaceChange } from './responses.js';
|
|
7
7
|
import { GitStatusService } from './status-service.js';
|
|
8
8
|
import { GitUntrackedFileService } from './untracked-files.js';
|
|
9
|
+
import { PullBackupStore } from '../persistence/pull-backup-store.js';
|
|
9
10
|
|
|
10
11
|
export class GitService {
|
|
11
12
|
constructor({
|
|
@@ -36,6 +37,7 @@ export class GitService {
|
|
|
36
37
|
statusService: this.statusService,
|
|
37
38
|
untrackedFileService: this.untrackedFileService,
|
|
38
39
|
});
|
|
40
|
+
this.pullBackupStore = new PullBackupStore({ vaultDir });
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
async isGitRepo() {
|
|
@@ -55,6 +57,14 @@ export class GitService {
|
|
|
55
57
|
return this.statusService.getStatus(options);
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
async listPullBackups() {
|
|
61
|
+
if (!(await this.isGitRepo())) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this.pullBackupStore.listBackups();
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
async stageFile(path) {
|
|
59
69
|
const normalizedPath = normalizeRelativeGitPath(path);
|
|
60
70
|
await this.commandRunner.execGit(['add', '-A', '--', normalizedPath]);
|
|
@@ -131,7 +141,45 @@ export class GitService {
|
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
const beforeRef = await this.getHeadRef();
|
|
134
|
-
|
|
144
|
+
await this.fetchUpstream(status.branch.upstream);
|
|
145
|
+
|
|
146
|
+
const targetRef = await this.resolveRef(status.branch.upstream);
|
|
147
|
+
if (!targetRef) {
|
|
148
|
+
throw createGitRequestError(409, 'Unable to resolve upstream branch for pull');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!(await this.canFastForwardTo(targetRef))) {
|
|
152
|
+
throw createGitRequestError(
|
|
153
|
+
409,
|
|
154
|
+
'Cannot pull because local and remote commits have diverged; fast-forward only pull is not possible.',
|
|
155
|
+
'pull_diverged_ff_only',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const dirtyEntries = this.collectDirtyEntries(status);
|
|
160
|
+
const upstreamWorkspaceChange = await this.createWorkspaceChangeFromRefs(beforeRef, targetRef);
|
|
161
|
+
const overlappingEntries = this.findOverlappingDirtyEntries({
|
|
162
|
+
dirtyEntries,
|
|
163
|
+
workspaceChange: upstreamWorkspaceChange,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let pullBackup = null;
|
|
167
|
+
if (overlappingEntries.length > 0) {
|
|
168
|
+
pullBackup = await this.backupAndClearOverlappingEntries({
|
|
169
|
+
beforeRef,
|
|
170
|
+
branchName: status.branch?.name ?? null,
|
|
171
|
+
entries: overlappingEntries,
|
|
172
|
+
targetRef,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let output;
|
|
177
|
+
try {
|
|
178
|
+
output = await this.commandRunner.execGit(['pull', '--ff-only', '--autostash']);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw await this.classifyPullError(error);
|
|
181
|
+
}
|
|
182
|
+
|
|
135
183
|
const afterRef = await this.getHeadRef();
|
|
136
184
|
this.invalidateStatusCache();
|
|
137
185
|
return {
|
|
@@ -139,6 +187,7 @@ export class GitService {
|
|
|
139
187
|
beforeRef,
|
|
140
188
|
ok: true,
|
|
141
189
|
output: output.trim(),
|
|
190
|
+
pullBackup,
|
|
142
191
|
workspaceChange: await this.createWorkspaceChangeFromRefs(beforeRef, afterRef),
|
|
143
192
|
};
|
|
144
193
|
}
|
|
@@ -202,6 +251,171 @@ export class GitService {
|
|
|
202
251
|
}
|
|
203
252
|
}
|
|
204
253
|
|
|
254
|
+
async resolveRef(ref) {
|
|
255
|
+
try {
|
|
256
|
+
const output = await this.commandRunner.execGit(['rev-parse', ref]);
|
|
257
|
+
return output.trim() || null;
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async fetchUpstream(upstreamRef) {
|
|
264
|
+
const remoteName = String(upstreamRef ?? '').split('/')[0] || null;
|
|
265
|
+
await this.commandRunner.execGit(remoteName
|
|
266
|
+
? ['fetch', '--prune', remoteName]
|
|
267
|
+
: ['fetch', '--prune']);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async canFastForwardTo(targetRef) {
|
|
271
|
+
const headRef = await this.getHeadRef();
|
|
272
|
+
if (!headRef || !targetRef || headRef === targetRef) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
await this.commandRunner.execGit(['merge-base', '--is-ancestor', headRef, targetRef]);
|
|
278
|
+
return true;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error?.code === 1) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
collectDirtyEntries(status = {}) {
|
|
288
|
+
const entries = new Map();
|
|
289
|
+
for (const section of status.sections ?? []) {
|
|
290
|
+
for (const file of section.files ?? []) {
|
|
291
|
+
if (!file?.path) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const existing = entries.get(file.path) ?? {
|
|
296
|
+
hasStagedChanges: false,
|
|
297
|
+
hasWorkingTreeChanges: false,
|
|
298
|
+
hasTrackedChanges: false,
|
|
299
|
+
isUntracked: false,
|
|
300
|
+
oldPath: null,
|
|
301
|
+
path: file.path,
|
|
302
|
+
touchPaths: new Set(),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
existing.isUntracked = existing.isUntracked || file.scope === 'untracked';
|
|
306
|
+
existing.hasStagedChanges = existing.hasStagedChanges || file.scope === 'staged';
|
|
307
|
+
existing.hasWorkingTreeChanges = existing.hasWorkingTreeChanges || file.scope === 'working-tree';
|
|
308
|
+
existing.hasTrackedChanges = existing.hasTrackedChanges || file.scope === 'staged' || file.scope === 'working-tree';
|
|
309
|
+
if (!existing.oldPath && file.oldPath) {
|
|
310
|
+
existing.oldPath = file.oldPath;
|
|
311
|
+
}
|
|
312
|
+
existing.touchPaths.add(file.path);
|
|
313
|
+
if (file.oldPath) {
|
|
314
|
+
existing.touchPaths.add(file.oldPath);
|
|
315
|
+
}
|
|
316
|
+
entries.set(file.path, existing);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return Array.from(entries.values());
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
findOverlappingDirtyEntries({ dirtyEntries = [], workspaceChange = createEmptyWorkspaceChange() } = {}) {
|
|
324
|
+
const upstreamTouchedPaths = new Set([
|
|
325
|
+
...(workspaceChange.changedPaths ?? []),
|
|
326
|
+
...(workspaceChange.deletedPaths ?? []),
|
|
327
|
+
...((workspaceChange.renamedPaths ?? []).flatMap((entry) => [entry.oldPath, entry.newPath])),
|
|
328
|
+
].filter(Boolean));
|
|
329
|
+
|
|
330
|
+
return dirtyEntries.filter((entry) => Array.from(entry.touchPaths).some((path) => upstreamTouchedPaths.has(path)));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async createPatchForEntry(entry, { cached = false } = {}) {
|
|
334
|
+
const pathspecs = Array.from(entry?.touchPaths ?? []).filter(Boolean);
|
|
335
|
+
if (pathspecs.length === 0) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const output = await this.commandRunner.execGit([
|
|
340
|
+
'diff',
|
|
341
|
+
'--binary',
|
|
342
|
+
'--find-renames',
|
|
343
|
+
...(cached ? ['--cached'] : []),
|
|
344
|
+
'--',
|
|
345
|
+
...pathspecs,
|
|
346
|
+
]);
|
|
347
|
+
|
|
348
|
+
return output || null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async backupAndClearOverlappingEntries({
|
|
352
|
+
beforeRef,
|
|
353
|
+
branchName = null,
|
|
354
|
+
entries = [],
|
|
355
|
+
targetRef = null,
|
|
356
|
+
} = {}) {
|
|
357
|
+
const backupEntries = await Promise.all(entries.map(async (entry) => ({
|
|
358
|
+
...entry,
|
|
359
|
+
stagedPatchContent: entry.hasStagedChanges
|
|
360
|
+
? await this.createPatchForEntry(entry, { cached: true })
|
|
361
|
+
: null,
|
|
362
|
+
worktreePatchContent: entry.hasWorkingTreeChanges
|
|
363
|
+
? await this.createPatchForEntry(entry, { cached: false })
|
|
364
|
+
: null,
|
|
365
|
+
})));
|
|
366
|
+
|
|
367
|
+
const pullBackup = await this.pullBackupStore.createBackup({
|
|
368
|
+
branch: branchName,
|
|
369
|
+
entries: backupEntries,
|
|
370
|
+
headRef: beforeRef,
|
|
371
|
+
targetRef,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
for (const entry of backupEntries) {
|
|
375
|
+
if (entry.isUntracked && !entry.hasTrackedChanges) {
|
|
376
|
+
await this.commandRunner.execGit(['clean', '-f', '--', entry.path]);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await this.restorePathToHead(entry.path);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return pullBackup;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async restorePathToHead(path) {
|
|
387
|
+
const normalizedPath = normalizeRelativeGitPath(path);
|
|
388
|
+
const sourceRef = 'HEAD';
|
|
389
|
+
const existsOnSource = await this.pathExistsAtRef(sourceRef, normalizedPath);
|
|
390
|
+
|
|
391
|
+
if (existsOnSource) {
|
|
392
|
+
await this.commandRunner.execGit(['restore', '--source', sourceRef, '--staged', '--worktree', '--', normalizedPath]);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await this.commandRunner.execGit(['rm', '-f', '--ignore-unmatch', '--', normalizedPath]);
|
|
397
|
+
await this.commandRunner.execGit(['clean', '-f', '--', normalizedPath]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async hasConflictedStatus() {
|
|
401
|
+
const status = await this.getStatus({ force: true });
|
|
402
|
+
return (status.sections ?? [])
|
|
403
|
+
.flatMap((section) => section.files ?? [])
|
|
404
|
+
.some((file) => file?.status === 'conflicted');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async classifyPullError(error) {
|
|
408
|
+
if (await this.hasConflictedStatus()) {
|
|
409
|
+
return createGitRequestError(
|
|
410
|
+
409,
|
|
411
|
+
'Pull applied remote updates, but reapplying local changes caused conflicts. Review the conflicted files and the pull backup summary.',
|
|
412
|
+
'pull_conflicted_after_autostash',
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return error;
|
|
417
|
+
}
|
|
418
|
+
|
|
205
419
|
async createWorkspaceChangeFromRefs(beforeRef, afterRef) {
|
|
206
420
|
if (!afterRef || beforeRef === afterRef) {
|
|
207
421
|
return createEmptyWorkspaceChange();
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEmptyWorkspaceChange as createSharedEmptyWorkspaceChange,
|
|
3
|
+
createWorkspaceChange as createSharedWorkspaceChange,
|
|
4
|
+
} from '../../../domain/workspace-change.js';
|
|
5
|
+
|
|
1
6
|
const STATUS_MAP = {
|
|
2
7
|
A: { code: 'A', label: 'added', status: 'added' },
|
|
3
8
|
C: { code: 'C', label: 'copied', status: 'copied' },
|
|
@@ -43,12 +48,7 @@ export function createEmptyBranchStatus() {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export function createEmptyWorkspaceChange() {
|
|
46
|
-
return
|
|
47
|
-
changedPaths: [],
|
|
48
|
-
deletedPaths: [],
|
|
49
|
-
refreshExplorer: true,
|
|
50
|
-
renamedPaths: [],
|
|
51
|
-
};
|
|
51
|
+
return createSharedEmptyWorkspaceChange();
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function createEmptyStatusResponse() {
|
|
@@ -92,17 +92,10 @@ export function createWorkspaceChange({
|
|
|
92
92
|
refreshExplorer = true,
|
|
93
93
|
renamedPaths = [],
|
|
94
94
|
} = {}) {
|
|
95
|
-
return {
|
|
96
|
-
changedPaths
|
|
97
|
-
deletedPaths
|
|
98
|
-
refreshExplorer
|
|
99
|
-
renamedPaths
|
|
100
|
-
|
|
101
|
-
.filter((entry) => entry?.oldPath && entry?.newPath && entry.oldPath !== entry.newPath)
|
|
102
|
-
.map((entry) => [`${entry.oldPath}:${entry.newPath}`, {
|
|
103
|
-
newPath: entry.newPath,
|
|
104
|
-
oldPath: entry.oldPath,
|
|
105
|
-
}]),
|
|
106
|
-
).values()),
|
|
107
|
-
};
|
|
95
|
+
return createSharedWorkspaceChange({
|
|
96
|
+
changedPaths,
|
|
97
|
+
deletedPaths,
|
|
98
|
+
refreshExplorer,
|
|
99
|
+
renamedPaths,
|
|
100
|
+
});
|
|
108
101
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getRequestErrorStatusCode } from './http-errors.js';
|
|
2
2
|
import { jsonResponse } from './http-response.js';
|
|
3
3
|
import { parseJsonBody } from './request-body.js';
|
|
4
|
-
import { createEmptyWorkspaceChange } from '
|
|
4
|
+
import { createEmptyWorkspaceChange, hasWorkspaceMutation } from '../../../domain/workspace-change.js';
|
|
5
5
|
|
|
6
6
|
async function parseRequiredBody(req, res, fieldName) {
|
|
7
7
|
const body = await parseJsonBody(req);
|
|
@@ -16,7 +16,11 @@ async function parseRequiredBody(req, res, fieldName) {
|
|
|
16
16
|
function handleGitError(req, res, error, logMessage, fallbackMessage) {
|
|
17
17
|
const statusCode = getRequestErrorStatusCode(error);
|
|
18
18
|
if (statusCode) {
|
|
19
|
-
|
|
19
|
+
const payload = { error: error.message };
|
|
20
|
+
if (typeof error?.requestCode === 'string') {
|
|
21
|
+
payload.code = error.requestCode;
|
|
22
|
+
}
|
|
23
|
+
jsonResponse(req, res, statusCode, payload);
|
|
20
24
|
return true;
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -25,19 +29,16 @@ function handleGitError(req, res, error, logMessage, fallbackMessage) {
|
|
|
25
29
|
return true;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|| (workspaceChange.deletedPaths?.length ?? 0) > 0
|
|
32
|
-
|| (workspaceChange.renamedPaths?.length ?? 0) > 0,
|
|
33
|
-
);
|
|
32
|
+
function readRequestId(req) {
|
|
33
|
+
const value = String(req.headers['x-collabmd-request-id'] || '').trim();
|
|
34
|
+
return value ? value.slice(0, 120) : null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
async function applyWorkspaceMutationEffects({
|
|
37
|
-
|
|
38
|
+
action,
|
|
39
|
+
req,
|
|
38
40
|
responsePayload,
|
|
39
|
-
|
|
40
|
-
vaultFileStore,
|
|
41
|
+
workspaceMutationCoordinator,
|
|
41
42
|
}) {
|
|
42
43
|
const workspaceChange = responsePayload?.workspaceChange ?? createEmptyWorkspaceChange();
|
|
43
44
|
responsePayload.workspaceChange = workspaceChange;
|
|
@@ -46,17 +47,19 @@ async function applyWorkspaceMutationEffects({
|
|
|
46
47
|
return responsePayload;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
await
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
51
|
+
action,
|
|
52
|
+
origin: 'git',
|
|
53
|
+
requestId: readRequestId(req),
|
|
54
|
+
sourceRef: responsePayload?.sourceRef ?? null,
|
|
55
|
+
workspaceChange,
|
|
56
|
+
});
|
|
52
57
|
return responsePayload;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export function createGitApiCommandHandler({
|
|
56
|
-
backlinkIndex = null,
|
|
57
61
|
gitService,
|
|
58
|
-
|
|
59
|
-
vaultFileStore = null,
|
|
62
|
+
workspaceMutationCoordinator = null,
|
|
60
63
|
}) {
|
|
61
64
|
return async function handleGitApiCommand(req, res, requestUrl) {
|
|
62
65
|
if (requestUrl.pathname === '/api/git/stage' && req.method === 'POST') {
|
|
@@ -67,10 +70,12 @@ export function createGitApiCommandHandler({
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
action: 'stage',
|
|
74
|
+
req,
|
|
75
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
76
|
+
() => gitService.stageFile(body.path),
|
|
77
|
+
),
|
|
78
|
+
workspaceMutationCoordinator,
|
|
74
79
|
}));
|
|
75
80
|
} catch (error) {
|
|
76
81
|
handleGitError(req, res, error, '[api] Failed to stage git file:', 'Failed to stage git file');
|
|
@@ -86,10 +91,12 @@ export function createGitApiCommandHandler({
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
action: 'unstage',
|
|
95
|
+
req,
|
|
96
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
97
|
+
() => gitService.unstageFile(body.path),
|
|
98
|
+
),
|
|
99
|
+
workspaceMutationCoordinator,
|
|
93
100
|
}));
|
|
94
101
|
} catch (error) {
|
|
95
102
|
handleGitError(req, res, error, '[api] Failed to unstage git file:', 'Failed to unstage git file');
|
|
@@ -105,12 +112,14 @@ export function createGitApiCommandHandler({
|
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
action: 'commit',
|
|
116
|
+
req,
|
|
117
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
118
|
+
() => gitService.commitStaged({
|
|
119
|
+
message: body.message,
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
workspaceMutationCoordinator,
|
|
114
123
|
}));
|
|
115
124
|
} catch (error) {
|
|
116
125
|
handleGitError(req, res, error, '[api] Failed to commit staged changes:', 'Failed to commit staged changes');
|
|
@@ -121,10 +130,12 @@ export function createGitApiCommandHandler({
|
|
|
121
130
|
if (requestUrl.pathname === '/api/git/push' && req.method === 'POST') {
|
|
122
131
|
try {
|
|
123
132
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
action: 'push',
|
|
134
|
+
req,
|
|
135
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
136
|
+
() => gitService.pushBranch(),
|
|
137
|
+
),
|
|
138
|
+
workspaceMutationCoordinator,
|
|
128
139
|
}));
|
|
129
140
|
} catch (error) {
|
|
130
141
|
handleGitError(req, res, error, '[api] Failed to push git branch:', 'Failed to push git branch');
|
|
@@ -135,10 +146,12 @@ export function createGitApiCommandHandler({
|
|
|
135
146
|
if (requestUrl.pathname === '/api/git/pull' && req.method === 'POST') {
|
|
136
147
|
try {
|
|
137
148
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
action: 'pull',
|
|
150
|
+
req,
|
|
151
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
152
|
+
() => gitService.pullBranch(),
|
|
153
|
+
),
|
|
154
|
+
workspaceMutationCoordinator,
|
|
142
155
|
}));
|
|
143
156
|
} catch (error) {
|
|
144
157
|
handleGitError(req, res, error, '[api] Failed to pull git branch:', 'Failed to pull git branch');
|
|
@@ -154,10 +167,12 @@ export function createGitApiCommandHandler({
|
|
|
154
167
|
}
|
|
155
168
|
|
|
156
169
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
action: 'reset-file',
|
|
171
|
+
req,
|
|
172
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
173
|
+
() => gitService.resetFileToHead(body.path),
|
|
174
|
+
),
|
|
175
|
+
workspaceMutationCoordinator,
|
|
161
176
|
}));
|
|
162
177
|
} catch (error) {
|
|
163
178
|
handleGitError(req, res, error, '[api] Failed to reset git file:', 'Failed to reset git file');
|
|
@@ -7,6 +7,7 @@ export function createGitApiHandler({
|
|
|
7
7
|
gitService = null,
|
|
8
8
|
roomRegistry = null,
|
|
9
9
|
vaultFileStore = null,
|
|
10
|
+
workspaceMutationCoordinator = null,
|
|
10
11
|
}) {
|
|
11
12
|
const handleGitApiQuery = createGitApiQueryHandler({ gitService });
|
|
12
13
|
const handleGitApiCommand = createGitApiCommandHandler({
|
|
@@ -14,6 +15,7 @@ export function createGitApiHandler({
|
|
|
14
15
|
gitService,
|
|
15
16
|
roomRegistry,
|
|
16
17
|
vaultFileStore,
|
|
18
|
+
workspaceMutationCoordinator,
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
return async function handleGitApi(req, res, requestUrl) {
|
|
@@ -21,7 +21,11 @@ function isTruthyParam(value) {
|
|
|
21
21
|
function handleGitError(req, res, error, message, fallback) {
|
|
22
22
|
const statusCode = getRequestErrorStatusCode(error);
|
|
23
23
|
if (statusCode) {
|
|
24
|
-
|
|
24
|
+
const payload = { error: error.message };
|
|
25
|
+
if (typeof error?.requestCode === 'string') {
|
|
26
|
+
payload.code = error.requestCode;
|
|
27
|
+
}
|
|
28
|
+
jsonResponse(req, res, statusCode, payload);
|
|
25
29
|
return true;
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -57,6 +61,17 @@ export function createGitApiQueryHandler({ gitService }) {
|
|
|
57
61
|
return true;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
if (requestUrl.pathname === '/api/git/pull-backups' && req.method === 'GET') {
|
|
65
|
+
try {
|
|
66
|
+
jsonResponse(req, res, 200, {
|
|
67
|
+
backups: await gitService.listPullBackups(),
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
handleGitError(req, res, error, '[api] Failed to read pull backups:', 'Failed to read pull backups');
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
return false;
|
|
61
76
|
};
|
|
62
77
|
}
|
|
@@ -42,6 +42,8 @@ export function createRequestHandler(
|
|
|
42
42
|
roomRegistry = null,
|
|
43
43
|
plantUmlRenderer = null,
|
|
44
44
|
gitService = null,
|
|
45
|
+
workspaceMutationCoordinator = null,
|
|
46
|
+
fileSystemSyncService = null,
|
|
45
47
|
) {
|
|
46
48
|
const handleEsmProxy = createEsmProxyHandler();
|
|
47
49
|
const handleStaticRequest = createStaticHandler(config, authService);
|
|
@@ -51,12 +53,14 @@ export function createRequestHandler(
|
|
|
51
53
|
gitService,
|
|
52
54
|
roomRegistry,
|
|
53
55
|
vaultFileStore,
|
|
56
|
+
workspaceMutationCoordinator,
|
|
54
57
|
});
|
|
55
58
|
const handleVaultApi = createVaultApiHandler({
|
|
56
59
|
backlinkIndex,
|
|
57
60
|
plantUmlRenderer,
|
|
58
61
|
roomRegistry,
|
|
59
62
|
vaultFileStore,
|
|
63
|
+
workspaceMutationCoordinator,
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
return async function handleRequest(req, res) {
|
|
@@ -103,8 +107,11 @@ export function createRequestHandler(
|
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
if (config.nodeEnv === 'test' && requestUrl.pathname === '/api/test/reset-state' && req.method === 'POST') {
|
|
110
|
+
await fileSystemSyncService?.resetForExternalStateChange?.();
|
|
106
111
|
await roomRegistry?.reset?.();
|
|
107
112
|
await backlinkIndex?.build?.();
|
|
113
|
+
await workspaceMutationCoordinator?.initialize?.();
|
|
114
|
+
await fileSystemSyncService?.resetForExternalStateChange?.();
|
|
108
115
|
jsonResponse(req, res, 200, { ok: true });
|
|
109
116
|
return;
|
|
110
117
|
}
|