viteboard 0.1.4 → 0.1.6

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/dist/index.html CHANGED
@@ -6,8 +6,8 @@
6
6
  <link rel="describedby" href="/llms.txt" />
7
7
  <link rel="alternate" type="application/json" href="/.well-known/viteboard.json" />
8
8
  <title>ViteBoard</title>
9
- <script type="module" crossorigin src="/assets/index-D2W76uu9.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-rMkUjGXP.css">
9
+ <script type="module" crossorigin src="/assets/index-WchUcu7k.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-2x-lv2vK.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viteboard",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Local-first documentation workspace and infinite-canvas whiteboard. Markdown docs, boards, and capability specs as plain files on disk.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/vite.config.ts CHANGED
@@ -39,8 +39,6 @@ const MIME_EXT: Record<string, string> = {
39
39
  "image/svg+xml": ".svg",
40
40
  };
41
41
  const MANAGED_ASSET_RE = /^assets\/[a-f0-9]{64}\.(png|jpe?g|webp|gif|svg)$/i;
42
- const ACTOR_TTL_MS = 30000;
43
- const LOCK_TTL_MS = 30000;
44
42
  const ROOT_LINK_HEADER = '</llms.txt>; rel="describedby"; type="text/plain", </.well-known/viteboard.json>; rel="alternate"; type="application/json"';
45
43
 
46
44
  const execFileAsync = promisify(execFile);
@@ -88,51 +86,12 @@ type TreeNode =
88
86
  | { type: "file"; name: string; path: string }
89
87
  | { type: "dir"; name: string; path: string; children: TreeNode[] };
90
88
 
