@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/package.json +9 -8
- package/src/claude-session.ts +2 -2
- package/src/code-api.ts +11 -6
- package/src/refactor/apply.ts +215 -0
- package/src/refactor/fs-events.ts +71 -0
- package/src/refactor/index.ts +11 -0
- package/src/refactor/paths.ts +187 -0
- package/src/refactor/refs.ts +236 -0
- package/src/refactor/watcher.ts +70 -0
- package/src/resolve.ts +41 -12
- package/src/server.ts +26 -14
- package/src/studio-api.ts +199 -91
- package/src/watch.ts +33 -2
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 =
|
|
115
|
-
behind =
|
|
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
|
|
120
|
-
const
|
|
121
|
-
const
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
432
|
+
...(adapter !== undefined ? { adapter } : {}),
|
|
433
|
+
...(description !== undefined ? { description } : {}),
|
|
434
|
+
...(siteUrl !== undefined ? { url: siteUrl } : {}),
|
|
346
435
|
});
|
|
347
436
|
|
|
348
|
-
const config = JSON.parse(
|
|
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
|
|
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
|
|
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
|
|
640
|
+
props: (decl.attributes || []).map((a) => ({
|
|
549
641
|
default: a.default,
|
|
550
642
|
description: a.description || null,
|
|
551
643
|
name: a.name,
|
|
552
|
-
type:
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
1290
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|