create-interview-cockpit 0.6.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.
@@ -690,7 +690,14 @@ app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
690
690
  code: string;
691
691
  language: string;
692
692
  label: string;
693
- origin: "user" | "ai" | "sandbox" | "infra" | "react" | "nextjs";
693
+ origin:
694
+ | "user"
695
+ | "ai"
696
+ | "sandbox"
697
+ | "infra"
698
+ | "react"
699
+ | "nextjs"
700
+ | "module-federation";
694
701
  };
695
702
  if (typeof code !== "string" || !code.trim()) {
696
703
  return res.status(400).json({ error: "code is required" });
@@ -701,11 +708,12 @@ app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
701
708
  origin !== "sandbox" &&
702
709
  origin !== "infra" &&
703
710
  origin !== "react" &&
704
- origin !== "nextjs"
711
+ origin !== "nextjs" &&
712
+ origin !== "module-federation"
705
713
  ) {
706
714
  return res.status(400).json({
707
715
  error:
708
- "origin must be 'user', 'ai', 'sandbox', 'infra', 'react', or 'nextjs'",
716
+ "origin must be 'user', 'ai', 'sandbox', 'infra', 'react', 'nextjs', or 'module-federation'",
709
717
  });
710
718
  }
711
719
  try {
@@ -994,7 +1002,7 @@ app.post("/api/frontend-lab/ask", async (req, res) => {
994
1002
  const { messages, workspace, labType, questionId } = req.body as {
995
1003
  messages?: Array<{ role: "user" | "assistant"; content: string }>;
996
1004
  workspace?: Record<string, string>;
997
- labType?: "react" | "nextjs";
1005
+ labType?: "react" | "nextjs" | "module-federation";
998
1006
  questionId?: string;
999
1007
  };
1000
1008
 
@@ -1008,7 +1016,11 @@ app.post("/api/frontend-lab/ask", async (req, res) => {
1008
1016
  const aiSettings = await storage.getAiSettings();
1009
1017
 
1010
1018
  const typeLabel =
1011
- labType === "nextjs" ? "Next.js App Router" : "React + TypeScript";
1019
+ labType === "nextjs"
1020
+ ? "Next.js App Router"
1021
+ : labType === "module-federation"
1022
+ ? "Webpack Module Federation"
1023
+ : "React + TypeScript";
1012
1024
 
1013
1025
  let workspaceBlock = "";
1014
1026
  if (workspace && typeof workspace === "object") {
@@ -2280,6 +2292,91 @@ const NEXT_MODULES_DIR = path.join(NEXT_NPX_DIR, "node_modules");
2280
2292
  // walking up the directory tree — no symlinks needed, no Turbopack restrictions.
2281
2293
  const NEXT_SANDBOX_BASE = path.join(NEXT_NPX_DIR, ".sandboxes");
2282
2294
 
2295
+ interface ModuleFederationSandboxEntry {
2296
+ child: ReturnType<typeof spawn>;
2297
+ dir: string;
2298
+ hostUrl: string;
2299
+ appUrls: Record<string, string>;
2300
+ workspaceFiles: Set<string>;
2301
+ logs: string[];
2302
+ ready: boolean;
2303
+ }
2304
+
2305
+ const moduleFederationSandboxes = new Map<
2306
+ string,
2307
+ ModuleFederationSandboxEntry
2308
+ >();
2309
+ const MODULE_FEDERATION_SANDBOX_BASE = path.join(
2310
+ os.tmpdir(),
2311
+ "interview-cockpit-module-federation",
2312
+ );
2313
+
2314
+ function appendSandboxLog(logs: string[], text: string): string {
2315
+ const clean = text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
2316
+ logs.push(clean);
2317
+ if (logs.length > 400) {
2318
+ logs.splice(0, logs.length - 400);
2319
+ }
2320
+ return clean;
2321
+ }
2322
+
2323
+ function npmCommand(): string {
2324
+ return process.platform === "win32" ? "npm.cmd" : "npm";
2325
+ }
2326
+
2327
+ async function writeModuleFederationSandboxFiles(
2328
+ dir: string,
2329
+ files: Record<string, string>,
2330
+ ) {
2331
+ for (const [filePath, content] of Object.entries(files)) {
2332
+ const fullPath = path.join(dir, filePath);
2333
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
2334
+ await fs.writeFile(fullPath, content, "utf8");
2335
+ }
2336
+ }
2337
+
2338
+ async function runLoggedCommand(
2339
+ command: string,
2340
+ args: string[],
2341
+ options: { cwd: string; env?: NodeJS.ProcessEnv },
2342
+ logs: string[],
2343
+ ): Promise<void> {
2344
+ await new Promise<void>((resolve, reject) => {
2345
+ const child = spawn(command, args, {
2346
+ cwd: options.cwd,
2347
+ env: options.env,
2348
+ });
2349
+
2350
+ child.stdout.on("data", (chunk: Buffer) => {
2351
+ appendSandboxLog(logs, chunk.toString());
2352
+ });
2353
+ child.stderr.on("data", (chunk: Buffer) => {
2354
+ appendSandboxLog(logs, chunk.toString());
2355
+ });
2356
+ child.on("error", reject);
2357
+ child.on("exit", (code) => {
2358
+ if (code === 0) {
2359
+ resolve();
2360
+ return;
2361
+ }
2362
+ reject(
2363
+ new Error(
2364
+ logs.join("").trim() ||
2365
+ `${command} ${args.join(" ")} exited with code ${String(code)}`,
2366
+ ),
2367
+ );
2368
+ });
2369
+ });
2370
+ }
2371
+
2372
+ async function getDistinctPorts(count: number): Promise<number[]> {
2373
+ const ports = new Set<number>();
2374
+ while (ports.size < count) {
2375
+ ports.add(await getFreePort());
2376
+ }
2377
+ return Array.from(ports);
2378
+ }
2379
+
2283
2380
  /** Write all user files and necessary config into a sandbox directory. */
2284
2381
  async function writeNextSandboxFiles(
2285
2382
  dir: string,
@@ -2480,6 +2577,170 @@ app.delete("/api/nextjs/:id", async (req, res) => {
2480
2577
  res.json({ ok: true });
2481
2578
  });
2482
2579
 
2580
+ app.post("/api/module-federation/start", async (req, res) => {
2581
+ const { files } = req.body as { files?: Record<string, string> };
2582
+ if (!files || typeof files !== "object") {
2583
+ return res.status(400).json({ error: "files is required" });
2584
+ }
2585
+ if (typeof files["package.json"] !== "string") {
2586
+ return res.status(400).json({ error: "package.json is required" });
2587
+ }
2588
+
2589
+ const id = randomUUID();
2590
+ const dir = path.join(MODULE_FEDERATION_SANDBOX_BASE, id);
2591
+ const logs: string[] = [];
2592
+
2593
+ try {
2594
+ await fs.mkdir(dir, { recursive: true });
2595
+ await writeModuleFederationSandboxFiles(dir, files);
2596
+
2597
+ await runLoggedCommand(
2598
+ npmCommand(),
2599
+ ["install", "--no-audit", "--no-fund", "--prefer-offline"],
2600
+ {
2601
+ cwd: dir,
2602
+ env: {
2603
+ ...process.env,
2604
+ npm_config_update_notifier: "false",
2605
+ },
2606
+ },
2607
+ logs,
2608
+ );
2609
+
2610
+ const [hostPort, profilePort, checkoutPort] = await getDistinctPorts(3);
2611
+ const appUrls = {
2612
+ host: `http://localhost:${hostPort}`,
2613
+ profile: `http://localhost:${profilePort}`,
2614
+ checkout: `http://localhost:${checkoutPort}`,
2615
+ };
2616
+ const readyPorts = new Set<string>();
2617
+
2618
+ const child = spawn(npmCommand(), ["run", "dev"], {
2619
+ cwd: dir,
2620
+ env: {
2621
+ ...process.env,
2622
+ HOST_PORT: String(hostPort),
2623
+ PROFILE_PORT: String(profilePort),
2624
+ CHECKOUT_PORT: String(checkoutPort),
2625
+ npm_config_update_notifier: "false",
2626
+ },
2627
+ });
2628
+
2629
+ const entry: ModuleFederationSandboxEntry = {
2630
+ child,
2631
+ dir,
2632
+ hostUrl: appUrls.host,
2633
+ appUrls,
2634
+ workspaceFiles: new Set(Object.keys(files)),
2635
+ logs,
2636
+ ready: false,
2637
+ };
2638
+
2639
+ const markReady = (text: string) => {
2640
+ if (text.includes(`localhost:${hostPort}`)) readyPorts.add("host");
2641
+ if (text.includes(`localhost:${profilePort}`)) readyPorts.add("profile");
2642
+ if (text.includes(`localhost:${checkoutPort}`))
2643
+ readyPorts.add("checkout");
2644
+ if (readyPorts.size === 3) {
2645
+ entry.ready = true;
2646
+ }
2647
+ };
2648
+
2649
+ child.stdout.on("data", (chunk: Buffer) => {
2650
+ markReady(appendSandboxLog(logs, chunk.toString()));
2651
+ });
2652
+ child.stderr.on("data", (chunk: Buffer) => {
2653
+ markReady(appendSandboxLog(logs, chunk.toString()));
2654
+ });
2655
+ child.on("exit", () => {
2656
+ moduleFederationSandboxes.delete(id);
2657
+ fs.rm(dir, { recursive: true, force: true }).catch(() => {});
2658
+ });
2659
+
2660
+ moduleFederationSandboxes.set(id, entry);
2661
+
2662
+ const deadline = Date.now() + 90_000;
2663
+ while (!entry.ready && Date.now() < deadline) {
2664
+ await new Promise((resolve) => setTimeout(resolve, 500));
2665
+ if (!moduleFederationSandboxes.has(id)) {
2666
+ return res.status(500).json({
2667
+ error: logs.join("").trim() || "Webpack module federation lab exited",
2668
+ });
2669
+ }
2670
+ }
2671
+
2672
+ if (!entry.ready) {
2673
+ return res.status(504).json({
2674
+ error: "Webpack module federation lab did not start in time",
2675
+ logs,
2676
+ });
2677
+ }
2678
+
2679
+ res.json({
2680
+ id,
2681
+ hostUrl: entry.hostUrl,
2682
+ appUrls: entry.appUrls,
2683
+ });
2684
+ } catch (error: any) {
2685
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
2686
+ res.status(500).json({
2687
+ error:
2688
+ logs.join("").trim() ||
2689
+ error?.message ||
2690
+ "Failed to start webpack module federation lab",
2691
+ });
2692
+ }
2693
+ });
2694
+
2695
+ app.post("/api/module-federation/:id/update-files", async (req, res) => {
2696
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
2697
+ if (!sandbox) return res.status(404).json({ error: "Sandbox not found" });
2698
+
2699
+ const { files } = req.body as { files?: Record<string, string> };
2700
+ if (!files || typeof files !== "object") {
2701
+ return res.status(400).json({ error: "files is required" });
2702
+ }
2703
+
2704
+ const nextFiles = new Set(Object.keys(files));
2705
+ await Promise.all(
2706
+ Array.from(sandbox.workspaceFiles)
2707
+ .filter((filePath) => !nextFiles.has(filePath))
2708
+ .map((filePath) =>
2709
+ fs
2710
+ .rm(path.join(sandbox.dir, filePath), { force: true })
2711
+ .catch(() => {}),
2712
+ ),
2713
+ );
2714
+ await writeModuleFederationSandboxFiles(sandbox.dir, files);
2715
+ sandbox.workspaceFiles = nextFiles;
2716
+ res.json({ ok: true });
2717
+ });
2718
+
2719
+ app.get("/api/module-federation/:id/status", (req, res) => {
2720
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
2721
+ if (!sandbox) {
2722
+ return res.json({ running: false });
2723
+ }
2724
+
2725
+ res.json({
2726
+ running: true,
2727
+ ready: sandbox.ready,
2728
+ hostUrl: sandbox.hostUrl,
2729
+ appUrls: sandbox.appUrls,
2730
+ logs: sandbox.logs.slice(-80),
2731
+ });
2732
+ });
2733
+
2734
+ app.delete("/api/module-federation/:id", async (req, res) => {
2735
+ const sandbox = moduleFederationSandboxes.get(req.params.id);
2736
+ if (sandbox) {
2737
+ sandbox.child.kill("SIGTERM");
2738
+ moduleFederationSandboxes.delete(req.params.id);
2739
+ await fs.rm(sandbox.dir, { recursive: true, force: true }).catch(() => {});
2740
+ }
2741
+ res.json({ ok: true });
2742
+ });
2743
+
2483
2744
  async function getFreePort(): Promise<number> {
2484
2745
  return new Promise((resolve, reject) => {
2485
2746
  const srv = net.createServer();
@@ -61,8 +61,17 @@ export interface ContextFile {
61
61
  * 'sandbox' = paired server+client sandbox saved as JSON,
62
62
  * 'infra' = Terraform-style infra lab workspace saved as JSON,
63
63
  * 'react' = React + TypeScript lab workspace,
64
- * 'nextjs' = Next.js App Router lab workspace. */
65
- origin?: "user" | "ai" | "upload" | "sandbox" | "infra" | "react" | "nextjs";
64
+ * 'nextjs' = Next.js App Router lab workspace,
65
+ * 'module-federation' = Webpack Module Federation lab workspace. */
66
+ origin?:
67
+ | "user"
68
+ | "ai"
69
+ | "upload"
70
+ | "sandbox"
71
+ | "infra"
72
+ | "react"
73
+ | "nextjs"
74
+ | "module-federation";
66
75
  /** Language hint for code snippets (e.g. 'typescript', 'javascript'). */
67
76
  language?: string;
68
77
  /** Short display label for code snippets. */