@jxsuite/server 0.28.5 → 0.30.1

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,18 +7,105 @@
7
7
  * All paths are relative to the project root. Directory traversal above root is rejected.
8
8
  */
9
9
 
10
- import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
10
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
11
11
  import { errorMessage } from "@jxsuite/schema/parse";
12
12
  import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
13
13
  import { existsSync, readFileSync, statSync } from "node:fs";
14
14
  import { buildProjectFormatRegistry } from "@jxsuite/compiler/format-host";
15
+ import { applyRename } from "./refactor/apply.ts";
15
16
  import type { FormatRegistry } from "@jxsuite/schema/format-registry";
16
17
  import * as claude from "./claude-session.ts";
17
18
  import type { ClassJsonDef } from "./types.ts";
19
+ import type { ProjectConfig } from "@jxsuite/schema/types";
18
20
 
19
21
  /** Normalise a path to forward slashes (Windows `path` module returns backslashes). */
20
22
  const fwd = (p: string) => p.replaceAll("\\", "/");
21
23
 
24
+ /**
25
+ * Expand a leading `~` to the user's home directory using path.join so separators are normalised (a
26
+ * plain string replace left the input's forward slashes intact, yielding mixed separators on
27
+ * Windows) and falling back to USERPROFILE where HOME is unset.
28
+ */
29
+ const expandTilde = (p: string) =>
30
+ p.startsWith("~") ? join(process.env.HOME || process.env.USERPROFILE || "", p.slice(1)) : p;
31
+
32
+ interface PackageJson {
33
+ name?: string;
34
+ workspaces?: unknown;
35
+ dependencies?: Record<string, string>;
36
+ devDependencies?: Record<string, string>;
37
+ customElements?: string;
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ /** A custom-element-manifest declaration (subset consumed here). */
42
+ interface CemDeclaration {
43
+ customElement?: boolean;
44
+ tagName?: string;
45
+ description?: string;
46
+ cssProperties?: unknown[];
47
+ events?: unknown[];
48
+ slots?: unknown[];
49
+ members?: { kind?: string; privacy?: string; [key: string]: unknown }[];
50
+ attributes?: {
51
+ name?: string;
52
+ default?: unknown;
53
+ description?: string;
54
+ type?: { text?: string };
55
+ }[];
56
+ [key: string]: unknown;
57
+ }
58
+
59
+ interface CemModule {
60
+ path?: string;
61
+ declarations?: CemDeclaration[];
62
+ }
63
+
64
+ interface Cem {
65
+ modules?: CemModule[];
66
+ }
67
+
68
+ /** A parsed Jx component document (subset consumed by component discovery). */
69
+ interface ComponentDoc {
70
+ $id?: string;
71
+ tagName?: string;
72
+ $elements?: unknown[];
73
+ state?: Record<string, unknown>;
74
+ [key: string]: unknown;
75
+ }
76
+
77
+ interface SlotDef {
78
+ name: string;
79
+ fallback?: unknown[];
80
+ }
81
+
82
+ /**
83
+ * Collect slot definitions (name + fallback children) from a parsed component tree. Whitespace-only
84
+ * names count as unnamed (""). Only static children arrays are walked.
85
+ */
86
+ function collectSlotDefs(node: unknown, out: SlotDef[] = []): SlotDef[] {
87
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
88
+ return out;
89
+ }
90
+ const el = node as Record<string, unknown>;
91
+ if (el.tagName === "slot") {
92
+ const attrs = el.attributes as Record<string, unknown> | undefined;
93
+ const rawName = attrs?.name;
94
+ const name = typeof rawName === "string" ? rawName.trim() : "";
95
+ const { children } = el;
96
+ out.push({
97
+ name,
98
+ ...(Array.isArray(children) && children.length > 0 ? { fallback: children } : {}),
99
+ });
100
+ }
101
+ if (Array.isArray(el.children)) {
102
+ for (const c of el.children) {
103
+ collectSlotDefs(c, out);
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+
22
109
  // ─── Format registry (per project root, invalidated on project.json change) ──
23
110
 
24
111
  const formatRegistryCache = new Map<string, { mtime: number; registry: FormatRegistry }>();
@@ -33,10 +120,10 @@ const formatRegistryCache = new Map<string, { mtime: number; registry: FormatReg
33
120
  async function getFormatRegistry(projectRoot: string): Promise<FormatRegistry> {
34
121
  const projectJsonPath = resolve(projectRoot, "project.json");
35
122
  let mtime = 0;
36
- let projectConfig;
123
+ let projectConfig: ProjectConfig | undefined;
37
124
  try {
38
125
  mtime = statSync(projectJsonPath).mtimeMs;
39
- projectConfig = JSON.parse(readFileSync(projectJsonPath, "utf8"));
126
+ projectConfig = JSON.parse(readFileSync(projectJsonPath, "utf8")) as ProjectConfig;
40
127
  } catch {
41
128
  projectConfig = undefined;
42
129
  }
@@ -111,14 +198,14 @@ export function parseGitStatus(out: string) {
111
198
  } else if (line.startsWith("# branch.ab ")) {
112
199
  const m = line.match(/\+(\d+) -(\d+)/);
113
200
  if (m) {
114
- ahead = Number.parseInt(m[1], 10);
115
- behind = Number.parseInt(m[2], 10);
201
+ ahead = Math.trunc(Number(m[1]!));
202
+ behind = Math.trunc(Number(m[2]!));
116
203
  }
117
204
  } else if (line.startsWith("1 ") || line.startsWith("2 ")) {
118
205
  const parts = line.split(" ");
119
- const [, xy] = parts;
120
- const [stagedCode] = xy;
121
- const [, unstagedCode] = xy;
206
+ const xy = parts[1]!;
207
+ const stagedCode = xy[0]!;
208
+ const unstagedCode = xy[1]!;
122
209
  let filePath;
123
210
  if (line.startsWith("2 ")) {
124
211
  const tabIdx = line.indexOf("\t");
@@ -169,7 +256,7 @@ export async function handleStudioApi(
169
256
  // Project metadata
170
257
  if (path === "/__studio/project" && req.method === "GET") {
171
258
  try {
172
- const pkg = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
259
+ const pkg = JSON.parse(await readFile(resolve(root, "package.json"), "utf8")) as PackageJson;
173
260
  return Response.json({
174
261
  name: pkg.name ?? basename(root),
175
262
  root,
@@ -211,12 +298,12 @@ export async function handleStudioApi(
211
298
  }
212
299
 
213
300
  let isSiteProject = false;
214
- let projectConfig = null;
301
+ let projectConfig: ProjectConfig | null = null;
215
302
  try {
216
- const raw = JSON.parse(await readFile(resolve(absDir, "project.json"), "utf8"));
303
+ const raw = JSON.parse(await readFile(resolve(absDir, "project.json"), "utf8")) as unknown;
217
304
  if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
218
305
  isSiteProject = true;
219
- projectConfig = raw;
306
+ projectConfig = raw as ProjectConfig;
220
307
  }
221
308
  } catch {}
222
309
 
@@ -239,17 +326,13 @@ export async function handleStudioApi(
239
326
  }
240
327
  try {
241
328
  // Walk up from file's directory looking for project.json
242
- let dir = dirname(
243
- filePath.startsWith("~") ? filePath.replace("~", process.env.HOME || "") : filePath,
244
- );
329
+ const absFile = expandTilde(filePath);
330
+ let dir = dirname(absFile);
245
331
  while (dir) {
246
332
  const candidate = resolve(dir, "project.json");
247
333
  if (existsSync(candidate)) {
248
- const config = JSON.parse(readFileSync(candidate, "utf8"));
334
+ const config = JSON.parse(readFileSync(candidate, "utf8")) as ProjectConfig;
249
335
  const relPath = fwd(dir);
250
- const absFile = filePath.startsWith("~")
251
- ? filePath.replace("~", process.env.HOME || "")
252
- : filePath;
253
336
  const fileRelPath = fwd(relative(dir, absFile));
254
337
  return Response.json({
255
338
  fileRelPath,
@@ -313,10 +396,10 @@ export async function handleStudioApi(
313
396
  }
314
397
  const fp = resolve(root, match);
315
398
  try {
316
- const raw = JSON.parse(await readFile(fp, "utf8"));
399
+ const raw = JSON.parse(await readFile(fp, "utf8")) as unknown;
317
400
  if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
318
401
  const projectDir = fwd(dirname(fp));
319
- sites.push({ config: raw, path: projectDir });
402
+ sites.push({ config: raw as ProjectConfig, path: projectDir });
320
403
  }
321
404
  } catch {}
322
405
  }
@@ -329,7 +412,13 @@ export async function handleStudioApi(
329
412
  // Create a new project
330
413
  if (path === "/__studio/create-project" && req.method === "POST") {
331
414
  try {
332
- const body = await req.json();
415
+ const body = (await req.json()) as {
416
+ name?: string;
417
+ description?: string;
418
+ url?: string;
419
+ adapter?: "static" | "cloudflare-pages" | "cloudflare-workers" | "node" | "bun";
420
+ directory?: string;
421
+ };
333
422
  const { name, description, url: siteUrl, adapter, directory } = body;
334
423
  if (!name || !directory) {
335
424
  return Response.json({ error: "name and directory are required" }, { status: 400 });
@@ -339,13 +428,15 @@ export async function handleStudioApi(
339
428
 
340
429
  const { generateProject } = await import("@jxsuite/create/generate");
341
430
  await generateProject(destPath, {
342
- adapter,
343
- description,
344
431
  name,
345
- url: siteUrl,
432
+ ...(adapter !== undefined ? { adapter } : {}),
433
+ ...(description !== undefined ? { description } : {}),
434
+ ...(siteUrl !== undefined ? { url: siteUrl } : {}),
346
435
  });
347
436
 
348
- const config = JSON.parse(await readFile(resolve(destPath, "project.json"), "utf8"));
437
+ const config = JSON.parse(
438
+ await readFile(resolve(destPath, "project.json"), "utf8"),
439
+ ) as ProjectConfig;
349
440
  const projectRoot = fwd(relative(root, destPath));
350
441
  return Response.json({ config, root: projectRoot });
351
442
  } catch (error) {
@@ -442,18 +533,19 @@ export async function handleStudioApi(
442
533
  }
443
534
  const fp = resolve(scanRoot, match);
444
535
  try {
445
- let content;
536
+ let content: ComponentDoc;
446
537
  if (match.endsWith(".json")) {
447
- content = JSON.parse(await readFile(fp, "utf8"));
538
+ content = JSON.parse(await readFile(fp, "utf8")) as ComponentDoc;
448
539
  } else {
449
540
  const entry = registry.byExtension(extname(match), "parse");
450
541
  if (!entry) {
451
542
  continue;
452
543
  }
453
544
  const source = await readFile(fp, "utf8");
454
- content = (await entry.call("parse", source)) as Record<string, unknown>;
545
+ content = (await entry.call("parse", source)) as ComponentDoc;
455
546
  }
456
547
  if (content.tagName && content.tagName.includes("-")) {
548
+ const slotDefs = collectSlotDefs(content);
457
549
  components.push({
458
550
  $id: content.$id || null,
459
551
  hasElements: Array.isArray(content.$elements) && content.$elements.length > 0,
@@ -484,6 +576,7 @@ export async function handleStudioApi(
484
576
  type: obj.type,
485
577
  };
486
578
  }),
579
+ ...(slotDefs.length > 0 ? { slots: slotDefs } : {}),
487
580
  source: "jx",
488
581
  tagName: content.tagName,
489
582
  });
@@ -495,7 +588,7 @@ export async function handleStudioApi(
495
588
  try {
496
589
  const projectPkgPath = resolve(scanRoot, "package.json");
497
590
  if (existsSync(projectPkgPath)) {
498
- const pkg = JSON.parse(await readFile(projectPkgPath, "utf8"));
591
+ const pkg = JSON.parse(await readFile(projectPkgPath, "utf8")) as PackageJson;
499
592
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
500
593
  for (const name of Object.keys(deps)) {
501
594
  try {
@@ -520,7 +613,7 @@ export async function handleStudioApi(
520
613
  if (!actualPath) {
521
614
  continue;
522
615
  }
523
- const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
616
+ const depPkg = JSON.parse(await readFile(actualPath, "utf8")) as PackageJson;
524
617
  if (!depPkg.customElements) {
525
618
  continue;
526
619
  }
@@ -528,7 +621,7 @@ export async function handleStudioApi(
528
621
  if (!existsSync(cemPath)) {
529
622
  continue;
530
623
  }
531
- const cem = JSON.parse(await readFile(cemPath, "utf8"));
624
+ const cem = JSON.parse(await readFile(cemPath, "utf8")) as Cem;
532
625
  for (const mod of cem.modules || []) {
533
626
  for (const decl of mod.declarations || []) {
534
627
  if (decl.customElement && decl.tagName) {
@@ -539,17 +632,16 @@ export async function handleStudioApi(
539
632
  events: decl.events || [],
540
633
  hasElements: false,
541
634
  members: (decl.members || []).filter(
542
- (m: Record<string, unknown>) =>
543
- m.kind === "field" && m.privacy !== "private",
635
+ (m) => m.kind === "field" && m.privacy !== "private",
544
636
  ),
545
637
  modulePath: mod.path,
546
638
  package: name,
547
639
  path: null,
548
- props: (decl.attributes || []).map((a: Record<string, unknown>) => ({
640
+ props: (decl.attributes || []).map((a) => ({
549
641
  default: a.default,
550
642
  description: a.description || null,
551
643
  name: a.name,
552
- type: (a.type as Record<string, unknown> | undefined)?.text,
644
+ type: a.type?.text,
553
645
  })),
554
646
  slots: decl.slots || [],
555
647
  source: "npm",
@@ -580,17 +672,14 @@ export async function handleStudioApi(
580
672
  if (!existsSync(pkgPath)) {
581
673
  return Response.json([]);
582
674
  }
583
- const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
675
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8")) as PackageJson;
584
676
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
585
- /**
586
- * @type {{
587
- * name: string;
588
- * version: string;
589
- * hasCem: boolean;
590
- * customElementsPath: string | null;
591
- * }[]}
592
- */
593
- const packages = [];
677
+ const packages: {
678
+ name: string;
679
+ version: string;
680
+ hasCem: boolean;
681
+ customElementsPath: string | null;
682
+ }[] = [];
594
683
  for (const [name, version] of Object.entries(deps)) {
595
684
  const depPkgPath = resolve(scanRoot, "node_modules", ...name.split("/"), "package.json");
596
685
  const fallbackPath = resolve(root, "node_modules", ...name.split("/"), "package.json");
@@ -603,12 +692,12 @@ export async function handleStudioApi(
603
692
  continue;
604
693
  }
605
694
  try {
606
- const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
695
+ const depPkg = JSON.parse(await readFile(actualPath, "utf8")) as PackageJson;
607
696
  packages.push({
608
697
  customElementsPath: depPkg.customElements || null,
609
698
  hasCem: Boolean(depPkg.customElements),
610
699
  name,
611
- version: /** @type {string} */ version,
700
+ version,
612
701
  });
613
702
  } catch {}
614
703
  }
@@ -637,7 +726,7 @@ export async function handleStudioApi(
637
726
  if (!actualPath) {
638
727
  return Response.json({ cem: null });
639
728
  }
640
- const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
729
+ const depPkg = JSON.parse(await readFile(actualPath, "utf8")) as PackageJson;
641
730
  if (!depPkg.customElements) {
642
731
  return Response.json({ cem: null });
643
732
  }
@@ -645,7 +734,7 @@ export async function handleStudioApi(
645
734
  if (!existsSync(cemPath)) {
646
735
  return Response.json({ cem: null });
647
736
  }
648
- const cem = JSON.parse(await readFile(cemPath, "utf8"));
737
+ const cem = JSON.parse(await readFile(cemPath, "utf8")) as Cem;
649
738
  return Response.json({ cem });
650
739
  } catch (error) {
651
740
  return Response.json({ error: errorMessage(error) }, { status: 500 });
@@ -655,7 +744,7 @@ export async function handleStudioApi(
655
744
  // Add an npm package
656
745
  if (path === "/__studio/packages/add" && req.method === "POST") {
657
746
  try {
658
- const body = await req.json();
747
+ const body = (await req.json()) as { name?: string; dir?: string; dev?: boolean };
659
748
  const { name } = body;
660
749
  if (!name || typeof name !== "string") {
661
750
  return Response.json({ error: "Missing name" }, { status: 400 });
@@ -688,7 +777,7 @@ export async function handleStudioApi(
688
777
  // Remove an npm package
689
778
  if (path === "/__studio/packages/remove" && req.method === "POST") {
690
779
  try {
691
- const body = await req.json();
780
+ const body = (await req.json()) as { name?: string; dir?: string };
692
781
  const { name } = body;
693
782
  if (!name || typeof name !== "string") {
694
783
  return Response.json({ error: "Missing name" }, { status: 400 });
@@ -720,7 +809,7 @@ export async function handleStudioApi(
720
809
  if (!fp) {
721
810
  return new Response("Missing path", { status: 400 });
722
811
  }
723
- const abs = fp.startsWith("~") ? fp.replace("~", process.env.HOME || "") : fp;
812
+ const abs = expandTilde(fp);
724
813
  try {
725
814
  assertAccessible(abs, root, activeProjectRoot);
726
815
  } catch (error) {
@@ -808,9 +897,9 @@ export async function handleStudioApi(
808
897
 
809
898
  // Rename file
810
899
  if (path === "/__studio/file/rename" && req.method === "POST") {
811
- let body;
900
+ let body: { from?: string; to?: string };
812
901
  try {
813
- body = await req.json();
902
+ body = (await req.json()) as { from?: string; to?: string };
814
903
  } catch {
815
904
  return new Response("Invalid JSON", { status: 400 });
816
905
  }
@@ -829,21 +918,31 @@ export async function handleStudioApi(
829
918
  try {
830
919
  await mkdir(dirname(absTo), { recursive: true });
831
920
  await rename(absFrom, absTo);
921
+ } catch (error) {
922
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
923
+ }
924
+ // Refactor pass: rewrite path references project-wide and, for a component, auto-rename its tag.
925
+ // The move already succeeded, so a refactor failure is reported but never fails the rename.
926
+ const scanRoot = activeProjectRoot ?? root;
927
+ try {
928
+ const registry = await getFormatRegistry(scanRoot);
929
+ const report = await applyRename({ absFrom, absTo, registry, root: scanRoot });
930
+ return Response.json(report);
931
+ } catch (error) {
832
932
  return Response.json({
933
+ error: errorMessage(error),
833
934
  from: fwd(relative(root, absFrom)),
834
935
  ok: true,
835
936
  to: fwd(relative(root, absTo)),
836
937
  });
837
- } catch (error) {
838
- return Response.json({ error: errorMessage(error) }, { status: 500 });
839
938
  }
840
939
  }
841
940
 
842
941
  // Locate a file by name within the project root
843
942
  if (path === "/__studio/locate" && req.method === "POST") {
844
- let body;
943
+ let body: { name?: string };
845
944
  try {
846
- body = await req.json();
945
+ body = (await req.json()) as { name?: string };
847
946
  } catch {
848
947
  return new Response("Invalid JSON", { status: 400 });
849
948
  }
@@ -879,15 +978,16 @@ export async function handleStudioApi(
879
978
  // Dispatches parse/serialize through the project's format registry by import name.
880
979
  if (path === "/__studio/format" && req.method === "POST") {
881
980
  try {
882
- const body = await req.json();
883
- const { format, action, source, doc, options } = body as {
981
+ const body = (await req.json()) as {
884
982
  format?: string;
885
983
  action?: string;
886
984
  source?: string;
887
985
  doc?: Record<string, unknown>;
888
986
  options?: Record<string, unknown>;
987
+ dir?: string;
889
988
  };
890
- const dir = (body.dir as string | undefined) || activeProjectRoot || root;
989
+ const { format, action, source, doc, options } = body;
990
+ const dir = body.dir || activeProjectRoot || root;
891
991
  const projectRoot = isAbsolute(dir) ? dir : resolve(root, dir);
892
992
  assertAccessible(projectRoot, root, activeProjectRoot);
893
993
 
@@ -985,7 +1085,7 @@ export async function handleStudioApi(
985
1085
  if (moduleAbsPath.endsWith(".class.json")) {
986
1086
  try {
987
1087
  const content = readFileSync(moduleAbsPath, "utf8");
988
- const classDef = JSON.parse(content);
1088
+ const classDef = JSON.parse(content) as ClassJsonDef;
989
1089
  return Response.json({
990
1090
  schema: extractStudioSchema(classDef, moduleAbsPath),
991
1091
  });
@@ -1003,7 +1103,7 @@ export async function handleStudioApi(
1003
1103
  if (existsSync(classJsonPath)) {
1004
1104
  try {
1005
1105
  const content = readFileSync(classJsonPath, "utf8");
1006
- const classDef = JSON.parse(content);
1106
+ const classDef = JSON.parse(content) as ClassJsonDef;
1007
1107
  return Response.json({
1008
1108
  schema: extractStudioSchema(classDef, classJsonPath),
1009
1109
  });
@@ -1014,7 +1114,10 @@ export async function handleStudioApi(
1014
1114
 
1015
1115
  // Fallback: import JS module (backwards compat for classes without .class.json)
1016
1116
  try {
1017
- const mod = await import(moduleAbsPath);
1117
+ const mod = (await import(moduleAbsPath)) as {
1118
+ default?: Record<string, unknown>;
1119
+ [key: string]: unknown;
1120
+ };
1018
1121
  const ExportedClass = mod[exportName] ?? mod.default?.[exportName];
1019
1122
  if (typeof ExportedClass !== "function") {
1020
1123
  return Response.json({
@@ -1022,7 +1125,7 @@ export async function handleStudioApi(
1022
1125
  schema: null,
1023
1126
  });
1024
1127
  }
1025
- return Response.json({ schema: ExportedClass.schema ?? null });
1128
+ return Response.json({ schema: (ExportedClass as { schema?: unknown }).schema ?? null });
1026
1129
  } catch (error) {
1027
1130
  return Response.json({
1028
1131
  error: errorMessage(error),
@@ -1091,7 +1194,10 @@ export async function handleStudioApi(
1091
1194
  }
1092
1195
 
1093
1196
  if (gitCmd === "add-remote" && req.method === "POST") {
1094
- const { name, url: remoteUrl } = await req.json();
1197
+ const { name, url: remoteUrl } = (await req.json()) as {
1198
+ name?: string;
1199
+ url?: string;
1200
+ };
1095
1201
  if (!name || !remoteUrl) {
1096
1202
  return new Response("name and url required", { status: 400 });
1097
1203
  }
@@ -1108,9 +1214,9 @@ export async function handleStudioApi(
1108
1214
  continue;
1109
1215
  }
1110
1216
  const [name, head] = line.split("\t");
1111
- branches.push(name);
1217
+ branches.push(name!);
1112
1218
  if (head === "*") {
1113
- current = name;
1219
+ current = name!;
1114
1220
  }
1115
1221
  }
1116
1222
  return Response.json({ branches, current });
@@ -1131,7 +1237,7 @@ export async function handleStudioApi(
1131
1237
  }
1132
1238
 
1133
1239
  if (gitCmd === "stage" && req.method === "POST") {
1134
- const { files } = await req.json();
1240
+ const { files } = (await req.json()) as { files?: string[] };
1135
1241
  if (!Array.isArray(files) || files.length === 0) {
1136
1242
  return Response.json({ error: "Missing files" }, { status: 400 });
1137
1243
  }
@@ -1145,7 +1251,7 @@ export async function handleStudioApi(
1145
1251
  }
1146
1252
 
1147
1253
  if (gitCmd === "unstage" && req.method === "POST") {
1148
- const { files } = await req.json();
1254
+ const { files } = (await req.json()) as { files?: string[] };
1149
1255
  if (!Array.isArray(files) || files.length === 0) {
1150
1256
  return Response.json({ error: "Missing files" }, { status: 400 });
1151
1257
  }
@@ -1154,7 +1260,7 @@ export async function handleStudioApi(
1154
1260
  }
1155
1261
 
1156
1262
  if (gitCmd === "commit" && req.method === "POST") {
1157
- const { message } = await req.json();
1263
+ const { message } = (await req.json()) as { message?: string };
1158
1264
  if (!message || typeof message !== "string") {
1159
1265
  return Response.json({ error: "Missing message" }, { status: 400 });
1160
1266
  }
@@ -1169,11 +1275,11 @@ export async function handleStudioApi(
1169
1275
  }
1170
1276
 
1171
1277
  if (gitCmd === "push" && req.method === "POST") {
1172
- let body: Record<string, unknown> = {};
1278
+ let body: { setUpstream?: boolean } = {};
1173
1279
  try {
1174
- body = await req.json();
1280
+ body = (await req.json()) as { setUpstream?: boolean };
1175
1281
  } catch {}
1176
- const { setUpstream } = body as { setUpstream?: boolean };
1282
+ const { setUpstream } = body;
1177
1283
  if (setUpstream) {
1178
1284
  const branchRaw = await runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
1179
1285
  const branch = branchRaw.trim();
@@ -1195,7 +1301,7 @@ export async function handleStudioApi(
1195
1301
  }
1196
1302
 
1197
1303
  if (gitCmd === "checkout" && req.method === "POST") {
1198
- const { branch } = await req.json();
1304
+ const { branch } = (await req.json()) as { branch?: string };
1199
1305
  if (!branch || typeof branch !== "string") {
1200
1306
  return Response.json({ error: "Missing branch" }, { status: 400 });
1201
1307
  }
@@ -1204,7 +1310,7 @@ export async function handleStudioApi(
1204
1310
  }
1205
1311
 
1206
1312
  if (gitCmd === "create-branch" && req.method === "POST") {
1207
- const { name } = await req.json();
1313
+ const { name } = (await req.json()) as { name?: string };
1208
1314
  if (!name || typeof name !== "string") {
1209
1315
  return Response.json({ error: "Missing name" }, { status: 400 });
1210
1316
  }
@@ -1238,7 +1344,7 @@ export async function handleStudioApi(
1238
1344
  }
1239
1345
 
1240
1346
  if (gitCmd === "discard" && req.method === "POST") {
1241
- const { files } = await req.json();
1347
+ const { files } = (await req.json()) as { files?: string[] };
1242
1348
  if (!Array.isArray(files) || files.length === 0) {
1243
1349
  return Response.json({ error: "Missing files" }, { status: 400 });
1244
1350
  }
@@ -1252,7 +1358,7 @@ export async function handleStudioApi(
1252
1358
  }
1253
1359
 
1254
1360
  if (gitCmd === "clone" && req.method === "POST") {
1255
- const { url: repoUrl } = await req.json();
1361
+ const { url: repoUrl } = (await req.json()) as { url?: string };
1256
1362
  if (!repoUrl || typeof repoUrl !== "string") {
1257
1363
  return Response.json({ error: "Missing url" }, { status: 400 });
1258
1364
  }
@@ -1284,11 +1390,13 @@ export async function handleStudioApi(
1284
1390
 
1285
1391
  if (path === "/__studio/ai/session" && req.method === "POST") {
1286
1392
  try {
1287
- const body = await req.json();
1393
+ const body = (await req.json()) as { message?: string; systemPrompt?: string };
1288
1394
  const projectDir = activeProjectRoot || root;
1289
- const result = claude.createSession(projectDir, body.message, {
1290
- systemPrompt: body.systemPrompt,
1291
- });
1395
+ const opts: { systemPrompt?: string } = {};
1396
+ if (body.systemPrompt !== undefined) {
1397
+ opts.systemPrompt = body.systemPrompt;
1398
+ }
1399
+ const result = claude.createSession(projectDir, body.message!, opts);
1292
1400
  return Response.json(result);
1293
1401
  } catch (error) {
1294
1402
  return Response.json(
@@ -1303,7 +1411,7 @@ export async function handleStudioApi(
1303
1411
  path.endsWith("/stream") &&
1304
1412
  req.method === "GET"
1305
1413
  ) {
1306
- const [id] = path.split("/").slice(4);
1414
+ const id = path.split("/").slice(4)[0]!;
1307
1415
  return claude.streamSession(id);
1308
1416
  }
1309
1417
 
@@ -1313,9 +1421,9 @@ export async function handleStudioApi(
1313
1421
  req.method === "POST"
1314
1422
  ) {
1315
1423
  try {
1316
- const [id] = path.split("/").slice(4);
1317
- const body = await req.json();
1318
- claude.sendMessage(id, body.message);
1424
+ const id = path.split("/").slice(4)[0]!;
1425
+ const body = (await req.json()) as { message?: string };
1426
+ claude.sendMessage(id, body.message!);
1319
1427
  return Response.json({ ok: true });
1320
1428
  } catch (error) {
1321
1429
  return Response.json(
@@ -1326,19 +1434,19 @@ export async function handleStudioApi(
1326
1434
  }
1327
1435
 
1328
1436
  if (path.startsWith("/__studio/ai/session/") && path.endsWith("/stop") && req.method === "POST") {
1329
- const [id] = path.split("/").slice(4);
1437
+ const id = path.split("/").slice(4)[0]!;
1330
1438
  claude.stopSession(id);
1331
1439
  return Response.json({ ok: true });
1332
1440
  }
1333
1441
 
1334
1442
  if (path.startsWith("/__studio/ai/session/") && req.method === "DELETE") {
1335
- const [id] = path.split("/").slice(4);
1443
+ const id = path.split("/").slice(4)[0]!;
1336
1444
  claude.deleteSession(id);
1337
1445
  return Response.json({ ok: true });
1338
1446
  }
1339
1447
 
1340
1448
  if (path.startsWith("/__studio/ai/session/") && req.method === "GET") {
1341
- const [id] = path.split("/").slice(4);
1449
+ const id = path.split("/").slice(4)[0]!;
1342
1450
  const info = claude.getSession(id);
1343
1451
  if (!info) {
1344
1452
  return Response.json({ error: "Not found" }, { status: 404 });
@@ -1369,7 +1477,7 @@ function extractStudioSchema(classDef: ClassJsonDef, classJsonPath: string) {
1369
1477
  try {
1370
1478
  const parentPath = resolve(dirname(classJsonPath), classDef.extends.$ref);
1371
1479
  const parentContent = readFileSync(parentPath, "utf8");
1372
- const parentDef = JSON.parse(parentContent);
1480
+ const parentDef = JSON.parse(parentContent) as ClassJsonDef;
1373
1481
  parentSchema = extractStudioSchema(parentDef, parentPath);
1374
1482
  } catch {
1375
1483
  // Parent not found — proceed without inheritance