create-interview-cockpit 0.30.0 → 0.30.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-interview-cockpit",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "description": "Scaffold a personal AI-powered interview prep cockpit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -236,18 +236,103 @@ function mapJobStatusToCheck(
236
236
  return s;
237
237
  }
238
238
 
239
- // Tiny grouped-by-folder list to keep the modal lean.
240
- function groupByFolder(paths: string[]): { folder: string; files: string[] }[] {
241
- const map = new Map<string, string[]>();
242
- for (const p of paths) {
243
- const idx = p.lastIndexOf("/");
244
- const folder = idx === -1 ? "" : p.slice(0, idx);
245
- if (!map.has(folder)) map.set(folder, []);
246
- map.get(folder)!.push(p);
239
+ interface FileTreeFileNode {
240
+ type: "file";
241
+ name: string;
242
+ path: string;
243
+ }
244
+
245
+ interface FileTreeFolderNode {
246
+ type: "folder";
247
+ displayName: string;
248
+ path: string;
249
+ children: FileTreeNode[];
250
+ }
251
+
252
+ type FileTreeNode = FileTreeFileNode | FileTreeFolderNode;
253
+
254
+ interface MutableFileTreeFolder {
255
+ name: string;
256
+ path: string;
257
+ files: FileTreeFileNode[];
258
+ folders: Map<string, MutableFileTreeFolder>;
259
+ }
260
+
261
+ function sortFileTreeNodes<T extends { name?: string; displayName?: string }>(
262
+ a: T,
263
+ b: T,
264
+ ) {
265
+ return (a.displayName ?? a.name ?? "").localeCompare(
266
+ b.displayName ?? b.name ?? "",
267
+ );
268
+ }
269
+
270
+ function getMutableFolderChildren(folder: MutableFileTreeFolder): FileTreeNode[] {
271
+ const files = [...folder.files].sort(sortFileTreeNodes);
272
+ const folders = Array.from(folder.folders.values())
273
+ .map(compactFileTreeFolder)
274
+ .sort(sortFileTreeNodes);
275
+ return [...files, ...folders];
276
+ }
277
+
278
+ function compactFileTreeFolder(
279
+ folder: MutableFileTreeFolder,
280
+ ): FileTreeFolderNode {
281
+ const names = [folder.name];
282
+ let current = folder;
283
+
284
+ while (current.files.length === 0 && current.folders.size === 1) {
285
+ const next = Array.from(current.folders.values())[0];
286
+ names.push(next.name);
287
+ current = next;
247
288
  }
248
- return Array.from(map.entries())
249
- .sort(([a], [b]) => a.localeCompare(b))
250
- .map(([folder, files]) => ({ folder, files: files.sort() }));
289
+
290
+ return {
291
+ type: "folder",
292
+ displayName: names.join("/"),
293
+ path: current.path,
294
+ children: getMutableFolderChildren(current),
295
+ };
296
+ }
297
+
298
+ function buildCompactFileTree(paths: string[]): FileTreeNode[] {
299
+ const root: MutableFileTreeFolder = {
300
+ name: "",
301
+ path: "",
302
+ files: [],
303
+ folders: new Map(),
304
+ };
305
+
306
+ for (const filePath of paths) {
307
+ const parts = filePath.split("/").filter(Boolean);
308
+ if (parts.length === 0) continue;
309
+
310
+ let current = root;
311
+ let currentPath = "";
312
+ for (let i = 0; i < parts.length - 1; i += 1) {
313
+ const name = parts[i];
314
+ currentPath = currentPath ? `${currentPath}/${name}` : name;
315
+ let next = current.folders.get(name);
316
+ if (!next) {
317
+ next = {
318
+ name,
319
+ path: currentPath,
320
+ files: [],
321
+ folders: new Map(),
322
+ };
323
+ current.folders.set(name, next);
324
+ }
325
+ current = next;
326
+ }
327
+
328
+ current.files.push({
329
+ type: "file",
330
+ name: parts[parts.length - 1],
331
+ path: filePath,
332
+ });
333
+ }
334
+
335
+ return getMutableFolderChildren(root);
251
336
  }
252
337
 
253
338
  // ─── Component ───────────────────────────────────────────────────────────
@@ -514,7 +599,7 @@ export default function GithubActionsLabModal() {
514
599
 
515
600
  // ── File operations ───────────────────────────────────────────────
516
601
  const fileOrder = useMemo(() => getGhaLabFileOrder(workspace), [workspace]);
517
- const grouped = useMemo(() => groupByFolder(fileOrder), [fileOrder]);
602
+ const fileTree = useMemo(() => buildCompactFileTree(fileOrder), [fileOrder]);
518
603
  const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(
519
604
  () => new Set(),
520
605
  );
@@ -1706,6 +1791,159 @@ interface ImportMeta {
1706
1791
  minHeight: MIN_H,
1707
1792
  };
1708
1793
 
1794
+ const renderFileTreeNode = (node: FileTreeNode, depth: number) => {
1795
+ const paddingLeft = 6 + depth * 14;
1796
+
1797
+ if (node.type === "folder") {
1798
+ const collapsed = collapsedFolders.has(node.path);
1799
+ return (
1800
+ <div key={`folder:${node.path || node.displayName}`}>
1801
+ <button
1802
+ onClick={() => toggleFolder(node.path)}
1803
+ onDragOver={(e) => handleFolderDragOver(e, node.path)}
1804
+ onDragLeave={() =>
1805
+ setDragOverFolder((current) =>
1806
+ current === node.path ? null : current,
1807
+ )
1808
+ }
1809
+ onDrop={(e) => handleFolderDrop(e, node.path)}
1810
+ className="flex items-center gap-1 w-full pr-1 py-0.5 text-slate-400 hover:text-slate-200"
1811
+ style={{ paddingLeft }}
1812
+ title="Drop a file here to move it. Hold Option/Alt while dropping to copy."
1813
+ >
1814
+ {collapsed ? (
1815
+ <ChevronRight className="w-3 h-3 shrink-0" />
1816
+ ) : (
1817
+ <ChevronDown className="w-3 h-3 shrink-0" />
1818
+ )}
1819
+ <Folder className="w-3 h-3 shrink-0" />
1820
+ <span
1821
+ className={`truncate rounded px-1 ${
1822
+ dragOverFolder === node.path
1823
+ ? "bg-amber-500/15 text-amber-200"
1824
+ : ""
1825
+ }`}
1826
+ >
1827
+ {node.displayName}/
1828
+ </span>
1829
+ </button>
1830
+ {!collapsed &&
1831
+ node.children.map((child) =>
1832
+ renderFileTreeNode(child, depth + 1),
1833
+ )}
1834
+ </div>
1835
+ );
1836
+ }
1837
+
1838
+ const filePath = node.path;
1839
+ return (
1840
+ <div
1841
+ key={`file:${filePath}`}
1842
+ data-selected={selectedFiles.has(filePath)}
1843
+ draggable
1844
+ onDragStart={(e) => handleFileDragStart(e, filePath)}
1845
+ onDragEnd={() => {
1846
+ setDraggingFile(null);
1847
+ setDragOverFolder(null);
1848
+ }}
1849
+ className={`group relative flex items-center gap-1 pr-1 py-0.5 rounded cursor-pointer ${
1850
+ activeFile === filePath
1851
+ ? "bg-amber-500/15 text-amber-200"
1852
+ : selectedFiles.has(filePath)
1853
+ ? "bg-sky-500/10 text-sky-100 hover:bg-sky-500/15"
1854
+ : "text-slate-300 hover:bg-slate-800/40"
1855
+ }`}
1856
+ onClick={() => setActiveFile(filePath)}
1857
+ style={{ paddingLeft }}
1858
+ >
1859
+ {(selectMode || selectedFiles.has(filePath)) && (
1860
+ <input
1861
+ type="checkbox"
1862
+ checked={selectedFiles.has(filePath)}
1863
+ onClick={(e) => e.stopPropagation()}
1864
+ onChange={() => toggleFileSelection(filePath)}
1865
+ className="h-3 w-3 shrink-0 accent-amber-400"
1866
+ title="Select file"
1867
+ />
1868
+ )}
1869
+ <span className="truncate flex-1">{node.name}</span>
1870
+ <button
1871
+ onClick={(e) => {
1872
+ e.stopPropagation();
1873
+ setOpenFileMenu((current) =>
1874
+ current === filePath ? null : filePath,
1875
+ );
1876
+ setBulkMenuOpen(false);
1877
+ }}
1878
+ className="rounded px-1 text-slate-500 opacity-70 hover:bg-slate-800/70 hover:text-amber-200 group-hover:opacity-100"
1879
+ title="File actions"
1880
+ >
1881
+
1882
+ </button>
1883
+ {openFileMenu === filePath && (
1884
+ <div
1885
+ onClick={(e) => e.stopPropagation()}
1886
+ className="absolute right-1 top-6 z-40 w-40 overflow-hidden rounded-lg border border-slate-700 bg-slate-950 py-1 text-[11px] shadow-xl"
1887
+ >
1888
+ <button
1889
+ onClick={() => moveFile(filePath)}
1890
+ className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
1891
+ >
1892
+ <Pencil className="w-3 h-3 text-amber-300" />
1893
+ Move / rename…
1894
+ </button>
1895
+ <button
1896
+ onClick={() => copyFile(filePath)}
1897
+ className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
1898
+ >
1899
+ <Copy className="w-3 h-3 text-sky-300" />
1900
+ Copy to path…
1901
+ </button>
1902
+ <button
1903
+ onClick={() => {
1904
+ toggleFileSelection(filePath);
1905
+ setOpenFileMenu(null);
1906
+ }}
1907
+ className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
1908
+ >
1909
+ <ListChecks className="w-3 h-3 text-amber-300" />
1910
+ {selectedFiles.has(filePath) ? "Deselect" : "Select"}
1911
+ </button>
1912
+ {selectedFileList.length > 1 && selectedFiles.has(filePath) && (
1913
+ <>
1914
+ <button
1915
+ onClick={() => {
1916
+ moveFilesToFolder(selectedFileList);
1917
+ setOpenFileMenu(null);
1918
+ }}
1919
+ className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
1920
+ >
1921
+ Move selected…
1922
+ </button>
1923
+ <button
1924
+ onClick={() => {
1925
+ copyFilesToFolder(selectedFileList);
1926
+ setOpenFileMenu(null);
1927
+ }}
1928
+ className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
1929
+ >
1930
+ Copy selected…
1931
+ </button>
1932
+ </>
1933
+ )}
1934
+ <button
1935
+ onClick={() => deleteFile(filePath)}
1936
+ className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-red-300 hover:bg-red-500/10"
1937
+ >
1938
+ <Trash2 className="w-3 h-3" />
1939
+ Delete
1940
+ </button>
1941
+ </div>
1942
+ )}
1943
+ </div>
1944
+ );
1945
+ };
1946
+
1709
1947
  return (
1710
1948
  <div className="fixed inset-0 z-40 bg-black/40">
1711
1949
  <div
@@ -2021,155 +2259,7 @@ interface ImportMeta {
2021
2259
  in workspace root • hold Option/Alt to copy
2022
2260
  </div>
2023
2261
  )}
2024
- {grouped.map(({ folder, files }) => {
2025
- const collapsed = collapsedFolders.has(folder);
2026
- return (
2027
- <div key={folder || "root"} className="mb-1">
2028
- {folder && (
2029
- <button
2030
- onClick={() => toggleFolder(folder)}
2031
- onDragOver={(e) => handleFolderDragOver(e, folder)}
2032
- onDragLeave={() =>
2033
- setDragOverFolder((current) =>
2034
- current === folder ? null : current,
2035
- )
2036
- }
2037
- onDrop={(e) => handleFolderDrop(e, folder)}
2038
- className="flex items-center gap-1 w-full px-1 py-0.5 text-slate-400 hover:text-slate-200"
2039
- title="Drop a file here to move it. Hold Option/Alt while dropping to copy."
2040
- >
2041
- {collapsed ? (
2042
- <ChevronRight className="w-3 h-3" />
2043
- ) : (
2044
- <ChevronDown className="w-3 h-3" />
2045
- )}
2046
- <Folder className="w-3 h-3" />
2047
- <span
2048
- className={`truncate rounded px-1 ${
2049
- dragOverFolder === folder
2050
- ? "bg-amber-500/15 text-amber-200"
2051
- : ""
2052
- }`}
2053
- >
2054
- {folder}/
2055
- </span>
2056
- </button>
2057
- )}
2058
- {!collapsed &&
2059
- files.map((filePath) => (
2060
- <div
2061
- key={filePath}
2062
- data-selected={selectedFiles.has(filePath)}
2063
- draggable
2064
- onDragStart={(e) => handleFileDragStart(e, filePath)}
2065
- onDragEnd={() => {
2066
- setDraggingFile(null);
2067
- setDragOverFolder(null);
2068
- }}
2069
- className={`group relative flex items-center gap-1 pl-${folder ? 5 : 1} pr-1 py-0.5 rounded cursor-pointer ${
2070
- activeFile === filePath
2071
- ? "bg-amber-500/15 text-amber-200"
2072
- : selectedFiles.has(filePath)
2073
- ? "bg-sky-500/10 text-sky-100 hover:bg-sky-500/15"
2074
- : "text-slate-300 hover:bg-slate-800/40"
2075
- }`}
2076
- onClick={() => setActiveFile(filePath)}
2077
- style={{ paddingLeft: folder ? 20 : 6 }}
2078
- >
2079
- {(selectMode || selectedFiles.has(filePath)) && (
2080
- <input
2081
- type="checkbox"
2082
- checked={selectedFiles.has(filePath)}
2083
- onClick={(e) => e.stopPropagation()}
2084
- onChange={() => toggleFileSelection(filePath)}
2085
- className="h-3 w-3 shrink-0 accent-amber-400"
2086
- title="Select file"
2087
- />
2088
- )}
2089
- <span className="truncate flex-1">
2090
- {baseName(filePath)}
2091
- </span>
2092
- <button
2093
- onClick={(e) => {
2094
- e.stopPropagation();
2095
- setOpenFileMenu((current) =>
2096
- current === filePath ? null : filePath,
2097
- );
2098
- setBulkMenuOpen(false);
2099
- }}
2100
- className="rounded px-1 text-slate-500 opacity-70 hover:bg-slate-800/70 hover:text-amber-200 group-hover:opacity-100"
2101
- title="File actions"
2102
- >
2103
-
2104
- </button>
2105
- {openFileMenu === filePath && (
2106
- <div
2107
- onClick={(e) => e.stopPropagation()}
2108
- className="absolute right-1 top-6 z-40 w-40 overflow-hidden rounded-lg border border-slate-700 bg-slate-950 py-1 text-[11px] shadow-xl"
2109
- >
2110
- <button
2111
- onClick={() => moveFile(filePath)}
2112
- className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
2113
- >
2114
- <Pencil className="w-3 h-3 text-amber-300" />
2115
- Move / rename…
2116
- </button>
2117
- <button
2118
- onClick={() => copyFile(filePath)}
2119
- className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
2120
- >
2121
- <Copy className="w-3 h-3 text-sky-300" />
2122
- Copy to path…
2123
- </button>
2124
- <button
2125
- onClick={() => {
2126
- toggleFileSelection(filePath);
2127
- setOpenFileMenu(null);
2128
- }}
2129
- className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
2130
- >
2131
- <ListChecks className="w-3 h-3 text-amber-300" />
2132
- {selectedFiles.has(filePath)
2133
- ? "Deselect"
2134
- : "Select"}
2135
- </button>
2136
- {selectedFileList.length > 1 &&
2137
- selectedFiles.has(filePath) && (
2138
- <>
2139
- <button
2140
- onClick={() => {
2141
- moveFilesToFolder(selectedFileList);
2142
- setOpenFileMenu(null);
2143
- }}
2144
- className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
2145
- >
2146
- Move selected…
2147
- </button>
2148
- <button
2149
- onClick={() => {
2150
- copyFilesToFolder(selectedFileList);
2151
- setOpenFileMenu(null);
2152
- }}
2153
- className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/70"
2154
- >
2155
- Copy selected…
2156
- </button>
2157
- </>
2158
- )}
2159
- <button
2160
- onClick={() => deleteFile(filePath)}
2161
- className="flex w-full items-center gap-2 border-t border-slate-800 px-2 py-1.5 text-left text-red-300 hover:bg-red-500/10"
2162
- >
2163
- <Trash2 className="w-3 h-3" />
2164
- Delete
2165
- </button>
2166
- </div>
2167
- )}
2168
- </div>
2169
- ))}
2170
- </div>
2171
- );
2172
- })}
2262
+ {fileTree.map((node) => renderFileTreeNode(node, 0))}
2173
2263
  </div>
2174
2264
  </div>
2175
2265
 
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  AWS_GOVERNANCE_GHA_LAB,
12
12
  DEFAULT_GHA_LAB,
13
+ EMPTY_GITHUB_GHA_LAB,
13
14
  GOVERNANCE_GHA_LAB,
14
15
  parseGhaLabWorkspace,
15
16
  REACT_VITE_TYPESCRIPT_GHA_LAB,
@@ -693,6 +694,12 @@ export default function LabsPanel() {
693
694
  origin="github-actions"
694
695
  emptyText="Save a GitHub lab to reopen it here"
695
696
  newLabMenu={[
697
+ {
698
+ label: "Empty GitHub Template",
699
+ description:
700
+ "Minimal repo with blank .github/workflows/ci.yml and CODEOWNERS files",
701
+ onClick: () => openGhaLab(EMPTY_GITHUB_GHA_LAB),
702
+ },
696
703
  {
697
704
  label: "React Vite TypeScript Starter",
698
705
  description:
@@ -513,6 +513,11 @@ li {
513
513
  `,
514
514
  };
515
515
 
516
+ const EMPTY_GITHUB_LAB_FILES: Record<string, string> = {
517
+ ".github/workflows/ci.yml": "",
518
+ ".github/CODEOWNERS": "",
519
+ };
520
+
516
521
  // ─── Platform Governance Template ────────────────────────────────────────
517
522
  //
518
523
  // Mirrors a real-world "PLF-governance" mono-repo: one repo that owns
@@ -2435,6 +2440,15 @@ export const REACT_VITE_TYPESCRIPT_GHA_LAB: GithubActionsLabWorkspace = {
2435
2440
  files: REACT_VITE_TYPESCRIPT_FILES,
2436
2441
  };
2437
2442
 
2443
+ export const EMPTY_GITHUB_GHA_LAB: GithubActionsLabWorkspace = {
2444
+ version: 1,
2445
+ label: "Empty GitHub Lab Template",
2446
+ activeFile: ".github/workflows/ci.yml",
2447
+ defaultEvent: "push",
2448
+ defaultWorkflow: ".github/workflows/ci.yml",
2449
+ files: EMPTY_GITHUB_LAB_FILES,
2450
+ };
2451
+
2438
2452
  // ─── Helpers (mirror infraLab.ts API surface) ────────────────────────────
2439
2453
 
2440
2454
  function cloneGhaLabEnvironment(
@@ -1 +1 @@
1
- {"root":["./src/app.tsx","./src/api.ts","./src/browsersecuritytemplates.ts","./src/enterpriselocallab.ts","./src/ghaconcurrency.ts","./src/githubactionslab.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/canvaslabmodal.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/diagramsmodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/ghaconcurrencypanel.tsx","./src/components/ghahistorypanel.tsx","./src/components/ghajobspanel.tsx","./src/components/gitdiffpanel.tsx","./src/components/gitdiffviewermodal.tsx","./src/components/githubactionslabmodal.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"],"version":"5.9.3"}
1
+ {"root":["./src/app.tsx","./src/api.ts","./src/awsgovernanceiamlab.ts","./src/browsersecuritytemplates.ts","./src/codeowners.ts","./src/enterpriselocallab.ts","./src/ghaconcurrency.ts","./src/githubactionslab.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/canvaslabmodal.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/diagramsmodal.tsx","./src/components/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.tsx","./src/components/ghaconcurrencypanel.tsx","./src/components/ghahistorypanel.tsx","./src/components/ghajobspanel.tsx","./src/components/gitdiffpanel.tsx","./src/components/gitdiffviewermodal.tsx","./src/components/githubactionslabmodal.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/pullrequestpanel.tsx","./src/components/settingspanel.tsx","./src/components/sidebar.tsx","./src/components/textannotator.tsx","./src/components/vizcraftembed.tsx","./src/components/workspaceswitcher.tsx"],"version":"5.9.3"}
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.29.0"
2
+ "version": "0.30.0"
3
3
  }