91
- type ActorState =
92
- | "idle"
93
- | "viewing-doc"
94
- | "editing-doc"
95
- | "viewing-board"
96
- | "editing-board"
97
- | "viewing-project"
98
- | "writing";
99
-
100
- type ActorSessionRecord = {
101
- actorId: string;
102
- actorType: "human" | "agent";
103
- label: string;
104
- route: string;
105
- state: ActorState;
106
- rootId?: string;
107
- projectId?: string;
108
- docPath?: string;
109
- boardId?: string;
110
- updatedAt: number;
111
- expiresAt: number;
112
- };
113
-
114
- type LockRecord = {
115
- lockId: string;
116
- actorId: string;
117
- actorType: "human" | "agent";
118
- label: string;
119
- targetType: "doc" | "board";
120
- rootId: string;
121
- path?: string;
122
- boardId?: string;
123
- acquiredAt: number;
124
- expiresAt: number;
125
- };
126
-
127
89
  function contentApiPlugin(): Plugin {
128
90
  const cwd = process.cwd();
129
91
  const packageRoot = path.dirname(fileURLToPath(import.meta.url));
130
92
  const workspaceDir = path.resolve(cwd, WORKSPACE_DIR);
131
93
  const workspaceFile = path.join(workspaceDir, WORKSPACE_FILE);
132
94
  const machineDir = path.join(packageRoot, "content", "machine");
133
- const actorSessions = new Map<string, ActorSessionRecord>();
134
- const locks = new Map<string, LockRecord>();
135
- const sseClients = new Set<ServerResponse>();
136
95
 
137
96
  function send(res: ServerResponse, status: number, body: unknown): void {
138
97
  res.statusCode = status;
@@ -146,77 +105,6 @@ function contentApiPlugin(): Plugin {
146
105
  res.end(body);
147
106
  }
148
107
 
149
- function sendSse(res: ServerResponse, event: string, body: unknown): void {
150
- res.write(`event: ${event}\n`);
151
- res.write(`data: ${JSON.stringify(body)}\n\n`);
152
- }
153
-
154
- function broadcast(event: string, body: unknown): void {
155
- for (const client of sseClients) {
156
- try {
157
- sendSse(client, event, body);
158
- } catch {
159
- sseClients.delete(client);
160
- }
161
- }
162
- }
163
-
164
- function actorFromHeaders(req: IncomingMessage): { actorId: string; actorType: "human" | "agent"; label: string } | null {
165
- const actorId = String(req.headers["x-viteboard-actor-id"] ?? "").trim();
166
- if (!actorId) return null;
167
- const actorType = String(req.headers["x-viteboard-actor-type"] ?? "human") === "agent" ? "agent" : "human";
168
- const label = String(req.headers["x-viteboard-actor-label"] ?? "").trim() || (actorType === "agent" ? "Agent" : "ViteBoard UI");
169
- return { actorId, actorType, label };
170
- }
171
-
172
- function lockKey(target: { targetType: "doc" | "board"; rootId: string; path?: string; boardId?: string }): string {
173
- const suffix = target.targetType === "doc" ? target.path ?? "" : target.boardId ?? "";
174
- return `${target.targetType}:${target.rootId}:${suffix}`;
175
- }
176
-
177
- function activeActors(): ActorSessionRecord[] {
178
- return [...actorSessions.values()].sort((a, b) => a.label.localeCompare(b.label));
179
- }
180
-
181
- function activeLocks(): LockRecord[] {
182
- return [...locks.values()].sort((a, b) => a.acquiredAt - b.acquiredAt);
183
- }
184
-
185
- function broadcastPresence(): void {
186
- broadcast("presence", { actors: activeActors(), locks: activeLocks() });
187
- }
188
-
189
- function broadcastLocks(): void {
190
- broadcast("lock-changed", { locks: activeLocks() });
191
- }
192
-
193
- function pruneEphemeral(): void {
194
- const now = Date.now();
195
- let changedPresence = false;
196
- let changedLocks = false;
197
- for (const [actorId, actor] of actorSessions) {
198
- if (actor.expiresAt <= now) {
199
- actorSessions.delete(actorId);
200
- changedPresence = true;
201
- }
202
- }
203
- for (const [key, lock] of locks) {
204
- if (lock.expiresAt <= now || !actorSessions.has(lock.actorId)) {
205
- locks.delete(key);
206
- changedLocks = true;
207
- }
208
- }
209
- if (changedPresence) broadcastPresence();
210
- else if (changedLocks) broadcastLocks();
211
- }
212
-
213
- function extendActorLocks(actorId: string): void {
214
- const now = Date.now();
215
- for (const lock of locks.values()) {
216
- if (lock.actorId === actorId) lock.expiresAt = now + LOCK_TTL_MS;
217
- }
218
- }
219
-
220
108
  async function readMachineAsset(fileName: string): Promise<string> {
221
109
  return fs.readFile(path.join(machineDir, fileName), "utf-8");
222
110
  }
@@ -225,8 +113,6 @@ function contentApiPlugin(): Plugin {
225
113
  return createHash("sha1").update(content).digest("hex");
226
114
  }
227
115
 
228
- setInterval(pruneEphemeral, 10000).unref?.();
229
-
230
116
  function readBody(req: IncomingMessage): Promise<string> {
231
117
  return new Promise((resolve, reject) => {
232
118
  let data = "";
@@ -755,169 +641,11 @@ function contentApiPlugin(): Plugin {
755
641
  void (async () => {
756
642
  try {
757
643
  const method = (req.method ?? "GET").toUpperCase();
758
- const actor = actorFromHeaders(req);
759
- pruneEphemeral();
760
644
 
761
645
  if (route === "/__api/workspace" && method === "GET") {
762
646
  return send(res, 200, await toWorkspaceResponse(await getWorkspace()));
763
647
  }
764
648
 
765
- if (route === "/__api/events" && method === "GET") {
766
- res.statusCode = 200;
767
- res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
768
- res.setHeader("Cache-Control", "no-cache, no-transform");
769
- res.setHeader("Connection", "keep-alive");
770
- res.write(": connected\n\n");
771
- sseClients.add(res);
772
- sendSse(res, "hello", { actors: activeActors(), locks: activeLocks() });
773
- req.on("close", () => {
774
- sseClients.delete(res);
775
- });
776
- return;
777
- }
778
-
779
- if (route === "/__api/actors/session" && method === "GET") {
780
- return send(res, 200, { actors: activeActors() });
781
- }
782
-
783
- if (route === "/__api/actors/session" && method === "POST") {
784
- const body = JSON.parse(await readBody(req));
785
- const actorId = String(body.actorId ?? actor?.actorId ?? "").trim();
786
- if (!actorId) return send(res, 400, { error: "actorId is required" });
787
- const actorType = String(body.actorType ?? actor?.actorType ?? "human") === "agent" ? "agent" : "human";
788
- const label = String(body.label ?? actor?.label ?? "").trim() || (actorType === "agent" ? "Agent" : "ViteBoard UI");
789
- const now = Date.now();
790
- actorSessions.set(actorId, {
791
- actorId,
792
- actorType,
793
- label,
794
- route: String(body.route ?? "#/"),
795
- state: String(body.state ?? "idle") as ActorState,
796
- rootId: typeof body.rootId === "string" ? body.rootId : undefined,
797
- projectId: typeof body.projectId === "string" ? body.projectId : undefined,
798
- docPath: typeof body.docPath === "string" ? body.docPath : undefined,
799
- boardId: typeof body.boardId === "string" ? body.boardId : undefined,
800
- selectedElementIds: Array.isArray(body.selectedElementIds)
801
- ? body.selectedElementIds.filter((item: unknown): item is string => typeof item === "string")
802
- : undefined,
803
- updatedAt: now,
804
- expiresAt: now + ACTOR_TTL_MS,
805
- });
806
- extendActorLocks(actorId);
807
- broadcastPresence();
808
- return send(res, 200, { ok: true });
809
- }
810
-
811
- if (route === "/__api/actors/heartbeat" && method === "POST") {
812
- const body = JSON.parse(await readBody(req));
813
- const actorId = String(body.actorId ?? actor?.actorId ?? "").trim();
814
- if (!actorId) return send(res, 400, { error: "actorId is required" });
815
- const current = actorSessions.get(actorId);
816
- if (!current) return send(res, 404, { error: "Actor session not found" });
817
- const now = Date.now();
818
- const next: ActorSessionRecord = {
819
- ...current,
820
- route: typeof body.route === "string" ? body.route : current.route,
821
- state: typeof body.state === "string" ? (body.state as ActorState) : current.state,
822
- rootId: typeof body.rootId === "string" ? body.rootId : current.rootId,
823
- projectId: typeof body.projectId === "string" ? body.projectId : current.projectId,
824
- docPath: typeof body.docPath === "string" ? body.docPath : current.docPath,
825
- boardId: typeof body.boardId === "string" ? body.boardId : current.boardId,
826
- selectedElementIds: Array.isArray(body.selectedElementIds)
827
- ? body.selectedElementIds.filter((item: unknown): item is string => typeof item === "string")
828
- : current.selectedElementIds,
829
- updatedAt: now,
830
- expiresAt: now + ACTOR_TTL_MS,
831
- };
832
- actorSessions.set(actorId, next);
833
- extendActorLocks(actorId);
834
- broadcastPresence();
835
- return send(res, 200, { ok: true });
836
- }
837
-
838
- if (route === "/__api/actors/session" && method === "DELETE") {
839
- const actorId = parsed.searchParams.get("actorId") ?? actor?.actorId ?? "";
840
- if (!actorId) return send(res, 400, { error: "actorId is required" });
841
- actorSessions.delete(actorId);
842
- let changedLocks = false;
843
- for (const [key, lock] of locks) {
844
- if (lock.actorId === actorId) {
845
- locks.delete(key);
846
- changedLocks = true;
847
- }
848
- }
849
- if (changedLocks) broadcastLocks();
850
- broadcastPresence();
851
- return send(res, 200, { ok: true });
852
- }
853
-
854
- if (route === "/__api/locks" && method === "GET") {
855
- return send(res, 200, { locks: activeLocks() });
856
- }
857
-
858
- if (route === "/__api/locks/acquire" && method === "POST") {
859
- const body = JSON.parse(await readBody(req));
860
- if (!actor?.actorId) return send(res, 400, { error: "actor identity is required" });
861
- const targetType = body.targetType === "board" ? "board" : body.targetType === "doc" ? "doc" : null;
862
- const rootId = String(body.rootId ?? "").trim();
863
- if (!targetType || !rootId) return send(res, 400, { error: "targetType and rootId are required" });
864
- const resolved = await resolveRoot(rootId);
865
- if (!resolved) return send(res, 404, { error: "Docs root not found" });
866
- const targetPath = targetType === "doc" ? String(body.path ?? "").trim() : undefined;
867
- const targetBoardId = targetType === "board" ? String(body.boardId ?? "").trim() : undefined;
868
- const targetAbs = targetType === "doc"
869
- ? safeResolve(resolved.root.absPath, targetPath ?? "")
870
- : safeResolve(resolved.root.absPath, `${targetBoardId}.board.json`);
871
- if (!targetAbs) {
872
- return send(res, 400, { error: targetType === "doc" ? "Invalid path" : "Invalid boardId" });
873
- }
874
- const key = lockKey({ targetType, rootId, path: targetPath, boardId: targetBoardId });
875
- const now = Date.now();
876
- const existing = locks.get(key);
877
- if (existing && existing.actorId !== actor.actorId && existing.expiresAt > now) {
878
- return send(res, 409, { error: `${existing.label} is editing this ${targetType}.`, lock: existing });
879
- }
880
- const nextLock: LockRecord = existing && existing.actorId === actor.actorId
881
- ? { ...existing, expiresAt: now + LOCK_TTL_MS }
882
- : {
883
- lockId: `lock-${hashPath(`${key}:${now}`)}`,
884
- actorId: actor.actorId,
885
- actorType: actor.actorType,
886
- label: actor.label,
887
- targetType,
888
- rootId,
889
- path: targetPath,
890
- boardId: targetBoardId,
891
- acquiredAt: now,
892
- expiresAt: now + LOCK_TTL_MS,
893
- };
894
- locks.set(key, nextLock);
895
- broadcastLocks();
896
- broadcastPresence();
897
- return send(res, 200, { ok: true, lock: nextLock });
898
- }
899
-
900
- if (route === "/__api/locks/release" && method === "POST") {
901
- const body = JSON.parse(await readBody(req));
902
- if (!actor?.actorId) return send(res, 400, { error: "actor identity is required" });
903
- const targetType = body.targetType === "board" ? "board" : body.targetType === "doc" ? "doc" : null;
904
- const rootId = String(body.rootId ?? "").trim();
905
- if (!targetType || !rootId) return send(res, 400, { error: "targetType and rootId are required" });
906
- const key = lockKey({
907
- targetType,
908
- rootId,
909
- path: targetType === "doc" ? String(body.path ?? "").trim() : undefined,
910
- boardId: targetType === "board" ? String(body.boardId ?? "").trim() : undefined,
911
- });
912
- const existing = locks.get(key);
913
- if (!existing) return send(res, 200, { ok: true });
914
- if (existing.actorId !== actor.actorId) return send(res, 403, { error: "Lock owned by another actor" });
915
- locks.delete(key);
916
- broadcastLocks();
917
- broadcastPresence();
918
- return send(res, 200, { ok: true });
919
- }
920
-
921
649
  if (route === "/__api/workspace/project" && method === "POST") {
922
650
  const { rootPath, label } = JSON.parse(await readBody(req));
923
651
  const absRoot = await canonicalizeProjectRoot(String(rootPath ?? "").trim());
@@ -938,7 +666,6 @@ function contentApiPlugin(): Plugin {
938
666
  if (existingIndex === -1) manifest.projects.push(project);
939
667
  else manifest.projects.splice(existingIndex, 1, project);
940
668
  await saveManifest(manifest);
941
- broadcast("workspace-changed", { action: "project-added", projectId: project.id, actorId: actor?.actorId ?? null });
942
669
  return send(res, 200, await toWorkspaceResponse(await getWorkspace()));
943
670
  }
944
671
 
@@ -961,7 +688,6 @@ function contentApiPlugin(): Plugin {
961
688
  if (existingIndex === -1) manifest.projects.push(project);
962
689
  else manifest.projects.splice(existingIndex, 1, project);
963
690
  await saveManifest(manifest);
964
- broadcast("workspace-changed", { action: "project-added", projectId: project.id, actorId: actor?.actorId ?? null });
965
691
  return send(res, 200, { cancelled: false, workspace: await toWorkspaceResponse(await getWorkspace()), addedProjectId: project.id });
966
692
  }
967
693
 
@@ -977,7 +703,6 @@ function contentApiPlugin(): Plugin {
977
703
  const root = refreshedProject?.docsRoots.find((item) => item.absPath === docsPath)
978
704
  ?? refreshedProject?.docsRoots[0];
979
705
  if (!root) return send(res, 500, { error: "Could not create docs root" });
980
- broadcast("workspace-changed", { action: "docs-root-created", projectId: project.id, rootId: root.id, actorId: actor?.actorId ?? null });
981
706
  return send(res, 200, {
982
707
  root: {
983
708
  id: root.id,
@@ -1002,7 +727,6 @@ function contentApiPlugin(): Plugin {
1002
727
  const refreshedProject = workspace.find((item) => item.id === project.id);
1003
728
  const root = refreshedProject?.docsRoots.find((item) => item.absPath === docsPath);
1004
729
  if (!root) return send(res, 500, { error: "Could not create docs area" });
1005
- broadcast("workspace-changed", { action: "docs-area-created", projectId: project.id, rootId: root.id, actorId: actor?.actorId ?? null });
1006
730
  return send(res, 200, {
1007
731
  root: {
1008
732
  id: root.id,
@@ -1022,7 +746,6 @@ function contentApiPlugin(): Plugin {
1022
746
  if (!project) return send(res, 404, { error: "Project not found" });
1023
747
  const docsPath = path.join(project.rootPath, String(parentPath ?? ""), "docs");
1024
748
  await fs.mkdir(docsPath, { recursive: true });
1025
- broadcast("workspace-changed", { action: "docs-folder-created", projectId: project.id, actorId: actor?.actorId ?? null });
1026
749
  return send(res, 200, { ok: true, workspace: await toWorkspaceResponse(await getWorkspace()) });
1027
750
  }
1028
751
 
@@ -1031,7 +754,6 @@ function contentApiPlugin(): Plugin {
1031
754
  const manifest = await loadManifest();
1032
755
  manifest.projects = manifest.projects.filter((item) => item.id !== id);
1033
756
  await saveManifest(manifest);
1034
- broadcast("workspace-changed", { action: "project-removed", projectId: id, actorId: actor?.actorId ?? null });
1035
757
  return send(res, 200, await toWorkspaceResponse(await getWorkspace()));
1036
758
  }
1037
759
 
@@ -1091,7 +813,6 @@ function contentApiPlugin(): Plugin {
1091
813
  const nextContent = String(content ?? "");
1092
814
  await fs.writeFile(abs, nextContent, "utf-8");
1093
815
  const revision = revisionForText(nextContent);
1094
- broadcast("file-changed", { rootId: resolved.root.id, path: String(rel), action: "write", actorId: actor?.actorId ?? null });
1095
816
  return send(res, 200, { ok: true, path: rel, revision });
1096
817
  }
1097
818
 
@@ -1102,7 +823,6 @@ function contentApiPlugin(): Plugin {
1102
823
  const abs = safeResolve(resolved.root.absPath, String(rel));
1103
824
  if (!abs) return send(res, 400, { error: "Invalid path" });
1104
825
  await fs.mkdir(abs, { recursive: true });
1105
- broadcast("workspace-changed", { action: "directory-created", rootId: resolved.root.id, path: String(rel), actorId: actor?.actorId ?? null });
1106
826
  return send(res, 200, { ok: true, path: rel });
1107
827
  }
1108
828
 
@@ -1118,7 +838,6 @@ function contentApiPlugin(): Plugin {
1118
838
  } catch (err) {
1119
839
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
1120
840
  }
1121
- broadcast("file-changed", { rootId: resolved.root.id, path: rel, action: "delete", actorId: actor?.actorId ?? null });
1122
841
  return send(res, 200, { ok: true });
1123
842
  }
1124
843
 
@@ -1139,7 +858,6 @@ function contentApiPlugin(): Plugin {
1139
858
  }
1140
859
  throw err;
1141
860
  }
1142
- broadcast("file-changed", { rootId: resolved.root.id, path: String(newPath), oldPath: String(oldPath), action: "rename", actorId: actor?.actorId ?? null });
1143
861
  return send(res, 200, { ok: true, path: newPath });
1144
862
  }
1145
863
 
@@ -1182,7 +900,6 @@ function contentApiPlugin(): Plugin {
1182
900
  const nextContent = JSON.stringify(doc, null, 2) + "\n";
1183
901
  await fs.writeFile(abs, nextContent, "utf-8");
1184
902
  const revision = revisionForText(nextContent);
1185
- broadcast("board-changed", { rootId: resolved.root.id, boardId: String(id), action: "write", actorId: actor?.actorId ?? null });
1186
903
  return send(res, 200, { ok: true, id, revision });
1187
904
  }
1188
905
 
@@ -1198,7 +915,6 @@ function contentApiPlugin(): Plugin {
1198
915
  } catch (err) {
1199
916
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
1200
917
  }
1201
- broadcast("board-changed", { rootId: resolved.root.id, boardId: id, action: "delete", actorId: actor?.actorId ?? null });
1202
918
  return send(res, 200, { ok: true, id });
1203
919
  }
1204
920
 
@@ -1236,7 +952,6 @@ function contentApiPlugin(): Plugin {
1236
952
  } catch {
1237
953
  await fs.writeFile(abs, bytes);
1238
954
  }
1239
- broadcast("asset-changed", { rootId: resolved.root.id, path: rel, action: "write", actorId: actor?.actorId ?? null });
1240
955
  return send(res, 200, { ok: true, path: rel });
1241
956
  }
1242
957
 
@@ -1248,7 +963,6 @@ function contentApiPlugin(): Plugin {
1248
963
  resolved.root.absPath,
1249
964
  Array.isArray(preservePaths) ? preservePaths.filter((item): item is string => typeof item === "string") : []
1250
965
  );
1251
- if (removed > 0) broadcast("asset-changed", { rootId: resolved.root.id, action: "cleanup", removed, actorId: actor?.actorId ?? null });
1252
966
  return send(res, 200, { ok: true, removed });
1253
967
  }
1254
968