@maintainer-pro/ai-bridge 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/package.json +1 -1
- package/src/daemon.mjs +439 -6
package/README.md
CHANGED
|
@@ -14,7 +14,14 @@ npx --yes @maintainer-pro/ai-bridge --pair AB12-CD34 --admin-url https://your-ad
|
|
|
14
14
|
Credentials are stored in `~/.maintainer-pro/bridge.json` (not the cwd).
|
|
15
15
|
|
|
16
16
|
3. Keep the bridge running (`npx @maintainer-pro/ai-bridge`). It advertises the current folder (and any `--offer-folder` paths).
|
|
17
|
-
4. In admin → Bridges: pick a reported folder + client sandbox → **Setup sandbox in folder**.
|
|
17
|
+
4. In admin → Bridges: pick a reported folder + client sandbox → **Setup sandbox in folder**.
|
|
18
|
+
|
|
19
|
+
For **empty / new** folders the bridge:
|
|
20
|
+
- Writes `.env` (server + client keys)
|
|
21
|
+
- Scaffolds `index.html` with the chat widget (served by `ai-server`)
|
|
22
|
+
- Starts `ai-server` and marks the sandbox Online
|
|
23
|
+
|
|
24
|
+
For **existing** apps it injects the widget / Next env when possible. Use **Client setup** on the Bridges form for manual overrides (`empty`, `existing`, or keys-only).
|
|
18
25
|
|
|
19
26
|
One bridge process can run many sandboxes (different folders / ports) for different clients.
|
|
20
27
|
|
package/package.json
CHANGED
package/src/daemon.mjs
CHANGED
|
@@ -256,6 +256,401 @@ function mergeEnvFile(file, values) {
|
|
|
256
256
|
fs.writeFileSync(file, body + "\n", "utf8");
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
const IGNORE_NAMES = new Set([
|
|
260
|
+
".git",
|
|
261
|
+
".DS_Store",
|
|
262
|
+
"Thumbs.db",
|
|
263
|
+
"node_modules",
|
|
264
|
+
".maintainer-pro",
|
|
265
|
+
".maintainer-pro-bridge.json",
|
|
266
|
+
".cloudflare-tunnel-url",
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
function isIgnorableEntry(name) {
|
|
270
|
+
if (IGNORE_NAMES.has(name)) return true;
|
|
271
|
+
if (name.startsWith(".env")) return true;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Detect project shape in a folder.
|
|
277
|
+
* @returns {"empty"|"next"|"html"|"other"}
|
|
278
|
+
*/
|
|
279
|
+
function detectProjectKind(dir) {
|
|
280
|
+
if (!fs.existsSync(dir)) return "empty";
|
|
281
|
+
const names = fs.readdirSync(dir).filter((n) => !isIgnorableEntry(n));
|
|
282
|
+
if (names.length === 0) return "empty";
|
|
283
|
+
|
|
284
|
+
const has = (n) => names.includes(n) || fs.existsSync(path.join(dir, n));
|
|
285
|
+
if (
|
|
286
|
+
has("next.config.js") ||
|
|
287
|
+
has("next.config.mjs") ||
|
|
288
|
+
has("next.config.ts") ||
|
|
289
|
+
has("app") ||
|
|
290
|
+
has("pages")
|
|
291
|
+
) {
|
|
292
|
+
// package.json with next is a stronger signal
|
|
293
|
+
try {
|
|
294
|
+
const pkg = JSON.parse(
|
|
295
|
+
fs.readFileSync(path.join(dir, "package.json"), "utf8")
|
|
296
|
+
);
|
|
297
|
+
if (pkg.dependencies?.next || pkg.devDependencies?.next) return "next";
|
|
298
|
+
} catch {
|
|
299
|
+
/* fall through */
|
|
300
|
+
}
|
|
301
|
+
if (has("next.config.js") || has("next.config.mjs") || has("next.config.ts")) {
|
|
302
|
+
return "next";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (has("index.html") || has(path.join("public", "index.html"))) {
|
|
307
|
+
return "html";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Only README / license → treat as empty scaffold target
|
|
311
|
+
const meaningful = names.filter(
|
|
312
|
+
(n) =>
|
|
313
|
+
!/^readme/i.test(n) &&
|
|
314
|
+
!/^license/i.test(n) &&
|
|
315
|
+
n !== "package.json"
|
|
316
|
+
);
|
|
317
|
+
if (meaningful.length === 0 && !has("package.json")) return "empty";
|
|
318
|
+
|
|
319
|
+
return "other";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function escapeHtml(value) {
|
|
323
|
+
return String(value)
|
|
324
|
+
.replaceAll("&", "&")
|
|
325
|
+
.replaceAll("<", "<")
|
|
326
|
+
.replaceAll(">", ">")
|
|
327
|
+
.replaceAll('"', """);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function emptyProjectIndexHtml(appName) {
|
|
331
|
+
return `<!doctype html>
|
|
332
|
+
<html lang="en">
|
|
333
|
+
<head>
|
|
334
|
+
<meta charset="utf-8" />
|
|
335
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
336
|
+
<title>${escapeHtml(appName)}</title>
|
|
337
|
+
<style>
|
|
338
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; color: #1a2330; }
|
|
339
|
+
p { color: #5c6b7a; }
|
|
340
|
+
</style>
|
|
341
|
+
</head>
|
|
342
|
+
<body>
|
|
343
|
+
<h1>${escapeHtml(appName)}</h1>
|
|
344
|
+
<p>Ask the assistant to change this page.</p>
|
|
345
|
+
<script src="/embed-config.js"></script>
|
|
346
|
+
<script src="/ai-ui.iife.js"></script>
|
|
347
|
+
<script>
|
|
348
|
+
(function () {
|
|
349
|
+
var cfg = window.__MAINTAINER_PRO__ || {};
|
|
350
|
+
AiUi.init({
|
|
351
|
+
apiUrl: cfg.apiUrl || "/api/chat",
|
|
352
|
+
title: "AI Assistant",
|
|
353
|
+
maintainerProUrl: cfg.maintainerProUrl || undefined,
|
|
354
|
+
maintainerProApiKey: cfg.maintainerProApiKey || undefined,
|
|
355
|
+
});
|
|
356
|
+
})();
|
|
357
|
+
</script>
|
|
358
|
+
</body>
|
|
359
|
+
</html>
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const WIDGET_MARKER = "AiUi.init";
|
|
364
|
+
|
|
365
|
+
function injectHtmlWidget(html, aiServerUrl) {
|
|
366
|
+
if (html.includes(WIDGET_MARKER) || html.includes("ai-ui.iife.js")) {
|
|
367
|
+
return { html, injected: false };
|
|
368
|
+
}
|
|
369
|
+
const snippet = `
|
|
370
|
+
<script src="${aiServerUrl}/embed-config.js"></script>
|
|
371
|
+
<script src="${aiServerUrl}/ai-ui.iife.js"></script>
|
|
372
|
+
<script>
|
|
373
|
+
(function () {
|
|
374
|
+
var cfg = window.__MAINTAINER_PRO__ || {};
|
|
375
|
+
if (!cfg.apiUrl) {
|
|
376
|
+
console.error("Maintainer Pro: embed-config.js missing apiUrl (is ai-server running?)");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
AiUi.init({
|
|
380
|
+
apiUrl: cfg.apiUrl,
|
|
381
|
+
title: "AI Assistant",
|
|
382
|
+
maintainerProUrl: cfg.maintainerProUrl || undefined,
|
|
383
|
+
maintainerProApiKey: cfg.maintainerProApiKey || undefined,
|
|
384
|
+
});
|
|
385
|
+
})();
|
|
386
|
+
</script>
|
|
387
|
+
`;
|
|
388
|
+
if (/<\/body>/i.test(html)) {
|
|
389
|
+
return {
|
|
390
|
+
html: html.replace(/<\/body>/i, `${snippet}</body>`),
|
|
391
|
+
injected: true,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return { html: html + snippet, injected: true };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function nextWidgetComponentSource() {
|
|
398
|
+
return `"use client";
|
|
399
|
+
|
|
400
|
+
import { useEffect } from "react";
|
|
401
|
+
|
|
402
|
+
declare global {
|
|
403
|
+
interface Window {
|
|
404
|
+
AiUi?: { init: (opts: Record<string, unknown>) => void; destroy?: () => void };
|
|
405
|
+
__MAINTAINER_PRO__?: {
|
|
406
|
+
aiServerUrl?: string;
|
|
407
|
+
apiUrl?: string;
|
|
408
|
+
maintainerProUrl?: string;
|
|
409
|
+
maintainerProApiKey?: string;
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const AI_SERVER_URL = (process.env.NEXT_PUBLIC_AI_SERVER_URL || "").replace(/\\/$/, "");
|
|
415
|
+
|
|
416
|
+
export function MaintainerProWidget() {
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
if (!AI_SERVER_URL) {
|
|
419
|
+
console.error("Set NEXT_PUBLIC_AI_SERVER_URL to the ai-server origin");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
let cancelled = false;
|
|
423
|
+
|
|
424
|
+
const start = (cfg: {
|
|
425
|
+
apiUrl?: string;
|
|
426
|
+
maintainerProUrl?: string;
|
|
427
|
+
maintainerProApiKey?: string;
|
|
428
|
+
}) => {
|
|
429
|
+
if (cancelled) return;
|
|
430
|
+
const AiUi = window.AiUi;
|
|
431
|
+
if (!AiUi?.init) {
|
|
432
|
+
setTimeout(() => start(cfg), 40);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
AiUi.init({
|
|
436
|
+
apiUrl: cfg.apiUrl || \`\${AI_SERVER_URL}/api/chat\`,
|
|
437
|
+
title: "AI Assistant",
|
|
438
|
+
maintainerProUrl: cfg.maintainerProUrl,
|
|
439
|
+
maintainerProApiKey: cfg.maintainerProApiKey,
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const ensureScript = (src: string) =>
|
|
444
|
+
new Promise<void>((resolve, reject) => {
|
|
445
|
+
const existing = document.querySelector<HTMLScriptElement>(\`script[src="\${src}"]\`);
|
|
446
|
+
if (existing) {
|
|
447
|
+
if (existing.dataset.loaded === "1") resolve();
|
|
448
|
+
else existing.addEventListener("load", () => resolve(), { once: true });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const script = document.createElement("script");
|
|
452
|
+
script.src = src;
|
|
453
|
+
script.async = true;
|
|
454
|
+
script.onload = () => {
|
|
455
|
+
script.dataset.loaded = "1";
|
|
456
|
+
resolve();
|
|
457
|
+
};
|
|
458
|
+
script.onerror = () => reject(new Error(\`Failed to load \${src}\`));
|
|
459
|
+
document.body.appendChild(script);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
void (async () => {
|
|
463
|
+
try {
|
|
464
|
+
await ensureScript(\`\${AI_SERVER_URL}/embed-config.js\`);
|
|
465
|
+
await ensureScript(\`\${AI_SERVER_URL}/ai-ui.iife.js\`);
|
|
466
|
+
start(window.__MAINTAINER_PRO__ || {});
|
|
467
|
+
} catch (err) {
|
|
468
|
+
console.error(err);
|
|
469
|
+
}
|
|
470
|
+
})();
|
|
471
|
+
|
|
472
|
+
return () => {
|
|
473
|
+
cancelled = true;
|
|
474
|
+
window.AiUi?.destroy?.();
|
|
475
|
+
};
|
|
476
|
+
}, []);
|
|
477
|
+
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function tryMountNextWidget(dir) {
|
|
484
|
+
const candidates = [
|
|
485
|
+
path.join(dir, "app", "layout.tsx"),
|
|
486
|
+
path.join(dir, "app", "layout.jsx"),
|
|
487
|
+
path.join(dir, "src", "app", "layout.tsx"),
|
|
488
|
+
path.join(dir, "src", "app", "layout.jsx"),
|
|
489
|
+
];
|
|
490
|
+
for (const layout of candidates) {
|
|
491
|
+
if (!fs.existsSync(layout)) continue;
|
|
492
|
+
let text = fs.readFileSync(layout, "utf8");
|
|
493
|
+
if (text.includes("MaintainerProWidget")) {
|
|
494
|
+
return { mounted: false, reason: "already mounted" };
|
|
495
|
+
}
|
|
496
|
+
const fromAppRoot =
|
|
497
|
+
/[/\\]app[/\\]layout\.(t|j)sx$/.test(layout) &&
|
|
498
|
+
!/[/\\]src[/\\]app[/\\]/.test(layout);
|
|
499
|
+
const imp = fromAppRoot
|
|
500
|
+
? "../components/MaintainerProWidget"
|
|
501
|
+
: "@/components/MaintainerProWidget";
|
|
502
|
+
|
|
503
|
+
text = `import { MaintainerProWidget } from "${imp}";\n` + text;
|
|
504
|
+
if (/\{children\}/.test(text)) {
|
|
505
|
+
text = text.replace(
|
|
506
|
+
/\{children\}/,
|
|
507
|
+
"{children}\n <MaintainerProWidget />"
|
|
508
|
+
);
|
|
509
|
+
fs.writeFileSync(layout, text, "utf8");
|
|
510
|
+
return { mounted: true, layout };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return { mounted: false, reason: "no layout found" };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Configure client files for a workspace.
|
|
518
|
+
*/
|
|
519
|
+
function configureClient(opts) {
|
|
520
|
+
const {
|
|
521
|
+
dir,
|
|
522
|
+
port,
|
|
523
|
+
appName,
|
|
524
|
+
mode, // auto | empty | existing | skip
|
|
525
|
+
hostAppUrl,
|
|
526
|
+
} = opts;
|
|
527
|
+
const aiOrigin = `http://localhost:${port}`;
|
|
528
|
+
/** @type {string[]} */
|
|
529
|
+
const notes = [];
|
|
530
|
+
/** @type {string[]} */
|
|
531
|
+
const filesWritten = [];
|
|
532
|
+
|
|
533
|
+
let kind = detectProjectKind(dir);
|
|
534
|
+
if (mode === "empty") kind = "empty";
|
|
535
|
+
if (mode === "existing") {
|
|
536
|
+
if (kind === "empty") kind = "other";
|
|
537
|
+
}
|
|
538
|
+
if (mode === "skip") {
|
|
539
|
+
return {
|
|
540
|
+
kind: "skipped",
|
|
541
|
+
notes: ["Client scaffolding skipped (manual)."],
|
|
542
|
+
filesWritten,
|
|
543
|
+
corsOrigin: hostAppUrl || null,
|
|
544
|
+
appUrl: hostAppUrl || null,
|
|
545
|
+
sameOrigin: false,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (kind === "empty" || mode === "empty") {
|
|
550
|
+
const indexPath = path.join(dir, "index.html");
|
|
551
|
+
if (!fs.existsSync(indexPath)) {
|
|
552
|
+
fs.writeFileSync(indexPath, emptyProjectIndexHtml(appName), "utf8");
|
|
553
|
+
filesWritten.push("index.html");
|
|
554
|
+
notes.push("Created index.html (served by ai-server).");
|
|
555
|
+
} else {
|
|
556
|
+
notes.push("index.html already present.");
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
kind: "empty",
|
|
560
|
+
notes,
|
|
561
|
+
filesWritten,
|
|
562
|
+
corsOrigin: aiOrigin,
|
|
563
|
+
appUrl: hostAppUrl || aiOrigin,
|
|
564
|
+
sameOrigin: true,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (kind === "html") {
|
|
569
|
+
const candidates = [
|
|
570
|
+
path.join(dir, "index.html"),
|
|
571
|
+
path.join(dir, "public", "index.html"),
|
|
572
|
+
];
|
|
573
|
+
for (const htmlPath of candidates) {
|
|
574
|
+
if (!fs.existsSync(htmlPath)) continue;
|
|
575
|
+
const raw = fs.readFileSync(htmlPath, "utf8");
|
|
576
|
+
const { html, injected } = injectHtmlWidget(raw, aiOrigin);
|
|
577
|
+
if (injected) {
|
|
578
|
+
fs.writeFileSync(htmlPath, html, "utf8");
|
|
579
|
+
filesWritten.push(path.relative(dir, htmlPath));
|
|
580
|
+
notes.push(`Injected widget into ${path.relative(dir, htmlPath)}.`);
|
|
581
|
+
} else {
|
|
582
|
+
notes.push(`Widget already present in ${path.relative(dir, htmlPath)}.`);
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
const origin = hostAppUrl || "http://localhost:3000";
|
|
587
|
+
return {
|
|
588
|
+
kind: "html",
|
|
589
|
+
notes,
|
|
590
|
+
filesWritten,
|
|
591
|
+
corsOrigin: origin,
|
|
592
|
+
appUrl: origin,
|
|
593
|
+
sameOrigin: false,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (kind === "next") {
|
|
598
|
+
const envLocal = path.join(dir, ".env.local");
|
|
599
|
+
mergeEnvFile(envLocal, {
|
|
600
|
+
NEXT_PUBLIC_AI_SERVER_URL: aiOrigin,
|
|
601
|
+
});
|
|
602
|
+
filesWritten.push(".env.local");
|
|
603
|
+
notes.push("Set NEXT_PUBLIC_AI_SERVER_URL in .env.local.");
|
|
604
|
+
|
|
605
|
+
const useSrc = fs.existsSync(path.join(dir, "src", "app"));
|
|
606
|
+
const compDir = useSrc
|
|
607
|
+
? path.join(dir, "src", "components")
|
|
608
|
+
: path.join(dir, "components");
|
|
609
|
+
fs.mkdirSync(compDir, { recursive: true });
|
|
610
|
+
const widgetPath = path.join(compDir, "MaintainerProWidget.tsx");
|
|
611
|
+
if (!fs.existsSync(widgetPath)) {
|
|
612
|
+
fs.writeFileSync(widgetPath, nextWidgetComponentSource(), "utf8");
|
|
613
|
+
filesWritten.push(path.relative(dir, widgetPath));
|
|
614
|
+
notes.push("Added MaintainerProWidget.tsx.");
|
|
615
|
+
}
|
|
616
|
+
const mount = tryMountNextWidget(dir);
|
|
617
|
+
if (mount.mounted) {
|
|
618
|
+
notes.push(`Mounted widget in ${path.relative(dir, mount.layout)}.`);
|
|
619
|
+
} else {
|
|
620
|
+
notes.push(
|
|
621
|
+
"Add <MaintainerProWidget /> to your root layout if it is not mounted yet."
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
const origin = hostAppUrl || "http://localhost:3000";
|
|
625
|
+
return {
|
|
626
|
+
kind: "next",
|
|
627
|
+
notes,
|
|
628
|
+
filesWritten,
|
|
629
|
+
corsOrigin: origin,
|
|
630
|
+
appUrl: origin,
|
|
631
|
+
sameOrigin: false,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// other — write host env hints only
|
|
636
|
+
const origin = hostAppUrl || "http://localhost:3000";
|
|
637
|
+
mergeEnvFile(path.join(dir, ".env"), {
|
|
638
|
+
AI_SERVER_URL: aiOrigin,
|
|
639
|
+
NEXT_PUBLIC_AI_SERVER_URL: aiOrigin,
|
|
640
|
+
});
|
|
641
|
+
notes.push(
|
|
642
|
+
"Existing project detected. Set AI_SERVER_URL / mount the widget manually if needed."
|
|
643
|
+
);
|
|
644
|
+
return {
|
|
645
|
+
kind: "other",
|
|
646
|
+
notes,
|
|
647
|
+
filesWritten,
|
|
648
|
+
corsOrigin: origin,
|
|
649
|
+
appUrl: origin,
|
|
650
|
+
sameOrigin: false,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
259
654
|
function collectOfferedFolders(cfg) {
|
|
260
655
|
/** @type {string[]} */
|
|
261
656
|
const folders = [];
|
|
@@ -344,6 +739,13 @@ async function setupWorkspace(cfg, action) {
|
|
|
344
739
|
action.sandboxId || action.payload?.sandboxId || ""
|
|
345
740
|
);
|
|
346
741
|
const port = Number(action.payload?.port) || 3100;
|
|
742
|
+
const clientMode = String(action.payload?.clientMode || "auto"); // auto|empty|existing|skip
|
|
743
|
+
const hostAppUrl =
|
|
744
|
+
typeof action.payload?.hostAppUrl === "string" &&
|
|
745
|
+
action.payload.hostAppUrl.trim()
|
|
746
|
+
? action.payload.hostAppUrl.trim().replace(/\/$/, "")
|
|
747
|
+
: null;
|
|
748
|
+
|
|
347
749
|
if (!folderPath || !sandboxId) {
|
|
348
750
|
throw new Error("setup_workspace requires folderPath and sandboxId");
|
|
349
751
|
}
|
|
@@ -357,13 +759,34 @@ async function setupWorkspace(cfg, action) {
|
|
|
357
759
|
`/api/v1/bridge/machine/sandboxes/${sandboxId}/setup-config?port=${port}`
|
|
358
760
|
);
|
|
359
761
|
|
|
762
|
+
const appName =
|
|
763
|
+
config.sandbox?.applicationName ||
|
|
764
|
+
config.sandbox?.name ||
|
|
765
|
+
"Maintainer Pro App";
|
|
766
|
+
|
|
767
|
+
const client = configureClient({
|
|
768
|
+
dir: resolved,
|
|
769
|
+
port,
|
|
770
|
+
appName,
|
|
771
|
+
mode: clientMode,
|
|
772
|
+
hostAppUrl,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
const aiOrigin = `http://localhost:${port}`;
|
|
776
|
+
const corsOrigin = client.corsOrigin || aiOrigin;
|
|
777
|
+
const appUrl = client.appUrl || corsOrigin;
|
|
778
|
+
|
|
360
779
|
const envPath = path.join(resolved, ".env");
|
|
361
780
|
const envValues = {
|
|
362
781
|
...config.env,
|
|
363
782
|
AI_CLI_WORKSPACE: ".",
|
|
364
|
-
AI_SERVER_UI: ".",
|
|
365
|
-
|
|
366
|
-
|
|
783
|
+
AI_SERVER_UI: client.sameOrigin || client.kind === "empty" ? "." : ".",
|
|
784
|
+
PORT: String(port),
|
|
785
|
+
AI_SERVER_URL: aiOrigin,
|
|
786
|
+
NEXT_PUBLIC_AI_SERVER_URL: aiOrigin,
|
|
787
|
+
CORS_ORIGIN: corsOrigin,
|
|
788
|
+
APP_URL: appUrl,
|
|
789
|
+
AI_SERVER_PRODUCT_DESCRIPTION: appName,
|
|
367
790
|
};
|
|
368
791
|
mergeEnvFile(envPath, envValues);
|
|
369
792
|
|
|
@@ -375,6 +798,7 @@ async function setupWorkspace(cfg, action) {
|
|
|
375
798
|
port,
|
|
376
799
|
sandboxName: config.sandbox?.name,
|
|
377
800
|
applicationName: config.sandbox?.applicationName,
|
|
801
|
+
clientKind: client.kind,
|
|
378
802
|
};
|
|
379
803
|
if (existing >= 0) cfg.workspaces[existing] = entry;
|
|
380
804
|
else cfg.workspaces.push(entry);
|
|
@@ -384,16 +808,25 @@ async function setupWorkspace(cfg, action) {
|
|
|
384
808
|
|
|
385
809
|
if (!cfg.noAiServer) {
|
|
386
810
|
startAiServerForWorkspace(entry);
|
|
387
|
-
await new Promise((r) => setTimeout(r,
|
|
811
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
388
812
|
}
|
|
389
813
|
|
|
814
|
+
const aiServerUp = await probeUrl(`${aiOrigin.replace("localhost", "127.0.0.1")}/embed-config.js`);
|
|
815
|
+
|
|
816
|
+
for (const note of client.notes) log(note);
|
|
817
|
+
|
|
390
818
|
return {
|
|
391
819
|
sandboxId,
|
|
392
820
|
folderPath: resolved,
|
|
393
821
|
port,
|
|
394
|
-
appUrl
|
|
395
|
-
origins:
|
|
822
|
+
appUrl,
|
|
823
|
+
origins: [corsOrigin, aiOrigin].filter(Boolean),
|
|
396
824
|
wroteEnv: true,
|
|
825
|
+
clientKind: client.kind,
|
|
826
|
+
clientFiles: client.filesWritten,
|
|
827
|
+
clientNotes: client.notes,
|
|
828
|
+
aiServerUp,
|
|
829
|
+
openUrl: client.sameOrigin ? aiOrigin : appUrl,
|
|
397
830
|
};
|
|
398
831
|
}
|
|
399
832
|
|