@hyperframes/studio-server 0.8.77 → 0.8.78

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.
@@ -1,4 +1,4 @@
1
- export { g as PreviewApiAdapter, P as PreviewDocumentStore, n as injectMediaCodecMap, p as injectMediaCodecMapIntoHtml, q as isAutoProxyEnabled, r as proxyEtagSalt, s as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-CIayJ4Y4.js';
1
+ export { h as PreviewApiAdapter, P as PreviewDocumentStore, p as injectMediaCodecMap, q as injectMediaCodecMapIntoHtml, r as isAutoProxyEnabled, s as proxyEtagSalt, t as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-BmJ8AmLd.js';
2
2
  import './mediaCodecMap.js';
3
3
  import '@hyperframes/core';
4
4
  import '@hyperframes/parsers';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono';
2
- import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-CIayJ4Y4.js';
3
- export { D as DEFAULT_HISTORY_ROOT, a as HISTORY_START, H as HistoryEntry, b as HistoryFileChange, c as HistoryListItem, d as HistoryResult, e as HistoryWho, f as HistoryWindow, L as LintResult, g as PreviewApiAdapter, h as ProjectHistory, i as ProjectHistoryOptions, R as RenderJobState, j as ResolvedProject, k as StudioSelectionResponse, l as StudioSelectionSnapshot, m as StudioSelectionTextField, o as openProjectHistory } from './mediaProxyPreview-CIayJ4Y4.js';
2
+ import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-BmJ8AmLd.js';
3
+ export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, a as HISTORY_START, H as HistoryEntry, b as HistoryFileChange, c as HistoryListItem, d as HistoryResult, e as HistoryWho, f as HistoryWindow, L as LintResult, g as MAX_WINDOW_IDLE_MS, h as PreviewApiAdapter, i as ProjectHistory, j as ProjectHistoryOptions, R as RenderJobState, k as ResolvedProject, l as StudioSelectionResponse, m as StudioSelectionSnapshot, n as StudioSelectionTextField, o as openProjectHistory } from './mediaProxyPreview-BmJ8AmLd.js';
4
4
  import { SourceMutationTarget } from './helpers/sourceMutation.js';
5
5
  export { PatchOperation, dedupeClonedCompositionId, findTargetElement, parseSourceDocument, patchElementInHtml, removeElementFromHtml, splitElementInHtml } from './helpers/sourceMutation.js';
6
6
  export { ScreenshotClip, getElementScreenshotClip } from './helpers/screenshotClip.js';
@@ -57,7 +57,8 @@ declare class HistoryBusyError extends Error {
57
57
  constructor(pid: number);
58
58
  }
59
59
 
60
- /** Recursively walk a directory and return relative file paths. */
60
+ /** Recursively walk a directory and return relative file paths. A subfolder that is unreadable
61
+ * or removed mid-walk is skipped; any other failure, or an unreadable `dir` itself, throws. */
61
62
  declare function walkDir(dir: string, prefix?: string): string[];
62
63
 
63
64
  /** Pins hf-ids into every composition in the project. A host runs it once before it serves or
package/dist/index.js CHANGED
@@ -72,9 +72,18 @@ function isInHiddenOrVendorDir(relPath) {
72
72
  const segments = relPath.split("/");
73
73
  return segments.slice(0, -1).some((seg) => seg.startsWith(".") || seg === "node_modules");
74
74
  }
75
+ var UNREADABLE_DIR_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "ENOENT", "ENOTDIR"]);
75
76
  function walkDir(dir, prefix = "") {
77
+ let entries;
78
+ try {
79
+ entries = readdirSync(dir, { withFileTypes: true });
80
+ } catch (err) {
81
+ const code = err.code;
82
+ if (!prefix || !code || !UNREADABLE_DIR_CODES.has(code)) throw err;
83
+ return [];
84
+ }
76
85
  const files = [];
77
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
86
+ for (const entry of entries) {
78
87
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
79
88
  if (IGNORE_DIRS.has(entry.name) || shouldIgnoreDir(rel)) continue;
80
89
  if (entry.isDirectory()) {
@@ -161,6 +170,15 @@ function openNoFollow(filePath, flags) {
161
170
  return null;
162
171
  }
163
172
  }
173
+ function isUnchanged(filePath, expected) {
174
+ const fd = openNoFollow(filePath, constants.O_RDONLY);
175
+ if (fd === null) return false;
176
+ try {
177
+ return readFileSync(fd, "utf-8") === expected;
178
+ } finally {
179
+ closeSync(fd);
180
+ }
181
+ }
164
182
  function stampFileHfIds(filePath) {
165
183
  let fd = openNoFollow(filePath, constants.O_RDWR);
166
184
  let writable = true;
@@ -179,7 +197,7 @@ function stampFileHfIds(filePath) {
179
197
  const mode = fstatSync(fd).mode;
180
198
  closeSync(fd);
181
199
  fd = null;
182
- replaceFileAtomically(filePath, normalized, mode);
200
+ if (isUnchanged(filePath, html)) replaceFileAtomically(filePath, normalized, mode);
183
201
  }
184
202
  return normalized;
185
203
  } catch (err) {
@@ -776,6 +794,8 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
776
794
  }));
777
795
  const results = [];
778
796
  const attempted = [];
797
+ for (const mutation of prepared)
798
+ assertExpectedVersion(mutation.expectedVersion, readFileSync4(mutation.absPath, "utf-8"));
779
799
  try {
780
800
  for (const mutation of prepared) {
781
801
  results.push(applyOneMutation(projectDir, mutation, requestToken, writeFile, attempted));
@@ -785,7 +805,8 @@ function applyFileMutations(projectDir, mutations, requestToken, writeFile = (pa
785
805
  const rollbackErrors = [];
786
806
  for (const mutation of attempted.reverse()) {
787
807
  try {
788
- writeFile(mutation.absPath, mutation.before, "utf-8");
808
+ if (!mutation.written || readFileSync4(mutation.absPath, "utf-8") === mutation.after)
809
+ writeFile(mutation.absPath, mutation.before, "utf-8");
789
810
  clearFileWriteReceipt(mutation.absPath, mutation.version, mutation.writeToken);
790
811
  } catch (rollbackError) {
791
812
  rollbackErrors.push(rollbackError);
@@ -818,8 +839,10 @@ function applyOneMutation(projectDir, mutation, requestToken, writeFile, attempt
818
839
  const before = current;
819
840
  const version = fileContentVersion(mutation.after);
820
841
  const writeToken = createWriteToken(requestToken);
821
- attempted.push({ ...mutation, before, version, writeToken });
842
+ const attempt = { ...mutation, before, version, writeToken, written: false };
843
+ attempted.push(attempt);
822
844
  writeFile(mutation.absPath, mutation.after, "utf-8");
845
+ attempt.written = true;
823
846
  recordFileWriteReceipt(mutation.absPath, {
824
847
  path: mutation.sourceFile,
825
848
  version,
@@ -5174,151 +5197,6 @@ function registerGlobalAssetRoutes(api) {
5174
5197
  api.get("/assets/global", (c) => c.json({ assets: readGlobalAssets().map(toPublicAsset) }));
5175
5198
  }
5176
5199
 
5177
- // src/routes/history.ts
5178
- var YOU = { kind: "person", name: "You" };
5179
- var MAX_WINDOW_IDLE_MS = 10 * 6e4;
5180
- async function historyOf(adapter, c) {
5181
- const project = await adapter.resolveProject(c.req.param("id") ?? "");
5182
- return project ? await adapter.history?.(project) ?? null : null;
5183
- }
5184
- async function bodyOf(c) {
5185
- const body = await c.req.json().catch(() => null);
5186
- return body && typeof body === "object" ? body : {};
5187
- }
5188
- var text = (value) => typeof value === "string" && value ? value : null;
5189
- function writing(c) {
5190
- const header = c.req.header("X-Hyperframes-Write-Token");
5191
- return header ? { writeToken: createWriteToken(header) } : {};
5192
- }
5193
- function idleOf(body) {
5194
- const idleMs = body.idleMs;
5195
- return typeof idleMs === "number" && idleMs > 0 ? Math.min(idleMs, MAX_WINDOW_IDLE_MS) : void 0;
5196
- }
5197
- function versionsOf(value) {
5198
- if (!value || typeof value !== "object") return void 0;
5199
- const pairs = Object.entries(value).filter(
5200
- (pair) => typeof pair[1] === "string"
5201
- );
5202
- return pairs.length ? Object.fromEntries(pairs) : void 0;
5203
- }
5204
- function nextStep(history, direction) {
5205
- const target = history.next(direction);
5206
- if (!target) return null;
5207
- const paths = target.files.map((file) => file.path);
5208
- return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
5209
- }
5210
- async function withHistory(adapter, c, task) {
5211
- const history = await historyOf(adapter, c);
5212
- if (!history) return c.json({ error: "This project has no history here." }, 404);
5213
- try {
5214
- return c.json(await task(history, await bodyOf(c)) ?? null);
5215
- } catch (error) {
5216
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
5217
- }
5218
- }
5219
- function registerHistoryRoutes(api, adapter) {
5220
- const windows = /* @__PURE__ */ new Map();
5221
- const base = "/projects/:id/history";
5222
- api.get(
5223
- base,
5224
- (c) => withHistory(adapter, c, (history) => {
5225
- const entries = history.list();
5226
- return { entries, back: nextStep(history, "back"), forward: nextStep(history, "forward") };
5227
- })
5228
- );
5229
- api.post(
5230
- `${base}/step`,
5231
- (c) => withHistory(
5232
- adapter,
5233
- c,
5234
- (history, body) => history.step(body.direction === "forward" ? "forward" : "back", YOU, writing(c))
5235
- )
5236
- );
5237
- api.post(
5238
- `${base}/undo`,
5239
- (c) => withHistory(adapter, c, (history, body) => {
5240
- const mode = body.mode === "just-this" || body.mode === "back-to-before" ? body.mode : void 0;
5241
- return history.undo(text(body.entryId) ?? "", { who: YOU, mode, ...writing(c) });
5242
- })
5243
- );
5244
- api.post(
5245
- `${base}/restore`,
5246
- (c) => withHistory(
5247
- adapter,
5248
- c,
5249
- (history, body) => history.restore(text(body.point) ?? "", YOU, writing(c))
5250
- )
5251
- );
5252
- api.get(
5253
- `${base}/peek/:point`,
5254
- (c) => withHistory(adapter, c, (history) => ({ files: history.peek(c.req.param("point")) }))
5255
- );
5256
- api.post(
5257
- `${base}/pin`,
5258
- (c) => withHistory(adapter, c, (history, body) => {
5259
- history.pin(text(body.entryId) ?? "", body.pinned === true);
5260
- return { ok: true };
5261
- })
5262
- );
5263
- api.post(
5264
- `${base}/claim`,
5265
- (c) => withHistory(adapter, c, async (history, body) => {
5266
- const paths = Array.isArray(body.paths) ? body.paths.filter((path) => text(path)) : [];
5267
- const coalesceKey = text(body.coalesceKey) ?? void 0;
5268
- const idleMs = idleOf(body);
5269
- const overwrote = versionsOf(body.overwrote);
5270
- const claimed = await history.claim(YOU, text(body.label) ?? "Edited in Studio", paths, {
5271
- ...coalesceKey && { coalesceKey },
5272
- ...idleMs && { idleMs },
5273
- ...overwrote && { overwrote }
5274
- });
5275
- return { claimed };
5276
- })
5277
- );
5278
- api.post(
5279
- `${base}/window`,
5280
- (c) => withHistory(adapter, c, async (history, body) => {
5281
- const idleMs = idleOf(body);
5282
- const window = await history.beginWindow(
5283
- YOU,
5284
- text(body.label) ?? "Edited in Studio",
5285
- idleMs ? { idleMs } : void 0
5286
- );
5287
- windows.set(window.id, { history, window });
5288
- return { windowId: window.id };
5289
- })
5290
- );
5291
- api.post(
5292
- `${base}/window/:windowId/close`,
5293
- (c) => withHistory(adapter, c, async (history) => {
5294
- const id = c.req.param("windowId") ?? "";
5295
- const held = windows.get(id);
5296
- if (held?.history !== history) throw new Error("That window is not open in this project.");
5297
- windows.delete(id);
5298
- return { entry: await held.window.close() };
5299
- })
5300
- );
5301
- }
5302
-
5303
- // src/createStudioApi.ts
5304
- function createStudioApi(adapter) {
5305
- const api = new Hono();
5306
- registerProjectRoutes(api, adapter);
5307
- registerFileRoutes(api, adapter);
5308
- registerPreviewRoutes(api, adapter);
5309
- registerLintRoutes(api, adapter);
5310
- registerRenderRoutes(api, adapter);
5311
- registerThumbnailRoutes(api, adapter);
5312
- registerSelectionRoutes(api, adapter);
5313
- registerMediaRoutes(api, adapter);
5314
- registerWaveformRoutes(api, adapter);
5315
- registerFontRoutes(api);
5316
- registerRegistryRoutes(api, adapter);
5317
- registerGlobalAssetRoutes(api);
5318
- registerHistoryRoutes(api, adapter);
5319
- return api;
5320
- }
5321
-
5322
5200
  // src/history/projectHistory.ts
