@jxsuite/server 0.28.0 → 0.28.3

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/src/studio-api.ts CHANGED
@@ -7,9 +7,10 @@
7
7
  * All paths are relative to the project root. Directory traversal above root is rejected.
8
8
  */
9
9
 
10
- import { resolve, relative, basename, dirname, extname, isAbsolute } from "node:path";
11
- import { readdir, stat, readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
12
- import { readFileSync, existsSync, statSync } from "node:fs";
10
+ import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
11
+ import { errorMessage } from "@jxsuite/schema/parse";
12
+ import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
13
+ import { existsSync, readFileSync, statSync } from "node:fs";
13
14
  import { buildProjectFormatRegistry } from "@jxsuite/compiler/format-host";
14
15
  import type { FormatRegistry } from "@jxsuite/schema/format-registry";
15
16
  import * as claude from "./claude-session.ts";
@@ -40,7 +41,9 @@ async function getFormatRegistry(projectRoot: string): Promise<FormatRegistry> {
40
41
  projectConfig = undefined;
41
42
  }
42
43
  const cached = formatRegistryCache.get(projectRoot);
43
- if (cached && cached.mtime === mtime) return cached.registry;
44
+ if (cached && cached.mtime === mtime) {
45
+ return cached.registry;
46
+ }
44
47
  const registry = await buildProjectFormatRegistry(projectRoot, projectConfig);
45
48
  formatRegistryCache.set(projectRoot, { mtime, registry });
46
49
  return registry;
@@ -56,21 +59,25 @@ async function getFormatRegistry(projectRoot: string): Promise<FormatRegistry> {
56
59
  */
57
60
  function assertAccessible(filePath: string, root: string, activeProjectRoot: string | null) {
58
61
  const rel = relative(root, filePath);
59
- if (!rel.startsWith("..") && !rel.startsWith("/")) return;
62
+ if (!rel.startsWith("..") && !rel.startsWith("/")) {
63
+ return;
64
+ }
60
65
  if (activeProjectRoot) {
61
66
  const relActive = relative(activeProjectRoot, filePath);
62
- if (!relActive.startsWith("..") && !relActive.startsWith("/")) return;
67
+ if (!relActive.startsWith("..") && !relActive.startsWith("/")) {
68
+ return;
69
+ }
63
70
  }
64
71
  throw new Error("Path outside project root");
65
72
  }
66
73
 
67
74
  const statusMap: Record<string, string> = {
68
- M: "M",
69
- T: "T",
70
75
  A: "A",
76
+ C: "C",
71
77
  D: "D",
78
+ M: "M",
72
79
  R: "R",
73
- C: "C",
80
+ T: "T",
74
81
  U: "U",
75
82
  };
76
83
 
@@ -95,21 +102,23 @@ export function parseGitStatus(out: string) {
95
102
  const files = [];
96
103
 
97
104
  for (const line of out.split("\n")) {
98
- if (!line) continue;
105
+ if (!line) {
106
+ continue;
107
+ }
99
108
 
100
109
  if (line.startsWith("# branch.head ")) {
101
110
  branch = line.slice("# branch.head ".length);
102
111
  } else if (line.startsWith("# branch.ab ")) {
103
112
  const m = line.match(/\+(\d+) -(\d+)/);
104
113
  if (m) {
105
- ahead = parseInt(m[1], 10);
106
- behind = parseInt(m[2], 10);
114
+ ahead = Number.parseInt(m[1], 10);
115
+ behind = Number.parseInt(m[2], 10);
107
116
  }
108
117
  } else if (line.startsWith("1 ") || line.startsWith("2 ")) {
109
118
  const parts = line.split(" ");
110
- const xy = parts[1];
111
- const stagedCode = xy[0];
112
- const unstagedCode = xy[1];
119
+ const [, xy] = parts;
120
+ const [stagedCode] = xy;
121
+ const [, unstagedCode] = xy;
113
122
  let filePath;
114
123
  if (line.startsWith("2 ")) {
115
124
  const tabIdx = line.indexOf("\t");
@@ -121,23 +130,23 @@ export function parseGitStatus(out: string) {
121
130
  if (stagedCode !== ".") {
122
131
  files.push({
123
132
  path: filePath,
124
- status: statusMap[stagedCode] || stagedCode,
125
133
  staged: true,
134
+ status: statusMap[stagedCode] || stagedCode,
126
135
  });
127
136
  }
128
137
  if (unstagedCode !== ".") {
129
138
  files.push({
130
139
  path: filePath,
131
- status: statusMap[unstagedCode] || unstagedCode,
132
140
  staged: false,
141
+ status: statusMap[unstagedCode] || unstagedCode,
133
142
  });
134
143
  }
135
144
  } else if (line.startsWith("? ")) {
136
- files.push({ path: line.slice(2), status: "U", staged: false });
145
+ files.push({ path: line.slice(2), staged: false, status: "U" });
137
146
  }
138
147
  }
139
148
 
140
- return { branch, ahead, behind, files };
149
+ return { ahead, behind, branch, files };
141
150
  }
142
151
 
143
152
  /**
@@ -162,12 +171,12 @@ export async function handleStudioApi(
162
171
  try {
163
172
  const pkg = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
164
173
  return Response.json({
165
- root,
166
174
  name: pkg.name ?? basename(root),
175
+ root,
167
176
  workspaces: pkg.workspaces ?? [],
168
177
  });
169
178
  } catch {
170
- return Response.json({ root, name: basename(root), workspaces: [] });
179
+ return Response.json({ name: basename(root), root, workspaces: [] });
171
180
  }
172
181
  }
173
182
 
@@ -177,8 +186,8 @@ export async function handleStudioApi(
177
186
  const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
178
187
  try {
179
188
  assertAccessible(absDir, root, activeProjectRoot);
180
- } catch (e) {
181
- return Response.json({ error: (e as Error).message }, { status: 400 });
189
+ } catch (error) {
190
+ return Response.json({ error: errorMessage(error) }, { status: 400 });
182
191
  }
183
192
  try {
184
193
  const projectRoot = fwd(absDir);
@@ -195,7 +204,9 @@ export async function handleStudioApi(
195
204
  for (const d of conventionalDirs) {
196
205
  try {
197
206
  const s = await stat(resolve(absDir, d));
198
- if (s.isDirectory()) directories.push(d);
207
+ if (s.isDirectory()) {
208
+ directories.push(d);
209
+ }
199
210
  } catch {}
200
211
  }
201
212
 
@@ -210,20 +221,22 @@ export async function handleStudioApi(
210
221
  } catch {}
211
222
 
212
223
  return Response.json({
224
+ directories,
213
225
  isSiteProject,
214
226
  projectConfig,
215
- directories,
216
227
  projectRoot,
217
228
  });
218
- } catch (e) {
219
- return Response.json({ error: (e as Error).message }, { status: 500 });
229
+ } catch (error) {
230
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
220
231
  }
221
232
  }
222
233
 
223
234
  // Resolve nearest project.json ancestor for a given file path
224
235
  if (path === "/__studio/resolve-site" && req.method === "GET") {
225
236
  const filePath = url.searchParams.get("path");
226
- if (!filePath) return Response.json({ error: "Missing path param" }, { status: 400 });
237
+ if (!filePath) {
238
+ return Response.json({ error: "Missing path param" }, { status: 400 });
239
+ }
227
240
  try {
228
241
  // Walk up from file's directory looking for project.json
229
242
  let dir = dirname(
@@ -239,41 +252,49 @@ export async function handleStudioApi(
239
252
  : filePath;
240
253
  const fileRelPath = fwd(relative(dir, absFile));
241
254
  return Response.json({
242
- sitePath: dir,
243
- relPath: relPath,
244
255
  fileRelPath,
245
256
  projectConfig: config,
257
+ relPath,
258
+ sitePath: dir,
246
259
  });
247
260
  }
248
261
  const parent = dirname(dir);
249
- if (parent === dir) break;
262
+ if (parent === dir) {
263
+ break;
264
+ }
250
265
  dir = parent;
251
266
  }
252
267
  return Response.json({ sitePath: null });
253
- } catch (e) {
254
- return Response.json({ error: (e as Error).message }, { status: 500 });
268
+ } catch (error) {
269
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
255
270
  }
256
271
  }
257
272
 
258
273
  // Find a project directory by name — searches $HOME for the first matching directory with a
259
- // project.json. Dev-mode workaround for when showDirectoryPicker() can't provide absolute paths.
274
+ // Project.json. Dev-mode workaround for when showDirectoryPicker() can't provide absolute paths.
260
275
  if (path === "/__studio/find-project" && req.method === "GET") {
261
276
  const name = url.searchParams.get("name");
262
- if (!name) return Response.json({ error: "Missing name" }, { status: 400 });
277
+ if (!name) {
278
+ return Response.json({ error: "Missing name" }, { status: 400 });
279
+ }
263
280
  try {
264
281
  const home = process.env.HOME || process.env.USERPROFILE || "";
265
- if (!home) return Response.json({ path: null });
282
+ if (!home) {
283
+ return Response.json({ path: null });
284
+ }
266
285
  const glob = new Bun.Glob(`**/${name}/project.json`);
267
286
  try {
268
287
  for await (const match of glob.scan({ cwd: home, dot: false })) {
269
- if (match.includes("node_modules") || match.includes(".Trash")) continue;
288
+ if (match.includes("node_modules") || match.includes(".Trash")) {
289
+ continue;
290
+ }
270
291
  const abs = resolve(home, dirname(match));
271
292
  return Response.json({ path: abs });
272
293
  }
273
294
  } catch {}
274
295
  return Response.json({ path: null });
275
- } catch (e) {
276
- return Response.json({ error: (e as Error).message }, { status: 500 });
296
+ } catch (error) {
297
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
277
298
  }
278
299
  }
279
300
 
@@ -287,20 +308,21 @@ export async function handleStudioApi(
287
308
  match.includes("node_modules") ||
288
309
  fwd(match).includes("dist/") ||
289
310
  fwd(match).includes(".claude/")
290
- )
311
+ ) {
291
312
  continue;
313
+ }
292
314
  const fp = resolve(root, match);
293
315
  try {
294
316
  const raw = JSON.parse(await readFile(fp, "utf8"));
295
317
  if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
296
318
  const projectDir = fwd(dirname(fp));
297
- sites.push({ path: projectDir, config: raw });
319
+ sites.push({ config: raw, path: projectDir });
298
320
  }
299
321
  } catch {}
300
322
  }
301
323
  return Response.json(sites);
302
- } catch (e) {
303
- return Response.json({ error: (e as Error).message }, { status: 500 });
324
+ } catch (error) {
325
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
304
326
  }
305
327
  }
306
328
 
@@ -317,17 +339,17 @@ export async function handleStudioApi(
317
339
 
318
340
  const { generateProject } = await import("@jxsuite/create/generate");
319
341
  await generateProject(destPath, {
320
- name,
342
+ adapter,
321
343
  description,
344
+ name,
322
345
  url: siteUrl,
323
- adapter,
324
346
  });
325
347
 
326
348
  const config = JSON.parse(await readFile(resolve(destPath, "project.json"), "utf8"));
327
349
  const projectRoot = fwd(relative(root, destPath));
328
- return Response.json({ root: projectRoot, config });
329
- } catch (e) {
330
- return Response.json({ error: (e as Error).message }, { status: 500 });
350
+ return Response.json({ config, root: projectRoot });
351
+ } catch (error) {
352
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
331
353
  }
332
354
  }
333
355
 
@@ -338,15 +360,17 @@ export async function handleStudioApi(
338
360
  const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
339
361
  try {
340
362
  assertAccessible(absDir, root, activeProjectRoot);
341
- } catch (e) {
342
- return Response.json({ error: (e as Error).message }, { status: 400 });
363
+ } catch (error) {
364
+ return Response.json({ error: errorMessage(error) }, { status: 400 });
343
365
  }
344
366
 
345
367
  /** Report a path relative to the active project root (or server root as fallback). */
346
368
  const reportRelative = (fp: string) => {
347
369
  if (activeProjectRoot) {
348
370
  const rel = relative(activeProjectRoot, fp);
349
- if (!rel.startsWith("..")) return fwd(rel);
371
+ if (!rel.startsWith("..")) {
372
+ return fwd(rel);
373
+ }
350
374
  }
351
375
  return fwd(relative(root, fp));
352
376
  };
@@ -361,10 +385,10 @@ export async function handleStudioApi(
361
385
  const s = await stat(fp);
362
386
  if (!s.isDirectory()) {
363
387
  files.push({
388
+ modified: s.mtime.toISOString(),
364
389
  name: basename(match),
365
390
  path: reportRelative(fp),
366
391
  size: s.size,
367
- modified: s.mtime.toISOString(),
368
392
  });
369
393
  }
370
394
  } catch {}
@@ -375,20 +399,22 @@ export async function handleStudioApi(
375
399
  const entries = await readdir(absDir, { withFileTypes: true });
376
400
  const files = [];
377
401
  for (const entry of entries) {
378
- if (entry.name.startsWith(".")) continue;
402
+ if (entry.name.startsWith(".")) {
403
+ continue;
404
+ }
379
405
  const fp = resolve(absDir, entry.name);
380
406
  const s = await stat(fp);
381
407
  files.push({
408
+ modified: s.mtime.toISOString(),
382
409
  name: entry.name,
383
410
  path: reportRelative(fp),
384
- type: entry.isDirectory() ? "directory" : "file",
385
411
  size: s.size,
386
- modified: s.mtime.toISOString(),
412
+ type: entry.isDirectory() ? "directory" : "file",
387
413
  });
388
414
  }
389
415
  return Response.json(files);
390
- } catch (e) {
391
- return Response.json({ error: (e as Error).message }, { status: 500 });
416
+ } catch (error) {
417
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
392
418
  }
393
419
  }
394
420
 
@@ -398,8 +424,8 @@ export async function handleStudioApi(
398
424
  const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
399
425
  try {
400
426
  assertAccessible(scanRoot, root, activeProjectRoot);
401
- } catch (e) {
402
- return Response.json({ error: (e as Error).message }, { status: 400 });
427
+ } catch (error) {
428
+ return Response.json({ error: errorMessage(error) }, { status: 400 });
403
429
  }
404
430
  try {
405
431
  const registry = await getFormatRegistry(scanRoot);
@@ -411,8 +437,9 @@ export async function handleStudioApi(
411
437
  match.includes("node_modules") ||
412
438
  fwd(match).includes("dist/") ||
413
439
  fwd(match).includes(".claude/")
414
- )
440
+ ) {
415
441
  continue;
442
+ }
416
443
  const fp = resolve(scanRoot, match);
417
444
  try {
418
445
  let content;
@@ -420,21 +447,26 @@ export async function handleStudioApi(
420
447
  content = JSON.parse(await readFile(fp, "utf8"));
421
448
  } else {
422
449
  const entry = registry.byExtension(extname(match), "parse");
423
- if (!entry) continue;
450
+ if (!entry) {
451
+ continue;
452
+ }
424
453
  const source = await readFile(fp, "utf8");
425
454
  content = (await entry.call("parse", source)) as Record<string, unknown>;
426
455
  }
427
456
  if (content.tagName && content.tagName.includes("-")) {
428
457
  components.push({
429
- tagName: content.tagName,
430
458
  $id: content.$id || null,
459
+ hasElements: Array.isArray(content.$elements) && content.$elements.length > 0,
431
460
  path: fwd(match),
432
- source: "jx",
433
461
  props: Object.entries(content.state || {})
434
462
  .filter(([, d]) => {
435
- if (d == null) return false;
463
+ if (d == null) {
464
+ return false;
465
+ }
436
466
  // Shorthand: "key": "value" or "key": 0 etc.
437
- if (typeof d !== "object") return true;
467
+ if (typeof d !== "object") {
468
+ return true;
469
+ }
438
470
  // Full form: skip computed/handler/prototype entries
439
471
  const obj = d as Record<string, unknown>;
440
472
  return !obj.$prototype && !obj.$handler && !obj.$compute;
@@ -442,20 +474,21 @@ export async function handleStudioApi(
442
474
  .map(([name, d]) => {
443
475
  if (typeof d !== "object" || d === null) {
444
476
  // Shorthand: infer type from value
445
- return { name, type: typeof d, default: d };
477
+ return { default: d, name, type: typeof d };
446
478
  }
447
479
  const obj = d as Record<string, unknown>;
448
480
  return {
449
- name,
450
- type: obj.type,
451
481
  default: obj.default,
452
482
  format: obj.format,
483
+ name,
484
+ type: obj.type,
453
485
  };
454
486
  }),
455
- hasElements: Array.isArray(content.$elements) && content.$elements.length > 0,
487
+ source: "jx",
488
+ tagName: content.tagName,
456
489
  });
457
490
  }
458
- } catch {} // skip non-JSON or parse errors
491
+ } catch {} // Skip non-JSON or parse errors
459
492
  }
460
493
 
461
494
  // Discover CEM-bearing npm packages
@@ -484,49 +517,55 @@ export async function handleStudioApi(
484
517
  : existsSync(fallbackPath)
485
518
  ? fallbackPath
486
519
  : null;
487
- if (!actualPath) continue;
520
+ if (!actualPath) {
521
+ continue;
522
+ }
488
523
  const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
489
- if (!depPkg.customElements) continue;
524
+ if (!depPkg.customElements) {
525
+ continue;
526
+ }
490
527
  const cemPath = resolve(dirname(actualPath), depPkg.customElements);
491
- if (!existsSync(cemPath)) continue;
528
+ if (!existsSync(cemPath)) {
529
+ continue;
530
+ }
492
531
  const cem = JSON.parse(await readFile(cemPath, "utf8"));
493
532
  for (const mod of cem.modules || []) {
494
533
  for (const decl of mod.declarations || []) {
495
534
  if (decl.customElement && decl.tagName) {
496
535
  components.push({
497
- tagName: decl.tagName,
498
536
  $id: null,
499
- path: null,
537
+ cssProperties: decl.cssProperties || [],
538
+ description: decl.description || null,
539
+ events: decl.events || [],
540
+ hasElements: false,
541
+ members: (decl.members || []).filter(
542
+ (m: Record<string, unknown>) =>
543
+ m.kind === "field" && m.privacy !== "private",
544
+ ),
500
545
  modulePath: mod.path,
501
- source: "npm",
502
546
  package: name,
503
- description: decl.description || null,
547
+ path: null,
504
548
  props: (decl.attributes || []).map((a: Record<string, unknown>) => ({
505
- name: a.name,
506
- type: (a.type as Record<string, unknown> | undefined)?.text,
507
549
  default: a.default,
508
550
  description: a.description || null,
551
+ name: a.name,
552
+ type: (a.type as Record<string, unknown> | undefined)?.text,
509
553
  })),
510
- members: (decl.members || []).filter(
511
- (m: Record<string, unknown>) =>
512
- m.kind === "field" && m.privacy !== "private",
513
- ),
514
554
  slots: decl.slots || [],
515
- events: decl.events || [],
516
- cssProperties: decl.cssProperties || [],
517
- hasElements: false,
555
+ source: "npm",
556
+ tagName: decl.tagName,
518
557
  });
519
558
  }
520
559
  }
521
560
  }
522
- } catch {} // skip packages without valid CEM
561
+ } catch {} // Skip packages without valid CEM
523
562
  }
524
563
  }
525
- } catch {} // skip if no project package.json
564
+ } catch {} // Skip if no project package.json
526
565
 
527
566
  return Response.json(components);
528
- } catch (e) {
529
- return Response.json({ error: (e as Error).message }, { status: 500 });
567
+ } catch (error) {
568
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
530
569
  }
531
570
  }
532
571
 
@@ -538,7 +577,9 @@ export async function handleStudioApi(
538
577
  const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
539
578
  try {
540
579
  const pkgPath = resolve(scanRoot, "package.json");
541
- if (!existsSync(pkgPath)) return Response.json([]);
580
+ if (!existsSync(pkgPath)) {
581
+ return Response.json([]);
582
+ }
542
583
  const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
543
584
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
544
585
  /**
@@ -558,27 +599,31 @@ export async function handleStudioApi(
558
599
  : existsSync(fallbackPath)
559
600
  ? fallbackPath
560
601
  : null;
561
- if (!actualPath) continue;
602
+ if (!actualPath) {
603
+ continue;
604
+ }
562
605
  try {
563
606
  const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
564
607
  packages.push({
608
+ customElementsPath: depPkg.customElements || null,
609
+ hasCem: Boolean(depPkg.customElements),
565
610
  name,
566
611
  version: /** @type {string} */ version,
567
- hasCem: !!depPkg.customElements,
568
- customElementsPath: depPkg.customElements || null,
569
612
  });
570
613
  } catch {}
571
614
  }
572
615
  return Response.json(packages);
573
- } catch (e) {
574
- return Response.json({ error: (e as Error).message }, { status: 500 });
616
+ } catch (error) {
617
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
575
618
  }
576
619
  }
577
620
 
578
621
  // Read CEM from a specific package
579
622
  if (path === "/__studio/cem" && req.method === "GET") {
580
623
  const pkg = url.searchParams.get("pkg");
581
- if (!pkg) return new Response("Missing pkg", { status: 400 });
624
+ if (!pkg) {
625
+ return new Response("Missing pkg", { status: 400 });
626
+ }
582
627
  const dir = url.searchParams.get("dir") || activeProjectRoot || root;
583
628
  const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
584
629
  try {
@@ -589,15 +634,21 @@ export async function handleStudioApi(
589
634
  : existsSync(fallbackPath)
590
635
  ? fallbackPath
591
636
  : null;
592
- if (!actualPath) return Response.json({ cem: null });
637
+ if (!actualPath) {
638
+ return Response.json({ cem: null });
639
+ }
593
640
  const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
594
- if (!depPkg.customElements) return Response.json({ cem: null });
641
+ if (!depPkg.customElements) {
642
+ return Response.json({ cem: null });
643
+ }
595
644
  const cemPath = resolve(dirname(actualPath), depPkg.customElements);
596
- if (!existsSync(cemPath)) return Response.json({ cem: null });
645
+ if (!existsSync(cemPath)) {
646
+ return Response.json({ cem: null });
647
+ }
597
648
  const cem = JSON.parse(await readFile(cemPath, "utf8"));
598
649
  return Response.json({ cem });
599
- } catch (e) {
600
- return Response.json({ error: (e as Error).message }, { status: 500 });
650
+ } catch (error) {
651
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
601
652
  }
602
653
  }
603
654
 
@@ -605,17 +656,20 @@ export async function handleStudioApi(
605
656
  if (path === "/__studio/packages/add" && req.method === "POST") {
606
657
  try {
607
658
  const body = await req.json();
608
- const name = body.name;
609
- if (!name || typeof name !== "string")
659
+ const { name } = body;
660
+ if (!name || typeof name !== "string") {
610
661
  return Response.json({ error: "Missing name" }, { status: 400 });
662
+ }
611
663
  const dir = body.dir || activeProjectRoot;
612
664
  const cwd = dir ? (isAbsolute(dir) ? dir : resolve(root, dir)) : root;
613
665
  const args = ["add", name];
614
- if (body.dev) args.splice(1, 0, "-d");
666
+ if (body.dev) {
667
+ args.splice(1, 0, "-d");
668
+ }
615
669
  const proc = Bun.spawn(["bun", ...args], {
616
670
  cwd,
617
- stdout: "pipe",
618
671
  stderr: "pipe",
672
+ stdout: "pipe",
619
673
  });
620
674
  const exitCode = await proc.exited;
621
675
  if (exitCode !== 0) {
@@ -626,8 +680,8 @@ export async function handleStudioApi(
626
680
  );
627
681
  }
628
682
  return Response.json({ ok: true });
629
- } catch (e) {
630
- return Response.json({ error: (e as Error).message }, { status: 500 });
683
+ } catch (error) {
684
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
631
685
  }
632
686
  }
633
687
 
@@ -635,15 +689,16 @@ export async function handleStudioApi(
635
689
  if (path === "/__studio/packages/remove" && req.method === "POST") {
636
690
  try {
637
691
  const body = await req.json();
638
- const name = body.name;
639
- if (!name || typeof name !== "string")
692
+ const { name } = body;
693
+ if (!name || typeof name !== "string") {
640
694
  return Response.json({ error: "Missing name" }, { status: 400 });
695
+ }
641
696
  const dir = body.dir || activeProjectRoot;
642
697
  const cwd = dir ? (isAbsolute(dir) ? dir : resolve(root, dir)) : root;
643
698
  const proc = Bun.spawn(["bun", "remove", name], {
644
699
  cwd,
645
- stdout: "pipe",
646
700
  stderr: "pipe",
701
+ stdout: "pipe",
647
702
  });
648
703
  const exitCode = await proc.exited;
649
704
  if (exitCode !== 0) {
@@ -654,20 +709,22 @@ export async function handleStudioApi(
654
709
  );
655
710
  }
656
711
  return Response.json({ ok: true });
657
- } catch (e) {
658
- return Response.json({ error: (e as Error).message }, { status: 500 });
712
+ } catch (error) {
713
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
659
714
  }
660
715
  }
661
716
 
662
717
  // Read file
663
718
  if (path === "/__studio/file" && req.method === "GET") {
664
719
  const fp = url.searchParams.get("path");
665
- if (!fp) return new Response("Missing path", { status: 400 });
720
+ if (!fp) {
721
+ return new Response("Missing path", { status: 400 });
722
+ }
666
723
  const abs = fp.startsWith("~") ? fp.replace("~", process.env.HOME || "") : fp;
667
724
  try {
668
725
  assertAccessible(abs, root, activeProjectRoot);
669
- } catch (e) {
670
- return new Response((e as Error).message, { status: 400 });
726
+ } catch (error) {
727
+ return new Response(errorMessage(error), { status: 400 });
671
728
  }
672
729
  try {
673
730
  return Response.json(
@@ -677,69 +734,75 @@ export async function handleStudioApi(
677
734
  },
678
735
  { headers: { "Cache-Control": "public, max-age=5" } },
679
736
  );
680
- } catch (e) {
681
- return (e as NodeJS.ErrnoException).code === "ENOENT"
737
+ } catch (error) {
738
+ return (error as NodeJS.ErrnoException).code === "ENOENT"
682
739
  ? new Response("Not found", { status: 404 })
683
- : Response.json({ error: (e as Error).message }, { status: 500 });
740
+ : Response.json({ error: errorMessage(error) }, { status: 500 });
684
741
  }
685
742
  }
686
743
 
687
744
  // Write file
688
745
  if (path === "/__studio/file" && req.method === "PUT") {
689
746
  const fp = url.searchParams.get("path");
690
- if (!fp) return new Response("Missing path", { status: 400 });
747
+ if (!fp) {
748
+ return new Response("Missing path", { status: 400 });
749
+ }
691
750
  const abs = resolve(root, fp);
692
751
  try {
693
752
  assertAccessible(abs, root, activeProjectRoot);
694
- } catch (e) {
695
- return new Response((e as Error).message, { status: 400 });
753
+ } catch (error) {
754
+ return new Response(errorMessage(error), { status: 400 });
696
755
  }
697
756
  try {
698
757
  await mkdir(dirname(abs), { recursive: true });
699
758
  await writeFile(abs, await req.text(), "utf8");
700
759
  return Response.json({ ok: true, path: fwd(relative(root, abs)) });
701
- } catch (e) {
702
- return Response.json({ error: (e as Error).message }, { status: 500 });
760
+ } catch (error) {
761
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
703
762
  }
704
763
  }
705
764
 
706
765
  // Upload binary file
707
766
  if (path === "/__studio/file/upload" && req.method === "POST") {
708
767
  const fp = url.searchParams.get("path");
709
- if (!fp) return new Response("Missing path", { status: 400 });
768
+ if (!fp) {
769
+ return new Response("Missing path", { status: 400 });
770
+ }
710
771
  const abs = resolve(root, fp);
711
772
  try {
712
773
  assertAccessible(abs, root, activeProjectRoot);
713
- } catch (e) {
714
- return new Response((e as Error).message, { status: 400 });
774
+ } catch (error) {
775
+ return new Response(errorMessage(error), { status: 400 });
715
776
  }
716
777
  try {
717
778
  await mkdir(dirname(abs), { recursive: true });
718
779
  const buffer = await req.arrayBuffer();
719
780
  await Bun.write(abs, new Uint8Array(buffer));
720
781
  return Response.json({ ok: true, path: fwd(relative(root, abs)) });
721
- } catch (e) {
722
- return Response.json({ error: (e as Error).message }, { status: 500 });
782
+ } catch (error) {
783
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
723
784
  }
724
785
  }
725
786
 
726
787
  // Delete file
727
788
  if (path === "/__studio/file" && req.method === "DELETE") {
728
789
  const fp = url.searchParams.get("path");
729
- if (!fp) return new Response("Missing path", { status: 400 });
790
+ if (!fp) {
791
+ return new Response("Missing path", { status: 400 });
792
+ }
730
793
  const abs = resolve(root, fp);
731
794
  try {
732
795
  assertAccessible(abs, root, activeProjectRoot);
733
- } catch (e) {
734
- return new Response((e as Error).message, { status: 400 });
796
+ } catch (error) {
797
+ return new Response(errorMessage(error), { status: 400 });
735
798
  }
736
799
  try {
737
800
  await unlink(abs);
738
801
  return Response.json({ ok: true, path: fwd(relative(root, abs)) });
739
- } catch (e) {
740
- return (e as NodeJS.ErrnoException).code === "ENOENT"
802
+ } catch (error) {
803
+ return (error as NodeJS.ErrnoException).code === "ENOENT"
741
804
  ? new Response("Not found", { status: 404 })
742
- : Response.json({ error: (e as Error).message }, { status: 500 });
805
+ : Response.json({ error: errorMessage(error) }, { status: 500 });
743
806
  }
744
807
  }
745
808
 
@@ -752,25 +815,27 @@ export async function handleStudioApi(
752
815
  return new Response("Invalid JSON", { status: 400 });
753
816
  }
754
817
  const { from, to } = body;
755
- if (!from || !to) return new Response("Missing from or to", { status: 400 });
818
+ if (!from || !to) {
819
+ return new Response("Missing from or to", { status: 400 });
820
+ }
756
821
  const absFrom = resolve(root, from);
757
822
  const absTo = resolve(root, to);
758
823
  try {
759
824
  assertAccessible(absFrom, root, activeProjectRoot);
760
825
  assertAccessible(absTo, root, activeProjectRoot);
761
- } catch (e) {
762
- return new Response((e as Error).message, { status: 400 });
826
+ } catch (error) {
827
+ return new Response(errorMessage(error), { status: 400 });
763
828
  }
764
829
  try {
765
830
  await mkdir(dirname(absTo), { recursive: true });
766
831
  await rename(absFrom, absTo);
767
832
  return Response.json({
768
- ok: true,
769
833
  from: fwd(relative(root, absFrom)),
834
+ ok: true,
770
835
  to: fwd(relative(root, absTo)),
771
836
  });
772
- } catch (e) {
773
- return Response.json({ error: (e as Error).message }, { status: 500 });
837
+ } catch (error) {
838
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
774
839
  }
775
840
  }
776
841
 
@@ -783,23 +848,29 @@ export async function handleStudioApi(
783
848
  return new Response("Invalid JSON", { status: 400 });
784
849
  }
785
850
  const { name } = body;
786
- if (!name) return new Response("Missing name", { status: 400 });
851
+ if (!name) {
852
+ return new Response("Missing name", { status: 400 });
853
+ }
787
854
 
788
855
  try {
789
856
  const glob = new Bun.Glob(`**/${name}`);
790
857
  const matches = [];
791
858
  for await (const match of glob.scan({ cwd: root, dot: false })) {
792
859
  // Skip node_modules / dist / hidden dirs
793
- if (match.includes("node_modules") || fwd(match).includes("dist/")) continue;
860
+ if (match.includes("node_modules") || fwd(match).includes("dist/")) {
861
+ continue;
862
+ }
794
863
  matches.push(fwd(match));
795
864
  }
796
- if (matches.length === 0) return Response.json({ path: null });
865
+ if (matches.length === 0) {
866
+ return Response.json({ path: null });
867
+ }
797
868
  return Response.json({
798
869
  path: matches[0],
799
870
  ...(matches.length > 1 ? { alternatives: matches } : {}),
800
871
  });
801
- } catch (e) {
802
- return Response.json({ error: (e as Error).message }, { status: 500 });
872
+ } catch (error) {
873
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
803
874
  }
804
875
  }
805
876
 
@@ -839,13 +910,13 @@ export async function handleStudioApi(
839
910
  ? await entry.call("parse", source ?? "", options)
840
911
  : await entry.call("serialize", doc ?? {}, options);
841
912
  return Response.json({ result });
842
- } catch (e) {
843
- return Response.json({ error: (e as Error).message }, { status: 500 });
913
+ } catch (error) {
914
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
844
915
  }
845
916
  }
846
917
 
847
918
  // Format registry listing — lets the studio introspect available formats without
848
- // fetching each .class.json itself.
919
+ // Fetching each .class.json itself.
849
920
  if (path === "/__studio/formats" && req.method === "GET") {
850
921
  const dir = url.searchParams.get("dir") || activeProjectRoot || root;
851
922
  const projectRoot = isAbsolute(dir) ? dir : resolve(root, dir);
@@ -854,18 +925,18 @@ export async function handleStudioApi(
854
925
  const registry = await getFormatRegistry(projectRoot);
855
926
  return Response.json({
856
927
  formats: registry.entries.map((e) => ({
857
- name: e.name,
858
- extensions: e.extensions,
859
- mediaType: e.mediaType,
928
+ capabilities: e.capabilities,
860
929
  documentKinds: e.documentKinds,
861
930
  exportTarget: e.exportTarget,
931
+ extensions: e.extensions,
932
+ mediaType: e.mediaType,
933
+ name: e.name,
862
934
  remote: e.remote,
863
935
  studio: e.studio,
864
- capabilities: e.capabilities,
865
936
  })),
866
937
  });
867
- } catch (e) {
868
- return Response.json({ error: (e as Error).message }, { status: 400 });
938
+ } catch (error) {
939
+ return Response.json({ error: errorMessage(error) }, { status: 400 });
869
940
  }
870
941
  }
871
942
 
@@ -873,7 +944,9 @@ export async function handleStudioApi(
873
944
  const src = url.searchParams.get("src");
874
945
  const prototype = url.searchParams.get("prototype");
875
946
  const base = url.searchParams.get("base");
876
- if (!src) return new Response("Missing src param", { status: 400 });
947
+ if (!src) {
948
+ return new Response("Missing src param", { status: 400 });
949
+ }
877
950
 
878
951
  let moduleAbsPath;
879
952
  try {
@@ -882,7 +955,7 @@ export async function handleStudioApi(
882
955
  if (base) {
883
956
  const docUrlPath = new URL(base).pathname;
884
957
  const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
885
- moduleAbsPath = resolve(resolve(root, "." + docDir), src);
958
+ moduleAbsPath = resolve(resolve(root, `.${docDir}`), src);
886
959
  } else {
887
960
  moduleAbsPath = resolve(activeProjectRoot || root, src);
888
961
  if (!existsSync(moduleAbsPath) && activeProjectRoot) {
@@ -890,7 +963,7 @@ export async function handleStudioApi(
890
963
  }
891
964
  }
892
965
  } else {
893
- // npm/bare specifier — use createRequire from project root, fall back to server package
966
+ // Npm/bare specifier — use createRequire from project root, fall back to server package
894
967
  const projectRoot = activeProjectRoot || root;
895
968
  const { createRequire } = await import("node:module");
896
969
  const projRequire = createRequire(resolve(projectRoot, "package.json"));
@@ -901,10 +974,10 @@ export async function handleStudioApi(
901
974
  moduleAbsPath = serverRequire.resolve(src);
902
975
  }
903
976
  }
904
- } catch (e) {
977
+ } catch (error) {
905
978
  return Response.json({
979
+ error: errorMessage(error),
906
980
  schema: null,
907
- error: (e as Error).message,
908
981
  });
909
982
  }
910
983
 
@@ -916,10 +989,10 @@ export async function handleStudioApi(
916
989
  return Response.json({
917
990
  schema: extractStudioSchema(classDef, moduleAbsPath),
918
991
  });
919
- } catch (e) {
992
+ } catch (error) {
920
993
  return Response.json({
994
+ error: errorMessage(error),
921
995
  schema: null,
922
- error: (e as Error).message,
923
996
  });
924
997
  }
925
998
  }
@@ -945,15 +1018,15 @@ export async function handleStudioApi(
945
1018
  const ExportedClass = mod[exportName] ?? mod.default?.[exportName];
946
1019
  if (typeof ExportedClass !== "function") {
947
1020
  return Response.json({
948
- schema: null,
949
1021
  error: `Export "${exportName}" not found`,
1022
+ schema: null,
950
1023
  });
951
1024
  }
952
1025
  return Response.json({ schema: ExportedClass.schema ?? null });
953
- } catch (e) {
1026
+ } catch (error) {
954
1027
  return Response.json({
1028
+ error: errorMessage(error),
955
1029
  schema: null,
956
- error: (e as Error).message,
957
1030
  });
958
1031
  }
959
1032
  }
@@ -967,13 +1040,15 @@ export async function handleStudioApi(
967
1040
  const runGit = async (args: string[]) => {
968
1041
  const proc = Bun.spawn(["git", ...args], {
969
1042
  cwd,
970
- stdout: "pipe",
971
1043
  stderr: "pipe",
1044
+ stdout: "pipe",
972
1045
  });
973
1046
  const exitCode = await proc.exited;
974
1047
  const stdout = await new Response(proc.stdout).text();
975
1048
  const stderr = await new Response(proc.stderr).text();
976
- if (exitCode !== 0) throw new Error(stderr || `git exited with ${exitCode}`);
1049
+ if (exitCode !== 0) {
1050
+ throw new Error(stderr || `git exited with ${exitCode}`);
1051
+ }
977
1052
  return stdout;
978
1053
  };
979
1054
 
@@ -982,17 +1057,17 @@ export async function handleStudioApi(
982
1057
  // Check if we're in a git repo first
983
1058
  const checkProc = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
984
1059
  cwd,
985
- stdout: "pipe",
986
1060
  stderr: "pipe",
1061
+ stdout: "pipe",
987
1062
  });
988
1063
  const checkExit = await checkProc.exited;
989
1064
  if (checkExit !== 0) {
990
1065
  return Response.json({
991
- isRepo: false,
992
- branch: "",
993
- files: [],
994
1066
  ahead: 0,
995
1067
  behind: 0,
1068
+ branch: "",
1069
+ files: [],
1070
+ isRepo: false,
996
1071
  remotes: [],
997
1072
  });
998
1073
  }
@@ -1017,7 +1092,9 @@ export async function handleStudioApi(
1017
1092
 
1018
1093
  if (gitCmd === "add-remote" && req.method === "POST") {
1019
1094
  const { name, url: remoteUrl } = await req.json();
1020
- if (!name || !remoteUrl) return new Response("name and url required", { status: 400 });
1095
+ if (!name || !remoteUrl) {
1096
+ return new Response("name and url required", { status: 400 });
1097
+ }
1021
1098
  await runGit(["remote", "add", name, remoteUrl]);
1022
1099
  return Response.json({ ok: true });
1023
1100
  }
@@ -1027,12 +1104,16 @@ export async function handleStudioApi(
1027
1104
  let current = "";
1028
1105
  const branches = [];
1029
1106
  for (const line of out.trim().split("\n")) {
1030
- if (!line) continue;
1107
+ if (!line) {
1108
+ continue;
1109
+ }
1031
1110
  const [name, head] = line.split("\t");
1032
1111
  branches.push(name);
1033
- if (head === "*") current = name;
1112
+ if (head === "*") {
1113
+ current = name;
1114
+ }
1034
1115
  }
1035
- return Response.json({ current, branches });
1116
+ return Response.json({ branches, current });
1036
1117
  }
1037
1118
 
1038
1119
  if (gitCmd === "log" && req.method === "GET") {
@@ -1044,17 +1125,20 @@ export async function handleStudioApi(
1044
1125
  .filter(Boolean)
1045
1126
  .map((line) => {
1046
1127
  const [hash, message, author, date] = line.split("\t");
1047
- return { hash, message, author, date };
1128
+ return { author, date, hash, message };
1048
1129
  });
1049
1130
  return Response.json(entries);
1050
1131
  }
1051
1132
 
1052
1133
  if (gitCmd === "stage" && req.method === "POST") {
1053
1134
  const { files } = await req.json();
1054
- if (!Array.isArray(files) || files.length === 0)
1135
+ if (!Array.isArray(files) || files.length === 0) {
1055
1136
  return Response.json({ error: "Missing files" }, { status: 400 });
1137
+ }
1056
1138
  for (const f of files) {
1057
- if (f.includes("..")) return Response.json({ error: "Invalid path" }, { status: 400 });
1139
+ if (f.includes("..")) {
1140
+ return Response.json({ error: "Invalid path" }, { status: 400 });
1141
+ }
1058
1142
  }
1059
1143
  await runGit(["add", "--", ...files]);
1060
1144
  return Response.json({ ok: true });
@@ -1062,16 +1146,18 @@ export async function handleStudioApi(
1062
1146
 
1063
1147
  if (gitCmd === "unstage" && req.method === "POST") {
1064
1148
  const { files } = await req.json();
1065
- if (!Array.isArray(files) || files.length === 0)
1149
+ if (!Array.isArray(files) || files.length === 0) {
1066
1150
  return Response.json({ error: "Missing files" }, { status: 400 });
1151
+ }
1067
1152
  await runGit(["restore", "--staged", "--", ...files]);
1068
1153
  return Response.json({ ok: true });
1069
1154
  }
1070
1155
 
1071
1156
  if (gitCmd === "commit" && req.method === "POST") {
1072
1157
  const { message } = await req.json();
1073
- if (!message || typeof message !== "string")
1158
+ if (!message || typeof message !== "string") {
1074
1159
  return Response.json({ error: "Missing message" }, { status: 400 });
1160
+ }
1075
1161
  const statusOut = await runGit(["status", "--porcelain"]);
1076
1162
  const hasStaged = statusOut
1077
1163
  .split("\n")
@@ -1079,7 +1165,7 @@ export async function handleStudioApi(
1079
1165
  const args = hasStaged ? ["commit", "-m", message] : ["commit", "-a", "-m", message];
1080
1166
  const out = await runGit(args);
1081
1167
  const hashMatch = out.match(/\[[\w/]+ ([a-f0-9]+)\]/);
1082
- return Response.json({ ok: true, hash: hashMatch?.[1] || "" });
1168
+ return Response.json({ hash: hashMatch?.[1] || "", ok: true });
1083
1169
  }
1084
1170
 
1085
1171
  if (gitCmd === "push" && req.method === "POST") {
@@ -1089,7 +1175,8 @@ export async function handleStudioApi(
1089
1175
  } catch {}
1090
1176
  const { setUpstream } = body as { setUpstream?: boolean };
1091
1177
  if (setUpstream) {
1092
- const branch = (await runGit(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
1178
+ const branchRaw = await runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
1179
+ const branch = branchRaw.trim();
1093
1180
  await runGit(["push", "-u", "origin", branch]);
1094
1181
  } else {
1095
1182
  await runGit(["push"]);
@@ -1109,24 +1196,30 @@ export async function handleStudioApi(
1109
1196
 
1110
1197
  if (gitCmd === "checkout" && req.method === "POST") {
1111
1198
  const { branch } = await req.json();
1112
- if (!branch || typeof branch !== "string")
1199
+ if (!branch || typeof branch !== "string") {
1113
1200
  return Response.json({ error: "Missing branch" }, { status: 400 });
1201
+ }
1114
1202
  await runGit(["checkout", branch]);
1115
1203
  return Response.json({ ok: true });
1116
1204
  }
1117
1205
 
1118
1206
  if (gitCmd === "create-branch" && req.method === "POST") {
1119
1207
  const { name } = await req.json();
1120
- if (!name || typeof name !== "string")
1208
+ if (!name || typeof name !== "string") {
1121
1209
  return Response.json({ error: "Missing name" }, { status: 400 });
1210
+ }
1122
1211
  await runGit(["checkout", "-b", name]);
1123
1212
  return Response.json({ ok: true });
1124
1213
  }
1125
1214
 
1126
1215
  if (gitCmd === "diff" && req.method === "GET") {
1127
1216
  const fp = url.searchParams.get("path");
1128
- if (!fp) return Response.json({ error: "Missing path" }, { status: 400 });
1129
- if (fp.includes("..")) return Response.json({ error: "Invalid path" }, { status: 400 });
1217
+ if (!fp) {
1218
+ return Response.json({ error: "Missing path" }, { status: 400 });
1219
+ }
1220
+ if (fp.includes("..")) {
1221
+ return Response.json({ error: "Invalid path" }, { status: 400 });
1222
+ }
1130
1223
  const diff = await runGit(["diff", "--", fp]);
1131
1224
  return Response.json({ diff });
1132
1225
  }
@@ -1134,41 +1227,51 @@ export async function handleStudioApi(
1134
1227
  if (gitCmd === "show" && req.method === "GET") {
1135
1228
  const fp = url.searchParams.get("path");
1136
1229
  const ref = url.searchParams.get("ref") || "HEAD";
1137
- if (!fp) return Response.json({ error: "Missing path" }, { status: 400 });
1138
- if (fp.includes("..")) return Response.json({ error: "Invalid path" }, { status: 400 });
1230
+ if (!fp) {
1231
+ return Response.json({ error: "Missing path" }, { status: 400 });
1232
+ }
1233
+ if (fp.includes("..")) {
1234
+ return Response.json({ error: "Invalid path" }, { status: 400 });
1235
+ }
1139
1236
  const content = await runGit(["show", `${ref}:${fp}`]);
1140
1237
  return Response.json({ content });
1141
1238
  }
1142
1239
 
1143
1240
  if (gitCmd === "discard" && req.method === "POST") {
1144
1241
  const { files } = await req.json();
1145
- if (!Array.isArray(files) || files.length === 0)
1242
+ if (!Array.isArray(files) || files.length === 0) {
1146
1243
  return Response.json({ error: "Missing files" }, { status: 400 });
1244
+ }
1147
1245
  for (const f of files) {
1148
- if (f.includes("..")) return Response.json({ error: "Invalid path" }, { status: 400 });
1246
+ if (f.includes("..")) {
1247
+ return Response.json({ error: "Invalid path" }, { status: 400 });
1248
+ }
1149
1249
  }
1150
1250
  await runGit(["checkout", "--", ...files]);
1151
1251
  return Response.json({ ok: true });
1152
1252
  }
1153
1253
 
1154
1254
  if (gitCmd === "clone" && req.method === "POST") {
1155
- const { url } = await req.json();
1156
- if (!url || typeof url !== "string")
1255
+ const { url: repoUrl } = await req.json();
1256
+ if (!repoUrl || typeof repoUrl !== "string") {
1157
1257
  return Response.json({ error: "Missing url" }, { status: 400 });
1158
- const repoName = basename(url.replace(/\.git$/, ""));
1258
+ }
1259
+ const repoName = basename(repoUrl.replace(/\.git$/, ""));
1159
1260
  const dest = resolve(cwd, repoName);
1160
- const proc = Bun.spawn(["git", "clone", url, dest], {
1261
+ const proc = Bun.spawn(["git", "clone", repoUrl, dest], {
1161
1262
  cwd,
1162
- stdout: "pipe",
1163
1263
  stderr: "pipe",
1264
+ stdout: "pipe",
1164
1265
  });
1165
1266
  const exitCode = await proc.exited;
1166
1267
  const stderr = await new Response(proc.stderr).text();
1167
- if (exitCode !== 0) throw new Error(stderr || `git clone exited with ${exitCode}`);
1268
+ if (exitCode !== 0) {
1269
+ throw new Error(stderr || `git clone exited with ${exitCode}`);
1270
+ }
1168
1271
  return Response.json({ ok: true, root: dest });
1169
1272
  }
1170
- } catch (e) {
1171
- return Response.json({ error: (e as Error).message }, { status: 500 });
1273
+ } catch (error) {
1274
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
1172
1275
  }
1173
1276
  }
1174
1277
 
@@ -1187,8 +1290,11 @@ export async function handleStudioApi(
1187
1290
  systemPrompt: body.systemPrompt,
1188
1291
  });
1189
1292
  return Response.json(result);
1190
- } catch (e) {
1191
- return Response.json({ error: e instanceof Error ? e.message : String(e) }, { status: 500 });
1293
+ } catch (error) {
1294
+ return Response.json(
1295
+ { error: error instanceof Error ? error.message : String(error) },
1296
+ { status: 500 },
1297
+ );
1192
1298
  }
1193
1299
  }
1194
1300
 
@@ -1197,7 +1303,7 @@ export async function handleStudioApi(
1197
1303
  path.endsWith("/stream") &&
1198
1304
  req.method === "GET"
1199
1305
  ) {
1200
- const id = path.split("/")[4];
1306
+ const [id] = path.split("/").slice(4);
1201
1307
  return claude.streamSession(id);
1202
1308
  }
1203
1309
 
@@ -1207,31 +1313,36 @@ export async function handleStudioApi(
1207
1313
  req.method === "POST"
1208
1314
  ) {
1209
1315
  try {
1210
- const id = path.split("/")[4];
1316
+ const [id] = path.split("/").slice(4);
1211
1317
  const body = await req.json();
1212
1318
  claude.sendMessage(id, body.message);
1213
1319
  return Response.json({ ok: true });
1214
- } catch (e) {
1215
- return Response.json({ error: e instanceof Error ? e.message : String(e) }, { status: 500 });
1320
+ } catch (error) {
1321
+ return Response.json(
1322
+ { error: error instanceof Error ? error.message : String(error) },
1323
+ { status: 500 },
1324
+ );
1216
1325
  }
1217
1326
  }
1218
1327
 
1219
1328
  if (path.startsWith("/__studio/ai/session/") && path.endsWith("/stop") && req.method === "POST") {
1220
- const id = path.split("/")[4];
1329
+ const [id] = path.split("/").slice(4);
1221
1330
  claude.stopSession(id);
1222
1331
  return Response.json({ ok: true });
1223
1332
  }
1224
1333
 
1225
1334
  if (path.startsWith("/__studio/ai/session/") && req.method === "DELETE") {
1226
- const id = path.split("/")[4];
1335
+ const [id] = path.split("/").slice(4);
1227
1336
  claude.deleteSession(id);
1228
1337
  return Response.json({ ok: true });
1229
1338
  }
1230
1339
 
1231
1340
  if (path.startsWith("/__studio/ai/session/") && req.method === "GET") {
1232
- const id = path.split("/")[4];
1341
+ const [id] = path.split("/").slice(4);
1233
1342
  const info = claude.getSession(id);
1234
- if (!info) return Response.json({ error: "Not found" }, { status: 404 });
1343
+ if (!info) {
1344
+ return Response.json({ error: "Not found" }, { status: 404 });
1345
+ }
1235
1346
  return Response.json(info);
1236
1347
  }
1237
1348
 
@@ -1282,31 +1393,52 @@ function extractStudioSchema(classDef: ClassJsonDef, classJsonPath: string) {
1282
1393
  for (const [key, param] of Object.entries(params)) {
1283
1394
  const id = param.identifier ?? key;
1284
1395
  const prop: Record<string, unknown> = {};
1285
- if (param.type && typeof param.type === "object") Object.assign(prop, param.type);
1286
- if (param.description) prop.description = param.description;
1287
- if (param.examples) prop.examples = param.examples;
1288
- if (param.format) prop.format = param.format;
1396
+ if (param.type && typeof param.type === "object") {
1397
+ Object.assign(prop, param.type);
1398
+ }
1399
+ if (param.description) {
1400
+ prop.description = param.description;
1401
+ }
1402
+ if (param.examples) {
1403
+ prop.examples = param.examples;
1404
+ }
1405
+ if (param.format) {
1406
+ prop.format = param.format;
1407
+ }
1289
1408
  properties[id] = prop;
1290
1409
  }
1291
1410
 
1292
1411
  // Build properties from fields (config-visible ones only)
1293
1412
  for (const [key, field] of Object.entries(fields)) {
1294
- if (field.role !== "field") continue;
1295
- if (field.access === "private") continue;
1413
+ if (field.role !== "field") {
1414
+ continue;
1415
+ }
1416
+ if (field.access === "private") {
1417
+ continue;
1418
+ }
1296
1419
  const id = field.identifier ?? key;
1297
1420
  const prop: Record<string, unknown> = {};
1298
- if (field.type && typeof field.type === "object") Object.assign(prop, field.type);
1299
- if (field.description) prop.description = field.description;
1300
- if (field.default !== undefined) prop.default = field.default;
1301
- if (field.initializer !== undefined && prop.default === undefined)
1421
+ if (field.type && typeof field.type === "object") {
1422
+ Object.assign(prop, field.type);
1423
+ }
1424
+ if (field.description) {
1425
+ prop.description = field.description;
1426
+ }
1427
+ if (field.default !== undefined) {
1428
+ prop.default = field.default;
1429
+ }
1430
+ if (field.initializer !== undefined && prop.default === undefined) {
1302
1431
  prop.default = field.initializer;
1303
- if (field.examples) prop.examples = field.examples;
1432
+ }
1433
+ if (field.examples) {
1434
+ prop.examples = field.examples;
1435
+ }
1304
1436
  properties[id] = prop;
1305
1437
  }
1306
1438
 
1307
1439
  // Determine required from constructor parameters that have no default
1308
1440
  const ctorParams = classDef.$defs?.constructor?.parameters ?? [];
1309
- const requiredSet: Set<string> = new Set(required);
1441
+ const requiredSet = new Set<string>(required);
1310
1442
  for (const p of ctorParams) {
1311
1443
  const name = p.$ref ? p.$ref.split("/").pop() : (p.identifier ?? p.name);
1312
1444
  if (name && properties[name] && properties[name].default === undefined) {
@@ -1317,7 +1449,7 @@ function extractStudioSchema(classDef: ClassJsonDef, classJsonPath: string) {
1317
1449
  const resolveMethod = classDef.$defs?.methods?.resolve;
1318
1450
 
1319
1451
  // Surface format-extension metadata: the format block, studio hints, and a
1320
- // capability summary ({ parse: { timing }, serialize: { timing }, ... }).
1452
+ // Capability summary ({ parse: { timing }, serialize: { timing }, ... }).
1321
1453
  const def = classDef as Record<string, unknown>;
1322
1454
  const capabilityRoles = new Set(["parse", "serialize", "discover", "load"]);
1323
1455
  const capabilities: Record<string, { identifier: string; timing: string[] }> = {};