ryugi-artifacts 1.16.2 → 1.17.0

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/cli.cjs CHANGED
@@ -281,10 +281,16 @@ async function comment(options, emit) {
281
281
  throw new CliError(`The running artifacts server is older than this CLI. Run \`${invocation()} start --project-dir ${server.project}\` to replace it, then run this again.`);
282
282
  }
283
283
  if (status < 200 || status >= 300) throw new CliError(json.error || `HTTP ${status}`);
284
- if (action === "list") emit({ type: "comments", artifact: options.artifact, threads: json.threads, ...options.all ? { all: true } : {} });
284
+ if (action === "list") emit({ type: "comments", artifact: options.artifact, threads: json.threads, ...json.groups ? { groups: json.groups } : {}, ...options.all ? { all: true } : {} });
285
285
  else emit({ type: WRITTEN[action], artifact: options.artifact, threadId: json.threadId, ...json.messageId ? { messageId: json.messageId } : {} });
286
286
  return 0;
287
287
  }
288
+ var VERDICT_TEXT = { pass: "passed", reject: "rejected", pending: "waiting for a verdict" };
289
+ var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
290
+ function groupText(group) {
291
+ const parts = [VERDICT_TEXT[group.verdict], group.evidenceUpdated ? "evidence replaced since the last verdict" : "", group.rejectThreads ? `${plural(group.rejectThreads, "open reject thread")}` : ""];
292
+ return ` ${group.id}${group.title ? ` (${group.title})` : ""}: ${parts.filter(Boolean).join(" \xB7 ")}`;
293
+ }
288
294
  var clip = (text2, max) => text2.length > max ? `${text2.slice(0, max)}\u2026` : text2;
289
295
  var percent = (value) => `${Math.round(value * 100)}%`;
