@jxsuite/server 0.28.2 → 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/package.json +7 -7
- package/src/build.ts +18 -7
- package/src/claude-session.ts +50 -41
- package/src/code-api.ts +34 -24
- package/src/resolve.ts +54 -34
- package/src/server.ts +59 -31
- package/src/studio-api.ts +353 -221
- package/src/watch.ts +26 -16
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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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)
|
|
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("/"))
|
|
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("/"))
|
|
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
|
-
|
|
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)
|
|
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
|
|
111
|
-
const stagedCode = xy
|
|
112
|
-
const unstagedCode = xy
|
|
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"
|
|
145
|
+
files.push({ path: line.slice(2), staged: false, status: "U" });
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
return {
|
|
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({
|
|
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 (
|
|
181
|
-
return Response.json({ error: (
|
|
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())
|
|
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 (
|
|
219
|
-
return Response.json({ error: (
|
|
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)
|
|
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)
|
|
262
|
+
if (parent === dir) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
250
265
|
dir = parent;
|
|
251
266
|
}
|
|
252
267
|
return Response.json({ sitePath: null });
|
|
253
|
-
} catch (
|
|
254
|
-
return Response.json({ error: (
|
|
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
|
-
//
|
|
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)
|
|
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)
|
|
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"))
|
|
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 (
|
|
276
|
-
return Response.json({ error: (
|
|
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({
|
|
319
|
+
sites.push({ config: raw, path: projectDir });
|
|
298
320
|
}
|
|
299
321
|
} catch {}
|
|
300
322
|
}
|
|
301
323
|
return Response.json(sites);
|
|
302
|
-
} catch (
|
|
303
|
-
return Response.json({ error: (
|
|
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
|
-
|
|
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
|
|
329
|
-
} catch (
|
|
330
|
-
return Response.json({ error: (
|
|
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 (
|
|
342
|
-
return Response.json({ error: (
|
|
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(".."))
|
|
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("."))
|
|
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
|
-
|
|
412
|
+
type: entry.isDirectory() ? "directory" : "file",
|
|
387
413
|
});
|
|
388
414
|
}
|
|
389
415
|
return Response.json(files);
|
|
390
|
-
} catch (
|
|
391
|
-
return Response.json({ error: (
|
|
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 (
|
|
402
|
-
return Response.json({ error: (
|
|
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)
|
|
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)
|
|
463
|
+
if (d == null) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
436
466
|
// Shorthand: "key": "value" or "key": 0 etc.
|
|
437
|
-
if (typeof d !== "object")
|
|
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
|
|
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
|
-
|
|
487
|
+
source: "jx",
|
|
488
|
+
tagName: content.tagName,
|
|
456
489
|
});
|
|
457
490
|
}
|
|
458
|
-
} catch {} //
|
|
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)
|
|
520
|
+
if (!actualPath) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
488
523
|
const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
|
|
489
|
-
if (!depPkg.customElements)
|
|
524
|
+
if (!depPkg.customElements) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
490
527
|
const cemPath = resolve(dirname(actualPath), depPkg.customElements);
|
|
491
|
-
if (!existsSync(cemPath))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
hasElements: false,
|
|
555
|
+
source: "npm",
|
|
556
|
+
tagName: decl.tagName,
|
|
518
557
|
});
|
|
519
558
|
}
|
|
520
559
|
}
|
|
521
560
|
}
|
|
522
|
-
} catch {} //
|
|
561
|
+
} catch {} // Skip packages without valid CEM
|
|
523
562
|
}
|
|
524
563
|
}
|
|
525
|
-
} catch {} //
|
|
564
|
+
} catch {} // Skip if no project package.json
|
|
526
565
|
|
|
527
566
|
return Response.json(components);
|
|
528
|
-
} catch (
|
|
529
|
-
return Response.json({ error: (
|
|
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))
|
|
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)
|
|
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 (
|
|
574
|
-
return Response.json({ error: (
|
|
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)
|
|
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)
|
|
637
|
+
if (!actualPath) {
|
|
638
|
+
return Response.json({ cem: null });
|
|
639
|
+
}
|
|
593
640
|
const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
|
|
594
|
-
if (!depPkg.customElements)
|
|
641
|
+
if (!depPkg.customElements) {
|
|
642
|
+
return Response.json({ cem: null });
|
|
643
|
+
}
|
|
595
644
|
const cemPath = resolve(dirname(actualPath), depPkg.customElements);
|
|
596
|
-
if (!existsSync(cemPath))
|
|
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 (
|
|
600
|
-
return Response.json({ error: (
|
|
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
|
|
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)
|
|
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 (
|
|
630
|
-
return Response.json({ error: (
|
|
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
|
|
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 (
|
|
658
|
-
return Response.json({ error: (
|
|
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)
|
|
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 (
|
|
670
|
-
return new Response((
|
|
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 (
|
|
681
|
-
return (
|
|
737
|
+
} catch (error) {
|
|
738
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
682
739
|
? new Response("Not found", { status: 404 })
|
|
683
|
-
: Response.json({ error: (
|
|
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)
|
|
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 (
|
|
695
|
-
return new Response((
|
|
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 (
|
|
702
|
-
return Response.json({ error: (
|
|
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)
|
|
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 (
|
|
714
|
-
return new Response((
|
|
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 (
|
|
722
|
-
return Response.json({ error: (
|
|
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)
|
|
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 (
|
|
734
|
-
return new Response((
|
|
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 (
|
|
740
|
-
return (
|
|
802
|
+
} catch (error) {
|
|
803
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
741
804
|
? new Response("Not found", { status: 404 })
|
|
742
|
-
: Response.json({ error: (
|
|
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)
|
|
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 (
|
|
762
|
-
return new Response((
|
|
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 (
|
|
773
|
-
return Response.json({ error: (
|
|
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)
|
|
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/"))
|
|
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)
|
|
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 (
|
|
802
|
-
return Response.json({ error: (
|
|
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 (
|
|
843
|
-
return Response.json({ error: (
|
|
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
|
-
//
|
|
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
|
-
|
|
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 (
|
|
868
|
-
return Response.json({ error: (
|
|
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)
|
|
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,
|
|
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
|
-
//
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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)
|
|
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)
|
|
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)
|
|
1107
|
+
if (!line) {
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1031
1110
|
const [name, head] = line.split("\t");
|
|
1032
1111
|
branches.push(name);
|
|
1033
|
-
if (head === "*")
|
|
1112
|
+
if (head === "*") {
|
|
1113
|
+
current = name;
|
|
1114
|
+
}
|
|
1034
1115
|
}
|
|
1035
|
-
return Response.json({
|
|
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 {
|
|
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(".."))
|
|
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({
|
|
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
|
|
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)
|
|
1129
|
-
|
|
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)
|
|
1138
|
-
|
|
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(".."))
|
|
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 (!
|
|
1255
|
+
const { url: repoUrl } = await req.json();
|
|
1256
|
+
if (!repoUrl || typeof repoUrl !== "string") {
|
|
1157
1257
|
return Response.json({ error: "Missing url" }, { status: 400 });
|
|
1158
|
-
|
|
1258
|
+
}
|
|
1259
|
+
const repoName = basename(repoUrl.replace(/\.git$/, ""));
|
|
1159
1260
|
const dest = resolve(cwd, repoName);
|
|
1160
|
-
const proc = Bun.spawn(["git", "clone",
|
|
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)
|
|
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 (
|
|
1171
|
-
return Response.json({ error: (
|
|
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 (
|
|
1191
|
-
return Response.json(
|
|
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("/")
|
|
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("/")
|
|
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 (
|
|
1215
|
-
return Response.json(
|
|
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("/")
|
|
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("/")
|
|
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("/")
|
|
1341
|
+
const [id] = path.split("/").slice(4);
|
|
1233
1342
|
const info = claude.getSession(id);
|
|
1234
|
-
if (!info)
|
|
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")
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
if (param.
|
|
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")
|
|
1295
|
-
|
|
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")
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
if (field.
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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[] }> = {};
|