create-interview-cockpit 0.15.0 → 0.17.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.
@@ -224,7 +224,8 @@ interface Store {
224
224
  | "infra"
225
225
  | "react"
226
226
  | "nextjs"
227
- | "module-federation",
227
+ | "module-federation"
228
+ | "canvas",
228
229
  ) => Promise<import("./types").ContextFile>;
229
230
  clearMessages: (questionId: string) => Promise<void>;
230
231
 
@@ -356,6 +357,13 @@ interface Store {
356
357
  serverCode?: string,
357
358
  serverLang?: string,
358
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;
359
367
  }
360
368
 
361
369
  export const useStore = create<Store>((set, get) => ({
@@ -387,6 +395,9 @@ export const useStore = create<Store>((set, get) => ({
387
395
  showInfraLab: false,
388
396
  runnerInitialInfra: null,
389
397
  runnerInitialInfraFileId: null,
398
+ showCanvasLab: false,
399
+ canvasLabInitialCode: null,
400
+ canvasLabInitialFileId: null,
390
401
 
391
402
  // ── Workspaces ───────────────────────────────────────────────
392
403
  workspaces: [],
@@ -1076,6 +1087,18 @@ export const useStore = create<Store>((set, get) => ({
1076
1087
  openBrowserSecurityLab: () => set({ showBrowserSecurityLab: true }),
1077
1088
  closeBrowserSecurityLab: () => set({ showBrowserSecurityLab: false }),
1078
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
+ }),
1079
1102
 
1080
1103
  fetchAiSettings: async () => {
1081
1104
  const settings = await api.fetchAiSettings();
@@ -7,7 +7,8 @@ export type ContextFileOrigin =
7
7
  | "infra"
8
8
  | "react"
9
9
  | "nextjs"
10
- | "module-federation";
10
+ | "module-federation"
11
+ | "canvas";
11
12
 
12
13
  export interface ContextFile {
13
14
  id: string;
@@ -51,6 +52,7 @@ export interface WorkspaceMeta {
51
52
  id: string;
52
53
  name: string;
53
54
  type: "local" | "google_drive";
55
+ questionSortOrder?: "name" | "createdAt";
54
56
  driveConfig?: {
55
57
  folderId: string;
56
58
  folderName: string;
@@ -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.16.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 (
@@ -399,7 +401,7 @@ export async function syncWorkspace(
399
401
  contextFiles: restoredContextFiles,
400
402
  messages,
401
403
  codeAnnotations,
402
- createdAt: new Date().toISOString(),
404
+ createdAt: restoredCreatedAt ?? new Date().toISOString(),
403
405
  };
404
406
  await storage.saveQuestion(q);
405
407
  result.filesImported++;
@@ -798,6 +800,7 @@ export async function exportWorkspace(
798
800
  codeContextFiles: q.codeContextFiles,
799
801
  codeAnnotations: q.codeAnnotations ?? {},
800
802
  codeSnippets: contextFilesWithContent,
803
+ createdAt: q.createdAt,
801
804
  },
802
805
  null,
803
806
  2,
@@ -743,11 +743,12 @@ app.post("/api/questions/:questionId/save-code-snippet", async (req, res) => {
743
743
  origin !== "infra" &&
744
744
  origin !== "react" &&
745
745
  origin !== "nextjs" &&
746
- origin !== "module-federation"
746
+ origin !== "module-federation" &&
747
+ origin !== "canvas"
747
748
  ) {
748
749
  return res.status(400).json({
749
750
  error:
750
- "origin must be 'user', 'ai', 'sandbox', 'browser-security', 'infra', 'react', 'nextjs', or 'module-federation'",
751
+ "origin must be 'user', 'ai', 'sandbox', 'browser-security', 'infra', 'react', 'nextjs', 'module-federation', or 'canvas'",
751
752
  });
752
753
  }
753
754
  try {
@@ -1640,7 +1641,34 @@ Examples (illustrative only — use real ids and names from the list above):
1640
1641
 
1641
1642
  // ─── Code Context Browser ────────────────────────────────
1642
1643
 
1643
- const IGNORE_DIRS = new Set([
1644
+ function parseListEnv(value: string | undefined): string[] {
1645
+ if (!value) return [];
1646
+ return value
1647
+ .split(/[\n,;]/)
1648
+ .map((item) => item.trim())
1649
+ .filter(Boolean);
1650
+ }
1651
+
1652
+ function normalizeName(value: string): string {
1653
+ return path.basename(value).trim().toLowerCase();
1654
+ }
1655
+
1656
+ function normalizePathPrefix(value: string): string {
1657
+ return value
1658
+ .replaceAll("\\", "/")
1659
+ .replace(/^\.\//, "")
1660
+ .replace(/^\/+/, "")
1661
+ .replace(/\/+$/, "")
1662
+ .toLowerCase();
1663
+ }
1664
+
1665
+ function normalizeExtension(value: string): string {
1666
+ const ext = value.trim().toLowerCase();
1667
+ if (!ext) return "";
1668
+ return ext.startsWith(".") ? ext : `.${ext}`;
1669
+ }
1670
+
1671
+ const DEFAULT_IGNORE_DIRS = [
1644
1672
  "node_modules",
1645
1673
  ".git",
1646
1674
  "dist",
@@ -1666,70 +1694,130 @@ const IGNORE_DIRS = new Set([
1666
1694
  "venv",
1667
1695
  ".venv",
1668
1696
  // macOS / editor noise
1669
- ".DS_Store",
1670
1697
  ".Spotlight-V100",
1671
1698
  ".Trashes",
1699
+ ];
1700
+
1701
+ const DEFAULT_IGNORE_FILES = [
1702
+ ".DS_Store",
1672
1703
  ".env",
1673
1704
  ".env.local",
1674
1705
  ".env.development",
1675
1706
  ".env.production",
1676
- ]);
1707
+ ];
1708
+
1709
+ const IGNORE_DIRS = new Set(
1710
+ [
1711
+ ...DEFAULT_IGNORE_DIRS,
1712
+ ...parseListEnv(process.env.CODE_CONTEXT_IGNORE_DIRS),
1713
+ ...parseListEnv(process.env.IGNORE_DIRS),
1714
+ ].map(normalizeName),
1715
+ );
1716
+
1717
+ const IGNORE_FILES = new Set(
1718
+ [
1719
+ ...DEFAULT_IGNORE_FILES,
1720
+ ...parseListEnv(process.env.CODE_CONTEXT_IGNORE_FILES),
1721
+ ].map(normalizeName),
1722
+ );
1677
1723
 
1678
1724
  // File extensions that are binary or auto-generated — never useful as context
1679
- const IGNORE_EXTENSIONS = new Set([
1680
- // .NET compiled outputs
1681
- ".dll",
1682
- ".exe",
1683
- ".pdb",
1684
- ".nupkg",
1685
- ".snupkg",
1686
- ".suo",
1687
- // Lock / generated
1688
- ".lock",
1689
- ".user",
1690
- // Images
1691
- ".png",
1692
- ".jpg",
1693
- ".jpeg",
1694
- ".gif",
1695
- ".svg",
1696
- ".ico",
1697
- ".webp",
1698
- ".bmp",
1699
- // Fonts
1700
- ".woff",
1701
- ".woff2",
1702
- ".ttf",
1703
- ".eot",
1704
- // Archives / binaries
1705
- ".zip",
1706
- ".tar",
1707
- ".gz",
1708
- ".rar",
1709
- ".7z",
1710
- ".bin",
1711
- ".so",
1712
- ".dylib",
1713
- ".lib",
1714
- ".obj",
1715
- // Misc
1716
- ".min.js",
1717
- ".map",
1718
- ]);
1725
+ const IGNORE_EXTENSIONS = new Set(
1726
+ [
1727
+ // .NET compiled outputs
1728
+ ".dll",
1729
+ ".exe",
1730
+ ".pdb",
1731
+ ".nupkg",
1732
+ ".snupkg",
1733
+ ".suo",
1734
+ // Lock / generated
1735
+ ".lock",
1736
+ ".user",
1737
+ // Images
1738
+ ".png",
1739
+ ".jpg",
1740
+ ".jpeg",
1741
+ ".gif",
1742
+ ".svg",
1743
+ ".ico",
1744
+ ".webp",
1745
+ ".bmp",
1746
+ // Fonts
1747
+ ".woff",
1748
+ ".woff2",
1749
+ ".ttf",
1750
+ ".eot",
1751
+ // Archives / binaries
1752
+ ".zip",
1753
+ ".tar",
1754
+ ".gz",
1755
+ ".rar",
1756
+ ".7z",
1757
+ ".bin",
1758
+ ".so",
1759
+ ".dylib",
1760
+ ".lib",
1761
+ ".obj",
1762
+ // Misc
1763
+ ".min.js",
1764
+ ".d.ts",
1765
+ ".map",
1766
+ ...parseListEnv(process.env.CODE_CONTEXT_IGNORE_EXTENSIONS),
1767
+ ]
1768
+ .map(normalizeExtension)
1769
+ .filter(Boolean),
1770
+ );
1771
+
1772
+ const IGNORE_MULTI_EXTENSIONS = Array.from(IGNORE_EXTENSIONS).filter((ext) =>
1773
+ ext.slice(1).includes("."),
1774
+ );
1775
+
1776
+ const IGNORE_PATH_PREFIXES = new Set(
1777
+ parseListEnv(process.env.CODE_CONTEXT_IGNORE_PATHS)
1778
+ .map(normalizePathPrefix)
1779
+ .filter(Boolean),
1780
+ );
1781
+
1782
+ function shouldIgnorePath(relPath: string): boolean {
1783
+ if (IGNORE_PATH_PREFIXES.size === 0) return false;
1784
+ const normalized = normalizePathPrefix(relPath);
1785
+ if (!normalized) return false;
1786
+
1787
+ for (const prefix of IGNORE_PATH_PREFIXES) {
1788
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
1789
+ return true;
1790
+ }
1791
+ }
1792
+ return false;
1793
+ }
1794
+
1795
+ function hasIgnoredExtension(fileNameLower: string): boolean {
1796
+ if (IGNORE_EXTENSIONS.has(path.extname(fileNameLower))) {
1797
+ return true;
1798
+ }
1799
+ for (const ext of IGNORE_MULTI_EXTENSIONS) {
1800
+ if (fileNameLower.endsWith(ext)) {
1801
+ return true;
1802
+ }
1803
+ }
1804
+ return false;
1805
+ }
1719
1806
 
1720
1807
  async function walkDir(dir: string, prefix = ""): Promise<string[]> {
1721
1808
  const entries = await fs.readdir(dir, { withFileTypes: true });
1722
1809
  const files: string[] = [];
1723
1810
  for (const entry of entries) {
1724
- if (IGNORE_DIRS.has(entry.name)) continue;
1725
1811
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1812
+ const lowerName = entry.name.toLowerCase();
1813
+
1726
1814
  if (entry.isDirectory()) {
1815
+ if (IGNORE_DIRS.has(lowerName) || shouldIgnorePath(rel)) continue;
1727
1816
  files.push(...(await walkDir(path.join(dir, entry.name), rel)));
1728
1817
  } else {
1818
+ if (IGNORE_FILES.has(lowerName) || shouldIgnorePath(rel)) continue;
1729
1819
  const lower = entry.name.toLowerCase();
1730
- if (IGNORE_EXTENSIONS.has(path.extname(lower))) continue;
1731
- // Also catch compound extensions like .min.js, .d.ts generated files
1732
- if (lower.endsWith(".min.js") || lower.endsWith(".d.ts")) continue;
1820
+ if (hasIgnoredExtension(lower)) continue;
1733
1821
  files.push(rel);
1734
1822
  }
1735
1823
  }
@@ -2972,7 +3060,7 @@ app.post("/api/module-federation/start", async (req, res) => {
2972
3060
 
2973
3061
  await runLoggedCommand(
2974
3062
  npmCommand(),
2975
- ["install", "--no-audit", "--no-fund", "--prefer-offline"],
3063
+ ["install", "--no-audit", "--no-fund", "--legacy-peer-deps"],
2976
3064
  {
2977
3065
  cwd: dir,
2978
3066
  env: {
@@ -2983,12 +3071,36 @@ app.post("/api/module-federation/start", async (req, res) => {
2983
3071
  logs,
2984
3072
  );
2985
3073
 
3074
+ // Detect new modern Next.js MFE template types.
3075
+ const isMultiZones = typeof files["apps/zone-b/package.json"] === "string";
3076
+ const isMfRuntimeApi =
3077
+ typeof files["apps/mf-remote/package.json"] === "string";
3078
+ const isRspackShell =
3079
+ typeof files["apps/rspack-shell/package.json"] === "string";
3080
+
2986
3081
  // Detect isolated 2-app pattern (host + mfe-auth, no profile/checkout).
2987
3082
  const isIsolated =
2988
3083
  typeof files["apps/mfe-auth/package.json"] === "string" &&
2989
3084
  typeof files["apps/checkout/package.json"] !== "string";
2990
3085
 
2991
- const ports = await getDistinctPorts(isIsolated ? 2 : 3);
3086
+ // Detect Next.js MF pattern (shell + remote, no mfe-auth/checkout/profile).
3087
+ // Excludes Multi-Zones (zone-b) and MF Runtime API (mf-remote) which also use apps/shell.
3088
+ const isNextjsMf =
3089
+ typeof files["apps/shell/package.json"] === "string" &&
3090
+ !isMultiZones &&
3091
+ !isMfRuntimeApi &&
3092
+ typeof files["apps/mfe-auth/package.json"] !== "string" &&
3093
+ typeof files["apps/checkout/package.json"] !== "string";
3094
+
3095
+ const ports = await getDistinctPorts(
3096
+ isIsolated ||
3097
+ isNextjsMf ||
3098
+ isMultiZones ||
3099
+ isMfRuntimeApi ||
3100
+ isRspackShell
3101
+ ? 2
3102
+ : 3,
3103
+ );
2992
3104
  const [hostPort] = ports;
2993
3105
 
2994
3106
  const appUrls: Record<string, string> = {
@@ -3001,7 +3113,32 @@ app.post("/api/module-federation/start", async (req, res) => {
3001
3113
  npm_config_update_notifier: "false",
3002
3114
  };
3003
3115
 
3004
- if (isIsolated) {
3116
+ if (isNextjsMf) {
3117
+ const [, remotePort] = ports;
3118
+ appUrls.remote = `http://localhost:${remotePort}`;
3119
+ spawnEnv.REMOTE_PORT = String(remotePort);
3120
+ // Next.js remotes put the entry at /_next/static/chunks/; webpack remotes put it at root.
3121
+ const isNextjsRemote =
3122
+ typeof files["apps/remote/next.config.js"] === "string";
3123
+ const remoteEntryPath = isNextjsRemote
3124
+ ? "/_next/static/chunks/remoteEntry.js"
3125
+ : "/remoteEntry.js";
3126
+ spawnEnv.NEXT_PUBLIC_REMOTE_URL = `http://localhost:${remotePort}${remoteEntryPath}`;
3127
+ } else if (isMultiZones) {
3128
+ const [, remotePort] = ports;
3129
+ // Zone B serves everything under /store (basePath), so point the preview there.
3130
+ appUrls.zoneB = `http://localhost:${remotePort}/store`;
3131
+ spawnEnv.REMOTE_PORT = String(remotePort);
3132
+ } else if (isMfRuntimeApi) {
3133
+ const [, remotePort] = ports;
3134
+ appUrls.mfRemote = `http://localhost:${remotePort}`;
3135
+ spawnEnv.REMOTE_PORT = String(remotePort);
3136
+ spawnEnv.NEXT_PUBLIC_REMOTE_URL = `http://localhost:${remotePort}/remoteEntry.js`;
3137
+ } else if (isRspackShell) {
3138
+ const [, remotePort] = ports;
3139
+ appUrls.remote = `http://localhost:${remotePort}`;
3140
+ spawnEnv.REMOTE_PORT = String(remotePort);
3141
+ } else if (isIsolated) {
3005
3142
  const [, mfeAuthPort] = ports;
3006
3143
  appUrls.mfeAuth = `http://localhost:${mfeAuthPort}`;
3007
3144
  spawnEnv.MFE_AUTH_PORT = String(mfeAuthPort);
@@ -3014,7 +3151,14 @@ app.post("/api/module-federation/start", async (req, res) => {
3014
3151
  }
3015
3152
 
3016
3153
  const readyPorts = new Set<string>();
3017
- const requiredPorts = isIsolated ? 2 : 3;
3154
+ const requiredPorts =
3155
+ isIsolated ||
3156
+ isNextjsMf ||
3157
+ isMultiZones ||
3158
+ isMfRuntimeApi ||
3159
+ isRspackShell
3160
+ ? 2
3161
+ : 3;
3018
3162
 
3019
3163
  const child = spawn(npmCommand(), ["run", "dev"], {
3020
3164
  cwd: dir,
@@ -154,6 +154,7 @@ export interface WorkspaceMeta {
154
154
  id: string;
155
155
  name: string;
156
156
  type: "local" | "google_drive";
157
+ questionSortOrder?: "name" | "createdAt";
157
158
  driveConfig?: DriveConfig;
158
159
  createdAt: string;
159
160
  }