create-interview-cockpit 0.14.0 → 0.16.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.
@@ -220,10 +220,12 @@ interface Store {
220
220
  | "user"
221
221
  | "ai"
222
222
  | "sandbox"
223
+ | "browser-security"
223
224
  | "infra"
224
225
  | "react"
225
226
  | "nextjs"
226
- | "module-federation",
227
+ | "module-federation"
228
+ | "canvas",
227
229
  ) => Promise<import("./types").ContextFile>;
228
230
  clearMessages: (questionId: string) => Promise<void>;
229
231
 
@@ -283,6 +285,8 @@ interface Store {
283
285
  clientCode: string;
284
286
  clientLang: string;
285
287
  fileId?: string;
288
+ label?: string;
289
+ origin?: "sandbox" | "browser-security";
286
290
  /** If set, the client panel opens in React or Next.js preview mode instead of script mode */
287
291
  clientType?: "script" | "react" | "nextjs" | "module-federation";
288
292
  reactFiles?: Record<string, string> | null;
@@ -298,6 +302,8 @@ interface Store {
298
302
  clientLang: string,
299
303
  fileId?: string,
300
304
  opts?: {
305
+ label?: string;
306
+ origin?: "sandbox" | "browser-security";
301
307
  clientType?: "script" | "react" | "nextjs" | "module-federation";
302
308
  reactFiles?: Record<string, string>;
303
309
  reactActiveFile?: string;
@@ -320,6 +326,11 @@ interface Store {
320
326
  openDeploymentLab: () => void;
321
327
  closeDeploymentLab: () => void;
322
328
 
329
+ // ── Browser Security Lab ─────────────────────────────────────
330
+ showBrowserSecurityLab: boolean;
331
+ openBrowserSecurityLab: () => void;
332
+ closeBrowserSecurityLab: () => void;
333
+
323
334
  // ── Infra Lab ────────────────────────────────────────────────
324
335
  showInfraLab: boolean;
325
336
  runnerInitialInfra: InfraLabWorkspace | null;
@@ -346,6 +357,13 @@ interface Store {
346
357
  serverCode?: string,
347
358
  serverLang?: string,
348
359
  ) => void;
360
+
361
+ // ── Canvas Lab ───────────────────────────────────────────────
362
+ showCanvasLab: boolean;
363
+ canvasLabInitialCode: string | null;
364
+ canvasLabInitialFileId: string | null;
365
+ openCanvasLab: (code?: string, fileId?: string) => void;
366
+ closeCanvasLab: () => void;
349
367
  }
350
368
 
351
369
  export const useStore = create<Store>((set, get) => ({
@@ -373,9 +391,13 @@ export const useStore = create<Store>((set, get) => ({
373
391
  runnerInitialSandbox: null,
374
392
  runnerInitialFileId: null,
375
393
  showDeploymentLab: false,
394
+ showBrowserSecurityLab: false,
376
395
  showInfraLab: false,
377
396
  runnerInitialInfra: null,
378
397
  runnerInitialInfraFileId: null,
398
+ showCanvasLab: false,
399
+ canvasLabInitialCode: null,
400
+ canvasLabInitialFileId: null,
379
401
 
380
402
  // ── Workspaces ───────────────────────────────────────────────
381
403
  workspaces: [],
@@ -971,6 +993,8 @@ export const useStore = create<Store>((set, get) => ({
971
993
  clientCode,
972
994
  clientLang,
973
995
  fileId,
996
+ label: opts?.label,
997
+ origin: opts?.origin,
974
998
  clientType: opts?.clientType,
975
999
  reactFiles: opts?.reactFiles,
976
1000
  reactActiveFile: opts?.reactActiveFile,
@@ -1058,10 +1082,23 @@ export const useStore = create<Store>((set, get) => ({
1058
1082
  }));
1059
1083
  },
1060
1084
  closeCodeRunner: () => set({ showCodeRunner: false }),
1061
- showDeploymentLab: false,
1062
1085
  openDeploymentLab: () => set({ showDeploymentLab: true }),
1063
1086
  closeDeploymentLab: () => set({ showDeploymentLab: false }),
1087
+ openBrowserSecurityLab: () => set({ showBrowserSecurityLab: true }),
1088
+ closeBrowserSecurityLab: () => set({ showBrowserSecurityLab: false }),
1064
1089
  closeInfraLab: () => set({ showInfraLab: false }),
1090
+ openCanvasLab: (code?, fileId?) =>
1091
+ set({
1092
+ showCanvasLab: true,
1093
+ canvasLabInitialCode: code ?? null,
1094
+ canvasLabInitialFileId: fileId ?? null,
1095
+ }),
1096
+ closeCanvasLab: () =>
1097
+ set({
1098
+ showCanvasLab: false,
1099
+ canvasLabInitialCode: null,
1100
+ canvasLabInitialFileId: null,
1101
+ }),
1065
1102
 
1066
1103
  fetchAiSettings: async () => {
1067
1104
  const settings = await api.fetchAiSettings();
@@ -3,10 +3,12 @@ export type ContextFileOrigin =
3
3
  | "ai"
4
4
  | "upload"
5
5
  | "sandbox"
6
+ | "browser-security"
6
7
  | "infra"
7
8
  | "react"
8
9
  | "nextjs"
9
- | "module-federation";
10
+ | "module-federation"
11
+ | "canvas";
10
12
 
11
13
  export interface ContextFile {
12
14
  id: string;
@@ -17,6 +19,7 @@ export interface ContextFile {
17
19
  /** Distinguishes how this file was added. 'upload' = user-uploaded doc,
18
20
  * 'user' = code saved from Code Runner, 'ai' = AI-generated code block,
19
21
  * 'sandbox' = paired server+client sandbox saved as JSON,
22
+ * 'browser-security' = paired server+client security lab saved as JSON,
20
23
  * 'infra' = Terraform-style infra lab workspace saved as JSON. */
21
24
  origin?: ContextFileOrigin;
22
25
  /** Language hint for code snippets (e.g. 'typescript', 'javascript'). */
@@ -49,6 +52,7 @@ export interface WorkspaceMeta {
49
52
  id: string;
50
53
  name: string;
51
54
  type: "local" | "google_drive";
55
+ questionSortOrder?: "name" | "createdAt";
52
56
  driveConfig?: {
53
57
  folderId: string;
54
58
  folderName: string;
@@ -1 +1 @@
1
- {"root":["./src/app.tsx","./src/api.ts","./src/infralab.ts","./src/main.tsx","./src/reactlab.ts","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/aisettingsmodal.tsx","./src/components/annotationdialog.tsx","./src/components/chatmessage.tsx","./src/components/chatview.tsx","./src/components/codecontextpanel.tsx","./src/components/codelineannotationpopup.tsx","./src/components/coderunnermodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/infralabmodal.tsx","./src/components/linkedconvospicker.tsx","./src/components/markdownrenderer.tsx","./src/components/mermaiddiagram.tsx","./src/components/notesmodal.tsx","./src/components/plotembed.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx","./src/components/vizcraftembed.tsx","./src/components/workspaceswitcher.tsx"],"errors":true,"version":"5.9.3"}
1
+ {"root":["./src/app.tsx","./src/api.ts","./src/browsersecuritytemplates.ts","./src/infralab.ts","./src/main.tsx","./src/reactlab.ts","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/aisettingsmodal.tsx","./src/components/annotationdialog.tsx","./src/components/browsersecuritylabmodal.tsx","./src/components/chatmessage.tsx","./src/components/chatview.tsx","./src/components/codecontextpanel.tsx","./src/components/codelineannotationpopup.tsx","./src/components/coderunnermodal.tsx","./src/components/deploymentlabmodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/infralabmodal.tsx","./src/components/labspanel.tsx","./src/components/linkedconvospicker.tsx","./src/components/markdownrenderer.tsx","./src/components/mermaiddiagram.tsx","./src/components/notesmodal.tsx","./src/components/plotembed.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx","./src/components/vizcraftembed.tsx","./src/components/workspaceswitcher.tsx"],"errors":true,"version":"5.9.3"}
@@ -1,12 +1,19 @@
1
- import { defineConfig } from "vite";
1
+ import { defineConfig, loadEnv } from "vite";
2
2
  import react from "@vitejs/plugin-react";
3
3
 
4
- export default defineConfig({
5
- plugins: [react()],
6
- server: {
7
- port: 5173,
8
- proxy: {
9
- "/api": "http://localhost:3001",
4
+ export default defineConfig(({ mode }) => {
5
+ // Load the root .env (one level above client/)
6
+ const env = loadEnv(mode, "../", "");
7
+ const clientPort = parseInt(env.CLIENT_PORT || "5173", 10);
8
+ const serverPort = parseInt(env.PORT || "3001", 10);
9
+
10
+ return {
11
+ plugins: [react()],
12
+ server: {
13
+ port: clientPort,
14
+ proxy: {
15
+ "/api": `http://localhost:${serverPort}`,
16
+ },
10
17
  },
11
- },
18
+ };
12
19
  });
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.13.0"
2
+ "version": "0.14.0"
3
3
  }
@@ -330,6 +330,7 @@ export async function syncWorkspace(
330
330
  let codeContextFiles: string[] = [];
331
331
  let codeAnnotations: storage.Question["codeAnnotations"] = {};
332
332
  let parentTitle: string | null = null;
333
+ let restoredCreatedAt: string | null = null;
333
334
  const restoredContextFiles: storage.ContextFile[] = [];
334
335
 
335
336
  if (filename.endsWith(".json")) {
@@ -342,6 +343,7 @@ export async function syncWorkspace(
342
343
  codeContextFiles = parsed.codeContextFiles || [];
343
344
  codeAnnotations = parsed.codeAnnotations || {};
344
345
  parentTitle = parsed.parentTitle || null;
346
+ if (parsed.createdAt) restoredCreatedAt = parsed.createdAt;
345
347
 
346
348
  // Restore code snippet context files (user/ai origin)
347
349
  if (
@@ -358,6 +360,7 @@ export async function syncWorkspace(
358
360
  (cs.origin === "user" ||
359
361
  cs.origin === "ai" ||
360
362
  cs.origin === "sandbox" ||
363
+ cs.origin === "browser-security" ||
361
364
  cs.origin === "react" ||
362
365
  cs.origin === "nextjs" ||
363
366
  cs.origin === "module-federation" ||
@@ -398,7 +401,7 @@ export async function syncWorkspace(
398
401
  contextFiles: restoredContextFiles,
399
402
  messages,
400
403
  codeAnnotations,
401
- createdAt: new Date().toISOString(),
404
+ createdAt: restoredCreatedAt ?? new Date().toISOString(),
402
405
  };
403
406
  await storage.saveQuestion(q);
404
407
  result.filesImported++;
@@ -769,6 +772,7 @@ export async function exportWorkspace(
769
772
  cf.origin === "user" ||
770
773
  cf.origin === "ai" ||
771
774
  cf.origin === "sandbox" ||
775
+ cf.origin === "browser-security" ||
772
776
  cf.origin === "react" ||
773
777
  cf.origin === "nextjs" ||
774
778
  cf.origin === "module-federation" ||
@@ -796,6 +800,7 @@ export async function exportWorkspace(
796
800
  codeContextFiles: q.codeContextFiles,
797
801
  codeAnnotations: q.codeAnnotations ?? {},
798
802
  codeSnippets: contextFilesWithContent,
803
+ createdAt: q.createdAt,
799
804
  },
800
805
  null,
801
806
  2,
@@ -726,6 +726,7 @@ app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
726
726
  | "user"
727
727
  | "ai"
728
728
  | "sandbox"
729
+ | "browser-security"
729
730
  | "infra"
730
731
  | "react"
731
732
  | "nextjs"
@@ -738,14 +739,16 @@ app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
738
739
  origin !== "user" &&
739
740
  origin !== "ai" &&
740
741
  origin !== "sandbox" &&
742
+ origin !== "browser-security" &&
741
743
  origin !== "infra" &&
742
744
  origin !== "react" &&
743
745
  origin !== "nextjs" &&
744
- origin !== "module-federation"
746
+ origin !== "module-federation" &&
747
+ origin !== "canvas"
745
748
  ) {
746
749
  return res.status(400).json({
747
750
  error:
748
- "origin must be 'user', 'ai', 'sandbox', 'infra', 'react', 'nextjs', or 'module-federation'",
751
+ "origin must be 'user', 'ai', 'sandbox', 'browser-security', 'infra', 'react', 'nextjs', 'module-federation', or 'canvas'",
749
752
  });
750
753
  }
751
754
  try {
@@ -2885,6 +2888,51 @@ app.post("/api/nextjs/:id/update-files", async (req, res) => {
2885
2888
  res.json({ ok: true });
2886
2889
  });
2887
2890
 
2891
+ app.post("/api/nextjs/:id/command-stream", async (req, res) => {
2892
+ const sb = nextSandboxes.get(req.params.id);
2893
+
2894
+ res.setHeader("Content-Type", "text/event-stream");
2895
+ res.setHeader("Cache-Control", "no-cache");
2896
+ res.setHeader("Connection", "keep-alive");
2897
+ res.flushHeaders();
2898
+
2899
+ const send = (payload: unknown) => {
2900
+ res.write(`data: ${JSON.stringify(payload)}\n\n`);
2901
+ };
2902
+
2903
+ if (!sb) {
2904
+ send({ type: "error", error: "Sandbox not found" });
2905
+ res.end();
2906
+ return;
2907
+ }
2908
+
2909
+ const { command } = req.body as { command?: string };
2910
+ if (typeof command !== "string" || !command.trim()) {
2911
+ send({ type: "error", error: "command is required" });
2912
+ res.end();
2913
+ return;
2914
+ }
2915
+
2916
+ try {
2917
+ const parsed = parseReactLabCommand(command);
2918
+ send({ type: "output", kind: "info", text: `$ ${command.trim()}\n` });
2919
+ await runStreamedCommand(
2920
+ npmCommand(),
2921
+ parsed.args,
2922
+ {
2923
+ cwd: sb.dir,
2924
+ env: { ...process.env, npm_config_update_notifier: "false" },
2925
+ },
2926
+ ({ kind, text }) => send({ type: "output", kind, text }),
2927
+ );
2928
+ send({ type: "complete" });
2929
+ } catch (error: any) {
2930
+ send({ type: "error", error: error?.message || "Command failed" });
2931
+ }
2932
+
2933
+ res.end();
2934
+ });
2935
+
2888
2936
  app.get("/api/nextjs/:id/status", (req, res) => {
2889
2937
  const sb = nextSandboxes.get(req.params.id);
2890
2938
  if (!sb) return res.json({ running: false });
@@ -2925,7 +2973,7 @@ app.post("/api/module-federation/start", async (req, res) => {
2925
2973
 
2926
2974
  await runLoggedCommand(
2927
2975
  npmCommand(),
2928
- ["install", "--no-audit", "--no-fund", "--prefer-offline"],
2976
+ ["install", "--no-audit", "--no-fund", "--legacy-peer-deps"],
2929
2977
  {
2930
2978
  cwd: dir,
2931
2979
  env: {
@@ -2936,12 +2984,36 @@ app.post("/api/module-federation/start", async (req, res) => {
2936
2984
  logs,
2937
2985
  );
2938
2986
 
2987
+ // Detect new modern Next.js MFE template types.
2988
+ const isMultiZones = typeof files["apps/zone-b/package.json"] === "string";
2989
+ const isMfRuntimeApi =
2990
+ typeof files["apps/mf-remote/package.json"] === "string";
2991
+ const isRspackShell =
2992
+ typeof files["apps/rspack-shell/package.json"] === "string";
2993
+
2939
2994
  // Detect isolated 2-app pattern (host + mfe-auth, no profile/checkout).
2940
2995
  const isIsolated =
2941
2996
  typeof files["apps/mfe-auth/package.json"] === "string" &&
2942
2997
  typeof files["apps/checkout/package.json"] !== "string";
2943
2998
 
2944
- const ports = await getDistinctPorts(isIsolated ? 2 : 3);
2999
+ // Detect Next.js MF pattern (shell + remote, no mfe-auth/checkout/profile).
3000
+ // Excludes Multi-Zones (zone-b) and MF Runtime API (mf-remote) which also use apps/shell.
3001
+ const isNextjsMf =
3002
+ typeof files["apps/shell/package.json"] === "string" &&
3003
+ !isMultiZones &&
3004
+ !isMfRuntimeApi &&
3005
+ typeof files["apps/mfe-auth/package.json"] !== "string" &&
3006
+ typeof files["apps/checkout/package.json"] !== "string";
3007
+
3008
+ const ports = await getDistinctPorts(
3009
+ isIsolated ||
3010
+ isNextjsMf ||
3011
+ isMultiZones ||
3012
+ isMfRuntimeApi ||
3013
+ isRspackShell
3014
+ ? 2
3015
+ : 3,
3016
+ );
2945
3017
  const [hostPort] = ports;
2946
3018
 
2947
3019
  const appUrls: Record<string, string> = {
@@ -2954,7 +3026,32 @@ app.post("/api/module-federation/start", async (req, res) => {
2954
3026
  npm_config_update_notifier: "false",
2955
3027
  };
2956
3028
 
2957
- if (isIsolated) {
3029
+ if (isNextjsMf) {
3030
+ const [, remotePort] = ports;
3031
+ appUrls.remote = `http://localhost:${remotePort}`;
3032
+ spawnEnv.REMOTE_PORT = String(remotePort);
3033
+ // Next.js remotes put the entry at /_next/static/chunks/; webpack remotes put it at root.
3034
+ const isNextjsRemote =
3035
+ typeof files["apps/remote/next.config.js"] === "string";
3036
+ const remoteEntryPath = isNextjsRemote
3037
+ ? "/_next/static/chunks/remoteEntry.js"
3038
+ : "/remoteEntry.js";
3039
+ spawnEnv.NEXT_PUBLIC_REMOTE_URL = `http://localhost:${remotePort}${remoteEntryPath}`;
3040
+ } else if (isMultiZones) {
3041
+ const [, remotePort] = ports;
3042
+ // Zone B serves everything under /store (basePath), so point the preview there.
3043
+ appUrls.zoneB = `http://localhost:${remotePort}/store`;
3044
+ spawnEnv.REMOTE_PORT = String(remotePort);
3045
+ } else if (isMfRuntimeApi) {
3046
+ const [, remotePort] = ports;
3047
+ appUrls.mfRemote = `http://localhost:${remotePort}`;
3048
+ spawnEnv.REMOTE_PORT = String(remotePort);
3049
+ spawnEnv.NEXT_PUBLIC_REMOTE_URL = `http://localhost:${remotePort}/remoteEntry.js`;
3050
+ } else if (isRspackShell) {
3051
+ const [, remotePort] = ports;
3052
+ appUrls.remote = `http://localhost:${remotePort}`;
3053
+ spawnEnv.REMOTE_PORT = String(remotePort);
3054
+ } else if (isIsolated) {
2958
3055
  const [, mfeAuthPort] = ports;
2959
3056
  appUrls.mfeAuth = `http://localhost:${mfeAuthPort}`;
2960
3057
  spawnEnv.MFE_AUTH_PORT = String(mfeAuthPort);
@@ -2967,7 +3064,14 @@ app.post("/api/module-federation/start", async (req, res) => {
2967
3064
  }
2968
3065
 
2969
3066
  const readyPorts = new Set<string>();
2970
- const requiredPorts = isIsolated ? 2 : 3;
3067
+ const requiredPorts =
3068
+ isIsolated ||
3069
+ isNextjsMf ||
3070
+ isMultiZones ||
3071
+ isMfRuntimeApi ||
3072
+ isRspackShell
3073
+ ? 2
3074
+ : 3;
2971
3075
 
2972
3076
  const child = spawn(npmCommand(), ["run", "dev"], {
2973
3077
  cwd: dir,
@@ -59,6 +59,7 @@ export interface ContextFile {
59
59
  /** Distinguishes how this file was added. 'upload' = user-uploaded doc,
60
60
  * 'user' = code saved from Code Runner, 'ai' = AI-generated code block,
61
61
  * 'sandbox' = paired server+client sandbox saved as JSON,
62
+ * 'browser-security' = paired server+client security lab saved as JSON,
62
63
  * 'infra' = Terraform-style infra lab workspace saved as JSON,
63
64
  * 'react' = React + TypeScript lab workspace,
64
65
  * 'nextjs' = Next.js App Router lab workspace,
@@ -68,6 +69,7 @@ export interface ContextFile {
68
69
  | "ai"
69
70
  | "upload"
70
71
  | "sandbox"
72
+ | "browser-security"
71
73
  | "infra"
72
74
  | "react"
73
75
  | "nextjs"
@@ -152,6 +154,7 @@ export interface WorkspaceMeta {
152
154
  id: string;
153
155
  name: string;
154
156
  type: "local" | "google_drive";
157
+ questionSortOrder?: "name" | "createdAt";
155
158
  driveConfig?: DriveConfig;
156
159
  createdAt: string;
157
160
  }