create-interview-cockpit 0.5.0 → 0.7.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 +1 -1
- package/template/client/package-lock.json +734 -1
- package/template/client/package.json +1 -0
- package/template/client/src/App.tsx +3 -0
- package/template/client/src/api.ts +384 -4
- package/template/client/src/components/AiSettingsModal.tsx +818 -425
- package/template/client/src/components/ChatMessage.tsx +34 -12
- package/template/client/src/components/ChatView.tsx +298 -121
- package/template/client/src/components/CodeContextPanel.tsx +530 -2
- package/template/client/src/components/CodeRunnerModal.tsx +1895 -120
- package/template/client/src/components/DocRefModal.tsx +55 -6
- package/template/client/src/components/FileAttachments.tsx +20 -4
- package/template/client/src/components/InfraLabModal.tsx +1706 -0
- package/template/client/src/components/LinkedConvosPicker.tsx +128 -0
- package/template/client/src/components/MarkdownRenderer.tsx +22 -8
- package/template/client/src/components/NotesModal.tsx +977 -0
- package/template/client/src/components/PlotEmbed.tsx +173 -0
- package/template/client/src/components/Sidebar.tsx +184 -0
- package/template/client/src/components/VizCraftEmbed.tsx +257 -13
- package/template/client/src/components/WorkspaceSwitcher.tsx +4 -0
- package/template/client/src/infraLab.ts +124 -0
- package/template/client/src/reactLab.ts +960 -0
- package/template/client/src/store.ts +250 -6
- package/template/client/src/types.ts +36 -3
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/google-drive.ts +39 -3
- package/template/server/src/index.ts +954 -52
- package/template/server/src/infra-runner.ts +1104 -0
- package/template/server/src/storage.ts +22 -3
|
@@ -7,6 +7,7 @@ import FileViewerModal from "./components/FileViewerModal";
|
|
|
7
7
|
import DocRefModal from "./components/DocRefModal";
|
|
8
8
|
import AiSettingsModal from "./components/AiSettingsModal";
|
|
9
9
|
import CodeRunnerModal from "./components/CodeRunnerModal";
|
|
10
|
+
import InfraLabModal from "./components/InfraLabModal";
|
|
10
11
|
import { Code, Plane, PanelLeftClose, PanelLeft, Settings } from "lucide-react";
|
|
11
12
|
|
|
12
13
|
export default function App() {
|
|
@@ -30,6 +31,7 @@ export default function App() {
|
|
|
30
31
|
openSettings,
|
|
31
32
|
closeSettings,
|
|
32
33
|
showCodeRunner,
|
|
34
|
+
showInfraLab,
|
|
33
35
|
closeCodeRunner,
|
|
34
36
|
} = useStore();
|
|
35
37
|
|
|
@@ -157,6 +159,7 @@ export default function App() {
|
|
|
157
159
|
)}
|
|
158
160
|
{showSettings && <AiSettingsModal />}
|
|
159
161
|
{showCodeRunner && <CodeRunnerModal />}
|
|
162
|
+
{showInfraLab && <InfraLabModal />}
|
|
160
163
|
</div>
|
|
161
164
|
);
|
|
162
165
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Topic,
|
|
3
|
+
Question,
|
|
4
|
+
ContextFile,
|
|
5
|
+
WorkspacesRegistry,
|
|
6
|
+
InfraLabWorkspace,
|
|
7
|
+
} from "./types";
|
|
2
8
|
|
|
3
9
|
const BASE = "/api";
|
|
4
10
|
|
|
@@ -20,6 +26,7 @@ export interface AiSettings {
|
|
|
20
26
|
{ maxOutputTokens: number; maxSteps: number }
|
|
21
27
|
>;
|
|
22
28
|
vizGuide: string;
|
|
29
|
+
plotGuide: string;
|
|
23
30
|
/** All user-selectable prompt groups. Add new entries here to extend the UI. */
|
|
24
31
|
promptGroups: Record<string, PromptGroup>;
|
|
25
32
|
/** When true, preference prompt texts are appended to every message (not just on change). */
|
|
@@ -32,6 +39,65 @@ export interface AiSettings {
|
|
|
32
39
|
model?: string;
|
|
33
40
|
}
|
|
34
41
|
|
|
42
|
+
export type InfraRunAction = "validate" | "plan" | "command";
|
|
43
|
+
export type InfraRunStatus = "completed" | "failed";
|
|
44
|
+
|
|
45
|
+
export interface InfraRunArtifact {
|
|
46
|
+
key: string;
|
|
47
|
+
label: string;
|
|
48
|
+
kind: "text" | "json";
|
|
49
|
+
fileName: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface InfraDiagnostic {
|
|
53
|
+
severity: string;
|
|
54
|
+
summary: string;
|
|
55
|
+
detail?: string;
|
|
56
|
+
address?: string;
|
|
57
|
+
filename?: string;
|
|
58
|
+
line?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface InfraPlanSummary {
|
|
62
|
+
add: number;
|
|
63
|
+
change: number;
|
|
64
|
+
destroy: number;
|
|
65
|
+
replace: number;
|
|
66
|
+
resourceCount: number;
|
|
67
|
+
outputCount: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface InfraRunListItem {
|
|
71
|
+
id: string;
|
|
72
|
+
fileId?: string;
|
|
73
|
+
questionId?: string;
|
|
74
|
+
label: string;
|
|
75
|
+
action: InfraRunAction;
|
|
76
|
+
command?: string;
|
|
77
|
+
status: InfraRunStatus;
|
|
78
|
+
startedAt: string;
|
|
79
|
+
completedAt: string;
|
|
80
|
+
durationMs: number;
|
|
81
|
+
provider: "aws";
|
|
82
|
+
executionMode: "plan-only" | "localstack";
|
|
83
|
+
diagnostics: InfraDiagnostic[];
|
|
84
|
+
planSummary?: InfraPlanSummary;
|
|
85
|
+
error?: string;
|
|
86
|
+
artifacts: InfraRunArtifact[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface InfraRunDetails extends InfraRunListItem {
|
|
90
|
+
logs: string;
|
|
91
|
+
validationJson?: unknown;
|
|
92
|
+
planJson?: unknown;
|
|
93
|
+
workspaceSnapshot?: InfraLabWorkspace;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type InfraCommandStreamMessage =
|
|
97
|
+
| { type: "output"; kind: "stdout" | "stderr" | "info"; text: string }
|
|
98
|
+
| { type: "complete"; run: InfraRunDetails }
|
|
99
|
+
| { type: "error"; error: string };
|
|
100
|
+
|
|
35
101
|
export async function fetchAiSettings(): Promise<AiSettings> {
|
|
36
102
|
const res = await fetch(`${BASE}/settings`);
|
|
37
103
|
return res.json();
|
|
@@ -68,7 +134,7 @@ export async function deleteTopic(id: string): Promise<void> {
|
|
|
68
134
|
|
|
69
135
|
export async function updateTopic(
|
|
70
136
|
id: string,
|
|
71
|
-
data: { name?: string },
|
|
137
|
+
data: { name?: string; systemContext?: string },
|
|
72
138
|
): Promise<Topic> {
|
|
73
139
|
const res = await fetch(`${BASE}/topics/${id}`, {
|
|
74
140
|
method: "PATCH",
|
|
@@ -254,7 +320,14 @@ export async function saveCodeSnippet(
|
|
|
254
320
|
code: string,
|
|
255
321
|
language: string,
|
|
256
322
|
label: string,
|
|
257
|
-
origin:
|
|
323
|
+
origin:
|
|
324
|
+
| "user"
|
|
325
|
+
| "ai"
|
|
326
|
+
| "sandbox"
|
|
327
|
+
| "infra"
|
|
328
|
+
| "react"
|
|
329
|
+
| "nextjs"
|
|
330
|
+
| "module-federation",
|
|
258
331
|
): Promise<ContextFile> {
|
|
259
332
|
const res = await fetch(`${BASE}/questions/${questionId}/save-code-snippet`, {
|
|
260
333
|
method: "POST",
|
|
@@ -298,6 +371,217 @@ export async function renameContextFile(
|
|
|
298
371
|
return res.json();
|
|
299
372
|
}
|
|
300
373
|
|
|
374
|
+
export async function runInfraAction(input: {
|
|
375
|
+
questionId?: string;
|
|
376
|
+
fileId?: string;
|
|
377
|
+
label?: string;
|
|
378
|
+
action: "validate" | "plan";
|
|
379
|
+
workspace: InfraLabWorkspace;
|
|
380
|
+
}): Promise<InfraRunDetails> {
|
|
381
|
+
const res = await fetch(`${BASE}/infra/run`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: { "Content-Type": "application/json" },
|
|
384
|
+
body: JSON.stringify(input),
|
|
385
|
+
});
|
|
386
|
+
if (!res.ok) {
|
|
387
|
+
const err = await res.json().catch(() => ({ error: "Infra run failed" }));
|
|
388
|
+
throw new Error(err.error ?? "Infra run failed");
|
|
389
|
+
}
|
|
390
|
+
return res.json();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function fetchInfraRuns(
|
|
394
|
+
fileId: string,
|
|
395
|
+
): Promise<InfraRunListItem[]> {
|
|
396
|
+
const res = await fetch(
|
|
397
|
+
`${BASE}/infra/runs?fileId=${encodeURIComponent(fileId)}`,
|
|
398
|
+
);
|
|
399
|
+
if (!res.ok) throw new Error(await res.text());
|
|
400
|
+
return res.json();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function streamInfraCommand(
|
|
404
|
+
input: {
|
|
405
|
+
questionId?: string;
|
|
406
|
+
fileId?: string;
|
|
407
|
+
label?: string;
|
|
408
|
+
command: string;
|
|
409
|
+
workspace: InfraLabWorkspace;
|
|
410
|
+
},
|
|
411
|
+
onMessage: (message: InfraCommandStreamMessage) => void,
|
|
412
|
+
): Promise<void> {
|
|
413
|
+
const res = await fetch(`${BASE}/infra/command-stream`, {
|
|
414
|
+
method: "POST",
|
|
415
|
+
headers: { "Content-Type": "application/json" },
|
|
416
|
+
body: JSON.stringify(input),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (!res.ok || !res.body) {
|
|
420
|
+
const error = await res.text().catch(() => "Infra command failed");
|
|
421
|
+
throw new Error(error || "Infra command failed");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const reader = res.body.getReader();
|
|
425
|
+
const decoder = new TextDecoder();
|
|
426
|
+
let buffer = "";
|
|
427
|
+
|
|
428
|
+
const flushBuffer = () => {
|
|
429
|
+
const events = buffer.replace(/\r/g, "").split("\n\n");
|
|
430
|
+
buffer = events.pop() ?? "";
|
|
431
|
+
|
|
432
|
+
for (const event of events) {
|
|
433
|
+
if (!event.trim()) continue;
|
|
434
|
+
const payload = event
|
|
435
|
+
.split("\n")
|
|
436
|
+
.filter((line) => line.startsWith("data:"))
|
|
437
|
+
.map((line) => line.slice(5).trimStart())
|
|
438
|
+
.join("\n");
|
|
439
|
+
|
|
440
|
+
if (!payload) continue;
|
|
441
|
+
onMessage(JSON.parse(payload) as InfraCommandStreamMessage);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
while (true) {
|
|
446
|
+
const { value, done } = await reader.read();
|
|
447
|
+
buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
|
|
448
|
+
flushBuffer();
|
|
449
|
+
if (done) break;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (buffer.trim()) {
|
|
453
|
+
flushBuffer();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export async function fetchInfraRun(runId: string): Promise<InfraRunDetails> {
|
|
458
|
+
const res = await fetch(`${BASE}/infra/runs/${encodeURIComponent(runId)}`);
|
|
459
|
+
if (!res.ok) throw new Error(await res.text());
|
|
460
|
+
return res.json();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export async function streamInfraAsk(
|
|
464
|
+
input: {
|
|
465
|
+
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
466
|
+
workspace: Record<string, string>;
|
|
467
|
+
questionId?: string;
|
|
468
|
+
},
|
|
469
|
+
onDelta: (chunk: string) => void,
|
|
470
|
+
): Promise<void> {
|
|
471
|
+
const res = await fetch(`${BASE}/infra/ask`, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
headers: { "Content-Type": "application/json" },
|
|
474
|
+
body: JSON.stringify(input),
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
if (!res.ok || !res.body) {
|
|
478
|
+
const err = await res.text().catch(() => "Infra ask failed");
|
|
479
|
+
throw new Error(err || "Infra ask failed");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const reader = res.body.getReader();
|
|
483
|
+
const decoder = new TextDecoder();
|
|
484
|
+
|
|
485
|
+
while (true) {
|
|
486
|
+
const { value, done } = await reader.read();
|
|
487
|
+
if (done) break;
|
|
488
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
489
|
+
// AI SDK UIMessageStream emits SSE lines like: data: {"type":"text-delta","id":"0","delta":"..."}
|
|
490
|
+
for (const line of chunk.split("\n")) {
|
|
491
|
+
if (!line.startsWith("data: ")) continue;
|
|
492
|
+
try {
|
|
493
|
+
const json = JSON.parse(line.slice(6));
|
|
494
|
+
if (json.type === "text-delta" && typeof json.delta === "string") {
|
|
495
|
+
onDelta(json.delta);
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
// ignore malformed chunk
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export async function streamNotesAsk(
|
|
505
|
+
input: {
|
|
506
|
+
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
507
|
+
noteContent?: string;
|
|
508
|
+
noteName?: string;
|
|
509
|
+
},
|
|
510
|
+
onDelta: (chunk: string) => void,
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const res = await fetch(`${BASE}/notes/ask`, {
|
|
513
|
+
method: "POST",
|
|
514
|
+
headers: { "Content-Type": "application/json" },
|
|
515
|
+
body: JSON.stringify(input),
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (!res.ok || !res.body) {
|
|
519
|
+
const err = await res.text().catch(() => "Notes ask failed");
|
|
520
|
+
throw new Error(err || "Notes ask failed");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const reader = res.body.getReader();
|
|
524
|
+
const decoder = new TextDecoder();
|
|
525
|
+
|
|
526
|
+
while (true) {
|
|
527
|
+
const { value, done } = await reader.read();
|
|
528
|
+
if (done) break;
|
|
529
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
530
|
+
for (const line of chunk.split("\n")) {
|
|
531
|
+
if (!line.startsWith("data: ")) continue;
|
|
532
|
+
try {
|
|
533
|
+
const json = JSON.parse(line.slice(6));
|
|
534
|
+
if (json.type === "text-delta" && typeof json.delta === "string") {
|
|
535
|
+
onDelta(json.delta);
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
// ignore malformed chunk
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function streamFrontendLabAsk(
|
|
545
|
+
input: {
|
|
546
|
+
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
547
|
+
workspace: Record<string, string>;
|
|
548
|
+
labType: "react" | "nextjs" | "module-federation";
|
|
549
|
+
questionId?: string;
|
|
550
|
+
},
|
|
551
|
+
onDelta: (chunk: string) => void,
|
|
552
|
+
): Promise<void> {
|
|
553
|
+
const res = await fetch(`${BASE}/frontend-lab/ask`, {
|
|
554
|
+
method: "POST",
|
|
555
|
+
headers: { "Content-Type": "application/json" },
|
|
556
|
+
body: JSON.stringify(input),
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if (!res.ok || !res.body) {
|
|
560
|
+
const err = await res.text().catch(() => "Frontend lab ask failed");
|
|
561
|
+
throw new Error(err || "Frontend lab ask failed");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const reader = res.body.getReader();
|
|
565
|
+
const decoder = new TextDecoder();
|
|
566
|
+
|
|
567
|
+
while (true) {
|
|
568
|
+
const { value, done } = await reader.read();
|
|
569
|
+
if (done) break;
|
|
570
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
571
|
+
for (const line of chunk.split("\n")) {
|
|
572
|
+
if (!line.startsWith("data: ")) continue;
|
|
573
|
+
try {
|
|
574
|
+
const json = JSON.parse(line.slice(6));
|
|
575
|
+
if (json.type === "text-delta" && typeof json.delta === "string") {
|
|
576
|
+
onDelta(json.delta);
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
// ignore malformed chunk
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
301
585
|
// --- Code Context ---
|
|
302
586
|
|
|
303
587
|
export async function fetchCodeContextTree(): Promise<string[]> {
|
|
@@ -417,7 +701,7 @@ export async function fetchDriveSubfolders(
|
|
|
417
701
|
export async function createDriveSubfolder(
|
|
418
702
|
workspaceId: string,
|
|
419
703
|
name: string,
|
|
420
|
-
): Promise<DriveFolder> {
|
|
704
|
+
): Promise<DriveFolder | { needsAuth: true; authUrl: string }> {
|
|
421
705
|
const res = await fetch(
|
|
422
706
|
`${BASE}/workspaces/${workspaceId}/drive-subfolders`,
|
|
423
707
|
{
|
|
@@ -493,3 +777,99 @@ export async function attachDriveFolder(
|
|
|
493
777
|
const data = await res.json();
|
|
494
778
|
return data.registry;
|
|
495
779
|
}
|
|
780
|
+
|
|
781
|
+
// ── Next.js real-server sandbox ──────────────────────────────────────────────
|
|
782
|
+
|
|
783
|
+
export interface NextjsSandboxInfo {
|
|
784
|
+
id: string;
|
|
785
|
+
port: number;
|
|
786
|
+
url: string;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export interface ModuleFederationSandboxInfo {
|
|
790
|
+
id: string;
|
|
791
|
+
hostUrl: string;
|
|
792
|
+
appUrls: Record<string, string>;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export interface ModuleFederationSandboxStatus {
|
|
796
|
+
running: boolean;
|
|
797
|
+
ready?: boolean;
|
|
798
|
+
hostUrl?: string;
|
|
799
|
+
appUrls?: Record<string, string>;
|
|
800
|
+
logs?: string[];
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export async function startNextjsSandbox(
|
|
804
|
+
files: Record<string, string>,
|
|
805
|
+
): Promise<NextjsSandboxInfo> {
|
|
806
|
+
const res = await fetch(`${BASE}/nextjs/start`, {
|
|
807
|
+
method: "POST",
|
|
808
|
+
headers: { "Content-Type": "application/json" },
|
|
809
|
+
body: JSON.stringify({ files }),
|
|
810
|
+
});
|
|
811
|
+
if (!res.ok) {
|
|
812
|
+
const body = await res.json().catch(() => ({}));
|
|
813
|
+
throw new Error(
|
|
814
|
+
(body as any).error || `Failed to start Next.js (${res.status})`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return res.json();
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export async function updateNextjsFiles(
|
|
821
|
+
id: string,
|
|
822
|
+
files: Record<string, string>,
|
|
823
|
+
): Promise<void> {
|
|
824
|
+
await fetch(`${BASE}/nextjs/${id}/update-files`, {
|
|
825
|
+
method: "POST",
|
|
826
|
+
headers: { "Content-Type": "application/json" },
|
|
827
|
+
body: JSON.stringify({ files }),
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export async function stopNextjsSandbox(id: string): Promise<void> {
|
|
832
|
+
await fetch(`${BASE}/nextjs/${id}`, { method: "DELETE" });
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export async function startModuleFederationSandbox(
|
|
836
|
+
files: Record<string, string>,
|
|
837
|
+
): Promise<ModuleFederationSandboxInfo> {
|
|
838
|
+
const res = await fetch(`${BASE}/module-federation/start`, {
|
|
839
|
+
method: "POST",
|
|
840
|
+
headers: { "Content-Type": "application/json" },
|
|
841
|
+
body: JSON.stringify({ files }),
|
|
842
|
+
});
|
|
843
|
+
if (!res.ok) {
|
|
844
|
+
const body = await res.json().catch(() => ({}));
|
|
845
|
+
throw new Error(
|
|
846
|
+
(body as any).error ||
|
|
847
|
+
`Failed to start webpack module federation lab (${res.status})`,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
return res.json();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export async function updateModuleFederationFiles(
|
|
854
|
+
id: string,
|
|
855
|
+
files: Record<string, string>,
|
|
856
|
+
): Promise<void> {
|
|
857
|
+
const res = await fetch(`${BASE}/module-federation/${id}/update-files`, {
|
|
858
|
+
method: "POST",
|
|
859
|
+
headers: { "Content-Type": "application/json" },
|
|
860
|
+
body: JSON.stringify({ files }),
|
|
861
|
+
});
|
|
862
|
+
if (!res.ok) throw new Error(await res.text());
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export async function fetchModuleFederationStatus(
|
|
866
|
+
id: string,
|
|
867
|
+
): Promise<ModuleFederationSandboxStatus> {
|
|
868
|
+
const res = await fetch(`${BASE}/module-federation/${id}/status`);
|
|
869
|
+
if (!res.ok) throw new Error(await res.text());
|
|
870
|
+
return res.json();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
export async function stopModuleFederationSandbox(id: string): Promise<void> {
|
|
874
|
+
await fetch(`${BASE}/module-federation/${id}`, { method: "DELETE" });
|
|
875
|
+
}
|