5323
5201
  import { randomUUID as randomUUID8 } from "crypto";
5324
5202
  import { rm as rm2 } from "fs/promises";
@@ -5622,6 +5500,7 @@ function referencedHashes(log, current) {
5622
5500
 
5623
5501
  // src/history/projectHistory.ts
5624
5502
  var DEFAULT_HISTORY_ROOT = join19(homedir2(), ".cache", "hyperframes", "history");
5503
+ var MAX_WINDOW_IDLE_MS = 10 * 6e4;
5625
5504
  var OUTSIDE = { kind: "outside", name: "Outside" };
5626
5505
  var OUTSIDE_LABEL = "Changed outside the app";
5627
5506
  var RACY_MS = 2e3;
@@ -5635,6 +5514,7 @@ function splitAt(change, at, kept) {
5635
5514
  { ...change, before: at }
5636
5515
  ];
5637
5516
  }
5517
+ var takesWrite = (window, at) => window.idleMs === void 0 || at - (window.lastWriteAt ?? at) <= window.idleMs;
5638
5518
  function addChange(group, path, before, after) {
5639
5519
  const earlier = group.changes.get(path);
5640
5520
  const from = earlier ? earlier.before : before;
@@ -5693,9 +5573,17 @@ var Engine = class {
5693
5573
  const cached = cache.get(path);
5694
5574
  this.tracked.set(path, { hash, stat: cached?.hash === hash ? cached.stat : "" });
5695
5575
  }
5576
+ this.reopenClosedWindow();
5696
5577
  await this.sweep();
5578
+ for (const window of [...this.windows]) await this.endWindow(window);
5697
5579
  await this.commitOutside();
5698
5580
  }
