@jxsuite/server 0.19.0 → 0.20.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 +10 -3
- 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 +316 -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.20.0",
|
|
4
4
|
"description": "Jx development server with live reload, server-side proxy, and studio integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,7 +13,8 @@
|
|
|
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
|
|
@@ -22,6 +23,12 @@
|
|
|
22
23
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"
|
|
26
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.152",
|
|
27
|
+
"@anthropic-ai/sdk": "^0.99.0",
|
|
28
|
+
"@jxsuite/create": "^0.19.0",
|
|
29
|
+
"@jxsuite/parser": "^0.20.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
|
+
"chokidar": "^5.0.0",
|
|
32
|
+
"zod": "^4.4.3"
|
|
26
33
|
}
|
|
27
34
|
}
|
|
@@ -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,7 @@ 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 * as claude from "./claude-session.js";
|
|
14
15
|
|
|
15
16
|
/** Normalise a path to forward slashes (Windows `path` module returns backslashes). */
|
|
16
17
|
const fwd = (/** @type {string} */ p) => p.replaceAll("\\", "/");
|
|
@@ -40,6 +41,14 @@ const statusMap = { M: "M", T: "T", A: "A", D: "D", R: "R", C: "C", U: "U" };
|
|
|
40
41
|
* Parse raw `git status --porcelain=v2 --branch` output into structured data.
|
|
41
42
|
*
|
|
42
43
|
* @param {string} out
|
|
44
|
+
* @returns {{
|
|
45
|
+
* branch: string;
|
|
46
|
+
* ahead: number;
|
|
47
|
+
* behind: number;
|
|
48
|
+
* files: { path: string; status: string; staged: boolean }[];
|
|
49
|
+
* isRepo?: boolean;
|
|
50
|
+
* remotes?: string[];
|
|
51
|
+
* }}
|
|
43
52
|
*/
|
|
44
53
|
export function parseGitStatus(out) {
|
|
45
54
|
let branch = "";
|
|
@@ -97,6 +106,7 @@ export function parseGitStatus(out) {
|
|
|
97
106
|
* @param {URL} url
|
|
98
107
|
* @param {string} root
|
|
99
108
|
* @param {string | null} [activeProjectRoot]
|
|
109
|
+
* @returns {Promise<Response | null>}
|
|
100
110
|
*/
|
|
101
111
|
export async function handleStudioApi(req, url, root, activeProjectRoot = null) {
|
|
102
112
|
const path = url.pathname;
|
|
@@ -121,8 +131,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
121
131
|
const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
122
132
|
try {
|
|
123
133
|
assertAccessible(absDir, root, activeProjectRoot);
|
|
124
|
-
} catch (/** @type {
|
|
125
|
-
return Response.json(
|
|
134
|
+
} catch (/** @type {unknown} */ e) {
|
|
135
|
+
return Response.json(
|
|
136
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
137
|
+
{ status: 400 },
|
|
138
|
+
);
|
|
126
139
|
}
|
|
127
140
|
try {
|
|
128
141
|
const projectRoot = fwd(absDir);
|
|
@@ -154,8 +167,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
154
167
|
} catch {}
|
|
155
168
|
|
|
156
169
|
return Response.json({ isSiteProject, projectConfig, directories, projectRoot });
|
|
157
|
-
} catch (/** @type {
|
|
158
|
-
return Response.json(
|
|
170
|
+
} catch (/** @type {unknown} */ e) {
|
|
171
|
+
return Response.json(
|
|
172
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
173
|
+
{ status: 500 },
|
|
174
|
+
);
|
|
159
175
|
}
|
|
160
176
|
}
|
|
161
177
|
|
|
@@ -189,8 +205,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
189
205
|
dir = parent;
|
|
190
206
|
}
|
|
191
207
|
return Response.json({ sitePath: null });
|
|
192
|
-
} catch (/** @type {
|
|
193
|
-
return Response.json(
|
|
208
|
+
} catch (/** @type {unknown} */ e) {
|
|
209
|
+
return Response.json(
|
|
210
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
211
|
+
{ status: 500 },
|
|
212
|
+
);
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
|
|
@@ -211,8 +230,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
211
230
|
}
|
|
212
231
|
} catch {}
|
|
213
232
|
return Response.json({ path: null });
|
|
214
|
-
} catch (/** @type {
|
|
215
|
-
return Response.json(
|
|
233
|
+
} catch (/** @type {unknown} */ e) {
|
|
234
|
+
return Response.json(
|
|
235
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
236
|
+
{ status: 500 },
|
|
237
|
+
);
|
|
216
238
|
}
|
|
217
239
|
}
|
|
218
240
|
|
|
@@ -238,8 +260,36 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
238
260
|
} catch {}
|
|
239
261
|
}
|
|
240
262
|
return Response.json(sites);
|
|
241
|
-
} catch (/** @type {
|
|
242
|
-
return Response.json(
|
|
263
|
+
} catch (/** @type {unknown} */ e) {
|
|
264
|
+
return Response.json(
|
|
265
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
266
|
+
{ status: 500 },
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Create a new project
|
|
272
|
+
if (path === "/__studio/create-project" && req.method === "POST") {
|
|
273
|
+
try {
|
|
274
|
+
const body = await req.json();
|
|
275
|
+
const { name, description, url: siteUrl, adapter, directory } = body;
|
|
276
|
+
if (!name || !directory) {
|
|
277
|
+
return Response.json({ error: "name and directory are required" }, { status: 400 });
|
|
278
|
+
}
|
|
279
|
+
const destPath = resolve(root, directory);
|
|
280
|
+
assertAccessible(destPath, root, activeProjectRoot);
|
|
281
|
+
|
|
282
|
+
const { generateProject } = await import("@jxsuite/create/generate");
|
|
283
|
+
await generateProject(destPath, { name, description, url: siteUrl, adapter });
|
|
284
|
+
|
|
285
|
+
const config = JSON.parse(await readFile(resolve(destPath, "project.json"), "utf8"));
|
|
286
|
+
const projectRoot = fwd(relative(root, destPath));
|
|
287
|
+
return Response.json({ root: projectRoot, config });
|
|
288
|
+
} catch (/** @type {unknown} */ e) {
|
|
289
|
+
return Response.json(
|
|
290
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
291
|
+
{ status: 500 },
|
|
292
|
+
);
|
|
243
293
|
}
|
|
244
294
|
}
|
|
245
295
|
|
|
@@ -250,8 +300,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
250
300
|
const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
251
301
|
try {
|
|
252
302
|
assertAccessible(absDir, root, activeProjectRoot);
|
|
253
|
-
} catch (/** @type {
|
|
254
|
-
return Response.json(
|
|
303
|
+
} catch (/** @type {unknown} */ e) {
|
|
304
|
+
return Response.json(
|
|
305
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
306
|
+
{ status: 400 },
|
|
307
|
+
);
|
|
255
308
|
}
|
|
256
309
|
|
|
257
310
|
/** Report a path relative to the active project root (or server root as fallback). */
|
|
@@ -299,8 +352,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
299
352
|
});
|
|
300
353
|
}
|
|
301
354
|
return Response.json(files);
|
|
302
|
-
} catch (/** @type {
|
|
303
|
-
return Response.json(
|
|
355
|
+
} catch (/** @type {unknown} */ e) {
|
|
356
|
+
return Response.json(
|
|
357
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
358
|
+
{ status: 500 },
|
|
359
|
+
);
|
|
304
360
|
}
|
|
305
361
|
}
|
|
306
362
|
|
|
@@ -310,8 +366,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
310
366
|
const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
311
367
|
try {
|
|
312
368
|
assertAccessible(scanRoot, root, activeProjectRoot);
|
|
313
|
-
} catch (/** @type {
|
|
314
|
-
return Response.json(
|
|
369
|
+
} catch (/** @type {unknown} */ e) {
|
|
370
|
+
return Response.json(
|
|
371
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
372
|
+
{ status: 400 },
|
|
373
|
+
);
|
|
315
374
|
}
|
|
316
375
|
try {
|
|
317
376
|
const glob = new Bun.Glob("**/*.{json,md}");
|
|
@@ -325,7 +384,6 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
325
384
|
continue;
|
|
326
385
|
const fp = resolve(scanRoot, match);
|
|
327
386
|
try {
|
|
328
|
-
/** @type {any} */
|
|
329
387
|
let content;
|
|
330
388
|
if (match.endsWith(".md")) {
|
|
331
389
|
const source = await readFile(fp, "utf8");
|
|
@@ -406,14 +464,17 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
406
464
|
source: "npm",
|
|
407
465
|
package: name,
|
|
408
466
|
description: decl.description || null,
|
|
409
|
-
props: (decl.attributes || []).map(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
467
|
+
props: (decl.attributes || []).map(
|
|
468
|
+
(/** @type {Record<string, unknown>} */ a) => ({
|
|
469
|
+
name: a.name,
|
|
470
|
+
type: /** @type {Record<string, unknown> | undefined} */ (a.type)?.text,
|
|
471
|
+
default: a.default,
|
|
472
|
+
description: a.description || null,
|
|
473
|
+
}),
|
|
474
|
+
),
|
|
415
475
|
members: (decl.members || []).filter(
|
|
416
|
-
(/** @type {
|
|
476
|
+
(/** @type {Record<string, unknown>} */ m) =>
|
|
477
|
+
m.kind === "field" && m.privacy !== "private",
|
|
417
478
|
),
|
|
418
479
|
slots: decl.slots || [],
|
|
419
480
|
events: decl.events || [],
|
|
@@ -429,8 +490,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
429
490
|
} catch {} // skip if no project package.json
|
|
430
491
|
|
|
431
492
|
return Response.json(components);
|
|
432
|
-
} catch (/** @type {
|
|
433
|
-
return Response.json(
|
|
493
|
+
} catch (/** @type {unknown} */ e) {
|
|
494
|
+
return Response.json(
|
|
495
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
496
|
+
{ status: 500 },
|
|
497
|
+
);
|
|
434
498
|
}
|
|
435
499
|
}
|
|
436
500
|
|
|
@@ -445,7 +509,14 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
445
509
|
if (!existsSync(pkgPath)) return Response.json([]);
|
|
446
510
|
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
447
511
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
448
|
-
/**
|
|
512
|
+
/**
|
|
513
|
+
* @type {{
|
|
514
|
+
* name: string;
|
|
515
|
+
* version: string;
|
|
516
|
+
* hasCem: boolean;
|
|
517
|
+
* customElementsPath: string | null;
|
|
518
|
+
* }[]}
|
|
519
|
+
*/
|
|
449
520
|
const packages = [];
|
|
450
521
|
for (const [name, version] of Object.entries(deps)) {
|
|
451
522
|
const depPkgPath = resolve(scanRoot, "node_modules", ...name.split("/"), "package.json");
|
|
@@ -467,8 +538,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
467
538
|
} catch {}
|
|
468
539
|
}
|
|
469
540
|
return Response.json(packages);
|
|
470
|
-
} catch (/** @type {
|
|
471
|
-
return Response.json(
|
|
541
|
+
} catch (/** @type {unknown} */ e) {
|
|
542
|
+
return Response.json(
|
|
543
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
544
|
+
{ status: 500 },
|
|
545
|
+
);
|
|
472
546
|
}
|
|
473
547
|
}
|
|
474
548
|
|
|
@@ -493,8 +567,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
493
567
|
if (!existsSync(cemPath)) return Response.json({ cem: null });
|
|
494
568
|
const cem = JSON.parse(await readFile(cemPath, "utf8"));
|
|
495
569
|
return Response.json({ cem });
|
|
496
|
-
} catch (/** @type {
|
|
497
|
-
return Response.json(
|
|
570
|
+
} catch (/** @type {unknown} */ e) {
|
|
571
|
+
return Response.json(
|
|
572
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
573
|
+
{ status: 500 },
|
|
574
|
+
);
|
|
498
575
|
}
|
|
499
576
|
}
|
|
500
577
|
|
|
@@ -519,8 +596,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
519
596
|
);
|
|
520
597
|
}
|
|
521
598
|
return Response.json({ ok: true });
|
|
522
|
-
} catch (/** @type {
|
|
523
|
-
return Response.json(
|
|
599
|
+
} catch (/** @type {unknown} */ e) {
|
|
600
|
+
return Response.json(
|
|
601
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
602
|
+
{ status: 500 },
|
|
603
|
+
);
|
|
524
604
|
}
|
|
525
605
|
}
|
|
526
606
|
|
|
@@ -543,8 +623,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
543
623
|
);
|
|
544
624
|
}
|
|
545
625
|
return Response.json({ ok: true });
|
|
546
|
-
} catch (/** @type {
|
|
547
|
-
return Response.json(
|
|
626
|
+
} catch (/** @type {unknown} */ e) {
|
|
627
|
+
return Response.json(
|
|
628
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
629
|
+
{ status: 500 },
|
|
630
|
+
);
|
|
548
631
|
}
|
|
549
632
|
}
|
|
550
633
|
|
|
@@ -555,18 +638,21 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
555
638
|
const abs = fp.startsWith("~") ? fp.replace("~", process.env.HOME || "") : fp;
|
|
556
639
|
try {
|
|
557
640
|
assertAccessible(abs, root, activeProjectRoot);
|
|
558
|
-
} catch (/** @type {
|
|
559
|
-
return new Response(e.message, { status: 400 });
|
|
641
|
+
} catch (/** @type {unknown} */ e) {
|
|
642
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
560
643
|
}
|
|
561
644
|
try {
|
|
562
645
|
return Response.json({
|
|
563
646
|
content: await readFile(abs, "utf8"),
|
|
564
647
|
path: fp,
|
|
565
648
|
});
|
|
566
|
-
} catch (/** @type {
|
|
567
|
-
return e.code === "ENOENT"
|
|
649
|
+
} catch (/** @type {unknown} */ e) {
|
|
650
|
+
return /** @type {{ code?: string }} */ (e).code === "ENOENT"
|
|
568
651
|
? new Response("Not found", { status: 404 })
|
|
569
|
-
: Response.json(
|
|
652
|
+
: Response.json(
|
|
653
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
654
|
+
{ status: 500 },
|
|
655
|
+
);
|
|
570
656
|
}
|
|
571
657
|
}
|
|
572
658
|
|
|
@@ -577,15 +663,18 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
577
663
|
const abs = resolve(root, fp);
|
|
578
664
|
try {
|
|
579
665
|
assertAccessible(abs, root, activeProjectRoot);
|
|
580
|
-
} catch (/** @type {
|
|
581
|
-
return new Response(e.message, { status: 400 });
|
|
666
|
+
} catch (/** @type {unknown} */ e) {
|
|
667
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
582
668
|
}
|
|
583
669
|
try {
|
|
584
670
|
await mkdir(dirname(abs), { recursive: true });
|
|
585
671
|
await writeFile(abs, await req.text(), "utf8");
|
|
586
672
|
return Response.json({ ok: true, path: fwd(relative(root, abs)) });
|
|
587
|
-
} catch (/** @type {
|
|
588
|
-
return Response.json(
|
|
673
|
+
} catch (/** @type {unknown} */ e) {
|
|
674
|
+
return Response.json(
|
|
675
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
676
|
+
{ status: 500 },
|
|
677
|
+
);
|
|
589
678
|
}
|
|
590
679
|
}
|
|
591
680
|
|
|
@@ -596,16 +685,19 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
596
685
|
const abs = resolve(root, fp);
|
|
597
686
|
try {
|
|
598
687
|
assertAccessible(abs, root, activeProjectRoot);
|
|
599
|
-
} catch (/** @type {
|
|
600
|
-
return new Response(e.message, { status: 400 });
|
|
688
|
+
} catch (/** @type {unknown} */ e) {
|
|
689
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
601
690
|
}
|
|
602
691
|
try {
|
|
603
692
|
await mkdir(dirname(abs), { recursive: true });
|
|
604
693
|
const buffer = await req.arrayBuffer();
|
|
605
694
|
await Bun.write(abs, new Uint8Array(buffer));
|
|
606
695
|
return Response.json({ ok: true, path: fwd(relative(root, abs)) });
|
|
607
|
-
} catch (/** @type {
|
|
608
|
-
return Response.json(
|
|
696
|
+
} catch (/** @type {unknown} */ e) {
|
|
697
|
+
return Response.json(
|
|
698
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
699
|
+
{ status: 500 },
|
|
700
|
+
);
|
|
609
701
|
}
|
|
610
702
|
}
|
|
611
703
|
|
|
@@ -616,16 +708,19 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
616
708
|
const abs = resolve(root, fp);
|
|
617
709
|
try {
|
|
618
710
|
assertAccessible(abs, root, activeProjectRoot);
|
|
619
|
-
} catch (/** @type {
|
|
620
|
-
return new Response(e.message, { status: 400 });
|
|
711
|
+
} catch (/** @type {unknown} */ e) {
|
|
712
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
621
713
|
}
|
|
622
714
|
try {
|
|
623
715
|
await unlink(abs);
|
|
624
716
|
return Response.json({ ok: true, path: fwd(relative(root, abs)) });
|
|
625
|
-
} catch (/** @type {
|
|
626
|
-
return e.code === "ENOENT"
|
|
717
|
+
} catch (/** @type {unknown} */ e) {
|
|
718
|
+
return /** @type {{ code?: string }} */ (e).code === "ENOENT"
|
|
627
719
|
? new Response("Not found", { status: 404 })
|
|
628
|
-
: Response.json(
|
|
720
|
+
: Response.json(
|
|
721
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
722
|
+
{ status: 500 },
|
|
723
|
+
);
|
|
629
724
|
}
|
|
630
725
|
}
|
|
631
726
|
|
|
@@ -644,8 +739,8 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
644
739
|
try {
|
|
645
740
|
assertAccessible(absFrom, root, activeProjectRoot);
|
|
646
741
|
assertAccessible(absTo, root, activeProjectRoot);
|
|
647
|
-
} catch (/** @type {
|
|
648
|
-
return new Response(e.message, { status: 400 });
|
|
742
|
+
} catch (/** @type {unknown} */ e) {
|
|
743
|
+
return new Response(/** @type {{ message?: string }} */ (e).message, { status: 400 });
|
|
649
744
|
}
|
|
650
745
|
try {
|
|
651
746
|
await mkdir(dirname(absTo), { recursive: true });
|
|
@@ -655,8 +750,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
655
750
|
from: fwd(relative(root, absFrom)),
|
|
656
751
|
to: fwd(relative(root, absTo)),
|
|
657
752
|
});
|
|
658
|
-
} catch (/** @type {
|
|
659
|
-
return Response.json(
|
|
753
|
+
} catch (/** @type {unknown} */ e) {
|
|
754
|
+
return Response.json(
|
|
755
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
756
|
+
{ status: 500 },
|
|
757
|
+
);
|
|
660
758
|
}
|
|
661
759
|
}
|
|
662
760
|
|
|
@@ -684,8 +782,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
684
782
|
path: matches[0],
|
|
685
783
|
...(matches.length > 1 ? { alternatives: matches } : {}),
|
|
686
784
|
});
|
|
687
|
-
} catch (/** @type {
|
|
688
|
-
return Response.json(
|
|
785
|
+
} catch (/** @type {unknown} */ e) {
|
|
786
|
+
return Response.json(
|
|
787
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
788
|
+
{ status: 500 },
|
|
789
|
+
);
|
|
689
790
|
}
|
|
690
791
|
}
|
|
691
792
|
|
|
@@ -705,8 +806,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
705
806
|
} else {
|
|
706
807
|
moduleAbsPath = resolve(root, src);
|
|
707
808
|
}
|
|
708
|
-
} catch (/** @type {
|
|
709
|
-
return Response.json({
|
|
809
|
+
} catch (/** @type {unknown} */ e) {
|
|
810
|
+
return Response.json({
|
|
811
|
+
schema: null,
|
|
812
|
+
error: /** @type {{ message?: string }} */ (e).message,
|
|
813
|
+
});
|
|
710
814
|
}
|
|
711
815
|
|
|
712
816
|
// .class.json: read and extract schema directly
|
|
@@ -715,8 +819,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
715
819
|
const content = readFileSync(moduleAbsPath, "utf8");
|
|
716
820
|
const classDef = JSON.parse(content);
|
|
717
821
|
return Response.json({ schema: extractStudioSchema(classDef, moduleAbsPath) });
|
|
718
|
-
} catch (/** @type {
|
|
719
|
-
return Response.json({
|
|
822
|
+
} catch (/** @type {unknown} */ e) {
|
|
823
|
+
return Response.json({
|
|
824
|
+
schema: null,
|
|
825
|
+
error: /** @type {{ message?: string }} */ (e).message,
|
|
826
|
+
});
|
|
720
827
|
}
|
|
721
828
|
}
|
|
722
829
|
|
|
@@ -741,8 +848,11 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
741
848
|
return Response.json({ schema: null, error: `Export "${exportName}" not found` });
|
|
742
849
|
}
|
|
743
850
|
return Response.json({ schema: ExportedClass.schema ?? null });
|
|
744
|
-
} catch (/** @type {
|
|
745
|
-
return Response.json({
|
|
851
|
+
} catch (/** @type {unknown} */ e) {
|
|
852
|
+
return Response.json({
|
|
853
|
+
schema: null,
|
|
854
|
+
error: /** @type {{ message?: string }} */ (e).message,
|
|
855
|
+
});
|
|
746
856
|
}
|
|
747
857
|
}
|
|
748
858
|
|
|
@@ -763,8 +873,44 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
763
873
|
|
|
764
874
|
try {
|
|
765
875
|
if (gitCmd === "status" && req.method === "GET") {
|
|
766
|
-
|
|
767
|
-
|
|
876
|
+
// Check if we're in a git repo first
|
|
877
|
+
const checkProc = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
|
|
878
|
+
cwd,
|
|
879
|
+
stdout: "pipe",
|
|
880
|
+
stderr: "pipe",
|
|
881
|
+
});
|
|
882
|
+
const checkExit = await checkProc.exited;
|
|
883
|
+
if (checkExit !== 0) {
|
|
884
|
+
return Response.json({
|
|
885
|
+
isRepo: false,
|
|
886
|
+
branch: "",
|
|
887
|
+
files: [],
|
|
888
|
+
ahead: 0,
|
|
889
|
+
behind: 0,
|
|
890
|
+
remotes: [],
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const [out, remotesOut] = await Promise.all([
|
|
895
|
+
runGit(["status", "--porcelain=v2", "--branch"]),
|
|
896
|
+
runGit(["remote"]),
|
|
897
|
+
]);
|
|
898
|
+
const status = parseGitStatus(out);
|
|
899
|
+
status.isRepo = true;
|
|
900
|
+
status.remotes = remotesOut.trim().split("\n").filter(Boolean);
|
|
901
|
+
return Response.json(status);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (gitCmd === "init" && req.method === "POST") {
|
|
905
|
+
await runGit(["init"]);
|
|
906
|
+
return Response.json({ ok: true });
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (gitCmd === "add-remote" && req.method === "POST") {
|
|
910
|
+
const { name, url: remoteUrl } = await req.json();
|
|
911
|
+
if (!name || !remoteUrl) return new Response("name and url required", { status: 400 });
|
|
912
|
+
await runGit(["remote", "add", name, remoteUrl]);
|
|
913
|
+
return Response.json({ ok: true });
|
|
768
914
|
}
|
|
769
915
|
|
|
770
916
|
if (gitCmd === "branches" && req.method === "GET") {
|
|
@@ -828,7 +974,17 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
828
974
|
}
|
|
829
975
|
|
|
830
976
|
if (gitCmd === "push" && req.method === "POST") {
|
|
831
|
-
|
|
977
|
+
let body = {};
|
|
978
|
+
try {
|
|
979
|
+
body = await req.json();
|
|
980
|
+
} catch {}
|
|
981
|
+
const { setUpstream } = /** @type {{ setUpstream?: boolean }} */ (body);
|
|
982
|
+
if (setUpstream) {
|
|
983
|
+
const branch = (await runGit(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
984
|
+
await runGit(["push", "-u", "origin", branch]);
|
|
985
|
+
} else {
|
|
986
|
+
await runGit(["push"]);
|
|
987
|
+
}
|
|
832
988
|
return Response.json({ ok: true });
|
|
833
989
|
}
|
|
834
990
|
|
|
@@ -885,11 +1041,94 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
885
1041
|
await runGit(["checkout", "--", ...files]);
|
|
886
1042
|
return Response.json({ ok: true });
|
|
887
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
if (gitCmd === "clone" && req.method === "POST") {
|
|
1046
|
+
const { url } = await req.json();
|
|
1047
|
+
if (!url || typeof url !== "string")
|
|
1048
|
+
return Response.json({ error: "Missing url" }, { status: 400 });
|
|
1049
|
+
const repoName = basename(url.replace(/\.git$/, ""));
|
|
1050
|
+
const dest = resolve(cwd, repoName);
|
|
1051
|
+
const proc = Bun.spawn(["git", "clone", url, dest], {
|
|
1052
|
+
cwd,
|
|
1053
|
+
stdout: "pipe",
|
|
1054
|
+
stderr: "pipe",
|
|
1055
|
+
});
|
|
1056
|
+
const exitCode = await proc.exited;
|
|
1057
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1058
|
+
if (exitCode !== 0) throw new Error(stderr || `git clone exited with ${exitCode}`);
|
|
1059
|
+
return Response.json({ ok: true, root: dest });
|
|
1060
|
+
}
|
|
1061
|
+
} catch (/** @type {unknown} */ e) {
|
|
1062
|
+
return Response.json(
|
|
1063
|
+
{ error: /** @type {{ message?: string }} */ (e).message },
|
|
1064
|
+
{ status: 500 },
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ─── AI Assistant ─────────────────────────────────────────────────────────
|
|
1070
|
+
|
|
1071
|
+
if (path === "/__studio/ai/auth-status" && req.method === "GET") {
|
|
1072
|
+
const status = await claude.getAuthStatus();
|
|
1073
|
+
return Response.json(status);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (path === "/__studio/ai/session" && req.method === "POST") {
|
|
1077
|
+
try {
|
|
1078
|
+
const body = await req.json();
|
|
1079
|
+
const projectDir = activeProjectRoot || root;
|
|
1080
|
+
const result = claude.createSession(projectDir, body.message, {
|
|
1081
|
+
systemPrompt: body.systemPrompt,
|
|
1082
|
+
});
|
|
1083
|
+
return Response.json(result);
|
|
1084
|
+
} catch (/** @type {any} */ e) {
|
|
1085
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (
|
|
1090
|
+
path.startsWith("/__studio/ai/session/") &&
|
|
1091
|
+
path.endsWith("/stream") &&
|
|
1092
|
+
req.method === "GET"
|
|
1093
|
+
) {
|
|
1094
|
+
const id = path.split("/")[4];
|
|
1095
|
+
return claude.streamSession(id);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (
|
|
1099
|
+
path.startsWith("/__studio/ai/session/") &&
|
|
1100
|
+
path.endsWith("/message") &&
|
|
1101
|
+
req.method === "POST"
|
|
1102
|
+
) {
|
|
1103
|
+
try {
|
|
1104
|
+
const id = path.split("/")[4];
|
|
1105
|
+
const body = await req.json();
|
|
1106
|
+
claude.sendMessage(id, body.message);
|
|
1107
|
+
return Response.json({ ok: true });
|
|
888
1108
|
} catch (/** @type {any} */ e) {
|
|
889
1109
|
return Response.json({ error: e.message }, { status: 500 });
|
|
890
1110
|
}
|
|
891
1111
|
}
|
|
892
1112
|
|
|
1113
|
+
if (path.startsWith("/__studio/ai/session/") && path.endsWith("/stop") && req.method === "POST") {
|
|
1114
|
+
const id = path.split("/")[4];
|
|
1115
|
+
claude.stopSession(id);
|
|
1116
|
+
return Response.json({ ok: true });
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (path.startsWith("/__studio/ai/session/") && req.method === "DELETE") {
|
|
1120
|
+
const id = path.split("/")[4];
|
|
1121
|
+
claude.deleteSession(id);
|
|
1122
|
+
return Response.json({ ok: true });
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (path.startsWith("/__studio/ai/session/") && req.method === "GET") {
|
|
1126
|
+
const id = path.split("/")[4];
|
|
1127
|
+
const info = claude.getSession(id);
|
|
1128
|
+
if (!info) return Response.json({ error: "Not found" }, { status: 404 });
|
|
1129
|
+
return Response.json(info);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
893
1132
|
return null;
|
|
894
1133
|
}
|
|
895
1134
|
|
|
@@ -898,9 +1137,13 @@ export async function handleStudioApi(req, url, root, activeProjectRoot = null)
|
|
|
898
1137
|
* $defs.fields into the flat { description, properties, required } shape that renderSchemaFields()
|
|
899
1138
|
* in the studio already consumes.
|
|
900
1139
|
*
|
|
901
|
-
* @param {
|
|
1140
|
+
* @param {ClassJsonDef} classDef
|
|
902
1141
|
* @param {string} classJsonPath
|
|
903
|
-
* @returns {{
|
|
1142
|
+
* @returns {{
|
|
1143
|
+
* description: string | undefined;
|
|
1144
|
+
* properties: Record<string, Record<string, unknown>>;
|
|
1145
|
+
* required: string[];
|
|
1146
|
+
* }}
|
|
904
1147
|
*/
|
|
905
1148
|
function extractStudioSchema(classDef, classJsonPath) {
|
|
906
1149
|
// If extends.$ref points to a parent, recursively merge
|
|
@@ -918,7 +1161,7 @@ function extractStudioSchema(classDef, classJsonPath) {
|
|
|
918
1161
|
|
|
919
1162
|
const params = classDef.$defs?.parameters ?? {};
|
|
920
1163
|
const fields = classDef.$defs?.fields ?? {};
|
|
921
|
-
/** @type {Record<string,
|
|
1164
|
+
/** @type {Record<string, Record<string, unknown>>} */
|
|
922
1165
|
const properties = {};
|
|
923
1166
|
/** @type {string[]} */
|
|
924
1167
|
const required = [];
|
|
@@ -934,6 +1177,7 @@ function extractStudioSchema(classDef, classJsonPath) {
|
|
|
934
1177
|
// Build properties from parameters (constructor config surface)
|
|
935
1178
|
for (const [key, param] of Object.entries(params)) {
|
|
936
1179
|
const id = param.identifier ?? key;
|
|
1180
|
+
/** @type {Record<string, unknown>} */
|
|
937
1181
|
const prop = {};
|
|
938
1182
|
if (param.type && typeof param.type === "object") Object.assign(prop, param.type);
|
|
939
1183
|
if (param.description) prop.description = param.description;
|
|
@@ -947,6 +1191,7 @@ function extractStudioSchema(classDef, classJsonPath) {
|
|
|
947
1191
|
if (field.role !== "field") continue;
|
|
948
1192
|
if (field.access === "private") continue;
|
|
949
1193
|
const id = field.identifier ?? key;
|
|
1194
|
+
/** @type {Record<string, unknown>} */
|
|
950
1195
|
const prop = {};
|
|
951
1196
|
if (field.type && typeof field.type === "object") Object.assign(prop, field.type);
|
|
952
1197
|
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);
|