create-interview-cockpit 0.12.0 → 0.14.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.
@@ -106,6 +106,7 @@ interface Store {
106
106
  expandedTopics: string[];
107
107
  availableFiles: string[];
108
108
  showCodePanel: boolean;
109
+ showLabsPanel: boolean;
109
110
  showSidebar: boolean;
110
111
  viewingFile: string | null;
111
112
  viewingDoc: { fileId: string; quote: string; fileName: string } | null;
@@ -183,6 +184,7 @@ interface Store {
183
184
  selectQuestion: (topicId: string, questionId: string) => Promise<void>;
184
185
  toggleTopic: (topicId: string) => void;
185
186
  toggleCodePanel: () => void;
187
+ toggleLabsPanel: () => void;
186
188
  toggleSidebar: () => void;
187
189
  fetchAvailableFiles: () => Promise<void>;
188
190
  updateCodeContext: (questionId: string, files: string[]) => Promise<void>;
@@ -202,6 +204,8 @@ interface Store {
202
204
  files: FileList | File[],
203
205
  ) => Promise<void>;
204
206
  removeQuestionFile: (questionId: string, fileId: string) => Promise<void>;
207
+ detachLabFile: (questionId: string, fileId: string) => Promise<void>;
208
+ attachLabFile: (questionId: string, fileId: string) => Promise<void>;
205
209
  linkFileToQuestion: (
206
210
  questionId: string,
207
211
  fileId: string,
@@ -311,6 +315,11 @@ interface Store {
311
315
  ) => Promise<void>;
312
316
  closeCodeRunner: () => void;
313
317
 
318
+ // ── Deployment Lab ──────────────────────────────────────────
319
+ showDeploymentLab: boolean;
320
+ openDeploymentLab: () => void;
321
+ closeDeploymentLab: () => void;
322
+
314
323
  // ── Infra Lab ────────────────────────────────────────────────
315
324
  showInfraLab: boolean;
316
325
  runnerInitialInfra: InfraLabWorkspace | null;
@@ -349,6 +358,7 @@ export const useStore = create<Store>((set, get) => ({
349
358
  expandedTopics: [],
350
359
  availableFiles: [],
351
360
  showCodePanel: false,
361
+ showLabsPanel: false,
352
362
  showSidebar: true,
353
363
  viewingFile: null,
354
364
  viewingDoc: null,
@@ -362,6 +372,7 @@ export const useStore = create<Store>((set, get) => ({
362
372
  runnerInitialLanguage: "typescript",
363
373
  runnerInitialSandbox: null,
364
374
  runnerInitialFileId: null,
375
+ showDeploymentLab: false,
365
376
  showInfraLab: false,
366
377
  runnerInitialInfra: null,
367
378
  runnerInitialInfraFileId: null,
@@ -671,13 +682,20 @@ export const useStore = create<Store>((set, get) => ({
671
682
  },
672
683
 
673
684
  toggleCodePanel: () => {
674
- set((s) => ({ showCodePanel: !s.showCodePanel }));
685
+ set((s) => ({
686
+ showCodePanel: !s.showCodePanel,
687
+ showLabsPanel: false,
688
+ }));
675
689
  const { availableFiles, fetchAvailableFiles } = get();
676
690
  if (availableFiles.length === 0) {
677
691
  fetchAvailableFiles();
678
692
  }
679
693
  },
680
694
 
695
+ toggleLabsPanel: () => {
696
+ set((s) => ({ showLabsPanel: !s.showLabsPanel, showCodePanel: false }));
697
+ },
698
+
681
699
  fetchAvailableFiles: async () => {
682
700
  const files = await api.fetchCodeContextTree();
683
701
  set({ availableFiles: files });
@@ -769,6 +787,36 @@ export const useStore = create<Store>((set, get) => ({
769
787
  }));
770
788
  },
771
789
 
790
+ detachLabFile: async (questionId, fileId) => {
791
+ const cf = await api.detachQuestionLabFile(questionId, fileId);
792
+ set((s) => ({
793
+ currentQuestion:
794
+ s.currentQuestion?.id === questionId
795
+ ? {
796
+ ...s.currentQuestion,
797
+ contextFiles: (s.currentQuestion.contextFiles || []).map((f) =>
798
+ f.id === fileId ? { ...f, inContext: cf.inContext } : f,
799
+ ),
800
+ }
801
+ : s.currentQuestion,
802
+ }));
803
+ },
804
+
805
+ attachLabFile: async (questionId, fileId) => {
806
+ const cf = await api.attachQuestionLabFile(questionId, fileId);
807
+ set((s) => ({
808
+ currentQuestion:
809
+ s.currentQuestion?.id === questionId
810
+ ? {
811
+ ...s.currentQuestion,
812
+ contextFiles: (s.currentQuestion.contextFiles || []).map((f) =>
813
+ f.id === fileId ? { ...f, inContext: cf.inContext } : f,
814
+ ),
815
+ }
816
+ : s.currentQuestion,
817
+ }));
818
+ },
819
+
772
820
  linkFileToQuestion: async (questionId, fileId, originalName) => {
773
821
  const cf = await api.linkFileToQuestion(questionId, fileId, originalName);
774
822
  set((s) => ({
@@ -1010,6 +1058,9 @@ export const useStore = create<Store>((set, get) => ({
1010
1058
  }));
1011
1059
  },
1012
1060
  closeCodeRunner: () => set({ showCodeRunner: false }),
1061
+ showDeploymentLab: false,
1062
+ openDeploymentLab: () => set({ showDeploymentLab: true }),
1063
+ closeDeploymentLab: () => set({ showDeploymentLab: false }),
1013
1064
  closeInfraLab: () => set({ showInfraLab: false }),
1014
1065
 
1015
1066
  fetchAiSettings: async () => {
@@ -23,6 +23,8 @@ export interface ContextFile {
23
23
  language?: string;
24
24
  /** Short display label for code snippets. */
25
25
  label?: string;
26
+ /** When false, file is saved but excluded from AI prompt context (detached lab). */
27
+ inContext?: boolean;
26
28
  }
27
29
 
28
30
  export interface FrontendLabWorkspace {
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.10.0"
2
+ "version": "0.13.0"
3
3
  }
@@ -684,6 +684,38 @@ app.delete(
684
684
  },
685
685
  );
686
686
 
687
+ // Detach a lab file from AI context without deleting it
688
+ app.post(
689
+ "/api/questions/:questionId/context-files/:fileId/detach",
690
+ async (req, res) => {
691
+ try {
692
+ const cf = await storage.detachQuestionContextFile(
693
+ req.params.questionId,
694
+ req.params.fileId,
695
+ );
696
+ res.json(cf);
697
+ } catch (err: any) {
698
+ res.status(500).json({ error: err?.message || "Failed to detach" });
699
+ }
700
+ },
701
+ );
702
+
703
+ // Re-attach a previously detached lab file to AI context
704
+ app.post(
705
+ "/api/questions/:questionId/context-files/:fileId/attach",
706
+ async (req, res) => {
707
+ try {
708
+ const cf = await storage.attachQuestionContextFile(
709
+ req.params.questionId,
710
+ req.params.fileId,
711
+ );
712
+ res.json(cf);
713
+ } catch (err: any) {
714
+ res.status(500).json({ error: err?.message || "Failed to attach" });
715
+ }
716
+ },
717
+ );
718
+
687
719
  // Save a code snippet (from Code Runner or AI response) as a question context file
688
720
  app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
689
721
  const { code, language, label, origin } = req.body as {
@@ -1376,7 +1408,9 @@ app.post("/api/chat", async (req, res) => {
1376
1408
  if (questionId) {
1377
1409
  const question = await storage.getQuestion(questionId);
1378
1410
  if (question?.contextFiles?.length) {
1379
- for (const cf of question.contextFiles) {
1411
+ for (const cf of question.contextFiles.filter(
1412
+ (c) => c.inContext !== false,
1413
+ )) {
1380
1414
  fileRegistry.set(cf.id, {
1381
1415
  label: `[question] ${cf.originalName}`,
1382
1416
  reader: () => storage.readContextFileContent(cf.id),
@@ -2555,17 +2589,19 @@ function getModuleFederationCommandEnv(
2555
2589
  sandbox: ModuleFederationSandboxEntry,
2556
2590
  ): NodeJS.ProcessEnv {
2557
2591
  const hostPort = new URL(sandbox.appUrls.host).port;
2558
- const profilePort = new URL(sandbox.appUrls.profile).port;
2559
- const checkoutPort = new URL(sandbox.appUrls.checkout).port;
2560
-
2561
- return {
2592
+ const env: NodeJS.ProcessEnv = {
2562
2593
  ...process.env,
2563
2594
  HOST_PORT: hostPort,
2564
- PROFILE_PORT: profilePort,
2565
- CHECKOUT_PORT: checkoutPort,
2566
2595
  MF_SANDBOX_ID: sandbox.id,
2567
2596
  npm_config_update_notifier: "false",
2568
2597
  };
2598
+ if (sandbox.appUrls.profile)
2599
+ env.PROFILE_PORT = new URL(sandbox.appUrls.profile).port;
2600
+ if (sandbox.appUrls.checkout)
2601
+ env.CHECKOUT_PORT = new URL(sandbox.appUrls.checkout).port;
2602
+ if (sandbox.appUrls.mfeAuth)
2603
+ env.MFE_AUTH_PORT = new URL(sandbox.appUrls.mfeAuth).port;
2604
+ return env;
2569
2605
  }
2570
2606
 
2571
2607
  async function runStreamedCommand(
@@ -2900,24 +2936,42 @@ app.post("/api/module-federation/start", async (req, res) => {
2900
2936
  logs,
2901
2937
  );
2902
2938
 
2903
- const [hostPort, profilePort, checkoutPort] = await getDistinctPorts(3);
2904
- const appUrls = {
2939
+ // Detect isolated 2-app pattern (host + mfe-auth, no profile/checkout).
2940
+ const isIsolated =
2941
+ typeof files["apps/mfe-auth/package.json"] === "string" &&
2942
+ typeof files["apps/checkout/package.json"] !== "string";
2943
+
2944
+ const ports = await getDistinctPorts(isIsolated ? 2 : 3);
2945
+ const [hostPort] = ports;
2946
+
2947
+ const appUrls: Record<string, string> = {
2905
2948
  host: `http://localhost:${hostPort}`,
2906
- profile: `http://localhost:${profilePort}`,
2907
- checkout: `http://localhost:${checkoutPort}`,
2908
2949
  };
2950
+ const spawnEnv: NodeJS.ProcessEnv = {
2951
+ ...process.env,
2952
+ HOST_PORT: String(hostPort),
2953
+ MF_SANDBOX_ID: id,
2954
+ npm_config_update_notifier: "false",
2955
+ };
2956
+
2957
+ if (isIsolated) {
2958
+ const [, mfeAuthPort] = ports;
2959
+ appUrls.mfeAuth = `http://localhost:${mfeAuthPort}`;
2960
+ spawnEnv.MFE_AUTH_PORT = String(mfeAuthPort);
2961
+ } else {
2962
+ const [, profilePort, checkoutPort] = ports;
2963
+ appUrls.profile = `http://localhost:${profilePort}`;
2964
+ appUrls.checkout = `http://localhost:${checkoutPort}`;
2965
+ spawnEnv.PROFILE_PORT = String(profilePort);
2966
+ spawnEnv.CHECKOUT_PORT = String(checkoutPort);
2967
+ }
2968
+
2909
2969
  const readyPorts = new Set<string>();
2970
+ const requiredPorts = isIsolated ? 2 : 3;
2910
2971
 
2911
2972
  const child = spawn(npmCommand(), ["run", "dev"], {
2912
2973
  cwd: dir,
2913
- env: {
2914
- ...process.env,
2915
- HOST_PORT: String(hostPort),
2916
- PROFILE_PORT: String(profilePort),
2917
- CHECKOUT_PORT: String(checkoutPort),
2918
- MF_SANDBOX_ID: id,
2919
- npm_config_update_notifier: "false",
2920
- },
2974
+ env: spawnEnv,
2921
2975
  });
2922
2976
 
2923
2977
  const entry: ModuleFederationSandboxEntry = {
@@ -2932,11 +2986,10 @@ app.post("/api/module-federation/start", async (req, res) => {
2932
2986
  };
2933
2987
 
2934
2988
  const markReady = (text: string) => {
2935
- if (text.includes(`localhost:${hostPort}`)) readyPorts.add("host");
2936
- if (text.includes(`localhost:${profilePort}`)) readyPorts.add("profile");
2937
- if (text.includes(`localhost:${checkoutPort}`))
2938
- readyPorts.add("checkout");
2939
- if (readyPorts.size === 3) {
2989
+ for (const [key, url] of Object.entries(appUrls)) {
2990
+ if (text.includes(new URL(url).host)) readyPorts.add(key);
2991
+ }
2992
+ if (readyPorts.size >= requiredPorts) {
2940
2993
  entry.ready = true;
2941
2994
  }
2942
2995
  };
@@ -76,6 +76,9 @@ export interface ContextFile {
76
76
  language?: string;
77
77
  /** Short display label for code snippets. */
78
78
  label?: string;
79
+ /** When false, the file is saved but excluded from the AI prompt context.
80
+ * Used to detach lab files without permanently deleting them. */
81
+ inContext?: boolean;
79
82
  }
80
83
 
81
84
  export interface Message {
@@ -802,6 +805,34 @@ export async function deleteQuestionContextFile(
802
805
  }
803
806
  }
804
807
 
808
+ /** Removes a lab file from the AI context without deleting it from disk. */
809
+ export async function detachQuestionContextFile(
810
+ questionId: string,
811
+ fileId: string,
812
+ ): Promise<ContextFile> {
813
+ const q = await getQuestion(questionId);
814
+ if (!q) throw new Error("Question not found");
815
+ const cf = (q.contextFiles || []).find((f) => f.id === fileId);
816
+ if (!cf) throw new Error("Context file not found");
817
+ cf.inContext = false;
818
+ await saveQuestion(q);
819
+ return cf;
820
+ }
821
+
822
+ /** Re-attaches a detached lab file to the AI context. */
823
+ export async function attachQuestionContextFile(
824
+ questionId: string,
825
+ fileId: string,
826
+ ): Promise<ContextFile> {
827
+ const q = await getQuestion(questionId);
828
+ if (!q) throw new Error("Question not found");
829
+ const cf = (q.contextFiles || []).find((f) => f.id === fileId);
830
+ if (!cf) throw new Error("Context file not found");
831
+ cf.inContext = true;
832
+ await saveQuestion(q);
833
+ return cf;
834
+ }
835
+
805
836
  export async function updateQuestionMessages(
806
837
  questionId: string,
807
838
  messages: Message[],