@jxsuite/server 0.19.0 → 0.21.0
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 +14 -4
- package/src/claude-session.js +321 -0
- package/src/code-api.js +23 -8
- package/src/resolve.js +40 -23
- package/src/server.js +1 -1
- package/src/studio-api.js +361 -71
- package/src/watch.js +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Jx development server with live reload, server-side proxy, and studio integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,15 +13,25 @@
|
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
|
-
".": "./src/server.js"
|
|
16
|
+
".": "./src/server.js",
|
|
17
|
+
"./claude-session": "./src/claude-session.js"
|
|
17
18
|
},
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"provenance": true
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
|
-
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
23
|
+
"upgrade": "bunx npm-check-updates -u && bun install",
|
|
24
|
+
"test": "bun test --isolate",
|
|
25
|
+
"test:coverage": "bun test --isolate --coverage"
|
|
23
26
|
},
|
|
24
27
|
"dependencies": {
|
|
25
|
-
"
|
|
28
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.153",
|
|
29
|
+
"@anthropic-ai/sdk": "^0.99.0",
|
|
30
|
+
"@jxsuite/compiler": "^0.21.0",
|
|
31
|
+
"@jxsuite/create": "^0.19.0",
|
|
32
|
+
"@jxsuite/parser": "^0.21.0",
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
34
|
+
"chokidar": "^5.0.0",
|
|
35
|
+
"zod": "^4.4.3"
|
|
26
36
|
}
|
|
27
37
|
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-session.js — Claude Agent SDK session manager
|
|
3
|
+
*
|
|
4
|
+
* Manages Claude Code sessions via @anthropic-ai/claude-agent-sdk, providing: - Session creation
|
|
5
|
+
* with streaming output - Follow-up messages via session resume - SSE broadcasting to connected
|
|
6
|
+
* frontend clients - Session interruption and cleanup
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
10
|
+
|
|
11
|
+
/** @typedef {import("@anthropic-ai/claude-agent-sdk").SDKMessage} SDKMessage */
|
|
12
|
+
/** @typedef {import("@anthropic-ai/claude-agent-sdk").Options} AgentOptions */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build a clean env for SDK child processes. Strips session-specific vars from a parent Claude Code
|
|
16
|
+
* instance that would override the SDK's own credential resolution (OAuth from ~/.claude/).
|
|
17
|
+
*/
|
|
18
|
+
function buildSdkEnv() {
|
|
19
|
+
const env = { ...process.env };
|
|
20
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
21
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
22
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
23
|
+
delete env.CLAUDE_CODE_EXECPATH;
|
|
24
|
+
delete env.CLAUDE_AGENT_SDK_VERSION;
|
|
25
|
+
delete env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
|
|
26
|
+
delete env.CLAUDECODE;
|
|
27
|
+
env.CLAUDE_AGENT_SDK_CLIENT_APP = "jx-studio/0.17.0";
|
|
28
|
+
return env;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {{
|
|
33
|
+
* id: string;
|
|
34
|
+
* sessionId: string | null;
|
|
35
|
+
* projectRoot: string;
|
|
36
|
+
* messages: SDKMessage[];
|
|
37
|
+
* sseClients: Set<(event: string, data: unknown) => void>;
|
|
38
|
+
* status: "active" | "idle" | "stopped";
|
|
39
|
+
* abortController: AbortController | null;
|
|
40
|
+
* query: AsyncGenerator | null;
|
|
41
|
+
* }} Session
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** @type {Map<string, Session>} */
|
|
45
|
+
const sessions = new Map();
|
|
46
|
+
|
|
47
|
+
let idCounter = 0;
|
|
48
|
+
|
|
49
|
+
function genId() {
|
|
50
|
+
return `ai_${Date.now().toString(36)}_${(++idCounter).toString(36)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Broadcast an SSE event to all connected clients for a session.
|
|
55
|
+
*
|
|
56
|
+
* @param {Session} session
|
|
57
|
+
* @param {string} event
|
|
58
|
+
* @param {unknown} data
|
|
59
|
+
*/
|
|
60
|
+
function broadcast(session, event, data) {
|
|
61
|
+
session.messages.push(/** @type {any} */ (data));
|
|
62
|
+
for (const send of session.sseClients) {
|
|
63
|
+
send(event, data);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Process the async message stream from a query.
|
|
69
|
+
*
|
|
70
|
+
* @param {Session} session
|
|
71
|
+
* @param {AsyncGenerator<SDKMessage>} stream
|
|
72
|
+
*/
|
|
73
|
+
async function processStream(session, stream) {
|
|
74
|
+
session.status = "active";
|
|
75
|
+
try {
|
|
76
|
+
for await (const message of stream) {
|
|
77
|
+
if (/** @type {string} */ (session.status) === "stopped") break;
|
|
78
|
+
if (message.session_id && !session.sessionId) {
|
|
79
|
+
session.sessionId = message.session_id;
|
|
80
|
+
}
|
|
81
|
+
broadcast(session, message.type, message);
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (/** @type {any} */ (err).name !== "AbortError") {
|
|
85
|
+
broadcast(session, "error", { type: "error", error: String(err) });
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
session.status = "idle";
|
|
89
|
+
session.query = null;
|
|
90
|
+
session.abortController = null;
|
|
91
|
+
broadcast(session, "done", { type: "done" });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a new AI session and begin streaming.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} projectRoot
|
|
99
|
+
* @param {string} message
|
|
100
|
+
* @param {{ systemPrompt?: string }} [opts]
|
|
101
|
+
* @returns {{ id: string }}
|
|
102
|
+
*/
|
|
103
|
+
export function createSession(projectRoot, message, opts = {}) {
|
|
104
|
+
const id = genId();
|
|
105
|
+
const abortController = new AbortController();
|
|
106
|
+
|
|
107
|
+
/** @type {Session} */
|
|
108
|
+
const session = {
|
|
109
|
+
id,
|
|
110
|
+
sessionId: null,
|
|
111
|
+
projectRoot,
|
|
112
|
+
messages: [],
|
|
113
|
+
sseClients: new Set(),
|
|
114
|
+
status: "active",
|
|
115
|
+
abortController,
|
|
116
|
+
query: null,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
sessions.set(id, session);
|
|
120
|
+
|
|
121
|
+
/** @type {AgentOptions} */
|
|
122
|
+
const queryOpts = {
|
|
123
|
+
cwd: projectRoot,
|
|
124
|
+
abortController,
|
|
125
|
+
permissionMode: "acceptEdits",
|
|
126
|
+
allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
127
|
+
maxTurns: 30,
|
|
128
|
+
includePartialMessages: true,
|
|
129
|
+
persistSession: true,
|
|
130
|
+
env: buildSdkEnv(),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (opts.systemPrompt) {
|
|
134
|
+
queryOpts.systemPrompt = opts.systemPrompt;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const stream = query({ prompt: message, options: queryOpts });
|
|
138
|
+
session.query = stream;
|
|
139
|
+
processStream(session, stream);
|
|
140
|
+
|
|
141
|
+
return { id };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Send a follow-up message to an existing session.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} id
|
|
148
|
+
* @param {string} message
|
|
149
|
+
*/
|
|
150
|
+
export function sendMessage(id, message) {
|
|
151
|
+
const session = sessions.get(id);
|
|
152
|
+
if (!session) throw new Error(`Session not found: ${id}`);
|
|
153
|
+
if (session.status === "active") throw new Error("Session is still processing");
|
|
154
|
+
|
|
155
|
+
const abortController = new AbortController();
|
|
156
|
+
session.abortController = abortController;
|
|
157
|
+
|
|
158
|
+
/** @type {AgentOptions} */
|
|
159
|
+
const queryOpts = {
|
|
160
|
+
cwd: session.projectRoot,
|
|
161
|
+
abortController,
|
|
162
|
+
permissionMode: "acceptEdits",
|
|
163
|
+
allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
164
|
+
maxTurns: 30,
|
|
165
|
+
includePartialMessages: true,
|
|
166
|
+
continue: true,
|
|
167
|
+
env: buildSdkEnv(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (session.sessionId) {
|
|
171
|
+
queryOpts.resume = session.sessionId;
|
|
172
|
+
delete queryOpts.continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const stream = query({ prompt: message, options: queryOpts });
|
|
176
|
+
session.query = stream;
|
|
177
|
+
processStream(session, stream);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Stop a running session (interrupt the current query).
|
|
182
|
+
*
|
|
183
|
+
* @param {string} id
|
|
184
|
+
*/
|
|
185
|
+
export function stopSession(id) {
|
|
186
|
+
const session = sessions.get(id);
|
|
187
|
+
if (!session) return;
|
|
188
|
+
session.status = "stopped";
|
|
189
|
+
if (session.abortController) {
|
|
190
|
+
session.abortController.abort();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Remove a session entirely.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} id
|
|
198
|
+
*/
|
|
199
|
+
export function deleteSession(id) {
|
|
200
|
+
stopSession(id);
|
|
201
|
+
sessions.delete(id);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get an SSE Response stream for a session.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} id
|
|
208
|
+
* @returns {Response}
|
|
209
|
+
*/
|
|
210
|
+
export function streamSession(id) {
|
|
211
|
+
const session = sessions.get(id);
|
|
212
|
+
if (!session) {
|
|
213
|
+
return new Response("Session not found", { status: 404 });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const encoder = new TextEncoder();
|
|
217
|
+
/** @type {(event: string, data: unknown) => void} */
|
|
218
|
+
let sendRef = () => {};
|
|
219
|
+
const stream = new ReadableStream({
|
|
220
|
+
start(controller) {
|
|
221
|
+
/** @param {string} event @param {unknown} data */
|
|
222
|
+
sendRef = (event, data) => {
|
|
223
|
+
try {
|
|
224
|
+
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
225
|
+
} catch {
|
|
226
|
+
// Stream closed
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
session.sseClients.add(sendRef);
|
|
231
|
+
|
|
232
|
+
for (const msg of session.messages) {
|
|
233
|
+
sendRef(/** @type {any} */ (msg).type || "message", msg);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const heartbeat = setInterval(() => {
|
|
237
|
+
try {
|
|
238
|
+
controller.enqueue(encoder.encode(": heartbeat\n\n"));
|
|
239
|
+
} catch {
|
|
240
|
+
clearInterval(heartbeat);
|
|
241
|
+
session.sseClients.delete(sendRef);
|
|
242
|
+
}
|
|
243
|
+
}, 15000);
|
|
244
|
+
},
|
|
245
|
+
cancel() {
|
|
246
|
+
session.sseClients.delete(sendRef);
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return new Response(stream, {
|
|
251
|
+
headers: {
|
|
252
|
+
"Content-Type": "text/event-stream",
|
|
253
|
+
"Cache-Control": "no-cache",
|
|
254
|
+
Connection: "keep-alive",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if the claude agent SDK can authenticate.
|
|
261
|
+
*
|
|
262
|
+
* @returns {Promise<{ authenticated: boolean; error?: string }>}
|
|
263
|
+
*/
|
|
264
|
+
export async function getAuthStatus() {
|
|
265
|
+
try {
|
|
266
|
+
const ctrl = new AbortController();
|
|
267
|
+
const timeout = setTimeout(() => ctrl.abort(), 15000);
|
|
268
|
+
const testStream = query({
|
|
269
|
+
prompt: "Say OK",
|
|
270
|
+
options: {
|
|
271
|
+
abortController: ctrl,
|
|
272
|
+
maxTurns: 1,
|
|
273
|
+
persistSession: false,
|
|
274
|
+
includePartialMessages: true,
|
|
275
|
+
env: buildSdkEnv(),
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
for await (const msg of testStream) {
|
|
280
|
+
if (msg.type === "assistant") {
|
|
281
|
+
const content = /** @type {any} */ (msg).message?.content;
|
|
282
|
+
if (Array.isArray(content)) {
|
|
283
|
+
const text = content.find((b) => b.type === "text");
|
|
284
|
+
if (text?.text?.toLowerCase().includes("error")) {
|
|
285
|
+
clearTimeout(timeout);
|
|
286
|
+
ctrl.abort();
|
|
287
|
+
return { authenticated: false, error: text.text };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
clearTimeout(timeout);
|
|
291
|
+
ctrl.abort();
|
|
292
|
+
return { authenticated: true };
|
|
293
|
+
}
|
|
294
|
+
if (msg.type === "result") {
|
|
295
|
+
const result = /** @type {any} */ (msg);
|
|
296
|
+
clearTimeout(timeout);
|
|
297
|
+
ctrl.abort();
|
|
298
|
+
if (result.is_error) {
|
|
299
|
+
return { authenticated: false, error: result.result || "API error" };
|
|
300
|
+
}
|
|
301
|
+
return { authenticated: true };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
clearTimeout(timeout);
|
|
306
|
+
return { authenticated: true };
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return { authenticated: false, error: String(err) };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get session info.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} id
|
|
316
|
+
*/
|
|
317
|
+
export function getSession(id) {
|
|
318
|
+
const session = sessions.get(id);
|
|
319
|
+
if (!session) return null;
|
|
320
|
+
return { id: session.id, status: session.status, messageCount: session.messages.length };
|
|
321
|
+
}
|
package/src/code-api.js
CHANGED
|
@@ -10,6 +10,15 @@ import { join, resolve } from "node:path";
|
|
|
10
10
|
import { unlink } from "node:fs/promises";
|
|
11
11
|
import { format } from "oxfmt";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{
|
|
15
|
+
* span: { offset: number; line: number; [key: string]: unknown };
|
|
16
|
+
* [key: string]: unknown;
|
|
17
|
+
* }} OxcLabel
|
|
18
|
+
*
|
|
19
|
+
* @typedef {{ labels?: OxcLabel[]; [key: string]: unknown }} OxcDiagnostic
|
|
20
|
+
*/
|
|
21
|
+
|
|
13
22
|
const OXLINT_BIN = resolve(
|
|
14
23
|
import.meta.dir,
|
|
15
24
|
"../../node_modules/.bin",
|
|
@@ -40,7 +49,7 @@ function unwrapFormatted(formatted) {
|
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/**
|
|
43
|
-
* @param {
|
|
52
|
+
* @param {OxcDiagnostic[]} diagnostics
|
|
44
53
|
* @param {number} headerLen
|
|
45
54
|
*/
|
|
46
55
|
function adjustDiagnostics(diagnostics, headerLen) {
|
|
@@ -51,7 +60,7 @@ function adjustDiagnostics(diagnostics, headerLen) {
|
|
|
51
60
|
})
|
|
52
61
|
.map((d) => ({
|
|
53
62
|
...d,
|
|
54
|
-
labels: d.labels
|
|
63
|
+
labels: d.labels?.map((label) => ({
|
|
55
64
|
...label,
|
|
56
65
|
span: {
|
|
57
66
|
...label.span,
|
|
@@ -98,8 +107,11 @@ export async function handleCodeApi(req, url) {
|
|
|
98
107
|
code: unwrapFormatted(result.code),
|
|
99
108
|
errors: result.errors,
|
|
100
109
|
});
|
|
101
|
-
} catch (/** @type {
|
|
102
|
-
return Response.json({
|
|
110
|
+
} catch (/** @type {unknown} */ e) {
|
|
111
|
+
return Response.json({
|
|
112
|
+
code,
|
|
113
|
+
errors: [{ message: /** @type {{ message?: string }} */ (e).message }],
|
|
114
|
+
});
|
|
103
115
|
}
|
|
104
116
|
}
|
|
105
117
|
|
|
@@ -112,8 +124,8 @@ export async function handleCodeApi(req, url) {
|
|
|
112
124
|
try {
|
|
113
125
|
const minified = minifier.transformSync(code).trim();
|
|
114
126
|
return Response.json({ code: minified });
|
|
115
|
-
} catch (/** @type {
|
|
116
|
-
return Response.json({ code, error: e.message });
|
|
127
|
+
} catch (/** @type {unknown} */ e) {
|
|
128
|
+
return Response.json({ code, error: /** @type {{ message?: string }} */ (e).message });
|
|
117
129
|
}
|
|
118
130
|
}
|
|
119
131
|
|
|
@@ -142,8 +154,11 @@ export async function handleCodeApi(req, url) {
|
|
|
142
154
|
const parsed = JSON.parse(output);
|
|
143
155
|
const adjusted = adjustDiagnostics(parsed.diagnostics || [], headerLen);
|
|
144
156
|
return Response.json({ diagnostics: adjusted });
|
|
145
|
-
} catch (/** @type {
|
|
146
|
-
return Response.json({
|
|
157
|
+
} catch (/** @type {unknown} */ e) {
|
|
158
|
+
return Response.json({
|
|
159
|
+
diagnostics: [],
|
|
160
|
+
error: /** @type {{ message?: string }} */ (e).message,
|
|
161
|
+
});
|
|
147
162
|
} finally {
|
|
148
163
|
try {
|
|
149
164
|
await unlink(tmpFile);
|
package/src/resolve.js
CHANGED
|
@@ -29,8 +29,11 @@ export async function handleResolve(req, root) {
|
|
|
29
29
|
} else {
|
|
30
30
|
moduleAbsPath = resolve(root, $src);
|
|
31
31
|
}
|
|
32
|
-
} catch (/** @type {
|
|
33
|
-
return new Response(
|
|
32
|
+
} catch (/** @type {unknown} */ e) {
|
|
33
|
+
return new Response(
|
|
34
|
+
`Cannot resolve $src "${$src}": ${/** @type {{ message?: string }} */ (e).message}`,
|
|
35
|
+
{ status: 400 },
|
|
36
|
+
);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// Rebase relative config paths from doc-relative to CWD-relative
|
|
@@ -74,7 +77,9 @@ export async function handleResolve(req, root) {
|
|
|
74
77
|
|
|
75
78
|
// Self-contained: construct class from schema
|
|
76
79
|
const DynClass = classFromSchema(classDef);
|
|
77
|
-
const instance = /** @type {
|
|
80
|
+
const instance = /** @type {{ resolve?: () => unknown; value?: unknown }} */ (
|
|
81
|
+
new DynClass(config)
|
|
82
|
+
);
|
|
78
83
|
const value =
|
|
79
84
|
typeof instance.resolve === "function"
|
|
80
85
|
? await instance.resolve()
|
|
@@ -82,8 +87,11 @@ export async function handleResolve(req, root) {
|
|
|
82
87
|
? instance.value
|
|
83
88
|
: instance;
|
|
84
89
|
return Response.json(value);
|
|
85
|
-
} catch (/** @type {
|
|
86
|
-
return Response.json(
|
|
90
|
+
} catch (/** @type {unknown} */ e) {
|
|
91
|
+
return Response.json(
|
|
92
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
93
|
+
{ status: 500 },
|
|
94
|
+
);
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
97
|
|
|
@@ -122,15 +130,20 @@ export async function handleServerFunction(req, root) {
|
|
|
122
130
|
} else {
|
|
123
131
|
moduleAbsPath = resolve(root, $src);
|
|
124
132
|
}
|
|
125
|
-
} catch (/** @type {
|
|
126
|
-
return new Response(`Cannot resolve $src: ${e.message}`, {
|
|
133
|
+
} catch (/** @type {unknown} */ e) {
|
|
134
|
+
return new Response(`Cannot resolve $src: ${/** @type {{ message?: string }} */ (e).message}`, {
|
|
135
|
+
status: 400,
|
|
136
|
+
});
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
let mod;
|
|
130
140
|
try {
|
|
131
141
|
mod = await import(moduleAbsPath);
|
|
132
|
-
} catch (/** @type {
|
|
133
|
-
return new Response(
|
|
142
|
+
} catch (/** @type {unknown} */ e) {
|
|
143
|
+
return new Response(
|
|
144
|
+
`Failed to import "${$src}": ${/** @type {{ message?: string }} */ (e).message}`,
|
|
145
|
+
{ status: 500 },
|
|
146
|
+
);
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
const fn = mod[xport] ?? mod.default?.[xport];
|
|
@@ -141,8 +154,11 @@ export async function handleServerFunction(req, root) {
|
|
|
141
154
|
try {
|
|
142
155
|
const result = await fn(args);
|
|
143
156
|
return Response.json(result ?? null);
|
|
144
|
-
} catch (/** @type {
|
|
145
|
-
return Response.json(
|
|
157
|
+
} catch (/** @type {unknown} */ e) {
|
|
158
|
+
return Response.json(
|
|
159
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
160
|
+
{ status: 500 },
|
|
161
|
+
);
|
|
146
162
|
}
|
|
147
163
|
}
|
|
148
164
|
|
|
@@ -150,7 +166,7 @@ export async function handleServerFunction(req, root) {
|
|
|
150
166
|
* Dynamically construct a class from a .class.json schema definition. Server-side variant — no
|
|
151
167
|
* private field limitations.
|
|
152
168
|
*
|
|
153
|
-
* @param {
|
|
169
|
+
* @param {ClassJsonDef} classDef
|
|
154
170
|
*/
|
|
155
171
|
function classFromSchema(classDef) {
|
|
156
172
|
const fields = classDef.$defs?.fields ?? {};
|
|
@@ -159,8 +175,8 @@ function classFromSchema(classDef) {
|
|
|
159
175
|
|
|
160
176
|
class DynClass {
|
|
161
177
|
constructor(config = {}) {
|
|
162
|
-
const self = /** @type {
|
|
163
|
-
const cfg = /** @type {Record<string,
|
|
178
|
+
const self = /** @type {Record<string, unknown>} */ (this);
|
|
179
|
+
const cfg = /** @type {Record<string, unknown>} */ (config);
|
|
164
180
|
for (const [key, field] of Object.entries(fields)) {
|
|
165
181
|
const id = field.identifier ?? key;
|
|
166
182
|
if (cfg[id] !== undefined) self[id] = cfg[id];
|
|
@@ -177,27 +193,28 @@ function classFromSchema(classDef) {
|
|
|
177
193
|
|
|
178
194
|
for (const [key, method] of Object.entries(methods)) {
|
|
179
195
|
const name = method.identifier ?? key;
|
|
180
|
-
const params = (method.parameters ?? []).map((
|
|
181
|
-
if (p.$ref) return p.$ref.split("/").pop();
|
|
196
|
+
const params = (method.parameters ?? []).map((p) => {
|
|
197
|
+
if (p.$ref) return p.$ref.split("/").pop() ?? "arg";
|
|
182
198
|
return p.identifier ?? p.name ?? "arg";
|
|
183
199
|
});
|
|
184
200
|
const bodyStr = Array.isArray(method.body) ? method.body.join("\n") : (method.body ?? "");
|
|
185
201
|
|
|
186
202
|
if (method.role === "accessor") {
|
|
187
|
-
/** @type {
|
|
203
|
+
/** @type {PropertyDescriptor} */
|
|
188
204
|
const descriptor = {};
|
|
189
|
-
if (method.getter)
|
|
205
|
+
if (method.getter)
|
|
206
|
+
descriptor.get = /** @type {() => unknown} */ (new Function(method.getter.body));
|
|
190
207
|
if (method.setter) {
|
|
191
|
-
const sp = (method.setter.parameters ?? []).map(
|
|
192
|
-
|
|
208
|
+
const sp = (method.setter.parameters ?? []).map((p) => p.$ref?.split("/").pop() ?? "v");
|
|
209
|
+
descriptor.set = /** @type {(v: unknown) => void} */ (
|
|
210
|
+
new Function(...sp, method.setter.body)
|
|
193
211
|
);
|
|
194
|
-
descriptor.set = new Function(...sp, method.setter.body);
|
|
195
212
|
}
|
|
196
213
|
Object.defineProperty(DynClass.prototype, name, { ...descriptor, configurable: true });
|
|
197
214
|
} else if (method.scope === "static") {
|
|
198
|
-
/** @type {
|
|
215
|
+
/** @type {DynamicClass} */ (DynClass)[name] = new Function(...params, bodyStr);
|
|
199
216
|
} else {
|
|
200
|
-
/** @type {
|
|
217
|
+
/** @type {DynamicClass} */ (DynClass).prototype[name] = new Function(...params, bodyStr);
|
|
201
218
|
}
|
|
202
219
|
}
|
|
203
220
|
|
package/src/server.js
CHANGED
|
@@ -270,7 +270,7 @@ export async function createDevServer(options) {
|
|
|
270
270
|
if (result.success && result.outputs.length > 0) {
|
|
271
271
|
bundleCache.set(cacheKey, await result.outputs[0].text());
|
|
272
272
|
}
|
|
273
|
-
} catch (/** @type {
|
|
273
|
+
} catch (/** @type {unknown} */ e) {
|
|
274
274
|
console.error("Bundle failed for", resolved, e);
|
|
275
275
|
}
|
|
276
276
|
}
|
package/src/studio-api.js
CHANGED
|
@@ -11,6 +11,8 @@ import { resolve, relative, basename, dirname, isAbsolute } from "node:path";
|
|
|
11
11
|
import { readdir, stat, readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
12
12
|
import { readFileSync, existsSync } from "node:fs";
|
|
13
13
|
import { transpileJxMarkdown } from "@jxsuite/parser/transpile";
|
|
14
|
+
import { loadContentTypes, queryContentType, findEntry } from "@jxsuite/compiler/content-loader";
|
|
15
|
+
import * as claude from "./claude-session.js";
|
|
14
16
|
|
|
15
17
|
/** Normalise a path to forward slashes (Windows `path` module returns backslashes). */
|
|
16
18
|
const fwd = (/** @type {string} */ p) => p.replaceAll("\\", "/");
|
|
@@ -40,6 +42,14 @@ const statusMap = { M: "M", T: "T", A: "A", D: "D", R: "R", C: "C", U: "U" };
|
|
|
40
42
|
* Parse raw `git status --porcelain=v2 --branch` output into structured data.
|
|
41
43
|
*
|
|
42
44
|
* @param {string} out
|
|
45
|
+
* @returns {{
|
|
46
|
+
* branch: string;
|
|
47
|
+
* ahead: number;
|
|
48
|
+
* behind: number;
|
|
49
|
+
* files: { path: string; status: string; staged: boolean }[];
|
|
50
|
+
* isRepo?: boolean;
|
|
51
|
+
* remotes?: string[];
|
|
52
|
+
* }}
|
|
43
53
|
*/
|
|
44
54
|
export function parseGitStatus(out) {
|
|
45
55
|
let branch = "";
|
|
@@ -97,6 +107,7 @@ export function parseGitStatus(out) {
|
|
|
97
107
|
* @param {URL} url
|
|
98
108
|
* @param {string} root
|
|
99
109
|
* @param {string | null} [activeProjectRoot]
|
|
110
|
+
* @returns {Promise<Response | null>}
|
|
100
111
|
*/
|
|
101
112
|
export async function handleStudioApi(req, url, root, activeProjectRoot = null) {
|
|
102
113
|
const path = url.pathname;
|
|
@@ -121,8 +132,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
121
132
|
const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
122
133
|
try {
|
|
123
134
|
assertAccessible(absDir, root, activeProjectRoot);
|
|
124
|
-
} catch (/** @type {
|
|
125
|
-
return Response.json(
|
|
135
|
+
} catch (/** @type {unknown} */ e) {
|
|
136
|
+
return Response.json(
|
|
137
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
138
|
+
{ status: 400 },
|
|
139
|
+
);
|
|
126
140
|
}
|
|
127
141
|
try {
|
|
128
142
|
const projectRoot = fwd(absDir);
|
|
@@ -154,8 +168,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
154
168
|
} catch {}
|
|
155
169
|
|
|
156
170
|
return Response.json({ isSiteProject, projectConfig, directories, projectRoot });
|
|
157
|
-
} catch (/** @type {
|
|
158
|
-
return Response.json(
|
|
171
|
+
} catch (/** @type {unknown} */ e) {
|
|
172
|
+
return Response.json(
|
|
173
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
174
|
+
{ status: 500 },
|
|
175
|
+
);
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
178
|
|
|
@@ -189,8 +206,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
189
206
|
dir = parent;
|
|
190
207
|
}
|
|
191
208
|
return Response.json({ sitePath: null });
|
|
192
|
-
} catch (/** @type {
|
|
193
|
-
return Response.json(
|
|
209
|
+
} catch (/** @type {unknown} */ e) {
|
|
210
|
+
return Response.json(
|
|
211
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
212
|
+
{ status: 500 },
|
|
213
|
+
);
|
|
194
214
|
}
|
|
195
215
|
}
|
|
196
216
|
|
|
@@ -211,8 +231,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
211
231
|
}
|
|
212
232
|
} catch {}
|
|
213
233
|
return Response.json({ path: null });
|
|
214
|
-
} catch (/** @type {
|
|
215
|
-
return Response.json(
|
|
234
|
+
} catch (/** @type {unknown} */ e) {
|
|
235
|
+
return Response.json(
|
|
236
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
237
|
+
{ status: 500 },
|
|
238
|
+
);
|
|
216
239
|
}
|
|
217
240
|
}
|
|
218
241
|
|
|
@@ -238,8 +261,36 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
238
261
|
} catch {}
|
|
239
262
|
}
|
|
240
263
|
return Response.json(sites);
|
|
241
|
-
} catch (/** @type {
|
|
242
|
-
return Response.json(
|
|
264
|
+
} catch (/** @type {unknown} */ e) {
|
|
265
|
+
return Response.json(
|
|
266
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
267
|
+
{ status: 500 },
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Create a new project
|
|
273
|
+
if (path === "/__studio/create-project" && req.method === "POST") {
|
|
274
|
+
try {
|
|
275
|
+
const body = await req.json();
|
|
276
|
+
const { name, description, url: siteUrl, adapter, directory } = body;
|
|
277
|
+
if (!name || !directory) {
|
|
278
|
+
return Response.json({ error: "name and directory are required" }, { status: 400 });
|
|
279
|
+
}
|
|
280
|
+
const destPath = resolve(root, directory);
|
|
281
|
+
assertAccessible(destPath, root, activeProjectRoot);
|
|
282
|
+
|
|
283
|
+
const { generateProject } = await import("@jxsuite/create/generate");
|
|
284
|
+
await generateProject(destPath, { name, description, url: siteUrl, adapter });
|
|
285
|
+
|
|
286
|
+
const config = JSON.parse(await readFile(resolve(destPath, "project.json"), "utf8"));
|
|
287
|
+
const projectRoot = fwd(relative(root, destPath));
|
|
288
|
+
return Response.json({ root: projectRoot, config });
|
|
289
|
+
} catch (/** @type {unknown} */ e) {
|
|
290
|
+
return Response.json(
|
|
291
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
292
|
+
{ status: 500 },
|
|
293
|
+
);
|
|
243
294
|
}
|
|
244
295
|
}
|
|
245
296
|
|
|
@@ -250,8 +301,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
250
301
|
const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
251
302
|
try {
|
|
252
303
|
assertAccessible(absDir, root, activeProjectRoot);
|
|
253
|
-
} catch (/** @type {
|
|
254
|
-
return Response.json(
|
|
304
|
+
} catch (/** @type {unknown} */ e) {
|
|
305
|
+
return Response.json(
|
|
306
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
307
|
+
{ status: 400 },
|
|
308
|
+
);
|
|
255
309
|
}
|
|
256
310
|
|
|
257
311
|
/** Report a path relative to the active project root (or server root as fallback). */
|
|
@@ -299,8 +353,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
299
353
|
});
|
|
300
354
|
}
|
|
301
355
|
return Response.json(files);
|
|
302
|
-
} catch (/** @type {
|
|
303
|
-
return Response.json(
|
|
356
|
+
} catch (/** @type {unknown} */ e) {
|
|
357
|
+
return Response.json(
|
|
358
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
359
|
+
{ status: 500 },
|
|
360
|
+
);
|
|
304
361
|
}
|
|
305
362
|
}
|
|
306
363
|
|
|
@@ -310,8 +367,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
310
367
|
const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
311
368
|
try {
|
|
312
369
|
assertAccessible(scanRoot, root, activeProjectRoot);
|
|
313
|
-
} catch (/** @type {
|
|
314
|
-
return Response.json(
|
|
370
|
+
} catch (/** @type {unknown} */ e) {
|
|
371
|
+
return Response.json(
|
|
372
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
373
|
+
{ status: 400 },
|
|
374
|
+
);
|
|
315
375
|
}
|
|
316
376
|
try {
|
|
317
377
|
const glob = new Bun.Glob("**/*.{json,md}");
|
|
@@ -325,7 +385,6 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
325
385
|
continue;
|
|
326
386
|
const fp = resolve(scanRoot, match);
|
|
327
387
|
try {
|
|
328
|
-
/** @type {any} */
|
|
329
388
|
let content;
|
|
330
389
|
if (match.endsWith(".md")) {
|
|
331
390
|
const source = await readFile(fp, "utf8");
|
|
@@ -406,14 +465,17 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
406
465
|
source: "npm",
|
|
407
466
|
package: name,
|
|
408
467
|
description: decl.description || null,
|
|
409
|
-
props: (decl.attributes || []).map(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
468
|
+
props: (decl.attributes || []).map(
|
|
469
|
+
(/** @type {Record<string, unknown>} */ a) => ({
|
|
470
|
+
name: a.name,
|
|
471
|
+
type: /** @type {Record<string, unknown> | undefined} */ (a.type)?.text,
|
|
472
|
+
default: a.default,
|
|
473
|
+
description: a.description || null,
|
|
474
|
+
}),
|
|
475
|
+
),
|
|
415
476
|
members: (decl.members || []).filter(
|
|
416
|
-
(/** @type {
|
|
477
|
+
(/** @type {Record<string, unknown>} */ m) =>
|
|
478
|
+
m.kind === "field" && m.privacy !== "private",
|
|
417
479
|
),
|
|
418
480
|
slots: decl.slots || [],
|
|
419
481
|
events: decl.events || [],
|
|
@@ -429,8 +491,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
429
491
|
} catch {} // skip if no project package.json
|
|
430
492
|
|
|
431
493
|
return Response.json(components);
|
|
432
|
-
} catch (/** @type {
|
|
433
|
-
return Response.json(
|
|
494
|
+
} catch (/** @type {unknown} */ e) {
|
|
495
|
+
return Response.json(
|
|
496
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
497
|
+
{ status: 500 },
|
|
498
|
+
);
|
|
434
499
|
}
|
|
435
500
|
}
|
|
436
501
|
|
|
@@ -445,7 +510,14 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
445
510
|
if (!existsSync(pkgPath)) return Response.json([]);
|
|
446
511
|
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
447
512
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
448
|
-
/**
|
|
513
|
+
/**
|
|
514
|
+
* @type {{
|
|
515
|
+
* name: string;
|
|
516
|
+
* version: string;
|
|
517
|
+
* hasCem: boolean;
|
|
518
|
+
* customElementsPath: string | null;
|
|
519
|
+
* }[]}
|
|
520
|
+
*/
|
|
449
521
|
const packages = [];
|
|
450
522
|
for (const [name, version] of Object.entries(deps)) {
|
|
451
523
|
const depPkgPath = resolve(scanRoot, "node_modules", ...name.split("/"), "package.json");
|
|
@@ -467,8 +539,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
467
539
|
} catch {}
|
|
468
540
|
}
|
|
469
541
|
return Response.json(packages);
|
|
470
|
-
} catch (/** @type {
|
|
471
|
-
return Response.json(
|
|
542
|
+
} catch (/** @type {unknown} */ e) {
|
|
543
|
+
return Response.json(
|
|
544
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
545
|
+
{ status: 500 },
|
|
546
|
+
);
|
|
472
547
|
}
|
|
473
548
|
}
|
|
474
549
|
|
|
@@ -493,8 +568,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
493
568
|
if (!existsSync(cemPath)) return Response.json({ cem: null });
|
|
494
569
|
const cem = JSON.parse(await readFile(cemPath, "utf8"));
|
|
495
570
|
return Response.json({ cem });
|
|
496
|
-
} catch (/** @type {
|
|
497
|
-
return Response.json(
|
|
571
|
+
} catch (/** @type {unknown} */ e) {
|
|
572
|
+
return Response.json(
|
|
573
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
574
|
+
{ status: 500 },
|
|
575
|
+
);
|
|
498
576
|
}
|
|
499
577
|
}
|
|
500
578
|
|
|
@@ -519,8 +597,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
519
597
|
);
|
|
520
598
|
}
|
|
521
599
|
return Response.json({ ok: true });
|
|
522
|
-
} catch (/** @type {
|
|
523
|
-
return Response.json(
|
|
600
|
+
} catch (/** @type {unknown} */ e) {
|
|
601
|
+
return Response.json(
|
|
602
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
603
|
+
{ status: 500 },
|
|
604
|
+
);
|
|
524
605
|
}
|
|
525
606
|
}
|
|
526
607
|
|
|
@@ -543,8 +624,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
543
624
|
);
|
|
544
625
|
}
|
|
545
626
|
return Response.json({ ok: true });
|
|
546
|
-
} catch (/** @type {
|
|
547
|
-
return Response.json(
|
|
627
|
+
} catch (/** @type {unknown} */ e) {
|
|
628
|
+
return Response.json(
|
|
629
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
630
|
+
{ status: 500 },
|
|
631
|
+
);
|
|
548
632
|
}
|
|
549
633
|
}
|
|
550
634
|
|
|
@@ -555,18 +639,21 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
555
639
|
const abs = fp.startsWith("~") ? fp.replace("~", process.env.HOME || "") : fp;
|
|
556
640
|
try {
|
|
557
641
|
assertAccessible(abs, root, activeProjectRoot);
|
|
558
|
-
} catch (/** @type {
|
|
559
|
-
return new Response(e.message, { status: 400 });
|
|
642
|
+
} catch (/** @type {unknown} */ e) {
|
|
643
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
560
644
|
}
|
|
561
645
|
try {
|
|
562
646
|
return Response.json({
|
|
563
647
|
content: await readFile(abs, "utf8"),
|
|
564
648
|
path: fp,
|
|
565
649
|
});
|
|
566
|
-
} catch (/** @type {
|
|
567
|
-
return e.code === "ENOENT"
|
|
650
|
+
} catch (/** @type {unknown} */ e) {
|
|
651
|
+
return /** @type {{ code?: string }} */ (e).code === "ENOENT"
|
|
568
652
|
? new Response("Not found", { status: 404 })
|
|
569
|
-
: Response.json(
|
|
653
|
+
: Response.json(
|
|
654
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
655
|
+
{ status: 500 },
|
|
656
|
+
);
|
|
570
657
|
}
|
|
571
658
|
}
|
|
572
659
|
|
|
@@ -577,15 +664,18 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
577
664
|
const abs = resolve(root, fp);
|
|
578
665
|
try {
|
|
579
666
|
assertAccessible(abs, root, activeProjectRoot);
|
|
580
|
-
} catch (/** @type {
|
|
581
|
-
return new Response(e.message, { status: 400 });
|
|
667
|
+
} catch (/** @type {unknown} */ e) {
|
|
668
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
582
669
|
}
|
|
583
670
|
try {
|
|
584
671
|
await mkdir(dirname(abs), { recursive: true });
|
|
585
672
|
await writeFile(abs, await req.text(), "utf8");
|
|
586
673
|
return Response.json({ ok: true, path: fwd(relative(root, abs)) });
|
|
587
|
-
} catch (/** @type {
|
|
588
|
-
return Response.json(
|
|
674
|
+
} catch (/** @type {unknown} */ e) {
|
|
675
|
+
return Response.json(
|
|
676
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
677
|
+
{ status: 500 },
|
|
678
|
+
);
|
|
589
679
|
}
|
|
590
680
|
}
|
|
591
681
|
|
|
@@ -596,16 +686,19 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
596
686
|
const abs = resolve(root, fp);
|
|
597
687
|
try {
|
|
598
688
|
assertAccessible(abs, root, activeProjectRoot);
|
|
599
|
-
} catch (/** @type {
|
|
600
|
-
return new Response(e.message, { status: 400 });
|
|
689
|
+
} catch (/** @type {unknown} */ e) {
|
|
690
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
601
691
|
}
|
|
602
692
|
try {
|
|
603
693
|
await mkdir(dirname(abs), { recursive: true });
|
|
604
694
|
const buffer = await req.arrayBuffer();
|
|
605
695
|
await Bun.write(abs, new Uint8Array(buffer));
|
|
606
696
|
return Response.json({ ok: true, path: fwd(relative(root, abs)) });
|
|
607
|
-
} catch (/** @type {
|
|
608
|
-
return Response.json(
|
|
697
|
+
} catch (/** @type {unknown} */ e) {
|
|
698
|
+
return Response.json(
|
|
699
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
700
|
+
{ status: 500 },
|
|
701
|
+
);
|
|
609
702
|
}
|
|
610
703
|
}
|
|
611
704
|
|
|
@@ -616,16 +709,19 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
616
709
|
const abs = resolve(root, fp);
|
|
617
710
|
try {
|
|
618
711
|
assertAccessible(abs, root, activeProjectRoot);
|
|
619
|
-
} catch (/** @type {
|
|
620
|
-
return new Response(e.message, { status: 400 });
|
|
712
|
+
} catch (/** @type {unknown} */ e) {
|
|
713
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
621
714
|
}
|
|
622
715
|
try {
|
|
623
716
|
await unlink(abs);
|
|
624
717
|
return Response.json({ ok: true, path: fwd(relative(root, abs)) });
|
|
625
|
-
} catch (/** @type {
|
|
626
|
-
return e.code === "ENOENT"
|
|
718
|
+
} catch (/** @type {unknown} */ e) {
|
|
719
|
+
return /** @type {{ code?: string }} */ (e).code === "ENOENT"
|
|
627
720
|
? new Response("Not found", { status: 404 })
|
|
628
|
-
: Response.json(
|
|
721
|
+
: Response.json(
|
|
722
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
723
|
+
{ status: 500 },
|
|
724
|
+
);
|
|
629
725
|
}
|
|
630
726
|
}
|
|
631
727
|
|
|
@@ -644,8 +740,8 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
644
740
|
try {
|
|
645
741
|
assertAccessible(absFrom, root, activeProjectRoot);
|
|
646
742
|
assertAccessible(absTo, root, activeProjectRoot);
|
|
647
|
-
} catch (/** @type {
|
|
648
|
-
return new Response(e.message, { status: 400 });
|
|
743
|
+
} catch (/** @type {unknown} */ e) {
|
|
744
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
649
745
|
}
|
|
650
746
|
try {
|
|
651
747
|
await mkdir(dirname(absTo), { recursive: true });
|
|
@@ -655,8 +751,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
655
751
|
from: fwd(relative(root, absFrom)),
|
|
656
752
|
to: fwd(relative(root, absTo)),
|
|
657
753
|
});
|
|
658
|
-
} catch (/** @type {
|
|
659
|
-
return Response.json(
|
|
754
|
+
} catch (/** @type {unknown} */ e) {
|
|
755
|
+
return Response.json(
|
|
756
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
757
|
+
{ status: 500 },
|
|
758
|
+
);
|
|
660
759
|
}
|
|
661
760
|
}
|
|
662
761
|
|
|
@@ -684,8 +783,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
684
783
|
path: matches[0],
|
|
685
784
|
...(matches.length > 1 ? { alternatives: matches } : {}),
|
|
686
785
|
});
|
|
687
|
-
} catch (/** @type {
|
|
688
|
-
return Response.json(
|
|
786
|
+
} catch (/** @type {unknown} */ e) {
|
|
787
|
+
return Response.json(
|
|
788
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
789
|
+
{ status: 500 },
|
|
790
|
+
);
|
|
689
791
|
}
|
|
690
792
|
}
|
|
691
793
|
|
|
@@ -705,8 +807,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
705
807
|
} else {
|
|
706
808
|
moduleAbsPath = resolve(root, src);
|
|
707
809
|
}
|
|
708
|
-
} catch (/** @type {
|
|
709
|
-
return Response.json({
|
|
810
|
+
} catch (/** @type {unknown} */ e) {
|
|
811
|
+
return Response.json({
|
|
812
|
+
schema: null,
|
|
813
|
+
error: /** @type {{ message?: string }} */ (e).message,
|
|
814
|
+
});
|
|
710
815
|
}
|
|
711
816
|
|
|
712
817
|
// .class.json: read and extract schema directly
|
|
@@ -715,8 +820,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
715
820
|
const content = readFileSync(moduleAbsPath, "utf8");
|
|
716
821
|
const classDef = JSON.parse(content);
|
|
717
822
|
return Response.json({ schema: extractStudioSchema(classDef, moduleAbsPath) });
|
|
718
|
-
} catch (/** @type {
|
|
719
|
-
return Response.json({
|
|
823
|
+
} catch (/** @type {unknown} */ e) {
|
|
824
|
+
return Response.json({
|
|
825
|
+
schema: null,
|
|
826
|
+
error: /** @type {{ message?: string }} */ (e).message,
|
|
827
|
+
});
|
|
720
828
|
}
|
|
721
829
|
}
|
|
722
830
|
|
|
@@ -741,8 +849,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
741
849
|
return Response.json({ schema: null, error: `Export "${exportName}" not found` });
|
|
742
850
|
}
|
|
743
851
|
return Response.json({ schema: ExportedClass.schema ?? null });
|
|
744
|
-
} catch (/** @type {
|
|
745
|
-
return Response.json({
|
|
852
|
+
} catch (/** @type {unknown} */ e) {
|
|
853
|
+
return Response.json({
|
|
854
|
+
schema: null,
|
|
855
|
+
error: /** @type {{ message?: string }} */ (e).message,
|
|
856
|
+
});
|
|
746
857
|
}
|
|
747
858
|
}
|
|
748
859
|
|
|
@@ -763,8 +874,44 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
763
874
|
|
|
764
875
|
try {
|
|
765
876
|
if (gitCmd === "status" && req.method === "GET") {
|
|
766
|
-
|
|
767
|
-
|
|
877
|
+
// Check if we're in a git repo first
|
|
878
|
+
const checkProc = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
|
|
879
|
+
cwd,
|
|
880
|
+
stdout: "pipe",
|
|
881
|
+
stderr: "pipe",
|
|
882
|
+
});
|
|
883
|
+
const checkExit = await checkProc.exited;
|
|
884
|
+
if (checkExit !== 0) {
|
|
885
|
+
return Response.json({
|
|
886
|
+
isRepo: false,
|
|
887
|
+
branch: "",
|
|
888
|
+
files: [],
|
|
889
|
+
ahead: 0,
|
|
890
|
+
behind: 0,
|
|
891
|
+
remotes: [],
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const [out, remotesOut] = await Promise.all([
|
|
896
|
+
runGit(["status", "--porcelain=v2", "--branch"]),
|
|
897
|
+
runGit(["remote"]),
|
|
898
|
+
]);
|
|
899
|
+
const status = parseGitStatus(out);
|
|
900
|
+
status.isRepo = true;
|
|
901
|
+
status.remotes = remotesOut.trim().split("\n").filter(Boolean);
|
|
902
|
+
return Response.json(status);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (gitCmd === "init" && req.method === "POST") {
|
|
906
|
+
await runGit(["init"]);
|
|
907
|
+
return Response.json({ ok: true });
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (gitCmd === "add-remote" && req.method === "POST") {
|
|
911
|
+
const { name, url: remoteUrl } = await req.json();
|
|
912
|
+
if (!name || !remoteUrl) return new Response("name and url required", { status: 400 });
|
|
913
|
+
await runGit(["remote", "add", name, remoteUrl]);
|
|
914
|
+
return Response.json({ ok: true });
|
|
768
915
|
}
|
|
769
916
|
|
|
770
917
|
if (gitCmd === "branches" && req.method === "GET") {
|
|
@@ -828,7 +975,17 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
828
975
|
}
|
|
829
976
|
|
|
830
977
|
if (gitCmd === "push" && req.method === "POST") {
|
|
831
|
-
|
|
978
|
+
let body = {};
|
|
979
|
+
try {
|
|
980
|
+
body = await req.json();
|
|
981
|
+
} catch {}
|
|
982
|
+
const { setUpstream } = /** @type {{ setUpstream?: boolean }} */ (body);
|
|
983
|
+
if (setUpstream) {
|
|
984
|
+
const branch = (await runGit(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
985
|
+
await runGit(["push", "-u", "origin", branch]);
|
|
986
|
+
} else {
|
|
987
|
+
await runGit(["push"]);
|
|
988
|
+
}
|
|
832
989
|
return Response.json({ ok: true });
|
|
833
990
|
}
|
|
834
991
|
|
|
@@ -885,11 +1042,138 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
885
1042
|
await runGit(["checkout", "--", ...files]);
|
|
886
1043
|
return Response.json({ ok: true });
|
|
887
1044
|
}
|
|
1045
|
+
|
|
1046
|
+
if (gitCmd === "clone" && req.method === "POST") {
|
|
1047
|
+
const { url } = await req.json();
|
|
1048
|
+
if (!url || typeof url !== "string")
|
|
1049
|
+
return Response.json({ error: "Missing url" }, { status: 400 });
|
|
1050
|
+
const repoName = basename(url.replace(/\.git$/, ""));
|
|
1051
|
+
const dest = resolve(cwd, repoName);
|
|
1052
|
+
const proc = Bun.spawn(["git", "clone", url, dest], {
|
|
1053
|
+
cwd,
|
|
1054
|
+
stdout: "pipe",
|
|
1055
|
+
stderr: "pipe",
|
|
1056
|
+
});
|
|
1057
|
+
const exitCode = await proc.exited;
|
|
1058
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1059
|
+
if (exitCode !== 0) throw new Error(stderr || `git clone exited with ${exitCode}`);
|
|
1060
|
+
return Response.json({ ok: true, root: dest });
|
|
1061
|
+
}
|
|
1062
|
+
} catch (/** @type {unknown} */ e) {
|
|
1063
|
+
return Response.json(
|
|
1064
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
1065
|
+
{ status: 500 },
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ─── Content Type Resolution ──────────────────────────────────────────────
|
|
1071
|
+
|
|
1072
|
+
if (path === "/__studio/content" && req.method === "POST") {
|
|
1073
|
+
try {
|
|
1074
|
+
const body = await req.json();
|
|
1075
|
+
const { $prototype, contentType, id, filter, sort, limit } = body;
|
|
1076
|
+
const projectRoot = activeProjectRoot || root;
|
|
1077
|
+
|
|
1078
|
+
let projectConfig;
|
|
1079
|
+
try {
|
|
1080
|
+
projectConfig = JSON.parse(readFileSync(resolve(projectRoot, "project.json"), "utf8"));
|
|
1081
|
+
} catch {
|
|
1082
|
+
return Response.json({ error: "No project.json found" }, { status: 404 });
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (!projectConfig.contentTypes?.[contentType]) {
|
|
1086
|
+
return Response.json({ error: `Content type "${contentType}" not found` }, { status: 404 });
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const contentTypes = await loadContentTypes(projectRoot, projectConfig);
|
|
1090
|
+
const entries = contentTypes.get(contentType);
|
|
1091
|
+
if (!entries || entries.length === 0) {
|
|
1092
|
+
return Response.json($prototype === "ContentCollection" ? [] : null);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if ($prototype === "ContentCollection") {
|
|
1096
|
+
const result = queryContentType(entries, { filter, sort, limit });
|
|
1097
|
+
return Response.json(result);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if ($prototype === "ContentEntry") {
|
|
1101
|
+
const entry = findEntry(entries, id);
|
|
1102
|
+
return Response.json(entry ?? null);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return Response.json({ error: "Invalid $prototype" }, { status: 400 });
|
|
1106
|
+
} catch (/** @type {unknown} */ e) {
|
|
1107
|
+
return Response.json(
|
|
1108
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
1109
|
+
{ status: 500 },
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ─── AI Assistant ─────────────────────────────────────────────────────────
|
|
1115
|
+
|
|
1116
|
+
if (path === "/__studio/ai/auth-status" && req.method === "GET") {
|
|
1117
|
+
const status = await claude.getAuthStatus();
|
|
1118
|
+
return Response.json(status);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (path === "/__studio/ai/session" && req.method === "POST") {
|
|
1122
|
+
try {
|
|
1123
|
+
const body = await req.json();
|
|
1124
|
+
const projectDir = activeProjectRoot || root;
|
|
1125
|
+
const result = claude.createSession(projectDir, body.message, {
|
|
1126
|
+
systemPrompt: body.systemPrompt,
|
|
1127
|
+
});
|
|
1128
|
+
return Response.json(result);
|
|
888
1129
|
} catch (/** @type {any} */ e) {
|
|
889
1130
|
return Response.json({ error: e.message }, { status: 500 });
|
|
890
1131
|
}
|
|
891
1132
|
}
|
|
892
1133
|
|
|
1134
|
+
if (
|
|
1135
|
+
path.startsWith("/__studio/ai/session/") &&
|
|
1136
|
+
path.endsWith("/stream") &&
|
|
1137
|
+
req.method === "GET"
|
|
1138
|
+
) {
|
|
1139
|
+
const id = path.split("/")[4];
|
|
1140
|
+
return claude.streamSession(id);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (
|
|
1144
|
+
path.startsWith("/__studio/ai/session/") &&
|
|
1145
|
+
path.endsWith("/message") &&
|
|
1146
|
+
req.method === "POST"
|
|
1147
|
+
) {
|
|
1148
|
+
try {
|
|
1149
|
+
const id = path.split("/")[4];
|
|
1150
|
+
const body = await req.json();
|
|
1151
|
+
claude.sendMessage(id, body.message);
|
|
1152
|
+
return Response.json({ ok: true });
|
|
1153
|
+
} catch (/** @type {any} */ e) {
|
|
1154
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (path.startsWith("/__studio/ai/session/") && path.endsWith("/stop") && req.method === "POST") {
|
|
1159
|
+
const id = path.split("/")[4];
|
|
1160
|
+
claude.stopSession(id);
|
|
1161
|
+
return Response.json({ ok: true });
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (path.startsWith("/__studio/ai/session/") && req.method === "DELETE") {
|
|
1165
|
+
const id = path.split("/")[4];
|
|
1166
|
+
claude.deleteSession(id);
|
|
1167
|
+
return Response.json({ ok: true });
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (path.startsWith("/__studio/ai/session/") && req.method === "GET") {
|
|
1171
|
+
const id = path.split("/")[4];
|
|
1172
|
+
const info = claude.getSession(id);
|
|
1173
|
+
if (!info) return Response.json({ error: "Not found" }, { status: 404 });
|
|
1174
|
+
return Response.json(info);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
893
1177
|
return null;
|
|
894
1178
|
}
|
|
895
1179
|
|
|
@@ -898,9 +1182,13 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
898
1182
|
* $defs.fields into the flat { description, properties, required } shape that renderSchemaFields()
|
|
899
1183
|
* in the studio already consumes.
|
|
900
1184
|
*
|
|
901
|
-
* @param {
|
|
1185
|
+
* @param {ClassJsonDef} classDef
|
|
902
1186
|
* @param {string} classJsonPath
|
|
903
|
-
* @returns {{
|
|
1187
|
+
* @returns {{
|
|
1188
|
+
* description: string | undefined;
|
|
1189
|
+
* properties: Record<string, Record<string, unknown>>;
|
|
1190
|
+
* required: string[];
|
|
1191
|
+
* }}
|
|
904
1192
|
*/
|
|
905
1193
|
function extractStudioSchema(classDef, classJsonPath) {
|
|
906
1194
|
// If extends.$ref points to a parent, recursively merge
|
|
@@ -918,7 +1206,7 @@ function extractStudioSchema(classDef, classJsonPath) {
|
|
|
918
1206
|
|
|
919
1207
|
const params = classDef.$defs?.parameters ?? {};
|
|
920
1208
|
const fields = classDef.$defs?.fields ?? {};
|
|
921
|
-
/** @type {Record<string,
|
|
1209
|
+
/** @type {Record<string, Record<string, unknown>>} */
|
|
922
1210
|
const properties = {};
|
|
923
1211
|
/** @type {string[]} */
|
|
924
1212
|
const required = [];
|
|
@@ -934,6 +1222,7 @@ function extractStudioSchema(classDef, classJsonPath) {
|
|
|
934
1222
|
// Build properties from parameters (constructor config surface)
|
|
935
1223
|
for (const [key, param] of Object.entries(params)) {
|
|
936
1224
|
const id = param.identifier ?? key;
|
|
1225
|
+
/** @type {Record<string, unknown>} */
|
|
937
1226
|
const prop = {};
|
|
938
1227
|
if (param.type && typeof param.type === "object") Object.assign(prop, param.type);
|
|
939
1228
|
if (param.description) prop.description = param.description;
|
|
@@ -947,6 +1236,7 @@ function extractStudioSchema(classDef, classJsonPath) {
|
|
|
947
1236
|
if (field.role !== "field") continue;
|
|
948
1237
|
if (field.access === "private") continue;
|
|
949
1238
|
const id = field.identifier ?? key;
|
|
1239
|
+
/** @type {Record<string, unknown>} */
|
|
950
1240
|
const prop = {};
|
|
951
1241
|
if (field.type && typeof field.type === "object") Object.assign(prop, field.type);
|
|
952
1242
|
if (field.description) prop.description = field.description;
|
package/src/watch.js
CHANGED
|
@@ -55,7 +55,7 @@ export function injectSSE(html) {
|
|
|
55
55
|
* Create the file watcher + SSE system.
|
|
56
56
|
*
|
|
57
57
|
* @param {string} root - Absolute path to watch
|
|
58
|
-
* @param {
|
|
58
|
+
* @param {BuildEntry[]} builds - Build entries (for selective rebuild)
|
|
59
59
|
* @param {{ ignore?: string[]; debounce?: number; reloadOnAnyChange?: boolean }} [opts]
|
|
60
60
|
* @returns {{
|
|
61
61
|
* broadcast: () => void;
|
|
@@ -77,7 +77,7 @@ export function createWatcher(root, builds, opts = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function handleSSE() {
|
|
80
|
-
/** @type {
|
|
80
|
+
/** @type {((msg: string) => void) | undefined} */
|
|
81
81
|
let send;
|
|
82
82
|
const stream = new ReadableStream({
|
|
83
83
|
start(c) {
|
|
@@ -96,7 +96,7 @@ export function createWatcher(root, builds, opts = {}) {
|
|
|
96
96
|
}, 15_000);
|
|
97
97
|
},
|
|
98
98
|
cancel() {
|
|
99
|
-
clients.delete(send);
|
|
99
|
+
if (send) clients.delete(send);
|
|
100
100
|
},
|
|
101
101
|
});
|
|
102
102
|
return new Response(stream, {
|
|
@@ -108,7 +108,7 @@ export function createWatcher(root, builds, opts = {}) {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/** @type {
|
|
111
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
112
112
|
let timer = null;
|
|
113
113
|
const watcher = chokidar.watch(root, {
|
|
114
114
|
ignored: (watchedPath) => shouldIgnore(watchedPath, ignore),
|
|
@@ -123,7 +123,7 @@ export function createWatcher(root, builds, opts = {}) {
|
|
|
123
123
|
watcher.on("all", (_, changedPath) => {
|
|
124
124
|
const filename = relative(root, changedPath);
|
|
125
125
|
if (!filename || filename.startsWith("..")) return;
|
|
126
|
-
clearTimeout(timer);
|
|
126
|
+
clearTimeout(timer ?? undefined);
|
|
127
127
|
timer = setTimeout(async () => {
|
|
128
128
|
if (builds.length > 0) {
|
|
129
129
|
const result = await rebuild(builds, filename);
|