@ubean/devtools 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1894 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createHooks } from "hookable";
5
+ import { execSync, spawn } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
7
+ //#region src/shared/env.ts
8
+ const SENSITIVE_KEY_FRAGMENTS = [
9
+ "KEY",
10
+ "SECRET",
11
+ "TOKEN",
12
+ "PASSWORD",
13
+ "AUTH",
14
+ "CREDENTIAL"
15
+ ];
16
+ function isSensitiveKey(key) {
17
+ const upper = key.toUpperCase();
18
+ return SENSITIVE_KEY_FRAGMENTS.some((fragment) => upper.includes(fragment));
19
+ }
20
+ function maskSensitiveEnv(env) {
21
+ return Object.fromEntries(Object.entries(env).map(([key, value]) => [key, isSensitiveKey(key) ? "***" : value]));
22
+ }
23
+ //#endregion
24
+ //#region src/server/hooks.ts
25
+ function createDevToolsHooks() {
26
+ const hooks = createHooks();
27
+ function registerHook(type, handler) {
28
+ hooks.hook(type, handler);
29
+ }
30
+ async function runHook(type, ctx) {
31
+ await hooks.callHook(type, ctx);
32
+ }
33
+ function removeHook(type, handler) {
34
+ hooks.removeHook(type, handler);
35
+ }
36
+ function removeAllHooks() {
37
+ hooks.removeAllHooks();
38
+ }
39
+ return {
40
+ registerHook,
41
+ runHook,
42
+ removeHook,
43
+ removeAllHooks,
44
+ hook: hooks.hook,
45
+ callHook: hooks.callHook
46
+ };
47
+ }
48
+ const DEEPSEEK_PROVIDER_NAME = "deepseek";
49
+ const SYSTEM_PROMPT = `You are ubean Assistant, an AI developer assistant built into the ubean Vue meta-framework DevTools.
50
+ You help developers scaffold pages, APIs, middleware, layouts, cron jobs, plugins, and manage environment variables.
51
+ You can also inspect the project structure, list resources, and read configuration.
52
+
53
+ Available resource types for creation: page, api, layout, middleware, cron, plugin
54
+ - page: Vue page component under src/pages/
55
+ - api: API route handler under src/api/
56
+ - layout: Layout component under src/layouts/
57
+ - middleware: Middleware under src/middleware/
58
+ - cron: Scheduled task under src/server/crons/
59
+ - plugin: Plugin under src/plugins/
60
+
61
+ When creating resources, always use the create_resource tool and confirm the path with the user if ambiguous.
62
+ Be concise and helpful. Use code examples when relevant.
63
+
64
+ When the user asks about the current project, use the list_resources or get_project_info tools to get up-to-date information.
65
+ Do not guess resource paths — always verify with tools first.`;
66
+ let toolCallCounter = 0;
67
+ function nextToolId() {
68
+ return `call_${++toolCallCounter}`;
69
+ }
70
+ function createAiServer(crud, getInfo, onStreamChunk) {
71
+ function getToolDefinitions() {
72
+ return [
73
+ {
74
+ name: "list_resources",
75
+ description: "List all resources of a given type in the project",
76
+ parameters: {
77
+ type: "object",
78
+ properties: { type: {
79
+ type: "string",
80
+ description: "Type of resource to list",
81
+ enum: [
82
+ "pages",
83
+ "apis",
84
+ "layouts",
85
+ "middlewares",
86
+ "crons",
87
+ "all"
88
+ ]
89
+ } },
90
+ required: ["type"]
91
+ }
92
+ },
93
+ {
94
+ name: "create_resource",
95
+ description: "Create a new resource (page, api, layout, middleware, cron, plugin)",
96
+ parameters: {
97
+ type: "object",
98
+ properties: {
99
+ type: {
100
+ type: "string",
101
+ description: "Type of resource to create",
102
+ enum: [
103
+ "page",
104
+ "api",
105
+ "layout",
106
+ "middleware",
107
+ "cron",
108
+ "plugin"
109
+ ]
110
+ },
111
+ path: {
112
+ type: "string",
113
+ description: "Route path (e.g., \"about\", \"users/[id]\", \"auth\")"
114
+ },
115
+ method: {
116
+ type: "string",
117
+ description: "HTTP method for API routes",
118
+ enum: [
119
+ "GET",
120
+ "POST",
121
+ "PUT",
122
+ "PATCH",
123
+ "DELETE"
124
+ ]
125
+ },
126
+ schedule: {
127
+ type: "string",
128
+ description: "Cron schedule expression (e.g., \"0 0 * * *\" for daily at midnight)"
129
+ }
130
+ },
131
+ required: ["type", "path"]
132
+ }
133
+ },
134
+ {
135
+ name: "delete_resource",
136
+ description: "Delete a resource (creates backup by default)",
137
+ parameters: {
138
+ type: "object",
139
+ properties: {
140
+ type: {
141
+ type: "string",
142
+ description: "Type of resource to delete",
143
+ enum: [
144
+ "page",
145
+ "api",
146
+ "layout",
147
+ "middleware",
148
+ "cron",
149
+ "plugin"
150
+ ]
151
+ },
152
+ path: {
153
+ type: "string",
154
+ description: "Resource path"
155
+ },
156
+ force: {
157
+ type: "boolean",
158
+ description: "Permanently delete without backup"
159
+ }
160
+ },
161
+ required: ["type", "path"]
162
+ }
163
+ },
164
+ {
165
+ name: "get_project_info",
166
+ description: "Get project overview information (version, uptime, counts, config)",
167
+ parameters: {
168
+ type: "object",
169
+ properties: {}
170
+ }
171
+ },
172
+ {
173
+ name: "read_resource",
174
+ description: "Read the content of a resource file",
175
+ parameters: {
176
+ type: "object",
177
+ properties: {
178
+ type: {
179
+ type: "string",
180
+ description: "Resource type",
181
+ enum: [
182
+ "page",
183
+ "api",
184
+ "layout",
185
+ "middleware",
186
+ "cron",
187
+ "env",
188
+ "config"
189
+ ]
190
+ },
191
+ path: {
192
+ type: "string",
193
+ description: "Resource path (not needed for env/config)"
194
+ }
195
+ },
196
+ required: ["type"]
197
+ }
198
+ },
199
+ {
200
+ name: "set_env",
201
+ description: "Set or update an environment variable",
202
+ parameters: {
203
+ type: "object",
204
+ properties: {
205
+ key: {
206
+ type: "string",
207
+ description: "Environment variable name"
208
+ },
209
+ value: {
210
+ type: "string",
211
+ description: "Environment variable value"
212
+ }
213
+ },
214
+ required: ["key", "value"]
215
+ }
216
+ }
217
+ ];
218
+ }
219
+ async function executeToolCall(call) {
220
+ try {
221
+ const args = call.arguments;
222
+ switch (call.name) {
223
+ case "list_resources": {
224
+ const type = args.type;
225
+ const info = getInfo();
226
+ const result = {};
227
+ if (type === "pages" || type === "all") result.pages = info.pagesList?.map((p) => ({
228
+ path: p.path,
229
+ name: p.name,
230
+ file: p.filePath
231
+ })) || [];
232
+ if (type === "apis" || type === "all") result.apis = info.routes?.filter((r) => r.filePath).map((r) => ({
233
+ method: r.method,
234
+ path: r.path,
235
+ file: r.filePath
236
+ })) || [];
237
+ if (type === "layouts" || type === "all") result.layouts = info.layoutsList?.map((l) => ({
238
+ name: l.name,
239
+ path: l.path,
240
+ default: l.isDefault,
241
+ file: l.filePath
242
+ })) || [];
243
+ if (type === "middlewares" || type === "all") result.middlewares = info.middlewaresList?.map((m) => ({
244
+ path: m.path,
245
+ global: m.global,
246
+ file: m.filePath
247
+ })) || [];
248
+ if (type === "crons" || type === "all") result.crons = info.cronsList?.map((c) => ({
249
+ name: c.name,
250
+ schedule: c.schedule,
251
+ file: c.filePath
252
+ })) || [];
253
+ return {
254
+ toolCallId: call.id,
255
+ result
256
+ };
257
+ }
258
+ case "create_resource": {
259
+ const result = await crud.create({
260
+ type: args.type,
261
+ path: args.path,
262
+ method: args.method,
263
+ schedule: args.schedule,
264
+ force: false
265
+ });
266
+ return {
267
+ toolCallId: call.id,
268
+ result
269
+ };
270
+ }
271
+ case "delete_resource": {
272
+ const result = await crud.delete({
273
+ type: args.type,
274
+ path: args.path,
275
+ force: args.force || false
276
+ });
277
+ return {
278
+ toolCallId: call.id,
279
+ result
280
+ };
281
+ }
282
+ case "get_project_info": {
283
+ const info = getInfo();
284
+ return {
285
+ toolCallId: call.id,
286
+ result: {
287
+ version: info.version,
288
+ uptime: Date.now() - info.startTime,
289
+ pages: info.pages,
290
+ apiRoutes: info.apiRoutes,
291
+ middlewares: info.middleware,
292
+ layouts: info.layouts,
293
+ crons: info.crons,
294
+ presets: info.presets
295
+ }
296
+ };
297
+ }
298
+ case "read_resource": {
299
+ const result = await crud.read({
300
+ type: args.type,
301
+ path: args.path
302
+ });
303
+ return {
304
+ toolCallId: call.id,
305
+ result
306
+ };
307
+ }
308
+ case "set_env": {
309
+ const result = await crud.update({
310
+ type: "env",
311
+ key: args.key,
312
+ value: args.value
313
+ });
314
+ return {
315
+ toolCallId: call.id,
316
+ result
317
+ };
318
+ }
319
+ default: return {
320
+ toolCallId: call.id,
321
+ error: `Unknown tool: ${call.name}`
322
+ };
323
+ }
324
+ } catch (err) {
325
+ return {
326
+ toolCallId: call.id,
327
+ error: err instanceof Error ? err.message : String(err)
328
+ };
329
+ }
330
+ }
331
+ function parseCommand(input) {
332
+ const lower = input.trim().toLowerCase();
333
+ const createMatch = input.match(/^(?:create|add|new|make|generate|scaffold|g)\s+(?:a\s+)?(page|api|layout|middleware|cron|plugin)\s+(?:at\s+)?["']?([\w\-/[\].]+)["']?(?:\s+(?:with|using)\s+(?:method\s+)?(GET|POST|PUT|PATCH|DELETE))?(?:\s+(?:schedule|cron)?\s+["']?([*\d/\-,\s]+)["']?)?/i);
334
+ if (createMatch) {
335
+ const [, type, path, method, schedule] = createMatch;
336
+ const params = {
337
+ type: type.toLowerCase(),
338
+ path
339
+ };
340
+ if (method) params.method = method.toUpperCase();
341
+ if (schedule) params.schedule = schedule.trim();
342
+ return { toolCalls: [{
343
+ id: nextToolId(),
344
+ name: "create_resource",
345
+ arguments: params
346
+ }] };
347
+ }
348
+ const deleteMatch = input.match(/^(?:delete|remove|rm|del)\s+(?:the\s+)?(page|api|layout|middleware|cron|plugin)\s+(?:at\s+)?["']?([\w\-/[\].]+)["']?/i);
349
+ if (deleteMatch) {
350
+ const [, type, path] = deleteMatch;
351
+ return { toolCalls: [{
352
+ id: nextToolId(),
353
+ name: "delete_resource",
354
+ arguments: {
355
+ type: type.toLowerCase(),
356
+ path,
357
+ force: lower.includes("force") || lower.includes("permanent")
358
+ }
359
+ }] };
360
+ }
361
+ const listMatch = input.match(/^(?:list|show|ls|get)\s+(all|pages|apis?|layouts?|middlewares?|crons?)(?:\s*$)/i);
362
+ if (listMatch) {
363
+ let type = listMatch[1].toLowerCase();
364
+ if (type === "all") type = "all";
365
+ else if (type.startsWith("api")) type = "apis";
366
+ else if (type.startsWith("layout")) type = "layouts";
367
+ else if (type.startsWith("middleware")) type = "middlewares";
368
+ else if (type.startsWith("cron")) type = "crons";
369
+ else type = "pages";
370
+ return { toolCalls: [{
371
+ id: nextToolId(),
372
+ name: "list_resources",
373
+ arguments: { type }
374
+ }] };
375
+ }
376
+ if (/^(?:project\s+)?(?:info|status|overview|stats|about)$/i.test(input.trim())) return { toolCalls: [{
377
+ id: nextToolId(),
378
+ name: "get_project_info",
379
+ arguments: {}
380
+ }] };
381
+ if (/^(help|\?|commands|what can you do)/i.test(input.trim())) return { response: `I can help you with the following:
382
+
383
+ **Create resources:**
384
+ - \`create page about\` — Create a new page at /about
385
+ - \`create api users with method POST\` — Create a POST API route
386
+ - \`create cron daily-cleanup schedule "0 0 * * *"\` — Create a daily cron job
387
+ - \`create layout admin\` — Create an admin layout
388
+
389
+ **Delete resources:**
390
+ - \`delete page about\` — Delete a page (with backup)
391
+ - \`delete api users --force\` — Permanently delete an API
392
+
393
+ **Inspect project:**
394
+ - \`list pages\` / \`list apis\` / \`list all\` — List resources
395
+ - \`project info\` — Show project overview
396
+
397
+ **Environment:**
398
+ - \`set env DATABASE_URL=postgres://...\` — Set environment variable
399
+
400
+ You can also ask me questions in natural language, or configure a DeepSeek/OpenAI-compatible API key for more advanced AI assistance.` };
401
+ const setEnvMatch = input.match(/^(?:set\s+env|env\s+set|add\s+env)\s+(\w+)\s*=\s*(.+)$/i);
402
+ if (setEnvMatch) return { toolCalls: [{
403
+ id: nextToolId(),
404
+ name: "set_env",
405
+ arguments: {
406
+ key: setEnvMatch[1],
407
+ value: setEnvMatch[2].trim()
408
+ }
409
+ }] };
410
+ return null;
411
+ }
412
+ function formatToolResult(name, result) {
413
+ if (name === "list_resources") {
414
+ const r = result;
415
+ const lines = [];
416
+ for (const [key, items] of Object.entries(r)) {
417
+ lines.push(`**${key}** (${items.length}):`);
418
+ for (const item of items.slice(0, 20)) {
419
+ const obj = item;
420
+ const path = obj.path || obj.name || "";
421
+ const extra = obj.method ? ` [${obj.method}]` : obj.default ? " (default)" : "";
422
+ lines.push(` - ${path}${extra}`);
423
+ }
424
+ if (items.length > 20) lines.push(` ... and ${items.length - 20} more`);
425
+ lines.push("");
426
+ }
427
+ return lines.join("\n");
428
+ }
429
+ if (name === "create_resource") {
430
+ const r = result;
431
+ if (r.success && r.created?.length) return `✅ Created successfully:\n${r.created.map((f) => ` - ${f}`).join("\n")}`;
432
+ if (r.errors?.length) return `❌ Failed: ${r.errors.join(", ")}`;
433
+ if (r.skipped?.length) return `⚠️ Skipped (already exists, use --force to overwrite):\n${r.skipped.map((f) => ` - ${f}`).join("\n")}`;
434
+ return JSON.stringify(result, null, 2);
435
+ }
436
+ if (name === "delete_resource") {
437
+ const r = result;
438
+ if (r.success && r.deleted?.length) return `🗑️ Deleted successfully:\n${r.deleted.map((f) => ` - ${f}`).join("\n")}`;
439
+ if (r.errors?.length) return `❌ Failed: ${r.errors.join(", ")}`;
440
+ return JSON.stringify(result, null, 2);
441
+ }
442
+ if (name === "get_project_info") {
443
+ const r = result;
444
+ const uptimeMs = r.uptime;
445
+ const s = Math.floor(uptimeMs / 1e3);
446
+ const m = Math.floor(s / 60);
447
+ const h = Math.floor(m / 60);
448
+ const uptime = h > 0 ? `${h}h ${m % 60}m` : m > 0 ? `${m}m ${s % 60}s` : `${s}s`;
449
+ return `**Project Info:**
450
+ - Version: ${r.version}
451
+ - Uptime: ${uptime}
452
+ - Pages: ${r.pages}
453
+ - API Routes: ${r.apiRoutes}
454
+ - Middlewares: ${r.middlewares}
455
+ - Layouts: ${r.layouts}
456
+ - Cron Jobs: ${r.crons}
457
+ - Presets: ${r.presets.join(", ") || "none"}`;
458
+ }
459
+ if (name === "set_env") {
460
+ const r = result;
461
+ if (r.success) return "✅ Environment variable set successfully.";
462
+ return `❌ Failed: ${r.errors?.join(", ") || "Unknown error"}`;
463
+ }
464
+ if (name === "read_resource") {
465
+ const r = result;
466
+ if (r.error) return `❌ ${r.error}`;
467
+ if (r.content) return `\`\`\`\n${r.content}\n\`\`\``;
468
+ if (r.data) return `\`\`\`json\n${JSON.stringify(r.data, null, 2)}\n\`\`\``;
469
+ return "(empty)";
470
+ }
471
+ return JSON.stringify(result, null, 2);
472
+ }
473
+ function convertToCoreMessages(messages) {
474
+ return messages.filter((m) => m.role !== "tool" || m.toolCallId).map((m) => {
475
+ if (m.role === "assistant" && m.toolCalls?.length) return {
476
+ role: "assistant",
477
+ content: [...m.content ? [{
478
+ type: "text",
479
+ text: m.content
480
+ }] : [], ...m.toolCalls.map((tc) => ({
481
+ type: "tool-call",
482
+ toolCallId: tc.id,
483
+ toolName: tc.name,
484
+ input: tc.arguments
485
+ }))]
486
+ };
487
+ if (m.role === "tool") return {
488
+ role: "tool",
489
+ content: [{
490
+ type: "tool-result",
491
+ toolCallId: m.toolCallId,
492
+ toolName: "tool",
493
+ output: {
494
+ type: "json",
495
+ value: typeof m.content === "string" ? m.content : JSON.parse(m.content)
496
+ }
497
+ }]
498
+ };
499
+ return {
500
+ role: m.role,
501
+ content: m.content
502
+ };
503
+ });
504
+ }
505
+ async function buildAiSdkTools() {
506
+ const ai = await import("ai");
507
+ const defs = getToolDefinitions();
508
+ const tools = {};
509
+ for (const def of defs) tools[def.name] = ai.tool({
510
+ description: def.description,
511
+ inputSchema: ai.jsonSchema(def.parameters),
512
+ execute: async (args) => {
513
+ const result = await executeToolCall({
514
+ id: nextToolId(),
515
+ name: def.name,
516
+ arguments: args
517
+ });
518
+ return result.error ? { error: result.error } : result.result;
519
+ }
520
+ });
521
+ return tools;
522
+ }
523
+ async function chat(options) {
524
+ const { messages, apiKey, apiBase, model, requestId } = options;
525
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
526
+ if (!lastUserMsg) return { message: {
527
+ role: "assistant",
528
+ content: "Hello! I am ubean Assistant. Type \"help\" to see what I can do.",
529
+ timestamp: Date.now()
530
+ } };
531
+ const parsed = parseCommand(lastUserMsg.content);
532
+ if (parsed?.response) return { message: {
533
+ role: "assistant",
534
+ content: parsed.response,
535
+ timestamp: Date.now()
536
+ } };
537
+ if (parsed?.toolCalls && parsed.toolCalls.length > 0) {
538
+ const toolResults = [];
539
+ for (const call of parsed.toolCalls) {
540
+ const result = await executeToolCall(call);
541
+ toolResults.push(result);
542
+ }
543
+ const responseParts = [];
544
+ for (let i = 0; i < parsed.toolCalls.length; i++) {
545
+ const call = parsed.toolCalls[i];
546
+ const tr = toolResults[i];
547
+ if (tr.error) responseParts.push(`❌ Error executing ${call.name}: ${tr.error}`);
548
+ else responseParts.push(formatToolResult(call.name, tr.result));
549
+ }
550
+ return {
551
+ message: {
552
+ role: "assistant",
553
+ content: responseParts.join("\n\n"),
554
+ toolCalls: parsed.toolCalls,
555
+ timestamp: Date.now()
556
+ },
557
+ toolResults
558
+ };
559
+ }
560
+ const resolvedApiKey = apiKey || process.env.DEEPSEEK_API_KEY || process.env.UBEAN_AI_API_KEY || process.env.OPENAI_API_KEY;
561
+ const resolvedApiBase = apiBase || process.env.UBEAN_AI_API_BASE || "https://api.deepseek.com/v1";
562
+ const resolvedModel = model || process.env.UBEAN_AI_MODEL || "deepseek-chat";
563
+ if (resolvedApiKey) try {
564
+ return await callLlmApi({
565
+ messages,
566
+ apiKey: resolvedApiKey,
567
+ apiBase: resolvedApiBase,
568
+ model: resolvedModel,
569
+ requestId
570
+ });
571
+ } catch (err) {
572
+ return { message: {
573
+ role: "assistant",
574
+ content: `I didn't understand that command. Type "help" to see available commands.\n\n(LLM API error: ${err instanceof Error ? err.message : String(err)})`,
575
+ timestamp: Date.now()
576
+ } };
577
+ }
578
+ return { message: {
579
+ role: "assistant",
580
+ content: `I didn't understand that command. Type "help" to see what I can do.\n\nTip: For natural language assistance, configure a DeepSeek or OpenAI-compatible API endpoint:\n\n\`\`\`ts\n// ubean.config.ts\nexport default defineConfig({\n devtools: {\n ai: {\n apiKey: process.env.DEEPSEEK_API_KEY,\n apiBase: 'https://api.deepseek.com/v1',\n model: 'deepseek-chat'\n }\n }\n});\n\`\`\`\n\nOr set the \`DEEPSEEK_API_KEY\` environment variable.`,
581
+ timestamp: Date.now()
582
+ } };
583
+ }
584
+ async function callLlmApi(options) {
585
+ const { messages, apiKey, apiBase, model, requestId } = options;
586
+ const [{ streamText, stepCountIs }, { createOpenAICompatible }] = await Promise.all([import("ai"), import("@ai-sdk/openai-compatible")]);
587
+ const aiModel = createOpenAICompatible({
588
+ baseURL: apiBase,
589
+ name: apiBase.includes("deepseek") ? DEEPSEEK_PROVIDER_NAME : "ubean-ai",
590
+ apiKey
591
+ }).chatModel(model);
592
+ const coreMessages = convertToCoreMessages(messages);
593
+ const tools = await buildAiSdkTools();
594
+ const result = streamText({
595
+ model: aiModel,
596
+ system: SYSTEM_PROMPT,
597
+ messages: coreMessages,
598
+ tools,
599
+ stopWhen: stepCountIs(5)
600
+ });
601
+ let accumulated = "";
602
+ const streamToolCalls = [];
603
+ const streamToolResults = [];
604
+ let streamError;
605
+ if (requestId && onStreamChunk) {
606
+ try {
607
+ for await (const part of result.fullStream) switch (part.type) {
608
+ case "text-delta":
609
+ accumulated += part.text;
610
+ onStreamChunk({
611
+ requestId,
612
+ text: accumulated,
613
+ done: false
614
+ });
615
+ break;
616
+ case "tool-call":
617
+ streamToolCalls.push({
618
+ id: part.toolCallId,
619
+ name: part.toolName,
620
+ arguments: part.input
621
+ });
622
+ onStreamChunk({
623
+ requestId,
624
+ text: accumulated,
625
+ done: false,
626
+ toolCalls: [...streamToolCalls]
627
+ });
628
+ break;
629
+ case "tool-result":
630
+ streamToolResults.push({
631
+ toolCallId: part.toolCallId,
632
+ result: part.output
633
+ });
634
+ onStreamChunk({
635
+ requestId,
636
+ text: accumulated,
637
+ done: false,
638
+ toolCalls: [...streamToolCalls],
639
+ toolResults: [...streamToolResults]
640
+ });
641
+ break;
642
+ case "error":
643
+ streamError = part.error instanceof Error ? part.error.message : String(part.error);
644
+ break;
645
+ }
646
+ } catch (err) {
647
+ streamError = err instanceof Error ? err.message : String(err);
648
+ }
649
+ if (streamError) {
650
+ onStreamChunk({
651
+ requestId,
652
+ text: accumulated,
653
+ done: true,
654
+ error: streamError
655
+ });
656
+ return {
657
+ message: {
658
+ role: "assistant",
659
+ content: accumulated || `Error: ${streamError}`,
660
+ toolCalls: streamToolCalls.length ? streamToolCalls : void 0,
661
+ timestamp: Date.now()
662
+ },
663
+ toolResults: streamToolResults.length ? streamToolResults : void 0
664
+ };
665
+ }
666
+ } else try {
667
+ for await (const _ of result.fullStream) {
668
+ if (_.type === "text-delta") accumulated += _.text;
669
+ if (_.type === "tool-call") streamToolCalls.push({
670
+ id: _.toolCallId,
671
+ name: _.toolName,
672
+ arguments: _.input
673
+ });
674
+ if (_.type === "tool-result") streamToolResults.push({
675
+ toolCallId: _.toolCallId,
676
+ result: _.output
677
+ });
678
+ if (_.type === "error") streamError = _.error instanceof Error ? _.error.message : String(_.error);
679
+ }
680
+ } catch (err) {
681
+ streamError = err instanceof Error ? err.message : String(err);
682
+ }
683
+ return {
684
+ message: {
685
+ role: "assistant",
686
+ content: accumulated || (streamError ? `Error: ${streamError}` : ""),
687
+ toolCalls: streamToolCalls.length ? streamToolCalls : void 0,
688
+ timestamp: Date.now()
689
+ },
690
+ toolResults: streamToolResults.length ? streamToolResults : void 0
691
+ };
692
+ }
693
+ return {
694
+ getToolDefinitions,
695
+ executeToolCall,
696
+ chat,
697
+ parseCommand,
698
+ formatToolResult
699
+ };
700
+ }
701
+ //#endregion
702
+ //#region src/server/crud.ts
703
+ const SCAFFOLD_TYPES = [
704
+ "page",
705
+ "api",
706
+ "layout",
707
+ "middleware",
708
+ "reuse"
709
+ ];
710
+ const SCAFFOLD_TYPE_SET = new Set(SCAFFOLD_TYPES);
711
+ function createCrudServer(options) {
712
+ const { cwd, hooks, getEnv, setEnv, getConfig, onFileChange, scaffoldOps } = options;
713
+ if (!scaffoldOps) throw new Error("[ubean:devtools] scaffoldOps is required for CRUD server. Make sure to pass scaffold functions via ubeanDevtoolsPlugin options.");
714
+ const fs = scaffoldOps.createFsOps(cwd);
715
+ /**
716
+ * Determine the correct base directory for a scaffold type, taking the
717
+ * project's `dir` config into account. Returns an absolute path.
718
+ *
719
+ * For the 'api' type:
720
+ * 1. Use `config.dir.routes` (default 'routes') relative to srcDir.
721
+ * 2. If that directory doesn't exist, fall back to src/routes.
722
+ *
723
+ * For other types, the scaffold function's built-in defaults are used.
724
+ */
725
+ function getBaseDirForType(type) {
726
+ if (type !== "api") return void 0;
727
+ const config = getConfig?.() ?? {};
728
+ const dir = config.dir ?? {};
729
+ const rawSrcDir = config.srcDir || "src";
730
+ const srcDir = isAbsolute(rawSrcDir) ? rawSrcDir : resolve(cwd, rawSrcDir);
731
+ const primaryDir = join(srcDir, dir.routes || "routes");
732
+ if (existsSync(primaryDir)) return primaryDir;
733
+ const fallbackDir = join(srcDir, "routes");
734
+ if (existsSync(fallbackDir)) return fallbackDir;
735
+ return primaryDir;
736
+ }
737
+ function normalizeResult(scaffoldRes) {
738
+ return {
739
+ success: !(scaffoldRes.errors && scaffoldRes.errors.length > 0),
740
+ created: scaffoldRes.created,
741
+ deleted: scaffoldRes.deleted,
742
+ restored: scaffoldRes.restored,
743
+ skipped: scaffoldRes.skipped,
744
+ errors: scaffoldRes.errors
745
+ };
746
+ }
747
+ async function notifyChange() {
748
+ if (onFileChange) await onFileChange();
749
+ }
750
+ async function create(params) {
751
+ const { type, path, method, schedule, content, force } = params;
752
+ if (hooks) await hooks.runHook("beforeCreate", {
753
+ type,
754
+ path,
755
+ content
756
+ });
757
+ try {
758
+ let result;
759
+ if (SCAFFOLD_TYPE_SET.has(type)) {
760
+ const baseDir = getBaseDirForType(type);
761
+ const scaffoldRes = await scaffoldOps?.scaffold({
762
+ cwd,
763
+ type,
764
+ path,
765
+ method,
766
+ force,
767
+ dry: false,
768
+ ...baseDir ? { baseDir } : {}
769
+ });
770
+ if (content && scaffoldRes?.created?.length) await fs.writeFile(scaffoldRes.created[0], content);
771
+ result = normalizeResult(scaffoldRes || {});
772
+ } else if (type === "cron") {
773
+ const cronDir = "src/server/crons";
774
+ const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
775
+ const filePath = join(cronDir, normalizedPath.endsWith(".ts") ? normalizedPath : `${normalizedPath}.ts`);
776
+ if (await fs.exists(filePath) && !force) result = {
777
+ success: false,
778
+ skipped: [filePath],
779
+ errors: ["File already exists"]
780
+ };
781
+ else {
782
+ const cronContent = content || `import { defineScheduled } from 'ubean';
783
+
784
+ export default defineScheduled({
785
+ schedule: '${schedule || "* * * * *"}',
786
+ async run() {
787
+ console.log('Cron job running');
788
+ }
789
+ });
790
+ `;
791
+ await fs.writeFile(filePath, cronContent);
792
+ result = {
793
+ success: true,
794
+ created: [filePath]
795
+ };
796
+ }
797
+ } else if (type === "plugin") {
798
+ const pluginDir = "src/plugins";
799
+ const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
800
+ const fileName = normalizedPath.endsWith(".ts") ? normalizedPath : `${normalizedPath}.ts`;
801
+ const filePath = join(pluginDir, fileName);
802
+ if (await fs.exists(filePath) && !force) result = {
803
+ success: false,
804
+ skipped: [filePath],
805
+ errors: ["File already exists"]
806
+ };
807
+ else {
808
+ const pluginContent = content || `import { definePlugin } from 'ubean';
809
+
810
+ export default definePlugin({
811
+ name: '${fileName.replace(/\.ts$/, "").replace(/[^\w]/g, "-")}',
812
+ setup() {
813
+ console.log('Plugin loaded');
814
+ }
815
+ });
816
+ `;
817
+ await fs.writeFile(filePath, pluginContent);
818
+ result = {
819
+ success: true,
820
+ created: [filePath]
821
+ };
822
+ }
823
+ } else result = {
824
+ success: false,
825
+ errors: [`Unsupported resource type: ${type}`]
826
+ };
827
+ if (hooks) await hooks.runHook("afterCreate", {
828
+ type,
829
+ path,
830
+ content
831
+ });
832
+ if (result.success) await notifyChange();
833
+ return result;
834
+ } catch (err) {
835
+ return {
836
+ success: false,
837
+ errors: [err instanceof Error ? err.message : String(err)]
838
+ };
839
+ }
840
+ }
841
+ async function read(params) {
842
+ const { type, path } = params;
843
+ try {
844
+ if (type === "env") {
845
+ if (getEnv) return {
846
+ success: true,
847
+ data: getEnv()
848
+ };
849
+ return {
850
+ success: false,
851
+ error: "Env not available"
852
+ };
853
+ }
854
+ if (type === "config") {
855
+ if (path) {
856
+ if (!await fs.exists(path)) return {
857
+ success: false,
858
+ error: `File not found: ${path}`
859
+ };
860
+ return {
861
+ success: true,
862
+ content: await fs.readFile(path)
863
+ };
864
+ }
865
+ if (getConfig) return {
866
+ success: true,
867
+ data: getConfig()
868
+ };
869
+ return {
870
+ success: false,
871
+ error: "Config not available"
872
+ };
873
+ }
874
+ if (!path) return {
875
+ success: false,
876
+ error: "Path is required for file read"
877
+ };
878
+ if (SCAFFOLD_TYPE_SET.has(type) || type === "cron" || type === "plugin") {
879
+ if (!await fs.exists(path)) return {
880
+ success: false,
881
+ error: `File not found: ${path}`
882
+ };
883
+ return {
884
+ success: true,
885
+ content: await fs.readFile(path)
886
+ };
887
+ }
888
+ return {
889
+ success: false,
890
+ error: `Unsupported resource type: ${type}`
891
+ };
892
+ } catch (err) {
893
+ return {
894
+ success: false,
895
+ error: err instanceof Error ? err.message : String(err)
896
+ };
897
+ }
898
+ }
899
+ async function update(params) {
900
+ const { type, path, key, content, value } = params;
901
+ if (hooks) await hooks.runHook("beforeUpdate", {
902
+ type,
903
+ path,
904
+ key,
905
+ content,
906
+ value
907
+ });
908
+ try {
909
+ let result;
910
+ if (type === "env") if (!key) result = {
911
+ success: false,
912
+ errors: ["Key is required for env update"]
913
+ };
914
+ else if (!setEnv || !getEnv) result = {
915
+ success: false,
916
+ errors: ["Env not available"]
917
+ };
918
+ else {
919
+ const env = { ...getEnv() };
920
+ if (value !== void 0) env[key] = value;
921
+ else delete env[key];
922
+ setEnv(env);
923
+ result = {
924
+ success: true,
925
+ updated: [`env:${key}`]
926
+ };
927
+ }
928
+ else if (type === "config") if (!path) result = {
929
+ success: false,
930
+ errors: ["Path is required for config file update"]
931
+ };
932
+ else if (!await fs.exists(path)) result = {
933
+ success: false,
934
+ errors: [`File not found: ${path}`]
935
+ };
936
+ else {
937
+ await fs.writeFile(path, content || "");
938
+ result = {
939
+ success: true,
940
+ updated: [path]
941
+ };
942
+ }
943
+ else if (!path) result = {
944
+ success: false,
945
+ errors: ["Path is required for file update"]
946
+ };
947
+ else if (SCAFFOLD_TYPE_SET.has(type) || type === "cron" || type === "plugin") if (!await fs.exists(path)) result = {
948
+ success: false,
949
+ errors: [`File not found: ${path}`]
950
+ };
951
+ else {
952
+ await fs.writeFile(path, content || "");
953
+ result = {
954
+ success: true,
955
+ updated: [path]
956
+ };
957
+ }
958
+ else result = {
959
+ success: false,
960
+ errors: [`Unsupported resource type: ${type}`]
961
+ };
962
+ if (hooks) await hooks.runHook("afterUpdate", {
963
+ type,
964
+ path,
965
+ key,
966
+ content,
967
+ value
968
+ });
969
+ if (result.success) await notifyChange();
970
+ return result;
971
+ } catch (err) {
972
+ return {
973
+ success: false,
974
+ errors: [err instanceof Error ? err.message : String(err)]
975
+ };
976
+ }
977
+ }
978
+ async function del(params) {
979
+ const { type, path, key, force } = params;
980
+ if (hooks) await hooks.runHook("beforeDelete", {
981
+ type,
982
+ path,
983
+ key
984
+ });
985
+ try {
986
+ let result;
987
+ if (type === "env") if (!key) result = {
988
+ success: false,
989
+ errors: ["Key is required for env delete"]
990
+ };
991
+ else if (!setEnv || !getEnv) result = {
992
+ success: false,
993
+ errors: ["Env not available"]
994
+ };
995
+ else {
996
+ const env = { ...getEnv() };
997
+ delete env[key];
998
+ setEnv(env);
999
+ result = {
1000
+ success: true,
1001
+ deleted: [`env:${key}`]
1002
+ };
1003
+ }
1004
+ else if (!path) result = {
1005
+ success: false,
1006
+ errors: ["Path is required for file delete"]
1007
+ };
1008
+ else if (SCAFFOLD_TYPE_SET.has(type)) {
1009
+ const baseDir = getBaseDirForType(type);
1010
+ result = normalizeResult(await scaffoldOps?.deleteScaffold({
1011
+ cwd,
1012
+ type,
1013
+ path,
1014
+ force,
1015
+ dry: false,
1016
+ ...baseDir ? { baseDir } : {}
1017
+ }) || {});
1018
+ } else if (type === "cron" || type === "plugin") if (!await fs.exists(path)) result = {
1019
+ success: false,
1020
+ errors: [`File not found: ${path}`]
1021
+ };
1022
+ else {
1023
+ if (force) await fs.remove(path);
1024
+ else await fs.createBackup(path, { removeOriginal: true });
1025
+ result = {
1026
+ success: true,
1027
+ deleted: [path]
1028
+ };
1029
+ }
1030
+ else result = {
1031
+ success: false,
1032
+ errors: [`Unsupported resource type: ${type}`]
1033
+ };
1034
+ if (hooks) await hooks.runHook("afterDelete", {
1035
+ type,
1036
+ path,
1037
+ key
1038
+ });
1039
+ if (result.success) await notifyChange();
1040
+ return result;
1041
+ } catch (err) {
1042
+ return {
1043
+ success: false,
1044
+ errors: [err instanceof Error ? err.message : String(err)]
1045
+ };
1046
+ }
1047
+ }
1048
+ async function restore(path) {
1049
+ try {
1050
+ for (const type of SCAFFOLD_TYPES) {
1051
+ const baseDir = getBaseDirForType(type);
1052
+ const scaffoldRes = await scaffoldOps?.recoverScaffold({
1053
+ cwd,
1054
+ type,
1055
+ path,
1056
+ dry: false,
1057
+ ...baseDir ? { baseDir } : {}
1058
+ });
1059
+ if (scaffoldRes?.restored?.length) {
1060
+ await notifyChange();
1061
+ return normalizeResult(scaffoldRes);
1062
+ }
1063
+ }
1064
+ const backupPath = `${path}.bak`;
1065
+ if (await fs.exists(backupPath)) {
1066
+ await fs.copyFile(backupPath, path);
1067
+ await fs.removeBackup(path);
1068
+ await notifyChange();
1069
+ return {
1070
+ success: true,
1071
+ restored: [path]
1072
+ };
1073
+ }
1074
+ return {
1075
+ success: false,
1076
+ errors: [`No backup found for ${path}`]
1077
+ };
1078
+ } catch (err) {
1079
+ return {
1080
+ success: false,
1081
+ errors: [err instanceof Error ? err.message : String(err)]
1082
+ };
1083
+ }
1084
+ }
1085
+ return {
1086
+ create,
1087
+ read,
1088
+ update,
1089
+ delete: del,
1090
+ restore
1091
+ };
1092
+ }
1093
+ //#endregion
1094
+ //#region src/server/terminal.ts
1095
+ /**
1096
+ * Terminal session manager — spawns shell processes and buffers output
1097
+ * for polling-based retrieval by the xterm.js frontend.
1098
+ *
1099
+ * On macOS/Linux, uses Python's `pty` module to create a real pseudo-terminal.
1100
+ * This gives the shell (bash/zsh) a proper TTY, enabling:
1101
+ * - Input echo (characters appear as you type)
1102
+ * - Line editing (arrow keys, Ctrl+A/E, etc.)
1103
+ * - Tab completion
1104
+ * - Full TUI apps (vim, htop, etc.)
1105
+ *
1106
+ * Python3 is pre-installed on macOS (via Xcode CLT) and most Linux distros.
1107
+ * If Python3 is not available, falls back to direct shell spawn without a PTY
1108
+ * (commands still execute, but without echo or line editing).
1109
+ *
1110
+ * On Windows, cmd.exe/PowerShell work natively with pipe stdio — no PTY needed.
1111
+ */
1112
+ /**
1113
+ * Python one-liner that creates a PTY and spawns the given command.
1114
+ * `pty.spawn` handles fork/exec/relay: the child gets the PTY slave as its
1115
+ * controlling terminal, while the parent relays between its own stdin/stdout
1116
+ * and the PTY master. This gives the shell a real TTY with full interactive
1117
+ * features (echo, readline, TUI support).
1118
+ */
1119
+ const PTY_SCRIPT = "import pty, sys; pty.spawn(sys.argv[1:])";
1120
+ let _pythonAvailable = null;
1121
+ function isPythonAvailable() {
1122
+ if (_pythonAvailable !== null) return _pythonAvailable;
1123
+ if (process.platform === "win32") {
1124
+ _pythonAvailable = false;
1125
+ return false;
1126
+ }
1127
+ try {
1128
+ execSync("python3 --version", {
1129
+ stdio: "ignore",
1130
+ timeout: 3e3
1131
+ });
1132
+ _pythonAvailable = true;
1133
+ } catch {
1134
+ _pythonAvailable = false;
1135
+ }
1136
+ return _pythonAvailable;
1137
+ }
1138
+ /** Warning messages from bash/zsh when running -i without a real TTY (fallback only). */
1139
+ const TTY_WARNING_RE = /^(?:bash|zsh):\s+(?:cannot set terminal process group|no job control in this shell)/;
1140
+ function createTerminalServer() {
1141
+ const sessions = /* @__PURE__ */ new Map();
1142
+ function getDefaultShell() {
1143
+ if (process.platform === "win32") return process.env.COMSPEC || "cmd.exe";
1144
+ return process.env.SHELL || "/bin/bash";
1145
+ }
1146
+ /**
1147
+ * Build the spawn command for the current platform.
1148
+ *
1149
+ * On macOS/Linux with Python3 available: wraps the shell in `python3 -c`
1150
+ * with a `pty.spawn` script so the shell gets a real PTY. Without Python3,
1151
+ * spawns the shell directly (degraded experience: no echo, no readline).
1152
+ * On Windows: spawns cmd.exe/PowerShell directly (pipes work natively).
1153
+ */
1154
+ function buildSpawnCommand(shell, shellArgs) {
1155
+ if (process.platform === "win32") return {
1156
+ command: shell,
1157
+ args: shellArgs,
1158
+ usingPty: false
1159
+ };
1160
+ if (isPythonAvailable()) return {
1161
+ command: "python3",
1162
+ args: [
1163
+ "-c",
1164
+ PTY_SCRIPT,
1165
+ shell,
1166
+ ...shellArgs
1167
+ ],
1168
+ usingPty: true
1169
+ };
1170
+ return {
1171
+ command: shell,
1172
+ args: shellArgs,
1173
+ usingPty: false
1174
+ };
1175
+ }
1176
+ function start(params) {
1177
+ const id = randomUUID();
1178
+ const { command, args, usingPty } = buildSpawnCommand(params.shell || getDefaultShell(), process.platform === "win32" ? [] : ["-i"]);
1179
+ const cols = params.cols || 80;
1180
+ const rows = params.rows || 24;
1181
+ const proc = spawn(command, args, {
1182
+ cwd: params.cwd,
1183
+ env: {
1184
+ ...process.env,
1185
+ TERM: "xterm-256color",
1186
+ COLORTERM: "truecolor",
1187
+ FORCE_COLOR: "1",
1188
+ UBEAN_TERM_COLS: String(cols),
1189
+ UBEAN_TERM_ROWS: String(rows),
1190
+ LSCOLORS: "Gxfxcxdxbxegedabagacad",
1191
+ LS_COLORS: "di=34:ln=36:so=35:pi=33:ex=32:bd=34:cd=34:su=41;37:sg=41;37:tw=42;37:ow=42;37"
1192
+ },
1193
+ stdio: [
1194
+ "pipe",
1195
+ "pipe",
1196
+ "pipe"
1197
+ ]
1198
+ });
1199
+ const session = {
1200
+ id,
1201
+ proc,
1202
+ cwd: params.cwd,
1203
+ cols,
1204
+ rows,
1205
+ buffer: "",
1206
+ exited: false,
1207
+ exitCode: null
1208
+ };
1209
+ if (!usingPty && process.platform !== "win32") session.buffer += "\x1B[33m⚠ Terminal running without PTY (python3 not found).\r\n Commands execute but input echo and line editing are disabled.\r\n Install python3 for full terminal support.\x1B[0m\r\n\r\n";
1210
+ proc.stdout?.on("data", (data) => {
1211
+ session.buffer += data.toString();
1212
+ });
1213
+ proc.stderr?.on("data", (data) => {
1214
+ const text = data.toString();
1215
+ if (usingPty) session.buffer += text;
1216
+ else {
1217
+ const lines = text.split("\n").filter((line) => !TTY_WARNING_RE.test(line.trim()));
1218
+ if (lines.length > 0) session.buffer += lines.join("\n");
1219
+ }
1220
+ });
1221
+ proc.on("exit", (code) => {
1222
+ session.exited = true;
1223
+ session.exitCode = code;
1224
+ session.buffer += `\r\n\x1b[2m[Process exited with code ${code}]\x1b[0m\r\n`;
1225
+ });
1226
+ proc.on("error", (err) => {
1227
+ session.buffer += `\r\n\x1b[31m[Error: ${err.message}]\x1b[0m\r\n`;
1228
+ session.exited = true;
1229
+ session.exitCode = -1;
1230
+ });
1231
+ sessions.set(id, session);
1232
+ return { sessionId: id };
1233
+ }
1234
+ function input(sessionId, data) {
1235
+ const session = sessions.get(sessionId);
1236
+ if (!session || !session.proc || session.exited) return false;
1237
+ session.proc.stdin?.write(data);
1238
+ return true;
1239
+ }
1240
+ function resize(sessionId, cols, rows) {
1241
+ const session = sessions.get(sessionId);
1242
+ if (!session) return false;
1243
+ session.cols = cols;
1244
+ session.rows = rows;
1245
+ return true;
1246
+ }
1247
+ function poll(sessionId) {
1248
+ const session = sessions.get(sessionId);
1249
+ if (!session) return {
1250
+ data: "",
1251
+ exited: true,
1252
+ exitCode: -1
1253
+ };
1254
+ const data = session.buffer;
1255
+ session.buffer = "";
1256
+ return {
1257
+ data,
1258
+ exited: session.exited,
1259
+ exitCode: session.exitCode
1260
+ };
1261
+ }
1262
+ function kill(sessionId) {
1263
+ const session = sessions.get(sessionId);
1264
+ if (!session) return false;
1265
+ if (session.proc && !session.exited) try {
1266
+ session.proc.kill("SIGTERM");
1267
+ } catch {}
1268
+ sessions.delete(sessionId);
1269
+ return true;
1270
+ }
1271
+ function killAll() {
1272
+ for (const id of sessions.keys()) kill(id);
1273
+ }
1274
+ return {
1275
+ start,
1276
+ input,
1277
+ resize,
1278
+ poll,
1279
+ kill,
1280
+ killAll
1281
+ };
1282
+ }
1283
+ //#endregion
1284
+ //#region ../../node_modules/.pnpm/devframe@0.7.14_cac@7.0.0_srvx@0.11.22_typescript@6.0.3/node_modules/devframe/dist/define-BLWPsH6y.mjs
1285
+ function createDefineWrapperWithContext() {
1286
+ return function defineRpcFunctionWithContext(definition) {
1287
+ return definition;
1288
+ };
1289
+ }
1290
+ //#endregion
1291
+ //#region ../../node_modules/.pnpm/@vitejs+devtools-kit@0.4.8_@voidzero-dev+vite-plus-core@0.2.6_cac@7.0.0_srvx@0.11.22_typescript@6.0.3/node_modules/@vitejs/devtools-kit/dist/index.js
1292
+ const defineRpcFunction = createDefineWrapperWithContext();
1293
+ //#endregion
1294
+ //#region src/node/rpc/ai.ts
1295
+ /**
1296
+ * AI RPC functions — `ubean:ai:tools`, `ubean:ai:chat`, and `ubean:ai:chat-stream`.
1297
+ *
1298
+ * `ubean:ai:chat` — non-streaming chat (returns full response).
1299
+ * `ubean:ai:chat-stream` — streaming chat; pushes chunks to the
1300
+ * `ubean:ai:stream` sharedState key via the `onStreamChunk` callback
1301
+ * wired in `createAiServer`. The client subscribes to that sharedState
1302
+ * and filters by `requestId`.
1303
+ */
1304
+ function createAiRpcFunctions(ai) {
1305
+ return [
1306
+ defineRpcFunction({
1307
+ name: "ubean:ai:tools",
1308
+ type: "query",
1309
+ setup: () => ({ handler: () => ai.getToolDefinitions() })
1310
+ }),
1311
+ defineRpcFunction({
1312
+ name: "ubean:ai:chat",
1313
+ type: "action",
1314
+ setup: () => ({ handler: (params) => ai.chat({
1315
+ messages: params.messages,
1316
+ apiKey: params.apiKey,
1317
+ apiBase: params.apiBase,
1318
+ model: params.model
1319
+ }) })
1320
+ }),
1321
+ defineRpcFunction({
1322
+ name: "ubean:ai:chat-stream",
1323
+ type: "action",
1324
+ setup: () => ({ handler: (params) => ai.chat({
1325
+ messages: params.messages,
1326
+ apiKey: params.apiKey,
1327
+ apiBase: params.apiBase,
1328
+ model: params.model,
1329
+ requestId: params.requestId
1330
+ }) })
1331
+ })
1332
+ ];
1333
+ }
1334
+ //#endregion
1335
+ //#region src/node/rpc/crud.ts
1336
+ /**
1337
+ * CRUD RPC functions — `ubean:crud:create/read/update/delete/restore`.
1338
+ *
1339
+ * Each function delegates to the existing `createCrudServer` which
1340
+ * encapsulates all file-scaffolding, backup, and hook logic. The RPC
1341
+ * layer is purely a transport adapter.
1342
+ */
1343
+ function createCrudRpcFunctions(crud) {
1344
+ return [
1345
+ defineRpcFunction({
1346
+ name: "ubean:crud:create",
1347
+ type: "action",
1348
+ setup: () => ({ handler: (params) => crud.create(params) })
1349
+ }),
1350
+ defineRpcFunction({
1351
+ name: "ubean:crud:read",
1352
+ type: "query",
1353
+ setup: () => ({ handler: (params) => crud.read(params) })
1354
+ }),
1355
+ defineRpcFunction({
1356
+ name: "ubean:crud:update",
1357
+ type: "action",
1358
+ setup: () => ({ handler: (params) => crud.update(params) })
1359
+ }),
1360
+ defineRpcFunction({
1361
+ name: "ubean:crud:delete",
1362
+ type: "action",
1363
+ setup: () => ({ handler: (params) => crud.delete(params) })
1364
+ }),
1365
+ defineRpcFunction({
1366
+ name: "ubean:crud:restore",
1367
+ type: "action",
1368
+ setup: () => ({ handler: (path) => crud.restore(path) })
1369
+ })
1370
+ ];
1371
+ }
1372
+ //#endregion
1373
+ //#region src/node/rpc/info.ts
1374
+ /**
1375
+ * Info RPC functions — `ubean:get-info` and `ubean:get-env`.
1376
+ *
1377
+ * In the sharedState model, `ubean:get-info` is a fallback for clients
1378
+ * that haven't subscribed to the `ubean:info` state yet. Most clients
1379
+ * should use `client.sharedState.get('ubean:info')` instead.
1380
+ */
1381
+ function createInfoRpcFunctions(opts) {
1382
+ return [defineRpcFunction({
1383
+ name: "ubean:get-info",
1384
+ type: "query",
1385
+ setup: () => ({ handler: () => opts.state.value() })
1386
+ }), defineRpcFunction({
1387
+ name: "ubean:get-env",
1388
+ type: "query",
1389
+ setup: () => ({ handler: () => maskSensitiveEnv(opts.getEnvData()) })
1390
+ })];
1391
+ }
1392
+ //#endregion
1393
+ //#region src/node/rpc/playground.ts
1394
+ /**
1395
+ * Playground RPC function — `ubean:playground:invoke`.
1396
+ *
1397
+ * Forwards an HTTP request to the Hono app in-process (no network
1398
+ * round-trip), mirroring the `internalFetch` / `callInternal` pattern
1399
+ * used elsewhere in ubean.
1400
+ */
1401
+ function createPlaygroundRpcFunctions(opts) {
1402
+ return [defineRpcFunction({
1403
+ name: "ubean:playground:invoke",
1404
+ type: "action",
1405
+ setup: () => ({ handler: async (params) => {
1406
+ const app = opts.getApp?.();
1407
+ if (!app) return {
1408
+ status: 503,
1409
+ statusText: "Service Unavailable",
1410
+ body: { error: "App not available" },
1411
+ headers: {}
1412
+ };
1413
+ const url = `http://localhost${params.path}`;
1414
+ const headers = new Headers(params.headers || { "Content-Type": "application/json" });
1415
+ const hasBody = params.body !== void 0 && params.method !== "GET" && params.method !== "HEAD";
1416
+ const req = new Request(url, {
1417
+ method: params.method,
1418
+ headers,
1419
+ ...hasBody ? { body: JSON.stringify(params.body) } : {}
1420
+ });
1421
+ const res = await app.fetch(req);
1422
+ const resHeaders = {};
1423
+ res.headers.forEach((v, k) => {
1424
+ resHeaders[k] = v;
1425
+ });
1426
+ const contentType = res.headers.get("content-type") || "";
1427
+ let body;
1428
+ if (contentType.includes("application/json")) body = await res.json();
1429
+ else body = await res.text();
1430
+ return {
1431
+ status: res.status,
1432
+ statusText: res.statusText,
1433
+ body,
1434
+ headers: resHeaders
1435
+ };
1436
+ } })
1437
+ })];
1438
+ }
1439
+ //#endregion
1440
+ //#region src/node/rpc/terminal.ts
1441
+ /**
1442
+ * Terminal RPC functions — `ubean:terminal:start/input/resize/poll/kill`.
1443
+ *
1444
+ * Delegates to `createTerminalServer` which manages shell processes and
1445
+ * output buffering. The client polls for output via `ubean:terminal:poll`.
1446
+ */
1447
+ function createTerminalRpcFunctions(terminal) {
1448
+ return [
1449
+ defineRpcFunction({
1450
+ name: "ubean:terminal:start",
1451
+ type: "action",
1452
+ setup: () => ({ handler: (params) => Promise.resolve(terminal.start(params)) })
1453
+ }),
1454
+ defineRpcFunction({
1455
+ name: "ubean:terminal:input",
1456
+ type: "action",
1457
+ setup: () => ({ handler: (params) => Promise.resolve(terminal.input(params.sessionId, params.data)) })
1458
+ }),
1459
+ defineRpcFunction({
1460
+ name: "ubean:terminal:resize",
1461
+ type: "action",
1462
+ setup: () => ({ handler: (params) => Promise.resolve(terminal.resize(params.sessionId, params.cols, params.rows)) })
1463
+ }),
1464
+ defineRpcFunction({
1465
+ name: "ubean:terminal:poll",
1466
+ type: "query",
1467
+ setup: () => ({ handler: (params) => Promise.resolve(terminal.poll(params.sessionId)) })
1468
+ }),
1469
+ defineRpcFunction({
1470
+ name: "ubean:terminal:kill",
1471
+ type: "action",
1472
+ setup: () => ({ handler: (params) => Promise.resolve(terminal.kill(params.sessionId)) })
1473
+ })
1474
+ ];
1475
+ }
1476
+ //#endregion
1477
+ //#region src/node/rpc/index.ts
1478
+ /**
1479
+ * Returns an array of `defineRpcFunction` definitions. The return type is
1480
+ * intentionally loose (`any[]`) because the DTK `ctx.rpc.register()` accepts
1481
+ * definitions parameterized on `ViteDevToolsNodeContext`, and the exact
1482
+ * generic instantiation varies per function — the individual functions are
1483
+ * already type-checked at their definition site.
1484
+ */
1485
+ function createAllRpcFunctions(deps) {
1486
+ return [
1487
+ ...createInfoRpcFunctions({
1488
+ state: deps.state,
1489
+ getEnvData: deps.getEnvData
1490
+ }),
1491
+ ...createCrudRpcFunctions(deps.crud),
1492
+ ...createAiRpcFunctions(deps.ai),
1493
+ ...createPlaygroundRpcFunctions({ getApp: deps.getApp }),
1494
+ ...createTerminalRpcFunctions(deps.terminal)
1495
+ ];
1496
+ }
1497
+ //#endregion
1498
+ //#region src/node/state.ts
1499
+ /**
1500
+ * SharedState management for the DTK integration.
1501
+ *
1502
+ * Replaces the old 3-second polling pattern: the server builds a
1503
+ * `DevToolsInfo` snapshot and pushes patches to all connected clients
1504
+ * via `ctx.rpc.sharedState`.
1505
+ */
1506
+ /** SharedState key for the ubean devtools info snapshot. */
1507
+ const UBEAN_INFO_STATE_KEY = "ubean:info";
1508
+ /** Build a fresh `DevToolsInfo` from scan + config metadata. */
1509
+ function buildDevToolsInfo(opts) {
1510
+ const { scan, configMeta, customTabs, ai, startTime, envData } = opts;
1511
+ const rootDir = configMeta?.rootDir || "";
1512
+ const srcDir = configMeta?.srcDir || (rootDir ? join(rootDir, "src") : "src");
1513
+ const dirs = configMeta?.dir ?? {};
1514
+ const srcRel = rootDir ? relative(rootDir, srcDir).replace(/\\/g, "/") || "src" : "src";
1515
+ const routesDir = dirs.routes || "routes";
1516
+ const pagesDir = dirs.pages || "pages";
1517
+ const middlewareDir = dirs.middleware || "middleware";
1518
+ const layoutsDir = dirs.layouts || "layouts";
1519
+ const cronsDir = dirs.crons || "crons";
1520
+ const routesPrefix = `${srcRel}/${routesDir}`;
1521
+ const pagesPrefix = `${srcRel}/${pagesDir}`;
1522
+ const middlewarePrefix = `${srcRel}/${middlewareDir}`;
1523
+ const layoutsPrefix = `${srcRel}/${layoutsDir}`;
1524
+ const cronsPrefix = `${srcRel}/${cronsDir}`;
1525
+ const makeFilePath = (fullPath, relPath, prefix) => {
1526
+ if (!fullPath) return "";
1527
+ if (rootDir) {
1528
+ const rel = relative(rootDir, fullPath).replace(/\\/g, "/");
1529
+ if (rel && !rel.startsWith("..")) return rel;
1530
+ }
1531
+ return `${prefix}/${relPath.replace(/\\/g, "/")}`;
1532
+ };
1533
+ const routes = scan ? scan.apiRoutes.map((r) => ({
1534
+ method: (r.method || "GET").toUpperCase(),
1535
+ path: r.route,
1536
+ filePath: makeFilePath(r.fullPath, r.relativePath, routesPrefix)
1537
+ })) : [];
1538
+ const defaultLayoutName = scan?.layouts.find((l) => l.isDefault)?.name;
1539
+ const pagesList = scan ? scan.pages.filter((p) => !p.isReuse).map((p) => ({
1540
+ path: p.route,
1541
+ name: p.name,
1542
+ filePath: makeFilePath(p.fullPath, p.relativePath, pagesPrefix),
1543
+ layout: p.layout === false ? void 0 : p.layout || defaultLayoutName || void 0
1544
+ })) : [];
1545
+ const middlewaresList = scan ? scan.middlewares.map((m) => ({
1546
+ path: m.global ? "*" : m.relativePath,
1547
+ filePath: makeFilePath(m.fullPath, m.relativePath, middlewarePrefix),
1548
+ global: m.global
1549
+ })) : [];
1550
+ const layoutsList = scan ? scan.layouts.map((l) => ({
1551
+ name: l.name,
1552
+ path: l.path,
1553
+ filePath: makeFilePath(l.fullPath, l.relativePath, layoutsPrefix),
1554
+ isDefault: l.isDefault
1555
+ })) : [];
1556
+ const cronsList = scan ? scan.crons.map((c) => ({
1557
+ name: c.name,
1558
+ filePath: makeFilePath(c.fullPath, c.relativePath, cronsPrefix)
1559
+ })) : [];
1560
+ return {
1561
+ version: "0.0.1",
1562
+ startTime,
1563
+ config: configMeta ? {
1564
+ preset: configMeta.preset,
1565
+ rootDir: configMeta.rootDir,
1566
+ srcDir: configMeta.srcDir,
1567
+ dir: configMeta.dir
1568
+ } : {},
1569
+ env: maskSensitiveEnv(envData),
1570
+ pages: pagesList.length,
1571
+ apiRoutes: routes.length,
1572
+ middleware: middlewaresList.length,
1573
+ layouts: layoutsList.length,
1574
+ crons: cronsList.length,
1575
+ presets: configMeta ? [configMeta.preset] : [],
1576
+ routes,
1577
+ pagesList,
1578
+ middlewaresList,
1579
+ layoutsList,
1580
+ cronsList,
1581
+ customTabs,
1582
+ openAPI: configMeta?.openAPI ?? { enabled: false },
1583
+ database: configMeta?.database,
1584
+ ai: {
1585
+ enabled: !!(ai?.apiKey || process.env.DEEPSEEK_API_KEY || process.env.UBEAN_AI_API_KEY || process.env.OPENAI_API_KEY),
1586
+ provider: ai?.apiBase?.includes("deepseek") ? "deepseek" : ai?.apiBase?.includes("anthropic") ? "anthropic" : "deepseek",
1587
+ model: ai?.model || (ai?.apiBase?.includes("deepseek") || !ai?.apiBase ? "deepseek-chat" : ai.model)
1588
+ }
1589
+ };
1590
+ }
1591
+ /** Initial empty state (used before scan data arrives). */
1592
+ function emptyDevToolsInfo(startTime) {
1593
+ return {
1594
+ version: "0.0.1",
1595
+ startTime,
1596
+ config: {},
1597
+ env: {},
1598
+ pages: 0,
1599
+ apiRoutes: 0,
1600
+ middleware: 0,
1601
+ layouts: 0,
1602
+ crons: 0,
1603
+ presets: [],
1604
+ routes: [],
1605
+ pagesList: [],
1606
+ middlewaresList: [],
1607
+ layoutsList: [],
1608
+ cronsList: [],
1609
+ customTabs: [],
1610
+ ai: { enabled: false }
1611
+ };
1612
+ }
1613
+ /**
1614
+ * Create or retrieve the `ubean:info` shared state.
1615
+ * Called once during `devtools.setup`.
1616
+ */
1617
+ async function initSharedState(ctx, initialValue) {
1618
+ return ctx.rpc.sharedState.get(UBEAN_INFO_STATE_KEY, { initialValue });
1619
+ }
1620
+ /**
1621
+ * Push a fresh `DevToolsInfo` into the shared state, triggering
1622
+ * patch-sync to all connected clients.
1623
+ */
1624
+ function refreshSharedState(state, info) {
1625
+ state.mutate((draft) => {
1626
+ Object.assign(draft, info);
1627
+ });
1628
+ }
1629
+ //#endregion
1630
+ //#region src/node/index.ts
1631
+ /**
1632
+ * Path to the pre-built client SPA. Resolved relative to the built server
1633
+ * bundle (`dist/index.mjs`), so this points to `dist/client/` at runtime.
1634
+ */
1635
+ const CLIENT_DIST = resolve(dirname(fileURLToPath(import.meta.url)), "client");
1636
+ /**
1637
+ * Ubean DevTools as a Vite plugin.
1638
+ *
1639
+ * Registers a dock entry (iframe pointing at the pre-built SPA), hosts the
1640
+ * SPA's static assets, and wires up the type-safe RPC + sharedState used by
1641
+ * the SPA. This replaces the previous Hono-middleware-based transport with
1642
+ * the Vite DevTools Kit (DTK) birpc + shared-state machinery.
1643
+ */
1644
+ function ubeanDevtoolsPlugin(options = { getCwd: () => process.cwd() }) {
1645
+ return {
1646
+ name: "ubean:devtools",
1647
+ devtools: { async setup(ctx) {
1648
+ console.log("[ubean:devtools] devtools.setup hook fired — DTK integration active");
1649
+ ctx.views.hostStatic("/_devtools/", CLIENT_DIST);
1650
+ const viteServer = ctx.viteServer;
1651
+ if (viteServer?.middlewares) {
1652
+ const serveConnectionMeta = (req, res) => {
1653
+ const targetUrl = `${req.headers["x-forwarded-proto"] || "http"}://${req.headers.host || "localhost:9527"}/__devtools/__connection.json`;
1654
+ fetch(targetUrl).then(async (metaRes) => {
1655
+ const meta = await metaRes.json();
1656
+ if (meta.websocket && typeof meta.websocket === "object") meta.websocket = {
1657
+ ...meta.websocket,
1658
+ path: "/__devtools/__ws"
1659
+ };
1660
+ res.setHeader("Content-Type", "application/json");
1661
+ res.end(JSON.stringify(meta));
1662
+ }).catch(() => {
1663
+ res.statusCode = 500;
1664
+ res.setHeader("Content-Type", "application/json");
1665
+ res.end(JSON.stringify({ error: "Failed to resolve connection meta" }));
1666
+ });
1667
+ };
1668
+ viteServer.middlewares.use("/_devtools/__connection.json", serveConnectionMeta);
1669
+ viteServer.middlewares.use("/__connection.json", serveConnectionMeta);
1670
+ }
1671
+ const SPA_BASE = "/_devtools/index.html";
1672
+ const dockEntries = [
1673
+ {
1674
+ id: "ubean:overview",
1675
+ title: "Overview",
1676
+ icon: "lucide:layout-dashboard",
1677
+ url: `${SPA_BASE}#/overview`,
1678
+ order: 100
1679
+ },
1680
+ {
1681
+ id: "ubean:pages",
1682
+ title: "Pages",
1683
+ icon: "lucide:file-text",
1684
+ url: `${SPA_BASE}#/pages`,
1685
+ order: 95
1686
+ },
1687
+ {
1688
+ id: "ubean:api",
1689
+ title: "API",
1690
+ icon: "lucide:send",
1691
+ url: `${SPA_BASE}#/api`,
1692
+ order: 90
1693
+ },
1694
+ {
1695
+ id: "ubean:middleware",
1696
+ title: "Middleware",
1697
+ icon: "lucide:layers",
1698
+ url: `${SPA_BASE}#/middleware`,
1699
+ order: 85
1700
+ },
1701
+ {
1702
+ id: "ubean:crons",
1703
+ title: "Crons",
1704
+ icon: "lucide:clock",
1705
+ url: `${SPA_BASE}#/crons`,
1706
+ order: 80
1707
+ },
1708
+ {
1709
+ id: "ubean:config",
1710
+ title: "Config",
1711
+ icon: "lucide:settings",
1712
+ url: `${SPA_BASE}#/config`,
1713
+ order: 75
1714
+ },
1715
+ {
1716
+ id: "ubean:env",
1717
+ title: "Env",
1718
+ icon: "lucide:terminal",
1719
+ url: `${SPA_BASE}#/env`,
1720
+ order: 70
1721
+ },
1722
+ {
1723
+ id: "ubean:api-docs",
1724
+ title: "API Docs",
1725
+ icon: "lucide:book-open",
1726
+ url: `${SPA_BASE}#/api-docs`,
1727
+ order: 65
1728
+ },
1729
+ {
1730
+ id: "ubean:database",
1731
+ title: "Database",
1732
+ icon: "lucide:database",
1733
+ url: `${SPA_BASE}#/database`,
1734
+ order: 60
1735
+ },
1736
+ {
1737
+ id: "ubean:terminal",
1738
+ title: "Terminal",
1739
+ icon: "lucide:square-terminal",
1740
+ url: `${SPA_BASE}#/terminal`,
1741
+ order: 58
1742
+ },
1743
+ {
1744
+ id: "ubean:ai",
1745
+ title: "AI",
1746
+ icon: "lucide:sparkles",
1747
+ url: `${SPA_BASE}#/ai`,
1748
+ order: 55
1749
+ }
1750
+ ];
1751
+ for (const entry of dockEntries) ctx.docks.register({
1752
+ id: entry.id,
1753
+ title: entry.title,
1754
+ icon: entry.icon,
1755
+ type: "iframe",
1756
+ url: entry.url,
1757
+ category: "framework",
1758
+ defaultOrder: entry.order
1759
+ });
1760
+ const startTime = Date.now();
1761
+ const customTabs = options.getCustomTabs?.() ?? [];
1762
+ const envData = snapshotEnv(options.getCwd());
1763
+ const initialInfo = options.getScanResult ? buildDevToolsInfo({
1764
+ scan: options.getScanResult(),
1765
+ configMeta: options.getConfigMeta?.() ?? null,
1766
+ customTabs,
1767
+ ai: options.ai,
1768
+ startTime,
1769
+ envData
1770
+ }) : emptyDevToolsInfo(startTime);
1771
+ for (const tab of customTabs) ctx.docks.register({
1772
+ id: `ubean:custom:${tab.id}`,
1773
+ title: tab.label,
1774
+ icon: tab.icon || "lucide:plugin",
1775
+ type: "iframe",
1776
+ url: tab.src,
1777
+ category: "framework",
1778
+ defaultOrder: 50
1779
+ });
1780
+ const state = await initSharedState(ctx, initialInfo);
1781
+ const aiStreamState = await ctx.rpc.sharedState.get("ubean:ai:stream", { initialValue: {
1782
+ requestId: "",
1783
+ text: "",
1784
+ done: true
1785
+ } });
1786
+ const hooks = createDevToolsHooks();
1787
+ const crud = createCrudServer({
1788
+ cwd: options.getCwd(),
1789
+ hooks,
1790
+ scaffoldOps: options.scaffoldOps,
1791
+ getEnv: () => envData,
1792
+ setEnv: (env) => {
1793
+ Object.keys(envData).forEach((k) => delete envData[k]);
1794
+ Object.assign(envData, env);
1795
+ },
1796
+ getConfig: () => options.getConfigMeta?.() ?? {},
1797
+ onFileChange: async () => {
1798
+ if (options.triggerRescan) await options.triggerRescan();
1799
+ }
1800
+ });
1801
+ const fns = createAllRpcFunctions({
1802
+ state,
1803
+ getEnvData: () => envData,
1804
+ crud,
1805
+ ai: createAiServer(crud, () => state.value(), (chunk) => {
1806
+ aiStreamState.mutate(() => chunk);
1807
+ }),
1808
+ terminal: createTerminalServer(),
1809
+ getApp: options.getApp
1810
+ });
1811
+ for (const fn of fns) ctx.rpc.register(fn);
1812
+ const refresh = () => {
1813
+ const info = options.getScanResult ? buildDevToolsInfo({
1814
+ scan: options.getScanResult(),
1815
+ configMeta: options.getConfigMeta?.() ?? null,
1816
+ customTabs: options.getCustomTabs?.() ?? [],
1817
+ ai: options.ai,
1818
+ startTime,
1819
+ envData
1820
+ }) : initialInfo;
1821
+ refreshSharedState(state, info);
1822
+ };
1823
+ options.registerRefresh?.(refresh);
1824
+ } }
1825
+ };
1826
+ }
1827
+ /**
1828
+ * Parse a single .env file and return the key/value pairs.
1829
+ * - Ignores blank lines and `#` comments.
1830
+ * - Strips surrounding single/double quotes from values.
1831
+ */
1832
+ function parseEnvFile(filePath) {
1833
+ if (!existsSync(filePath)) return {};
1834
+ let content;
1835
+ try {
1836
+ content = readFileSync(filePath, "utf-8");
1837
+ } catch {
1838
+ return {};
1839
+ }
1840
+ const env = {};
1841
+ for (const line of content.split("\n")) {
1842
+ const trimmed = line.trim();
1843
+ if (!trimmed || trimmed.startsWith("#")) continue;
1844
+ const eqIndex = trimmed.indexOf("=");
1845
+ if (eqIndex === -1) continue;
1846
+ const key = trimmed.slice(0, eqIndex).trim();
1847
+ let value = trimmed.slice(eqIndex + 1).trim();
1848
+ const hashIndex = value.search(/\s+#.*$/);
1849
+ if (hashIndex !== -1 && !value.startsWith("\"") && !value.startsWith("'")) value = value.slice(0, hashIndex).trim();
1850
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1851
+ env[key] = value;
1852
+ }
1853
+ return env;
1854
+ }
1855
+ /**
1856
+ * Snapshot env vars relevant to the ubean app.
1857
+ *
1858
+ * Instead of dumping all of `process.env` (which includes noisy system vars
1859
+ * like PATH, HOME, SHELL, LANG, TERM, etc.), we only collect:
1860
+ * 1. Vars defined in .env / .env.local / .env.development / .env.development.local
1861
+ * 2. UBEAN_* prefixed vars from process.env (framework/CLI vars set by shell)
1862
+ *
1863
+ * Values from process.env override .env file values (shell overrides take
1864
+ * precedence), matching Vite's env loading semantics.
1865
+ */
1866
+ function snapshotEnv(cwd) {
1867
+ const envFiles = [
1868
+ ".env",
1869
+ ".env.local",
1870
+ ".env.development",
1871
+ ".env.development.local"
1872
+ ];
1873
+ const env = {};
1874
+ for (const file of envFiles) Object.assign(env, parseEnvFile(resolve(cwd, file)));
1875
+ for (const [key, value] of Object.entries(process.env)) if (typeof value === "string" && (key in env || key.startsWith("UBEAN_"))) env[key] = value;
1876
+ return env;
1877
+ }
1878
+ //#endregion
1879
+ //#region src/define-tab.ts
1880
+ const customTabs = [];
1881
+ function defineDevToolsTab(tab) {
1882
+ const exists = customTabs.findIndex((t) => t.id === tab.id);
1883
+ if (exists !== -1) customTabs[exists] = tab;
1884
+ else customTabs.push(tab);
1885
+ return tab;
1886
+ }
1887
+ function getCustomTabs() {
1888
+ return [...customTabs];
1889
+ }
1890
+ function clearCustomTabs() {
1891
+ customTabs.length = 0;
1892
+ }
1893
+ //#endregion
1894
+ export { UBEAN_INFO_STATE_KEY, buildDevToolsInfo, clearCustomTabs, createAiServer, createAllRpcFunctions, createCrudServer, createDevToolsHooks, createTerminalServer, defineDevToolsTab, defineRpcFunction, emptyDevToolsInfo, getCustomTabs, maskSensitiveEnv, ubeanDevtoolsPlugin };