290
296
  function anchorLines(thread) {
@@ -312,8 +318,12 @@ function threadText(thread) {
312
318
  function commentText(value) {
313
319
  if (value.type === "comments") {
314
320
  const threads = value.threads ?? [];
315
- if (!threads.length) return value.all ? `No comments on ${value.artifact}.` : `No new comments on ${value.artifact}.`;
316
- return threads.map(threadText).join("\n\n");
321
+ const groups = value.groups ?? [];
322
+ const head = threads.length ? threads.map(threadText).join("\n\n") : value.all ? `No comments on ${value.artifact}.` : `No new comments on ${value.artifact}.`;
323
+ return groups.length ? `${head}
324
+
325
+ Verify groups:
326
+ ${groups.map(groupText).join("\n")}` : head;
317
327
  }
318
328
  if (value.type === "comment-added") return `Added thread ${value.threadId} (message ${value.messageId})`;
319
329
  if (value.type === "comment-replied") return `Replied in thread ${value.threadId} (message ${value.messageId})`;
@@ -339,6 +349,7 @@ var import_node_path2 = __toESM(require("node:path"), 1);
339
349
  // src/shared/validate.ts
340
350
  var BOARD_NUMBERS = ["x", "y", "w", "h"];
341
351
  var BOARD_REQUIRED = ["file", "page", ...BOARD_NUMBERS];
352
+ var MEDIA_REQUIRED = ["src", "page", ...BOARD_NUMBERS];
342
353
 
343
354
  // src/server/state.ts
344
355
  function writeJsonAtomic(file, value) {
@@ -911,7 +922,7 @@ async function main(argv) {
911
922
  }, emit);
912
923
  }
913
924
  if (command === "--version" || command === "-v") {
914
- console.log("1.16.2");
925
+ console.log("1.17.0");
915
926
  return 0;
916
927
  }
917
928
  if (command === "--help" || command === "-h") {
package/dist/server.cjs CHANGED
@@ -5182,6 +5182,8 @@ var import_node_path20 = __toESM(require("node:path"), 1);
5182
5182
 
5183
5183
  // src/shared/types.ts
5184
5184
  var ARTIFACT_TYPES = ["artboard", "document", "html"];
5185
+ var BOARD_TYPES = ["html", "image", "video"];
5186
+ var isMediaBoard = (board) => board.type === "image" || board.type === "video";
5185
5187
 
5186
5188
  // src/server/artifacts.ts
5187
5189
  var import_node_fs = __toESM(require("node:fs"), 1);
@@ -17961,6 +17963,14 @@ function readBoardAnchor(value) {
17961
17963
  if (!isName(value.board)) return null;
17962
17964
  anchor.board = value.board;
17963
17965
  }
17966
+ if (value.group !== void 0) {
17967
+ if (!isName(value.group)) return null;
17968
+ anchor.group = value.group;
17969
+ }
17970
+ if (value.verdict !== void 0) {
17971
+ if (!anchor.group || value.verdict !== "pass" && value.verdict !== "reject") return null;
17972
+ anchor.verdict = value.verdict;
17973
+ }
17964
17974
  if (value.nodes !== void 0) {
17965
17975
  const nodes = readNodes(value.nodes);
17966
17976
  if (!nodes) return null;
@@ -17980,17 +17990,24 @@ function readBoardAnchor(value) {
17980
17990
  }
17981
17991
  var boardAnchors = {
17982
17992
  read: readBoardAnchor,
17983
- same: (a5, b6) => a5.page === b6.page && a5.board === b6.board && a5.x === b6.x && a5.y === b6.y,
17993
+ same: (a5, b6) => a5.page === b6.page && a5.board === b6.board && a5.group === b6.group && a5.verdict === b6.verdict && a5.x === b6.x && a5.y === b6.y,
17984
17994
  missing: "a new thread needs a board anchor (page, x, y); dropped it"
17985
17995
  };
17986
17996
  var emptyBoardComments = () => ({ version: 1, threads: [] });
17987
17997
  var savedBoardComments = (incoming) => savedComments(incoming, boardAnchors);
17988
17998
  var mergeBoardComments = (base, incoming, origin, now, newId, name) => mergeComments(base, incoming, origin, now, newId, name, boardAnchors);
17989
17999
  function applyBoardHumanOp(file, op, now, newId, who) {
17990
- if (op.op === "create") return { ...file, threads: [...file.threads, openThread(op.anchor, "human", op.text, now, newId, who)] };
17991
- return applyThreadOp(file, op, now, newId, who);
18000
+ if (op.op !== "create") return applyThreadOp(file, op, now, newId, who);
18001
+ const thread = openThread(op.anchor, "human", op.text, now, newId, who);
18002
+ if (op.anchor.verdict !== "pass") return { ...file, threads: [...file.threads, thread] };
18003
+ const closed = { status: "resolved", statusAt: now, statusBy: "human" };
18004
+ const rejected = (t7) => t7.anchor.group === op.anchor.group && t7.anchor.verdict === "reject" && t7.status === "open";
18005
+ return { ...file, threads: [...file.threads.map((t7) => rejected(t7) ? { ...t7, ...closed } : t7), { ...thread, ...closed }] };
17992
18006
  }
17993
18007
  function applyBoardAgentOp(file, op, now, newId, name) {
18008
+ if (op.op === "resolve" && file.threads.find((t7) => t7.id === op.threadId)?.anchor.verdict === "reject") {
18009
+ throw new Error("a reject closes only when your human partner passes the group");
18010
+ }
17994
18011
  if (op.op === "add") {
17995
18012
  const thread = openThread(op.anchor, "agent", op.text, now, newId, { name });
17996
18013
  return { file: { ...file, threads: [...file.threads, thread] }, threadId: thread.id, messageId: thread.messages[0].id };
@@ -17998,6 +18015,19 @@ function applyBoardAgentOp(file, op, now, newId, name) {
17998
18015
  return applyAgentThreadOp(file, op, now, newId, name);
17999
18016
  }
18000
18017
 
18018
+ // src/shared/verdicts.ts
18019
+ var openedAt = (thread) => thread.messages[0]?.at ?? thread.statusAt;
18020
+ function groupVerdict(threads, group, stale) {
18021
+ const own = threads.filter((thread) => thread.anchor.group === group && thread.anchor.verdict && thread.messages.some((m11) => !m11.deleted)).sort((a5, b6) => openedAt(a5) < openedAt(b6) ? -1 : openedAt(a5) > openedAt(b6) ? 1 : 0);
18022
+ const rejectThreads = own.filter((thread) => thread.anchor.verdict === "reject" && thread.status === "open").length;
18023
+ const latest = own.at(-1);
18024
+ if (!latest) return { state: "pending", evidenceUpdated: false, rejectThreads };
18025
+ const at7 = openedAt(latest);
18026
+ if (stale.has(latest.id)) return { state: "pending", at: at7, evidenceUpdated: true, rejectThreads };
18027
+ if (latest.anchor.verdict === "pass") return { state: "pass", at: at7, evidenceUpdated: false, rejectThreads };
18028
+ return { state: latest.status === "open" ? "reject" : "pending", at: at7, evidenceUpdated: false, rejectThreads };
18029
+ }
18030
+
18001
18031
  // src/server/comment-cursors.ts
18002
18032
  var import_node_fs6 = __toESM(require("node:fs"), 1);
18003
18033
  var import_node_path6 = __toESM(require("node:path"), 1);
@@ -18093,6 +18123,9 @@ var intersects = (a5, b6) => a5.x < b6.x + b6.w && b6.x < a5.x + a5.w && a5.y <
18093
18123
  // src/shared/validate.ts
18094
18124
  var BOARD_NUMBERS = ["x", "y", "w", "h"];
18095
18125
  var BOARD_REQUIRED = ["file", "page", ...BOARD_NUMBERS];
18126
+ var MEDIA_REQUIRED = ["src", "page", ...BOARD_NUMBERS];
18127
+ var fileOf = (board) => isMediaBoard(board) ? board.src ?? "" : board.file;
18128
+ var fileField = (board) => isMediaBoard(board) ? "src" : "file";
18096
18129
  var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18097
18130
  function structuralWarnings(raw) {
18098
18131
  if (!isObject2(raw)) return ["canvas.json must be a JSON object"];
@@ -18122,6 +18155,16 @@ function structuralWarnings(raw) {
18122
18155
  if (isObject2(board) && board.interactive !== void 0 && typeof board.interactive !== "boolean") {
18123
18156
  warnings.push(`board "${id2}" field "interactive" must be a boolean`);
18124
18157
  }
18158
+ if (isObject2(board) && board.type !== void 0 && !BOARD_TYPES.includes(board.type)) {
18159
+ warnings.push(`board "${id2}" field "type" must be "html", "image" or "video"`);
18160
+ }
18161
+ }
18162
+ }
18163
+ if (isObject2(raw.groups)) {
18164
+ for (const [id2, group] of Object.entries(raw.groups)) {
18165
+ if (isObject2(group) && group.verify !== void 0 && typeof group.verify !== "boolean") {
18166
+ warnings.push(`group "${id2}" field "verify" must be a boolean`);
18167
+ }
18125
18168
  }
18126
18169
  }
18127
18170
  if (warnings.length > 0) return warnings;
@@ -18135,7 +18178,7 @@ function structuralWarnings(raw) {
18135
18178
  warnings.push(`board "${id2}" must be an object`);
18136
18179
  continue;
18137
18180
  }
18138
- for (const key2 of BOARD_REQUIRED) {
18181
+ for (const key2 of isMediaBoard(board) ? MEDIA_REQUIRED : BOARD_REQUIRED) {
18139
18182
  if (board[key2] === void 0) warnings.push(`board "${id2}" is missing "${key2}"`);
18140
18183
  }
18141
18184
  for (const key2 of BOARD_NUMBERS) {
@@ -18143,7 +18186,7 @@ function structuralWarnings(raw) {
18143
18186
  warnings.push(`board "${id2}" field "${key2}" must be a number`);
18144
18187
  }
18145
18188
  }
18146
- for (const key2 of ["file", "page"]) {
18189
+ for (const key2 of ["file", "src", "page"]) {
18147
18190
  if (board[key2] !== void 0 && typeof board[key2] !== "string") {
18148
18191
  warnings.push(`board "${id2}" field "${key2}" must be a string`);
18149
18192
  }
@@ -18174,10 +18217,11 @@ function validateCanvas(raw, fileExists, options = {}) {
18174
18217
  const notes = Object.entries(canvas.notes ?? {});
18175
18218
  for (const [id2, board] of boards) {
18176
18219
  if (!pages.has(board.page)) warnings.push(`board "${id2}" uses unknown page "${board.page}"`);
18177
- if (!board.file.startsWith("pages/")) warnings.push(`board "${id2}" file must start with "pages/": ${board.file}`);
18178
- else if (!fileExists(board.file)) {
18220
+ const file = fileOf(board);
18221
+ if (!file.startsWith("pages/")) warnings.push(`board "${id2}" ${fileField(board)} must start with "pages/": ${file}`);
18222
+ else if (!fileExists(file)) {
18179
18223
  options.pending?.add(id2);
18180
- if (!options.allowPending) warnings.push(`board "${id2}" file not found: ${board.file}`);
18224
+ if (!options.allowPending) warnings.push(`board "${id2}" file not found: ${file}`);
18181
18225
  }
18182
18226
  }
18183
18227
  for (let i6 = 0; i6 < boards.length; i6++) {
@@ -18288,6 +18332,10 @@ function boardVersions(boardDir, canvas) {
18288
18332
  }
18289
18333
  try {
18290
18334
  const stat = import_node_fs5.default.statSync(target);
18335
+ if (isMediaBoard(board)) {
18336
+ versions[board.file] = stat.size > 0 ? stat.mtimeMs : 0;
18337
+ continue;
18338
+ }
18291
18339
  const html = import_node_fs5.default.readFileSync(target, "utf8");
18292
18340
  const document2 = /<(?:!doctype|html)\b/i.test(html);
18293
18341
  const ready = html.trim().length > 0 && (!document2 || /<\/html>\s*(?:<!--[\s\S]*?-->\s*)*$/i.test(html));
@@ -18309,6 +18357,7 @@ function readCanvas(boardDir) {
18309
18357
  const structural = structuralWarnings(raw);
18310
18358
  if (structural.length > 0) return { canvas: null, warnings: structural, pending: [], layout: null, versions: {} };
18311
18359
  const canvas = { ...raw };
18360
+ canvas.boards = Object.fromEntries(Object.entries(canvas.boards).map(([id2, board]) => [id2, isMediaBoard(board) ? { ...board, file: board.src ?? "" } : board]));
18312
18361
  const { versions, writing } = boardVersions(boardDir, canvas);
18313
18362
  const pending = /* @__PURE__ */ new Set();
18314
18363
  for (const [id2, board] of Object.entries(canvas.boards)) {
@@ -18487,6 +18536,7 @@ var boardRules = {
18487
18536
  agent: applyBoardAgentOp
18488
18537
  };
18489
18538
  var withoutHashes = ({ hashes: _hashes, ...anchor }) => anchor;
18539
+ var withoutVerdict = ({ hashes: _hashes, group: _group, verdict: _verdict, ...anchor }) => anchor;
18490
18540
  function asBoardHumanOp(value) {
18491
18541
  if (typeof value !== "object" || value === null) return null;
18492
18542
  const v6 = value;
@@ -18502,22 +18552,30 @@ function asBoardAgentOp(value) {
18502
18552
  const v6 = value;
18503
18553
  if (v6.op === "add") {
18504
18554
  const anchor = readBoardAnchor(v6.anchor);
18505
- return anchor && isText3(v6.text) && v6.text.length > 0 ? { op: "add", anchor: withoutHashes(anchor), text: v6.text } : null;
18555
+ return anchor && isText3(v6.text) && v6.text.length > 0 ? { op: "add", anchor: withoutVerdict(anchor), text: v6.text } : null;
18506
18556
  }
18507
18557
  if (v6.op === "reply") return isId(v6.threadId) && isText3(v6.text) && v6.text.length > 0 ? { op: "reply", threadId: v6.threadId, text: v6.text } : null;
18508
18558
  if (v6.op === "resolve") return isId(v6.threadId) ? { op: "resolve", threadId: v6.threadId } : null;
18509
18559
  return null;
18510
18560
  }
18511
- function boardSpecs(dir) {
18561
+ function readCanvasJson(dir) {
18512
18562
  try {
18513
- const canvas = JSON.parse(import_node_fs8.default.readFileSync(import_node_path7.default.join(dir, "canvas.json"), "utf8"));
18514
- return Object.fromEntries(Object.entries(canvas.boards ?? {}).flatMap(
18515
- ([id2, board]) => typeof board?.file === "string" ? [[id2, { file: board.file, page: typeof board.page === "string" ? board.page : "", w: typeof board.w === "number" ? board.w : 0 }]] : []
18516
- ));
18563
+ return JSON.parse(import_node_fs8.default.readFileSync(import_node_path7.default.join(dir, "canvas.json"), "utf8"));
18517
18564
  } catch {
18518
18565
  return {};
18519
18566
  }
18520
18567
  }
18568
+ function boardSpecs(dir) {
18569
+ return Object.fromEntries(Object.entries(readCanvasJson(dir).boards ?? {}).flatMap(([id2, board]) => {
18570
+ const file = board?.type === "image" || board?.type === "video" ? board.src : board?.file;
18571
+ return typeof file === "string" ? [[id2, { file, page: typeof board.page === "string" ? board.page : "", w: typeof board.w === "number" ? board.w : 0 }]] : [];
18572
+ }));
18573
+ }
18574
+ function groupSpecs(dir) {
18575
+ return Object.fromEntries(Object.entries(readCanvasJson(dir).groups ?? {}).flatMap(
18576
+ ([id2, group]) => Array.isArray(group?.boards) ? [[id2, { ...typeof group.title === "string" ? { title: group.title } : {}, boards: group.boards.filter((b6) => typeof b6 === "string"), verify: group.verify === true }]] : []
18577
+ ));
18578
+ }
18521
18579
  var boardFiles = (dir) => Object.fromEntries(Object.entries(boardSpecs(dir)).map(([id2, spec]) => [id2, spec.file]));
18522
18580
  function agentAddOnBoard(value, specs) {
18523
18581
  if (typeof value !== "object" || value === null) return null;
@@ -18527,11 +18585,18 @@ function agentAddOnBoard(value, specs) {
18527
18585
  if (!spec) return { error: `no board ${v6.board}` };
18528
18586
  return asBoardAgentOp({ op: "add", anchor: { page: spec.page, board: v6.board, x: v6.x ?? spec.w, y: v6.y ?? 0 }, text: v6.text });
18529
18587
  }
18588
+ var hashCache = /* @__PURE__ */ new Map();
18530
18589
  function fileHash(dir, relative) {
18531
18590
  const file = resolveBoardFile(dir, relative);
18532
18591
  if (!file) return null;
18533
18592
  try {
18534
- return import_node_crypto2.default.createHash("sha1").update(import_node_fs8.default.readFileSync(file)).digest("hex").slice(0, 12);
18593
+ const stat = import_node_fs8.default.statSync(file);
18594
+ const stamp2 = `${stat.size}:${stat.mtimeMs}`;
18595
+ const cached = hashCache.get(file);
18596
+ if (cached?.stamp === stamp2) return cached.hash;
18597
+ const hash = import_node_crypto2.default.createHash("sha1").update(import_node_fs8.default.readFileSync(file)).digest("hex").slice(0, 12);
18598
+ hashCache.set(file, { stamp: stamp2, hash });
18599
+ return hash;
18535
18600
  } catch {
18536
18601
  return null;
18537
18602
  }
@@ -18542,13 +18607,20 @@ function boardHashes(dir) {
18542
18607
  return hash ? [[id2, hash]] : [];
18543
18608
  }));
18544
18609
  }
18545
- var boardsOf = (anchor) => [.../* @__PURE__ */ new Set([...anchor.board ? [anchor.board] : [], ...(anchor.nodes ?? []).map((node) => node.board)])];
18546
- function stamp(anchor, hashes) {
18547
- const boards = boardsOf(anchor);
18610
+ var boardsOf = (anchor, groups = {}) => [
18611
+ .../* @__PURE__ */ new Set([...anchor.board ? [anchor.board] : [], ...(anchor.nodes ?? []).map((node) => node.board), ...anchor.group ? groups[anchor.group]?.boards ?? [] : []])
18612
+ ];
18613
+ function stamp(anchor, hashes, groups = {}) {
18614
+ const boards = boardsOf(anchor, groups);
18548
18615
  if (!boards.length) return withoutHashes(anchor);
18549
18616
  return { ...withoutHashes(anchor), hashes: Object.fromEntries(boards.map((board) => [board, hashes[board] ?? "missing"])) };
18550
18617
  }
18551
- var isStale = (thread, hashes) => Object.entries(thread.anchor.hashes ?? {}).some(([board, hash]) => hashes[board] !== hash);
18618
+ var sameMembers = (thread, groups) => {
18619
+ const group = thread.anchor.group;
18620
+ if (!group) return true;
18621
+ return Object.keys(thread.anchor.hashes ?? {}).sort().join("\n") === [...groups[group]?.boards ?? []].sort().join("\n");
18622
+ };
18623
+ var isStale = (thread, hashes, groups) => !sameMembers(thread, groups) || Object.entries(thread.anchor.hashes ?? {}).some(([board, hash]) => hashes[board] !== hash);
18552
18624
  function createBoardCommentRegistry(options) {
18553
18625
  const stores = /* @__PURE__ */ new Map();
18554
18626
  const hashes = /* @__PURE__ */ new Map();
@@ -18573,7 +18645,8 @@ function createBoardCommentRegistry(options) {
18573
18645
  };
18574
18646
  const stale = (id2) => {
18575
18647
  const now = currentHashes(id2);
18576
- return store(id2).current().threads.filter((thread) => isStale(thread, now)).map((thread) => thread.id);
18648
+ const groups = groupSpecs(dirOf(id2));
18649
+ return store(id2).current().threads.filter((thread) => isStale(thread, now, groups)).map((thread) => thread.id);
18577
18650
  };
18578
18651
  const push2 = (id2) => {
18579
18652
  const ids = stale(id2);
@@ -18586,11 +18659,12 @@ function createBoardCommentRegistry(options) {
18586
18659
  };
18587
18660
  const forAgent2 = (id2, staleIds) => {
18588
18661
  const files = boardFiles(dirOf(id2));
18662
+ const groups = groupSpecs(dirOf(id2));
18589
18663
  return (thread) => ({
18590
18664
  ...thread,
18591
18665
  stale: staleIds.has(thread.id),
18592
18666
  waitingOn: waitingOn(thread),
18593
- files: Object.fromEntries(boardsOf(thread.anchor).flatMap((board) => files[board] ? [[board, files[board]]] : [])),
18667
+ files: Object.fromEntries(boardsOf(thread.anchor, groups).flatMap((board) => files[board] ? [[board, files[board]]] : [])),
18594
18668
  messages: thread.messages.map(({ avatar: _avatar, ...message }) => message)
18595
18669
  });
18596
18670
  };
@@ -18599,7 +18673,9 @@ function createBoardCommentRegistry(options) {
18599
18673
  stale,
18600
18674
  warnings: (id2) => store(id2).warnings(),
18601
18675
  apply: (id2, op) => {
18602
- const stamped = op.op === "create" ? { ...op, anchor: stamp(op.anchor, fresh(id2)) } : op;
18676
+ const groups = groupSpecs(dirOf(id2));
18677
+ if (op.op === "create" && op.anchor.verdict && !groups[op.anchor.group]?.verify) throw new Error(`no verify group ${op.anchor.group} in artboard/${id2}`);
18678
+ const stamped = op.op === "create" ? { ...op, anchor: stamp(op.anchor, fresh(id2), groups) } : op;
18603
18679
  store(id2).apply(stamped);
18604
18680
  push2(id2);
18605
18681
  return { comments: store(id2).current(), stale: stale(id2) };
@@ -18620,6 +18696,16 @@ function createBoardCommentRegistry(options) {
18620
18696
  if (newest) cursors.set(id2, agent, newest);
18621
18697
  return threads.map(format);
18622
18698
  },
18699
+ groups: (id2) => {
18700
+ const specs = boardSpecs(dirOf(id2));
18701
+ const threads = store(id2).current().threads;
18702
+ const staleIds = new Set(stale(id2));
18703
+ return Object.entries(groupSpecs(dirOf(id2))).filter(([, group]) => group.verify).map(([groupId, group]) => {
18704
+ const { state, ...verdict } = groupVerdict(threads, groupId, staleIds);
18705
+ const page = group.boards.map((board) => specs[board]?.page).find(Boolean) ?? "";
18706
+ return { id: groupId, page, ...group.title ? { title: group.title } : {}, verdict: state, ...verdict, boards: group.boards };
18707
+ });
18708
+ },
18623
18709
  sync: (id2) => {
18624
18710
  if (store(id2).sync()) push2(id2);
18625
18711
  },
@@ -88587,10 +88673,29 @@ function send(res, status, body, headers = {}) {
88587
88673
  function sendJson(res, status, value) {
88588
88674
  send(res, status, JSON.stringify(value), { "Content-Type": MIME2[".json"] });
88589
88675
  }
88590
- function sendFile(res, file, headers = {}) {
88591
- import_node_fs10.default.readFile(file, (err, data) => {
88592
- if (err) send(res, 404, "Not found");
88593
- else send(res, 200, data, { "Content-Type": mimeOf(file), ...headers });
88676
+ function byteRange(range, size) {
88677
+ const match = range && /^bytes=(\d*)-(\d*)$/.exec(range.trim());
88678
+ if (!match || !match[1] && !match[2]) return null;
88679
+ const start = match[1] ? Number(match[1]) : Math.max(0, size - Number(match[2]));
88680
+ const end = match[1] && match[2] ? Math.min(Number(match[2]), size - 1) : size - 1;
88681
+ return start > end || start >= size ? "unsatisfiable" : { start, end };
88682
+ }
88683
+ function sendFile(res, file, headers = {}, range) {
88684
+ import_node_fs10.default.stat(file, (err, stat) => {
88685
+ if (err || !stat.isFile()) {
88686
+ send(res, 404, "Not found");
88687
+ return;
88688
+ }
88689
+ const base = { "Referrer-Policy": "no-referrer", "Cache-Control": "no-store", "Content-Type": mimeOf(file), "Accept-Ranges": "bytes", ...headers };
88690
+ const part = byteRange(range, stat.size);
88691
+ if (part === "unsatisfiable") {
88692
+ res.writeHead(416, { ...base, "Content-Range": `bytes */${stat.size}` });
88693
+ res.end();
88694
+ return;
88695
+ }
88696
+ if (part) res.writeHead(206, { ...base, "Content-Range": `bytes ${part.start}-${part.end}/${stat.size}`, "Content-Length": String(part.end - part.start + 1) });
88697
+ else res.writeHead(200, { ...base, "Content-Length": String(stat.size) });
88698
+ import_node_fs10.default.createReadStream(file, part ?? {}).on("error", () => res.destroy()).pipe(res);
88594
88699
  });
88595
88700
  }
88596
88701
 
@@ -88750,15 +88855,15 @@ function readAnySelection(value) {
88750
88855
  function createSelectionStore(stateDir, exists) {
88751
88856
  const tabs = /* @__PURE__ */ new Map();
88752
88857
  let clock = 0;
88753
- const fileOf = (kind, id2) => import_node_path12.default.join(stateDir, kind, id2, "selection.json");
88858
+ const fileOf2 = (kind, id2) => import_node_path12.default.join(stateDir, kind, id2, "selection.json");
88754
88859
  const write = (kind, id2) => {
88755
88860
  let winner;
88756
88861
  for (const tab of tabs.values()) {
88757
88862
  if (tab.kind !== kind || tab.id !== id2) continue;
88758
88863
  if (!winner || tab.humanAt > winner.humanAt || tab.humanAt === winner.humanAt && tab.seenAt > winner.seenAt) winner = tab;
88759
88864
  }
88760
- if (!winner?.state) import_node_fs12.default.rmSync(fileOf(kind, id2), { force: true });
88761
- else writeJsonAtomic(fileOf(kind, id2), { artifact: `${kind}/${id2}`, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...winner.state });
88865
+ if (!winner?.state) import_node_fs12.default.rmSync(fileOf2(kind, id2), { force: true });
88866
+ else writeJsonAtomic(fileOf2(kind, id2), { artifact: `${kind}/${id2}`, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...winner.state });
88762
88867
  };
88763
88868
  return {
88764
88869
  accept(socket, value) {
@@ -88792,7 +88897,7 @@ function createSelectionStore(stateDir, exists) {
88792
88897
  } catch {
88793
88898
  continue;
88794
88899
  }
88795
- for (const id2 of ids) import_node_fs12.default.rmSync(fileOf(kind, id2), { force: true });
88900
+ for (const id2 of ids) import_node_fs12.default.rmSync(fileOf2(kind, id2), { force: true });
88796
88901
  }
88797
88902
  }
88798
88903
  };
@@ -89998,14 +90103,16 @@ function createAutoPublisher(options) {
89998
90103
  var import_node_fs21 = __toESM(require("node:fs"), 1);
89999
90104
  var RESOLVE = { artboard: resolveBoardFile, document: resolveDocumentAsset, html: resolveHtmlFile };
90000
90105
  var isArtifactPage = (type, relative) => type === "html" ? relative === "index.html" : relative.endsWith(".html");
90001
- function readArtifactFile(dir, type, relative, uiRuntimeUrl) {
90106
+ function readArtifactFile(dir, type, relative, uiRuntimeUrl, maxBytes = Infinity) {
90002
90107
  if (!resolveInside(dir, relative)) return null;
90003
90108
  const target = RESOLVE[type](dir, relative);
90004
90109
  if (!target) return null;
90005
90110
  let data;
90006
90111
  try {
90112
+ if (import_node_fs21.default.statSync(target).size > maxBytes) throw new RangeError("too large");
90007
90113
  data = import_node_fs21.default.readFileSync(target);
90008
- } catch {
90114
+ } catch (err) {
90115
+ if (err instanceof RangeError) throw err;
90009
90116
  return null;
90010
90117
  }
90011
90118
  if (!isArtifactPage(type, relative)) return { contentType: mimeOf(target), data };
@@ -90348,7 +90455,11 @@ async function startArtifactsServer(options) {
90348
90455
  sendJson(res, 400, { error: "not a comment op" });
90349
90456
  return;
90350
90457
  }
90351
- sendJson(res, 200, boardComments.apply(id2, op));
90458
+ try {
90459
+ sendJson(res, 200, boardComments.apply(id2, op));
90460
+ } catch (err) {
90461
+ sendJson(res, 400, { error: err.message });
90462
+ }
90352
90463
  };
90353
90464
  const serveAgentBoardComments = async (req, res, id2) => {
90354
90465
  const dir = resolveArtifactDir(root, "artboard", id2);
@@ -90360,7 +90471,8 @@ async function startArtifactsServer(options) {
90360
90471
  const fields = typeof body === "object" && body !== null ? body : {};
90361
90472
  const agent = typeof fields.agent === "string" && fields.agent.length > 0 && fields.agent.length <= 100 ? fields.agent : void 0;
90362
90473
  if (fields.op === "list") {
90363
- sendJson(res, 200, { type: "comments", artifact: `artboard/${id2}`, threads: boardComments.list(id2, agent ?? "Agent", fields.all === true) });
90474
+ const threads = boardComments.list(id2, agent ?? "Agent", fields.all === true);
90475
+ sendJson(res, 200, { type: "comments", artifact: `artboard/${id2}`, threads, groups: boardComments.groups(id2) });
90364
90476
  return;
90365
90477
  }
90366
90478
  const shorthand = agentAddOnBoard(body, boardSpecs(dir));
@@ -90384,7 +90496,7 @@ async function startArtifactsServer(options) {
90384
90496
  }
90385
90497
  };
90386
90498
  const isAuthorized = (req, keyFromQuery) => keyFromQuery !== null && safeEqual(keyFromQuery, token) || safeEqual(parseCookies(req.headers.cookie)[cookieName] ?? "", token);
90387
- const serveContent = (res, rest) => {
90499
+ const serveContent = (res, rest, range) => {
90388
90500
  if (rest.startsWith("runtime/")) {
90389
90501
  if (!runtimeDir) {
90390
90502
  send(res, 404, "Not found");
@@ -90420,7 +90532,7 @@ async function startArtifactsServer(options) {
90420
90532
  const page = ref.type === "html" ? relative === "index.html" : relative.endsWith(".html");
90421
90533
  const headers = { "Content-Security-Policy": CONTENT_CSP[page && ref.type === "document" ? "html" : ref.type], "Access-Control-Allow-Origin": "*" };
90422
90534
  if (!page) {
90423
- sendFile(res, target, headers);
90535
+ sendFile(res, target, headers, range);
90424
90536
  return;
90425
90537
  }
90426
90538
  import_node_fs23.default.readFile(target, "utf8", (err, source) => {
@@ -90520,7 +90632,7 @@ async function startArtifactsServer(options) {
90520
90632
  }
90521
90633
  if (pathname.startsWith("/c/")) {
90522
90634
  if (!pathname.startsWith(contentPrefix)) send(res, 404, "Not found");
90523
- else serveContent(res, pathname.slice(contentPrefix.length));
90635
+ else serveContent(res, pathname.slice(contentPrefix.length), req.headers.range);
90524
90636
  return;
90525
90637
  }
90526
90638
  if (cloud && pathname === "/api/cloud/callback") {
@@ -90748,7 +90860,7 @@ async function startArtifactsServer(options) {
90748
90860
  const request = typeof body === "object" && body !== null ? body : {};
90749
90861
  if (request.op === "meta") return artifactMeta(ref, dir, contentOrigin);
90750
90862
  if (request.op === "file") {
90751
- const file = typeof request.path === "string" ? readArtifactFile(dir, ref.type, request.path, CLOUD_RUNTIME_URL) : null;
90863
+ const file = typeof request.path === "string" ? readArtifactFile(dir, ref.type, request.path, CLOUD_RUNTIME_URL, MAX_RELAY_FILE) : null;
90752
90864
  if (!file) throw new Error("not found");
90753
90865
  if (file.data.length > MAX_RELAY_FILE) throw new Error("too large");
90754
90866
  return { contentType: file.contentType, data: file.data.toString("base64") };
@@ -90967,7 +91079,7 @@ function openReady(payload) {
90967
91079
  }
90968
91080
 
90969
91081
  // package.json
90970
- var version = "1.16.2";
91082
+ var version = "1.17.0";
90971
91083
 
90972
91084
  // src/server/main.ts
90973
91085
  async function main() {
@@ -1 +1 @@
1
- import{i as e}from"./rolldown-runtime-COiEfhsA.js";import{n as t,t as n}from"./jsx-runtime-CldxteV5.js";import{a as r}from"./useSandboxFrame-N5uRz7Cc.js";import{t as i}from"./index.js";import a from"./CloudDocument-DQ8BLzr7.js";import{DocumentView as o,t as s}from"./DocumentPage-CgCHoFVv.js";var c=e(t(),1),l=`1.16.2`,u=e=>/^(\d+)\.(\d+)\./.exec(e??``)?.slice(1,3).join(`.`)??null;function d(e,t){let n=u(e);return n!==null&&n===u(t)}var f=(e,t)=>`电脑上的 ryugi 是 ${t??`未知版本`},网页是 ${e},版本对上之前只能评论`,p=n();function m({id:e,state:t,contentBase:n,liveContentBase:u,comments:m}){let{transport:h,presence:g}=s(e,u??n??``),_=g?.online??!1;r(e,{online:g?.online??null});let[v,y]=(0,c.useState)(!1);if((0,c.useEffect)(()=>{_&&y(!0)},[_]),!h||!(v||_))return!t||!n?(0,p.jsx)(i,{children:g===null?`正在连接…`:`还没发布。电脑在线时,这里会实时显示。`}):(0,p.jsx)(a,{id:e,state:t,contentBase:n,comments:m,offline:g!==null&&!_});let b=_&&d(l,g?.version??null),x=_&&!b?f(l,g?.version??null):null;return(0,p.jsx)(o,{id:e,transport:h,live:_,cloud:{online:_,canEdit:b,reason:x}})}export{m as default};
1
+ import{i as e}from"./rolldown-runtime-COiEfhsA.js";import{n as t,t as n}from"./jsx-runtime-CldxteV5.js";import{a as r}from"./useSandboxFrame-N5uRz7Cc.js";import{t as i}from"./index.js";import a from"./CloudDocument-DQ8BLzr7.js";import{DocumentView as o,t as s}from"./DocumentPage-CgCHoFVv.js";var c=e(t(),1),l=`1.17.0`,u=e=>/^(\d+)\.(\d+)\./.exec(e??``)?.slice(1,3).join(`.`)??null;function d(e,t){let n=u(e);return n!==null&&n===u(t)}var f=(e,t)=>`电脑上的 ryugi 是 ${t??`未知版本`},网页是 ${e},版本对上之前只能评论`,p=n();function m({id:e,state:t,contentBase:n,liveContentBase:u,comments:m}){let{transport:h,presence:g}=s(e,u??n??``),_=g?.online??!1;r(e,{online:g?.online??null});let[v,y]=(0,c.useState)(!1);if((0,c.useEffect)(()=>{_&&y(!0)},[_]),!h||!(v||_))return!t||!n?(0,p.jsx)(i,{children:g===null?`正在连接…`:`还没发布。电脑在线时,这里会实时显示。`}):(0,p.jsx)(a,{id:e,state:t,contentBase:n,comments:m,offline:g!==null&&!_});let b=_&&d(l,g?.version??null),x=_&&!b?f(l,g?.version??null):null;return(0,p.jsx)(o,{id:e,transport:h,live:_,cloud:{online:_,canEdit:b,reason:x}})}export{m as default};