5581
+ reopenClosedWindow() {
5582
+ const closed = this.options.closedWindow;
5583
+ if (!closed || this.log.entries.some((entry) => entry.id === closed.id)) return;
5584
+ const { id, who, label, startedAt, lastWriteAt, idleMs } = closed;
5585
+ this.windows.push({ id, who, label, startedAt, lastWriteAt, idleMs, changes: /* @__PURE__ */ new Map() });
5586
+ }
5699
5587
  async firstOpen() {
5700
5588
  const sweptAt = Date.now();
5701
5589
  for (const file of listProjectFiles(this.dir))
@@ -5730,15 +5618,17 @@ var Engine = class {
5730
5618
  async sweep() {
5731
5619
  if (!existsSync12(this.dir)) return;
5732
5620
  const sweptAt = Date.now();
5733
- const seen = listProjectFiles(this.dir);
5621
+ const changedAt = (file) => Math.min(sweptAt, Math.max(file.mtimeMs, file.ctimeMs));
5622
+ const seen = listProjectFiles(this.dir).sort((a, b) => changedAt(a) - changedAt(b));
5734
5623
  let changed = false;
5735
5624
  for (const file of seen)
5736
- changed = await this.observe(file.path, statKey(file, sweptAt)) || changed;
5625
+ changed = await this.observe(file.path, statKey(file, sweptAt), changedAt(file)) || changed;
5737
5626
  const present = new Set(seen.map((file) => file.path));
5738
5627
  for (const [path, known] of this.tracked) {
5739
5628
  if (present.has(path)) continue;
5740
5629
  this.tracked.delete(path);
5741
- this.record(path, known.hash, null);
5630
+ const removedAt = sweptAt;
5631
+ await this.record(path, known.hash, null, removedAt);
5742
5632
  changed = true;
5743
5633
  }
5744
5634
  if (changed) this.saveStatCache();
@@ -5752,18 +5642,21 @@ var Engine = class {
5752
5642
  throw error;
5753
5643
  }
5754
5644
  }
5755
- async observe(path, stat2) {
5645
+ async observe(path, stat2, at) {
5756
5646
  const known = this.tracked.get(path) ?? { hash: null, stat: null };
5757
5647
  if (stat2 && known.stat === stat2) return false;
5758
5648
  const hash = await this.storeIfPresent(path);
5759
5649
  if (hash === null) return false;
5760
5650
  this.tracked.set(path, { hash, stat: stat2 });
5761
- if (known.hash !== hash) this.record(path, known.hash, hash);
5651
+ if (known.hash !== hash) await this.record(path, known.hash, hash, at);
5762
5652
  return known.hash !== hash || known.stat !== stat2;
5763
5653
  }
5764
- record(path, before, after) {
5654
+ async record(path, before, after, at) {
5655
+ const endedBeforeThisWrite = this.windows.filter((open) => !takesWrite(open, at));
5656
+ if (endedBeforeThisWrite.length) await this.commitOutside();
5657
+ for (const window2 of endedBeforeThisWrite) await this.endWindow(window2);
5765
5658
  const window = this.windows.at(-1);
5766
- if (window) this.touch(window);
5659
+ if (window) this.touch(window, at);
5767
5660
  addChange(window ?? this.outsideGroup(), path, before, after);
5768
5661
  }
5769
5662
  /** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
@@ -5882,9 +5775,11 @@ var Engine = class {
5882
5775
  }
5883
5776
  async commit(group, extra = {}) {
5884
5777
  if (!group.changes.size) return null;
5885
- const { id, who, label, startedAt, changes } = group;
5778
+ const { id, who, label, startedAt, lastWriteAt, changes } = group;
5886
5779
  const files = [...changes.values()].sort((a, b) => a.path.localeCompare(b.path));
5887
- const entry = { id, who, label, startedAt, endedAt: this.now(), files, ...extra };
5780
+ const previousEnd = this.log.entries.at(-1)?.endedAt ?? 0;
5781
+ const endedAt = Math.max(lastWriteAt ?? this.now(), previousEnd);
5782
+ const entry = { id, who, label, startedAt, endedAt, files, ...extra };
5888
5783
  this.log.entries.push(entry);
5889
5784
  try {
5890
5785
  saveRecord(this.logFile, this.log, { type: "entry", entry });
@@ -5935,12 +5830,14 @@ var Engine = class {
5935
5830
  await this.sweep();
5936
5831
  const window = { ...this.newGroup(who, label), idleMs };
5937
5832
  this.windows.push(window);
5938
- this.touch(window);
5939
- return { id: window.id, close: () => this.queue(() => this.sweepAndEnd(window)) };
5833
+ this.touch(window, Date.now());
5834
+ const close = () => this.queue(() => this.sweepAndEnd(window));
5835
+ return { id: window.id, startedAt: window.startedAt, close };
5940
5836
  });
5941
5837
  }
5942
5838
  /** A window with no write for its idleMs ends, so a close that never comes cannot hold every later write. */
5943
- touch(window) {
5839
+ touch(window, at) {
5840
+ window.lastWriteAt = Math.max(window.lastWriteAt ?? at, at);
5944
5841
  clearTimeout(window.idleTimer);
5945
5842
  if (window.idleMs === void 0 || !Number.isFinite(window.idleMs)) return;
5946
5843
  window.idleTimer = setTimeout(
@@ -6133,6 +6030,163 @@ async function openProjectHistory(options) {
6133
6030
  }
6134
6031
  }
6135
6032
 
6033
+ // src/routes/history.ts
6034
+ var YOU = { kind: "person", name: "You" };
6035
+ async function historyOf(adapter, c) {
6036
+ const project = await adapter.resolveProject(c.req.param("id") ?? "");
6037
+ return project ? await adapter.history?.(project) ?? null : null;
6038
+ }
6039
+ async function bodyOf(c) {
6040
+ const body = await c.req.json().catch(() => null);
6041
+ return body && typeof body === "object" ? body : {};
6042
+ }
6043
+ var text = (value) => typeof value === "string" && value ? value : null;
6044
+ function writing(c) {
6045
+ const header = c.req.header("X-Hyperframes-Write-Token");
6046
+ return header ? { writeToken: createWriteToken(header) } : {};
6047
+ }
6048
+ function whoOf(body) {
6049
+ const who = body.who;
6050
+ const name = text(who?.name);
6051
+ return who?.kind === "agent" && name ? { kind: "agent", name } : YOU;
6052
+ }
6053
+ function idleOf(body) {
6054
+ const idleMs = body.idleMs;
6055
+ return typeof idleMs === "number" && idleMs > 0 ? Math.min(idleMs, MAX_WINDOW_IDLE_MS) : void 0;
6056
+ }
6057
+ function versionsOf(value) {
6058
+ if (!value || typeof value !== "object") return void 0;
6059
+ const pairs = Object.entries(value).filter(
6060
+ (pair) => typeof pair[1] === "string"
6061
+ );
6062
+ return pairs.length ? Object.fromEntries(pairs) : void 0;
6063
+ }
6064
+ function nextStep(history, direction) {
6065
+ const target = history.next(direction);
6066
+ if (!target) return null;
6067
+ const paths = target.files.map((file) => file.path);
6068
+ return { id: target.id, label: target.label, endedAt: target.endedAt, paths };
6069
+ }
6070
+ async function withHistory(adapter, c, task) {
6071
+ const history = await historyOf(adapter, c);
6072
+ if (!history) return c.json({ error: "This project has no history here." }, 404);
6073
+ try {
6074
+ return c.json(await task(history, await bodyOf(c)) ?? null);
6075
+ } catch (error) {
6076
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 409);
6077
+ }
6078
+ }
6079
+ function registerHistoryRoutes(api, adapter) {
6080
+ const windows = /* @__PURE__ */ new Map();
6081
+ const base = "/projects/:id/history";
6082
+ api.get(
6083
+ base,
6084
+ (c) => withHistory(adapter, c, (history) => {
6085
+ const entries = history.list();
6086
+ return { entries, back: nextStep(history, "back"), forward: nextStep(history, "forward") };
6087
+ })
6088
+ );
6089
+ api.post(
6090
+ `${base}/step`,
6091
+ (c) => withHistory(
6092
+ adapter,
6093
+ c,
6094
+ (history, body) => history.step(body.direction === "forward" ? "forward" : "back", YOU, writing(c))
6095
+ )
6096
+ );
6097
+ api.post(
6098
+ `${base}/undo`,
6099
+ (c) => withHistory(adapter, c, (history, body) => {
6100
+ const mode = body.mode === "just-this" || body.mode === "back-to-before" ? body.mode : void 0;
6101
+ return history.undo(text(body.entryId) ?? "", { who: whoOf(body), mode, ...writing(c) });
6102
+ })
6103
+ );
6104
+ api.post(
6105
+ `${base}/restore`,
6106
+ (c) => withHistory(
6107
+ adapter,
6108
+ c,
6109
+ (history, body) => history.restore(text(body.point) ?? "", whoOf(body), writing(c))
6110
+ )
6111
+ );
6112
+ api.get(
6113
+ `${base}/peek/:point`,
6114
+ (c) => withHistory(adapter, c, (history) => ({ files: history.peek(c.req.param("point")) }))
6115
+ );
6116
+ api.post(
6117
+ `${base}/pin`,
6118
+ (c) => withHistory(adapter, c, (history, body) => {
6119
+ history.pin(text(body.entryId) ?? "", body.pinned === true);
6120
+ return { ok: true };
6121
+ })
6122
+ );
6123
+ api.get(`${base}/blob/:hash`, async (c) => {
6124
+ const history = await historyOf(adapter, c);
6125
+ const hash = c.req.param("hash") ?? "";
6126
+ const missing = () => c.json({ error: "That file is not kept in this project's history." }, 404);
6127
+ if (!history || !/^[0-9a-f]{64}$/.test(hash)) return missing();
6128
+ const bytes = await history.readBlob(hash).catch(() => null);
6129
+ return bytes ? c.body(new Uint8Array(bytes)) : missing();
6130
+ });
6131
+ api.post(
6132
+ `${base}/claim`,
6133
+ (c) => withHistory(adapter, c, async (history, body) => {
6134
+ const paths = Array.isArray(body.paths) ? body.paths.filter((path) => text(path)) : [];
6135
+ const coalesceKey = text(body.coalesceKey) ?? void 0;
6136
+ const idleMs = idleOf(body);
6137
+ const overwrote = versionsOf(body.overwrote);
6138
+ const claimed = await history.claim(YOU, text(body.label) ?? "Edited in Studio", paths, {
6139
+ ...coalesceKey && { coalesceKey },
6140
+ ...idleMs && { idleMs },
6141
+ ...overwrote && { overwrote }
6142
+ });
6143
+ return { claimed };
6144
+ })
6145
+ );
6146
+ api.post(
6147
+ `${base}/window`,
6148
+ (c) => withHistory(adapter, c, async (history, body) => {
6149
+ const idleMs = idleOf(body);
6150
+ const window = await history.beginWindow(
6151
+ whoOf(body),
6152
+ text(body.label) ?? "Edited in Studio",
6153
+ idleMs ? { idleMs } : void 0
6154
+ );
6155
+ windows.set(window.id, { history, window });
6156
+ return { windowId: window.id, startedAt: window.startedAt };
6157
+ })
6158
+ );
6159
+ api.post(
6160
+ `${base}/window/:windowId/close`,
6161
+ (c) => withHistory(adapter, c, async (history) => {
6162
+ const id = c.req.param("windowId") ?? "";
6163
+ const held = windows.get(id);
6164
+ if (held?.history !== history) throw new Error("That window is not open in this project.");
6165
+ windows.delete(id);
6166
+ return { entry: await held.window.close() };
6167
+ })
6168
+ );
6169
+ }
6170
+
6171
+ // src/createStudioApi.ts
6172
+ function createStudioApi(adapter) {
6173
+ const api = new Hono();
6174
+ registerProjectRoutes(api, adapter);
6175
+ registerFileRoutes(api, adapter);
6176
+ registerPreviewRoutes(api, adapter);
6177
+ registerLintRoutes(api, adapter);
6178
+ registerRenderRoutes(api, adapter);
6179
+ registerThumbnailRoutes(api, adapter);
6180
+ registerSelectionRoutes(api, adapter);
6181
+ registerMediaRoutes(api, adapter);
6182
+ registerWaveformRoutes(api, adapter);
6183
+ registerFontRoutes(api);
6184
+ registerRegistryRoutes(api, adapter);
6185
+ registerGlobalAssetRoutes(api);
6186
+ registerHistoryRoutes(api, adapter);
6187
+ return api;
6188
+ }
6189
+
6136
6190
  // src/helpers/duplicateElement.ts
6137
6191
  import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
6138
6192
  function duplicateElementInHtml(source, target, newId, at) {
@@ -6285,6 +6339,7 @@ export {
6285
6339
  DELETED_VERSION,
6286
6340
  START as HISTORY_START,
6287
6341
  HistoryBusyError,
6342
+ MAX_WINDOW_IDLE_MS,
6288
6343
  MIME_TYPES,
6289
6344
  PREVIEW_BUNDLE_OPTIONS,
6290
6345
  STUDIO_MANUAL_EDITS_PATH,