collabmd 0.1.20 → 0.1.23
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 +98 -9
- package/bin/collabmd.js +3 -2
- package/docker-compose.yml +13 -1
- package/package.json +2 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/{chunk-HEWVH67U.js → chunk-CYK7KO7W.js} +8 -8
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-S5AT7BMV.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-5YWMGLKF.js} +145 -62
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +503 -182
- package/public/assets/js/preview-render-worker.js +15 -15
- package/public/index.html +104 -84
- package/src/client/application/app-shell/git-feature.js +304 -27
- package/src/client/application/app-shell/ui-feature.js +69 -7
- package/src/client/application/app-shell/workspace-feature.js +46 -0
- package/src/client/application/app-shell-elements.js +2 -0
- package/src/client/application/mermaid-preview-hydrator.js +1 -1
- package/src/client/application/plantuml-preview-hydrator.js +1 -1
- package/src/client/application/workspace-coordinator.js +147 -18
- package/src/client/application/workspace-preview-controller.js +34 -0
- package/src/client/application/workspace-route-controller.js +104 -0
- package/src/client/bootstrap/collabmd-app-shell.js +107 -0
- package/src/client/domain/runtime-paths.js +5 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +52 -3
- package/src/client/infrastructure/auth-client.js +219 -52
- package/src/client/infrastructure/browser-navigation-port.js +20 -0
- package/src/client/infrastructure/editor-collaboration-client.js +4 -0
- package/src/client/infrastructure/editor-session.js +51 -7
- package/src/client/infrastructure/editor-view-adapter.js +297 -53
- package/src/client/infrastructure/runtime-config.js +66 -0
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/comment-ui-controller.js +274 -31
- package/src/client/presentation/comments-panel.js +2 -2
- package/src/client/presentation/excalidraw-embed-controller.js +60 -2
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/presentation/file-history-view-controller.js +806 -0
- package/src/client/presentation/git-diff-view-controller.js +718 -121
- package/src/client/presentation/git-panel-controller.js +458 -66
- package/src/client/presentation/layout-controller.js +1 -1
- package/src/client/styles/components/actions.css +89 -0
- package/src/client/styles/components/feedback.css +43 -0
- package/src/client/styles/components/navigation.css +68 -0
- package/src/client/styles/components/scrollbars.css +104 -0
- package/src/client/styles/components/states.css +289 -0
- package/src/client/styles/components/surfaces.css +155 -0
- package/src/client/styles/features/backlinks.css +213 -0
- package/src/client/styles/features/collaboration-chat.css +228 -0
- package/src/client/styles/features/collaboration-dialogs.css +154 -0
- package/src/client/styles/features/collaboration-presence.css +103 -0
- package/src/client/styles/features/collaboration-ui.css +3 -0
- package/src/client/styles/features/comment-card.css +236 -0
- package/src/client/styles/features/comment-markdown.css +127 -0
- package/src/client/styles/features/comment-overlays.css +60 -0
- package/src/client/styles/features/comments-drawer.css +120 -0
- package/src/client/styles/features/comments.css +4 -0
- package/src/client/styles/features/diagram-preview.css +227 -0
- package/src/client/styles/features/diff-commit.css +268 -0
- package/src/client/styles/features/diff-lines.css +160 -0
- package/src/client/styles/features/diff-toolbar.css +98 -0
- package/src/client/styles/features/diff.css +3 -0
- package/src/client/styles/features/embedded-preview.css +218 -0
- package/src/client/styles/features/git-branch.css +155 -0
- package/src/client/styles/features/git-files.css +105 -0
- package/src/client/styles/features/git-history.css +339 -0
- package/src/client/styles/features/git.css +3 -0
- package/src/client/styles/features/media-lightbox.css +157 -0
- package/src/client/styles/features/outline.css +115 -0
- package/src/client/styles/features/preview-content.css +3 -0
- package/src/client/styles/features/preview-markdown.css +201 -0
- package/src/client/styles/features/preview-sidebars.css +2 -0
- package/src/client/styles/features/quick-switcher.css +167 -0
- package/src/client/styles/foundation/themes.css +347 -0
- package/src/client/styles/foundation/tokens.css +66 -0
- package/src/client/styles/foundation/utilities.css +36 -0
- package/src/client/styles/layout/editor-page.css +203 -0
- package/src/client/styles/layout/overlays.css +132 -0
- package/src/client/styles/layout/responsive.css +251 -0
- package/src/client/styles/layout/shell.css +314 -0
- package/src/client/styles/layout/sidebar.css +176 -0
- package/src/client/styles/layout/view-modes.css +92 -0
- package/src/client/styles/overrides/codemirror.css +53 -0
- package/src/client/styles/overrides/highlightjs.css +7 -0
- package/src/client/styles/overrides/vendor.css +2 -0
- package/src/client/styles/primitives/controls.css +276 -0
- package/src/client/styles/style.css +25 -4901
- 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/auth/create-auth-service.js +373 -15
- package/src/server/auth/session-cookie.js +30 -4
- package/src/server/config/env.js +121 -5
- package/src/server/create-app-server.js +49 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +281 -69
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/index.js +3 -1
- package/src/server/infrastructure/git/command-runner.js +2 -1
- package/src/server/infrastructure/git/git-service.js +48 -2
- package/src/server/infrastructure/git/history-service.js +683 -0
- package/src/server/infrastructure/git/parsers.js +30 -0
- package/src/server/infrastructure/git/responses.js +85 -19
- package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
- package/src/server/infrastructure/http/create-git-api-command-handler.js +55 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +4 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
- package/src/server/infrastructure/http/create-request-handler.js +22 -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/vault-file-store.js +163 -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-TAV2VXTA.js +0 -22
|
@@ -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() {
|
|
@@ -86,23 +86,89 @@ export function createDiffResponse({
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
export function createHistoryResponse({
|
|
90
|
+
commits = [],
|
|
91
|
+
hasMore = false,
|
|
92
|
+
isGitRepo = true,
|
|
93
|
+
limit = 30,
|
|
94
|
+
offset = 0,
|
|
95
|
+
} = {}) {
|
|
96
|
+
return {
|
|
97
|
+
commits,
|
|
98
|
+
hasMore,
|
|
99
|
+
isGitRepo,
|
|
100
|
+
limit,
|
|
101
|
+
offset,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createFileHistoryResponse({
|
|
106
|
+
commits = [],
|
|
107
|
+
hasMore = false,
|
|
108
|
+
isGitRepo = true,
|
|
109
|
+
limit = 30,
|
|
110
|
+
offset = 0,
|
|
111
|
+
path = null,
|
|
112
|
+
} = {}) {
|
|
113
|
+
return {
|
|
114
|
+
commits,
|
|
115
|
+
hasMore,
|
|
116
|
+
isGitRepo,
|
|
117
|
+
limit,
|
|
118
|
+
offset,
|
|
119
|
+
path,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createCommitDiffResponse({
|
|
124
|
+
commit = null,
|
|
125
|
+
files = [],
|
|
126
|
+
isGitRepo = true,
|
|
127
|
+
metaOnly = false,
|
|
128
|
+
path = null,
|
|
129
|
+
summary = {
|
|
130
|
+
additions: 0,
|
|
131
|
+
deletions: 0,
|
|
132
|
+
filesChanged: 0,
|
|
133
|
+
},
|
|
134
|
+
} = {}) {
|
|
135
|
+
return {
|
|
136
|
+
commit,
|
|
137
|
+
files,
|
|
138
|
+
isGitRepo,
|
|
139
|
+
metaOnly,
|
|
140
|
+
path,
|
|
141
|
+
source: 'commit',
|
|
142
|
+
summary,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createFileSnapshotResponse({
|
|
147
|
+
content = '',
|
|
148
|
+
fileKind = null,
|
|
149
|
+
hash = null,
|
|
150
|
+
isGitRepo = true,
|
|
151
|
+
path = null,
|
|
152
|
+
} = {}) {
|
|
153
|
+
return {
|
|
154
|
+
content,
|
|
155
|
+
fileKind,
|
|
156
|
+
hash,
|
|
157
|
+
isGitRepo,
|
|
158
|
+
path,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
89
162
|
export function createWorkspaceChange({
|
|
90
163
|
changedPaths = [],
|
|
91
164
|
deletedPaths = [],
|
|
92
165
|
refreshExplorer = true,
|
|
93
166
|
renamedPaths = [],
|
|
94
167
|
} = {}) {
|
|
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
|
-
};
|
|
168
|
+
return createSharedWorkspaceChange({
|
|
169
|
+
changedPaths,
|
|
170
|
+
deletedPaths,
|
|
171
|
+
refreshExplorer,
|
|
172
|
+
renamedPaths,
|
|
173
|
+
});
|
|
108
174
|
}
|
|
@@ -7,6 +7,14 @@ function applyAuthResponse(req, res, result) {
|
|
|
7
7
|
res.setHeader('Set-Cookie', result.setCookie);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
if (result?.redirectTo) {
|
|
11
|
+
res.writeHead(result?.statusCode ?? 302, {
|
|
12
|
+
Location: result.redirectTo,
|
|
13
|
+
});
|
|
14
|
+
res.end();
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
jsonResponse(req, res, result?.statusCode ?? 200, result?.body ?? { ok: true });
|
|
11
19
|
return true;
|
|
12
20
|
}
|
|
@@ -21,10 +29,30 @@ export function createAuthApiHandler({ authService }) {
|
|
|
21
29
|
return applyAuthResponse(req, res, authService.getStatus(req));
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
if (requestUrl.pathname === '/api/auth/oidc/login' && req.method === 'GET') {
|
|
33
|
+
try {
|
|
34
|
+
return applyAuthResponse(req, res, await authService.beginOidcLogin(req, requestUrl));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[api] Failed to start OIDC login:', error.message);
|
|
37
|
+
jsonResponse(req, res, 500, { error: 'Failed to start OIDC login' });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (requestUrl.pathname === '/api/auth/oidc/callback' && req.method === 'GET') {
|
|
43
|
+
try {
|
|
44
|
+
return applyAuthResponse(req, res, await authService.completeOidcLogin(req, requestUrl));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('[api] Failed to complete OIDC login:', error.message);
|
|
47
|
+
jsonResponse(req, res, 500, { error: 'Failed to complete OIDC login' });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
24
52
|
if (requestUrl.pathname === '/api/auth/session' && req.method === 'POST') {
|
|
25
53
|
try {
|
|
26
54
|
const body = await parseJsonBody(req);
|
|
27
|
-
return applyAuthResponse(req, res, authService.createSession(req, body));
|
|
55
|
+
return applyAuthResponse(req, res, await authService.createSession(req, body));
|
|
28
56
|
} catch (error) {
|
|
29
57
|
const statusCode = getRequestErrorStatusCode(error);
|
|
30
58
|
if (statusCode) {
|
|
@@ -39,7 +67,7 @@ export function createAuthApiHandler({ authService }) {
|
|
|
39
67
|
}
|
|
40
68
|
|
|
41
69
|
if (requestUrl.pathname === '/api/auth/session' && req.method === 'DELETE') {
|
|
42
|
-
return applyAuthResponse(req, res, authService.clearSession(req));
|
|
70
|
+
return applyAuthResponse(req, res, await authService.clearSession(req));
|
|
43
71
|
}
|
|
44
72
|
|
|
45
73
|
jsonResponse(req, res, 404, { error: 'Auth endpoint not found' });
|
|
@@ -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);
|
|
@@ -29,19 +29,16 @@ function handleGitError(req, res, error, logMessage, fallbackMessage) {
|
|
|
29
29
|
return true;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|| (workspaceChange.deletedPaths?.length ?? 0) > 0
|
|
36
|
-
|| (workspaceChange.renamedPaths?.length ?? 0) > 0,
|
|
37
|
-
);
|
|
32
|
+
function readRequestId(req) {
|
|
33
|
+
const value = String(req.headers['x-collabmd-request-id'] || '').trim();
|
|
34
|
+
return value ? value.slice(0, 120) : null;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
async function applyWorkspaceMutationEffects({
|
|
41
|
-
|
|
38
|
+
action,
|
|
39
|
+
req,
|
|
42
40
|
responsePayload,
|
|
43
|
-
|
|
44
|
-
vaultFileStore,
|
|
41
|
+
workspaceMutationCoordinator,
|
|
45
42
|
}) {
|
|
46
43
|
const workspaceChange = responsePayload?.workspaceChange ?? createEmptyWorkspaceChange();
|
|
47
44
|
responsePayload.workspaceChange = workspaceChange;
|
|
@@ -50,18 +47,20 @@ async function applyWorkspaceMutationEffects({
|
|
|
50
47
|
return responsePayload;
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
51
|
+
action,
|
|
52
|
+
origin: 'git',
|
|
53
|
+
requestId: readRequestId(req),
|
|
54
|
+
sourceRef: responsePayload?.sourceRef ?? null,
|
|
55
|
+
workspaceChange,
|
|
56
|
+
});
|
|
57
57
|
return responsePayload;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function createGitApiCommandHandler({
|
|
61
|
-
|
|
61
|
+
authService = null,
|
|
62
62
|
gitService,
|
|
63
|
-
|
|
64
|
-
vaultFileStore = null,
|
|
63
|
+
workspaceMutationCoordinator = null,
|
|
65
64
|
}) {
|
|
66
65
|
return async function handleGitApiCommand(req, res, requestUrl) {
|
|
67
66
|
if (requestUrl.pathname === '/api/git/stage' && req.method === 'POST') {
|
|
@@ -72,10 +71,12 @@ export function createGitApiCommandHandler({
|
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
action: 'stage',
|
|
75
|
+
req,
|
|
76
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
77
|
+
() => gitService.stageFile(body.path),
|
|
78
|
+
),
|
|
79
|
+
workspaceMutationCoordinator,
|
|
79
80
|
}));
|
|
80
81
|
} catch (error) {
|
|
81
82
|
handleGitError(req, res, error, '[api] Failed to stage git file:', 'Failed to stage git file');
|
|
@@ -91,10 +92,12 @@ export function createGitApiCommandHandler({
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
action: 'unstage',
|
|
96
|
+
req,
|
|
97
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
98
|
+
() => gitService.unstageFile(body.path),
|
|
99
|
+
),
|
|
100
|
+
workspaceMutationCoordinator,
|
|
98
101
|
}));
|
|
99
102
|
} catch (error) {
|
|
100
103
|
handleGitError(req, res, error, '[api] Failed to unstage git file:', 'Failed to unstage git file');
|
|
@@ -110,12 +113,15 @@ export function createGitApiCommandHandler({
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
action: 'commit',
|
|
117
|
+
req,
|
|
118
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
119
|
+
() => gitService.commitStaged({
|
|
120
|
+
author: authService?.getAuthenticatedUser?.(req) ?? null,
|
|
121
|
+
message: body.message,
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
workspaceMutationCoordinator,
|
|
119
125
|
}));
|
|
120
126
|
} catch (error) {
|
|
121
127
|
handleGitError(req, res, error, '[api] Failed to commit staged changes:', 'Failed to commit staged changes');
|
|
@@ -126,10 +132,12 @@ export function createGitApiCommandHandler({
|
|
|
126
132
|
if (requestUrl.pathname === '/api/git/push' && req.method === 'POST') {
|
|
127
133
|
try {
|
|
128
134
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
action: 'push',
|
|
136
|
+
req,
|
|
137
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
138
|
+
() => gitService.pushBranch(),
|
|
139
|
+
),
|
|
140
|
+
workspaceMutationCoordinator,
|
|
133
141
|
}));
|
|
134
142
|
} catch (error) {
|
|
135
143
|
handleGitError(req, res, error, '[api] Failed to push git branch:', 'Failed to push git branch');
|
|
@@ -140,10 +148,12 @@ export function createGitApiCommandHandler({
|
|
|
140
148
|
if (requestUrl.pathname === '/api/git/pull' && req.method === 'POST') {
|
|
141
149
|
try {
|
|
142
150
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
action: 'pull',
|
|
152
|
+
req,
|
|
153
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
154
|
+
() => gitService.pullBranch(),
|
|
155
|
+
),
|
|
156
|
+
workspaceMutationCoordinator,
|
|
147
157
|
}));
|
|
148
158
|
} catch (error) {
|
|
149
159
|
handleGitError(req, res, error, '[api] Failed to pull git branch:', 'Failed to pull git branch');
|
|
@@ -159,10 +169,12 @@ export function createGitApiCommandHandler({
|
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
action: 'reset-file',
|
|
173
|
+
req,
|
|
174
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
175
|
+
() => gitService.resetFileToHead(body.path),
|
|
176
|
+
),
|
|
177
|
+
workspaceMutationCoordinator,
|
|
166
178
|
}));
|
|
167
179
|
} catch (error) {
|
|
168
180
|
handleGitError(req, res, error, '[api] Failed to reset git file:', 'Failed to reset git file');
|
|
@@ -3,17 +3,21 @@ import { createGitApiCommandHandler } from './create-git-api-command-handler.js'
|
|
|
3
3
|
import { createGitApiQueryHandler } from './create-git-api-query-handler.js';
|
|
4
4
|
|
|
5
5
|
export function createGitApiHandler({
|
|
6
|
+
authService = null,
|
|
6
7
|
backlinkIndex = null,
|
|
7
8
|
gitService = null,
|
|
8
9
|
roomRegistry = null,
|
|
9
10
|
vaultFileStore = null,
|
|
11
|
+
workspaceMutationCoordinator = null,
|
|
10
12
|
}) {
|
|
11
13
|
const handleGitApiQuery = createGitApiQueryHandler({ gitService });
|
|
12
14
|
const handleGitApiCommand = createGitApiCommandHandler({
|
|
15
|
+
authService,
|
|
13
16
|
backlinkIndex,
|
|
14
17
|
gitService,
|
|
15
18
|
roomRegistry,
|
|
16
19
|
vaultFileStore,
|
|
20
|
+
workspaceMutationCoordinator,
|
|
17
21
|
});
|
|
18
22
|
|
|
19
23
|
return async function handleGitApi(req, res, requestUrl) {
|
|
@@ -18,6 +18,16 @@ function isTruthyParam(value) {
|
|
|
18
18
|
return value === '1' || value === 'true';
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function readHistoryLimit(value) {
|
|
22
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : 30;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readHistoryOffset(value) {
|
|
27
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
28
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
function handleGitError(req, res, error, message, fallback) {
|
|
22
32
|
const statusCode = getRequestErrorStatusCode(error);
|
|
23
33
|
if (statusCode) {
|
|
@@ -61,6 +71,57 @@ export function createGitApiQueryHandler({ gitService }) {
|
|
|
61
71
|
return true;
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
if (requestUrl.pathname === '/api/git/history' && req.method === 'GET') {
|
|
75
|
+
try {
|
|
76
|
+
jsonResponse(req, res, 200, await gitService.getHistory({
|
|
77
|
+
limit: readHistoryLimit(requestUrl.searchParams.get('limit')),
|
|
78
|
+
offset: readHistoryOffset(requestUrl.searchParams.get('offset')),
|
|
79
|
+
}));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
handleGitError(req, res, error, '[api] Failed to read git history:', 'Failed to read git history');
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (requestUrl.pathname === '/api/git/file-history' && req.method === 'GET') {
|
|
87
|
+
try {
|
|
88
|
+
jsonResponse(req, res, 200, await gitService.getFileHistory({
|
|
89
|
+
limit: readHistoryLimit(requestUrl.searchParams.get('limit')),
|
|
90
|
+
offset: readHistoryOffset(requestUrl.searchParams.get('offset')),
|
|
91
|
+
path: requestUrl.searchParams.get('path'),
|
|
92
|
+
}));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
handleGitError(req, res, error, '[api] Failed to read git file history:', 'Failed to read git file history');
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (requestUrl.pathname === '/api/git/commit' && req.method === 'GET') {
|
|
100
|
+
try {
|
|
101
|
+
jsonResponse(req, res, 200, await gitService.getCommit({
|
|
102
|
+
allowLargePatch: isTruthyParam(requestUrl.searchParams.get('allowLargePatch')),
|
|
103
|
+
hash: requestUrl.searchParams.get('hash'),
|
|
104
|
+
metaOnly: isTruthyParam(requestUrl.searchParams.get('metaOnly')),
|
|
105
|
+
path: requestUrl.searchParams.get('path'),
|
|
106
|
+
}));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
handleGitError(req, res, error, '[api] Failed to read git commit:', 'Failed to read git commit');
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (requestUrl.pathname === '/api/git/file-snapshot' && req.method === 'GET') {
|
|
114
|
+
try {
|
|
115
|
+
jsonResponse(req, res, 200, await gitService.getFileSnapshot({
|
|
116
|
+
hash: requestUrl.searchParams.get('hash'),
|
|
117
|
+
path: requestUrl.searchParams.get('path'),
|
|
118
|
+
}));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
handleGitError(req, res, error, '[api] Failed to read git file snapshot:', 'Failed to read git file snapshot');
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
64
125
|
if (requestUrl.pathname === '/api/git/pull-backups' && req.method === 'GET') {
|
|
65
126
|
try {
|
|
66
127
|
jsonResponse(req, res, 200, {
|
|
@@ -3,6 +3,7 @@ import { createGitApiHandler } from './create-git-api-handler.js';
|
|
|
3
3
|
import { createEsmProxyHandler } from './create-esm-proxy-handler.js';
|
|
4
4
|
import { createStaticHandler } from './create-static-handler.js';
|
|
5
5
|
import { createVaultApiHandler } from './create-vault-api-handler.js';
|
|
6
|
+
import { parseJsonBody } from './request-body.js';
|
|
6
7
|
import {
|
|
7
8
|
applyCorsHeaders,
|
|
8
9
|
jsonResponse,
|
|
@@ -42,21 +43,27 @@ export function createRequestHandler(
|
|
|
42
43
|
roomRegistry = null,
|
|
43
44
|
plantUmlRenderer = null,
|
|
44
45
|
gitService = null,
|
|
46
|
+
testControls = { wsRoomHydrateDelayMs: 0 },
|
|
47
|
+
workspaceMutationCoordinator = null,
|
|
48
|
+
fileSystemSyncService = null,
|
|
45
49
|
) {
|
|
46
50
|
const handleEsmProxy = createEsmProxyHandler();
|
|
47
51
|
const handleStaticRequest = createStaticHandler(config, authService);
|
|
48
52
|
const handleAuthApi = createAuthApiHandler({ authService });
|
|
49
53
|
const handleGitApi = createGitApiHandler({
|
|
54
|
+
authService,
|
|
50
55
|
backlinkIndex,
|
|
51
56
|
gitService,
|
|
52
57
|
roomRegistry,
|
|
53
58
|
vaultFileStore,
|
|
59
|
+
workspaceMutationCoordinator,
|
|
54
60
|
});
|
|
55
61
|
const handleVaultApi = createVaultApiHandler({
|
|
56
62
|
backlinkIndex,
|
|
57
63
|
plantUmlRenderer,
|
|
58
64
|
roomRegistry,
|
|
59
65
|
vaultFileStore,
|
|
66
|
+
workspaceMutationCoordinator,
|
|
60
67
|
});
|
|
61
68
|
|
|
62
69
|
return async function handleRequest(req, res) {
|
|
@@ -103,12 +110,27 @@ export function createRequestHandler(
|
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
if (config.nodeEnv === 'test' && requestUrl.pathname === '/api/test/reset-state' && req.method === 'POST') {
|
|
113
|
+
await fileSystemSyncService?.resetForExternalStateChange?.();
|
|
106
114
|
await roomRegistry?.reset?.();
|
|
107
115
|
await backlinkIndex?.build?.();
|
|
116
|
+
await workspaceMutationCoordinator?.initialize?.();
|
|
117
|
+
await fileSystemSyncService?.resetForExternalStateChange?.();
|
|
108
118
|
jsonResponse(req, res, 200, { ok: true });
|
|
109
119
|
return;
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
if (config.nodeEnv === 'test' && requestUrl.pathname === '/api/test/hydrate-delay' && req.method === 'POST') {
|
|
123
|
+
const body = await parseJsonBody(req).catch(() => ({}));
|
|
124
|
+
testControls.wsRoomHydrateDelayMs = Math.max(0, Number(body?.delayMs) || 0);
|
|
125
|
+
await fileSystemSyncService?.resetForExternalStateChange?.();
|
|
126
|
+
await roomRegistry?.reset?.();
|
|
127
|
+
await backlinkIndex?.build?.();
|
|
128
|
+
await workspaceMutationCoordinator?.initialize?.();
|
|
129
|
+
await fileSystemSyncService?.resetForExternalStateChange?.();
|
|
130
|
+
jsonResponse(req, res, 200, { delayMs: testControls.wsRoomHydrateDelayMs, ok: true });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
if (await handleAuthApi(req, res, requestUrl)) {
|
|
113
135
|
return;
|
|
114
136
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isMermaidFilePath,
|
|
4
4
|
isPlantUmlFilePath,
|
|
5
5
|
} from '../../../domain/file-kind.js';
|
|
6
|
+
import { createWorkspaceChange } from '../../../domain/workspace-change.js';
|
|
6
7
|
import { createRequestError, getRequestErrorStatusCode } from './http-errors.js';
|
|
7
8
|
import { jsonResponse } from './http-response.js';
|
|
8
9
|
import { parseJsonBody, readBinaryRequestBody } from './request-body.js';
|
|
@@ -36,6 +37,11 @@ function selectWriteOperation(vaultFileStore, filePath, content) {
|
|
|
36
37
|
return vaultFileStore.writeMarkdownFile(filePath, content);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function readRequestId(req) {
|
|
41
|
+
const value = String(req.headers['x-collabmd-request-id'] || '').trim();
|
|
42
|
+
return value ? value.slice(0, 120) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
function handleVaultError(req, res, error, logMessage, fallbackMessage) {
|
|
40
46
|
const statusCode = getRequestErrorStatusCode(error);
|
|
41
47
|
if (statusCode) {
|
|
@@ -49,9 +55,8 @@ function handleVaultError(req, res, error, logMessage, fallbackMessage) {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export function createVaultApiCommandHandler({
|
|
52
|
-
backlinkIndex,
|
|
53
|
-
roomRegistry = null,
|
|
54
58
|
vaultFileStore,
|
|
59
|
+
workspaceMutationCoordinator = null,
|
|
55
60
|
}) {
|
|
56
61
|
return async function handleVaultApiCommand(req, res, requestUrl) {
|
|
57
62
|
if (requestUrl.pathname === '/api/file' && req.method === 'PUT') {
|
|
@@ -68,6 +73,15 @@ export function createVaultApiCommandHandler({
|
|
|
68
73
|
return true;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
77
|
+
action: 'write-file',
|
|
78
|
+
origin: 'api',
|
|
79
|
+
requestId: readRequestId(req),
|
|
80
|
+
workspaceChange: createWorkspaceChange({
|
|
81
|
+
changedPaths: [body.path],
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
71
85
|
jsonResponse(req, res, 200, { ok: true });
|
|
72
86
|
} catch (error) {
|
|
73
87
|
handleVaultError(req, res, error, '[api] Failed to write file:', 'Failed to write file');
|
|
@@ -97,6 +111,15 @@ export function createVaultApiCommandHandler({
|
|
|
97
111
|
return true;
|
|
98
112
|
}
|
|
99
113
|
|
|
114
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
115
|
+
action: 'upload-attachment',
|
|
116
|
+
origin: 'api',
|
|
117
|
+
requestId: readRequestId(req),
|
|
118
|
+
workspaceChange: createWorkspaceChange({
|
|
119
|
+
changedPaths: [result.path],
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
|
|
100
123
|
jsonResponse(req, res, 201, {
|
|
101
124
|
markdown: result.markdownSnippet,
|
|
102
125
|
ok: true,
|
|
@@ -121,8 +144,14 @@ export function createVaultApiCommandHandler({
|
|
|
121
144
|
jsonResponse(req, res, 409, { error: result.error });
|
|
122
145
|
return true;
|
|
123
146
|
}
|
|
124
|
-
|
|
125
|
-
|
|
147
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
148
|
+
action: 'create-file',
|
|
149
|
+
origin: 'api',
|
|
150
|
+
requestId: readRequestId(req),
|
|
151
|
+
workspaceChange: createWorkspaceChange({
|
|
152
|
+
changedPaths: [body.path],
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
126
155
|
jsonResponse(req, res, 201, { ok: true, path: body.path });
|
|
127
156
|
} catch (error) {
|
|
128
157
|
handleVaultError(req, res, error, '[api] Failed to create file:', 'Failed to create file');
|
|
@@ -137,21 +166,22 @@ export function createVaultApiCommandHandler({
|
|
|
137
166
|
return true;
|
|
138
167
|
}
|
|
139
168
|
|
|
140
|
-
const activeRoom = roomRegistry?.get(filePath);
|
|
141
169
|
try {
|
|
142
|
-
activeRoom?.markDeleted?.();
|
|
143
170
|
const result = await vaultFileStore.deleteFile(filePath);
|
|
144
171
|
if (!result.ok) {
|
|
145
|
-
activeRoom?.unmarkDeleted?.();
|
|
146
172
|
jsonResponse(req, res, 400, { error: result.error });
|
|
147
173
|
return true;
|
|
148
174
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
176
|
+
action: 'delete-file',
|
|
177
|
+
origin: 'api',
|
|
178
|
+
requestId: readRequestId(req),
|
|
179
|
+
workspaceChange: createWorkspaceChange({
|
|
180
|
+
deletedPaths: [filePath],
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
152
183
|
jsonResponse(req, res, 200, { ok: true });
|
|
153
184
|
} catch (error) {
|
|
154
|
-
activeRoom?.unmarkDeleted?.();
|
|
155
185
|
console.error('[api] Failed to delete file:', error.message);
|
|
156
186
|
jsonResponse(req, res, 500, { error: 'Failed to delete file' });
|
|
157
187
|
}
|
|
@@ -171,9 +201,14 @@ export function createVaultApiCommandHandler({
|
|
|
171
201
|
jsonResponse(req, res, 400, { error: result.error });
|
|
172
202
|
return true;
|
|
173
203
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
204
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
205
|
+
action: 'rename-file',
|
|
206
|
+
origin: 'api',
|
|
207
|
+
requestId: readRequestId(req),
|
|
208
|
+
workspaceChange: createWorkspaceChange({
|
|
209
|
+
renamedPaths: [{ oldPath: body.oldPath, newPath: body.newPath }],
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
177
212
|
jsonResponse(req, res, 200, { ok: true, path: body.newPath });
|
|
178
213
|
} catch (error) {
|
|
179
214
|
handleVaultError(req, res, error, '[api] Failed to rename file:', 'Failed to rename file');
|
|
@@ -195,6 +230,15 @@ export function createVaultApiCommandHandler({
|
|
|
195
230
|
return true;
|
|
196
231
|
}
|
|
197
232
|
|
|
233
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
234
|
+
action: 'create-directory',
|
|
235
|
+
origin: 'api',
|
|
236
|
+
requestId: readRequestId(req),
|
|
237
|
+
workspaceChange: createWorkspaceChange({
|
|
238
|
+
changedPaths: [body.path],
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
241
|
+
|
|
198
242
|
jsonResponse(req, res, 201, { ok: true });
|
|
199
243
|
} catch (error) {
|
|
200
244
|
handleVaultError(req, res, error, '[api] Failed to create directory:', 'Failed to create directory');
|
|
@@ -7,15 +7,18 @@ export function createVaultApiHandler({
|
|
|
7
7
|
plantUmlRenderer = null,
|
|
8
8
|
roomRegistry = null,
|
|
9
9
|
vaultFileStore,
|
|
10
|
+
workspaceMutationCoordinator = null,
|
|
10
11
|
}) {
|
|
11
12
|
const handleVaultApiQuery = createVaultApiQueryHandler({
|
|
12
13
|
backlinkIndex,
|
|
13
14
|
vaultFileStore,
|
|
15
|
+
workspaceMutationCoordinator,
|
|
14
16
|
});
|
|
15
17
|
const handleVaultApiCommand = createVaultApiCommandHandler({
|
|
16
18
|
backlinkIndex,
|
|
17
19
|
roomRegistry,
|
|
18
20
|
vaultFileStore,
|
|
21
|
+
workspaceMutationCoordinator,
|
|
19
22
|
});
|
|
20
23
|
const handlePlantUmlApi = createPlantUmlApiHandler({
|
|
21
24
|
plantUmlRenderer,
|
|
@@ -59,11 +59,13 @@ function selectReadOperation(vaultFileStore, filePath) {
|
|
|
59
59
|
export function createVaultApiQueryHandler({
|
|
60
60
|
backlinkIndex,
|
|
61
61
|
vaultFileStore,
|
|
62
|
+
workspaceMutationCoordinator = null,
|
|
62
63
|
}) {
|
|
63
64
|
return async function handleVaultApiQuery(req, res, requestUrl) {
|
|
64
65
|
if (requestUrl.pathname === '/api/files' && req.method === 'GET') {
|
|
65
66
|
try {
|
|
66
|
-
|
|
67
|
+
const tree = workspaceMutationCoordinator?.getWorkspaceTree?.() ?? await vaultFileStore.tree();
|
|
68
|
+
jsonResponse(req, res, 200, { tree });
|
|
67
69
|
} catch (error) {
|
|
68
70
|
console.error('[api] Failed to read file tree:', error.message);
|
|
69
71
|
jsonResponse(req, res, 500, { error: 'Failed to read file tree' });
|