create-interview-cockpit 0.23.0 → 0.23.2

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.
@@ -1,4 +1,5 @@
1
1
  import type { GithubActionsLabWorkspace } from "./types";
2
+ import type { GithubActionsLabEnvironment } from "./types";
2
3
 
3
4
  // ─── Default Lab Template ────────────────────────────────────────────────
4
5
  //
@@ -401,6 +402,41 @@ export const REACT_VITE_TYPESCRIPT_GHA_LAB: GithubActionsLabWorkspace = {
401
402
 
402
403
  // ─── Helpers (mirror infraLab.ts API surface) ────────────────────────────
403
404
 
405
+ function cloneGhaLabEnvironment(
406
+ environment?: GithubActionsLabEnvironment,
407
+ ): GithubActionsLabEnvironment | undefined {
408
+ if (!environment || typeof environment !== "object") return undefined;
409
+
410
+ const cloneEntries = (entries: GithubActionsLabEnvironment["variables"]) =>
411
+ Array.isArray(entries)
412
+ ? entries
413
+ .filter(
414
+ (entry) =>
415
+ entry &&
416
+ typeof entry.name === "string" &&
417
+ typeof entry.value === "string",
418
+ )
419
+ .map((entry) => ({
420
+ name: entry.name,
421
+ value: entry.value,
422
+ ...(entry.enabled === false ? { enabled: false } : {}),
423
+ }))
424
+ : [];
425
+
426
+ const variables = cloneEntries(environment.variables);
427
+ const secrets = cloneEntries(environment.secrets);
428
+ const env = cloneEntries(environment.env);
429
+ if (variables.length === 0 && secrets.length === 0 && env.length === 0) {
430
+ return undefined;
431
+ }
432
+
433
+ return {
434
+ ...(variables.length ? { variables } : {}),
435
+ ...(secrets.length ? { secrets } : {}),
436
+ ...(env.length ? { env } : {}),
437
+ };
438
+ }
439
+
404
440
  export function cloneGhaLabWorkspace(
405
441
  workspace?: GithubActionsLabWorkspace | null,
406
442
  ): GithubActionsLabWorkspace {
@@ -415,6 +451,7 @@ export function cloneGhaLabWorkspace(
415
451
  )
416
452
  ? source.activeFile
417
453
  : (Object.keys(sourceFiles)[0] ?? ".github/workflows/ci.yml");
454
+ const environment = cloneGhaLabEnvironment(source.environment);
418
455
 
419
456
  return {
420
457
  version: 1,
@@ -433,6 +470,7 @@ export function cloneGhaLabWorkspace(
433
470
  ...(source.includeRunHistoryInContext
434
471
  ? { includeRunHistoryInContext: true }
435
472
  : {}),
473
+ ...(environment ? { environment } : {}),
436
474
  };
437
475
  }
438
476
 
@@ -517,6 +555,9 @@ export function parseGhaLabWorkspace(
517
555
  ...(parsed.includeRunHistoryInContext === true
518
556
  ? { includeRunHistoryInContext: true }
519
557
  : {}),
558
+ ...(parsed.environment && typeof parsed.environment === "object"
559
+ ? { environment: parsed.environment as GithubActionsLabEnvironment }
560
+ : {}),
520
561
  });
521
562
  } catch {
522
563
  return null;
@@ -49,6 +49,21 @@ export interface InfraLabWorkspace {
49
49
  files: Record<string, string>;
50
50
  }
51
51
 
52
+ export interface GithubActionsLabEnvironmentEntry {
53
+ name: string;
54
+ value: string;
55
+ enabled?: boolean;
56
+ }
57
+
58
+ export interface GithubActionsLabEnvironment {
59
+ /** Values available to workflows as `${{ vars.NAME }}` through act's --var-file. */
60
+ variables?: GithubActionsLabEnvironmentEntry[];
61
+ /** Values available to workflows as `${{ secrets.NAME }}` through act's --secret-file. */
62
+ secrets?: GithubActionsLabEnvironmentEntry[];
63
+ /** Runner/container environment values available to shell steps as `$NAME`. */
64
+ env?: GithubActionsLabEnvironmentEntry[];
65
+ }
66
+
52
67
  export interface GithubActionsLabWorkspace {
53
68
  version: 1;
54
69
  label: string;
@@ -64,6 +79,8 @@ export interface GithubActionsLabWorkspace {
64
79
  * (job statuses, durations, exit codes) instead of just the YAML.
65
80
  */
66
81
  includeRunHistoryInContext?: boolean;
82
+ /** Local act inputs mirroring GitHub repository variables, secrets, and runner env. */
83
+ environment?: GithubActionsLabEnvironment;
67
84
  }
68
85
 
69
86
  export interface WorkspaceMeta {
@@ -1 +1 @@
1
- {"root":["./src/app.tsx","./src/api.ts","./src/browsersecuritytemplates.ts","./src/enterpriselocallab.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/docrefmodal.tsx","./src/components/fileattachments.tsx","./src/components/filepickermodal.tsx","./src/components/fileviewermodal.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/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/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,3 +1,3 @@
1
1
  {
2
- "version": "0.21.0"
2
+ "version": "0.23.0"
3
3
  }
@@ -13,6 +13,19 @@ interface GithubActionsLabWorkspace {
13
13
  files: Record<string, string>;
14
14
  defaultEvent?: string;
15
15
  defaultWorkflow?: string;
16
+ environment?: GithubActionsLabEnvironment;
17
+ }
18
+
19
+ interface GithubActionsLabEnvironmentEntry {
20
+ name: string;
21
+ value: string;
22
+ enabled?: boolean;
23
+ }
24
+
25
+ interface GithubActionsLabEnvironment {
26
+ variables?: GithubActionsLabEnvironmentEntry[];
27
+ secrets?: GithubActionsLabEnvironmentEntry[];
28
+ env?: GithubActionsLabEnvironmentEntry[];
16
29
  }
17
30
 
18
31
  type OutputKind = "stdout" | "stderr" | "info";
@@ -69,8 +82,11 @@ export type GhaStreamMessage =
69
82
 
70
83
  const MAX_FILE_COUNT = 60;
71
84
  const MAX_TOTAL_SOURCE_BYTES = 1_000_000;
85
+ const MAX_GHA_ENV_ENTRY_COUNT = 80;
86
+ const MAX_GHA_ENV_TOTAL_BYTES = 100_000;
72
87
  const MAX_LOG_CHARS = 400_000;
73
88
  const SOURCE_MANIFEST = ".gha-source-files.json";
89
+ const GHA_ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
74
90
 
75
91
  // `act` accepts these events; we restrict to common ones so the console
76
92
  // can't be used to run arbitrary subcommands.
@@ -155,6 +171,69 @@ function assertSafeRelativePath(filePath: string, label: string): void {
155
171
  }
156
172
  }
157
173
 
174
+ function parseEnvironmentEntries(
175
+ input: unknown,
176
+ label: string,
177
+ ): GithubActionsLabEnvironmentEntry[] {
178
+ if (!Array.isArray(input)) return [];
179
+ if (input.length > MAX_GHA_ENV_ENTRY_COUNT) {
180
+ throw new Error(
181
+ `${label} exceeds the ${MAX_GHA_ENV_ENTRY_COUNT} item limit`,
182
+ );
183
+ }
184
+
185
+ let totalBytes = 0;
186
+ return input
187
+ .filter((entry): entry is Record<string, unknown> => {
188
+ return !!entry && typeof entry === "object";
189
+ })
190
+ .map((entry) => {
191
+ const name = typeof entry.name === "string" ? entry.name.trim() : "";
192
+ if (!name) return null;
193
+ if (!GHA_ENV_NAME_RE.test(name)) {
194
+ throw new Error(
195
+ `${label} name '${name}' is invalid. Use letters, numbers, and underscores; do not start with a number.`,
196
+ );
197
+ }
198
+ const value = typeof entry.value === "string" ? entry.value : "";
199
+ totalBytes += Buffer.byteLength(name, "utf8");
200
+ totalBytes += Buffer.byteLength(value, "utf8");
201
+ if (totalBytes > MAX_GHA_ENV_TOTAL_BYTES) {
202
+ throw new Error(`${label} values exceed the allowed size limit`);
203
+ }
204
+ return {
205
+ name,
206
+ value,
207
+ ...(entry.enabled === false ? { enabled: false } : {}),
208
+ };
209
+ })
210
+ .filter((entry): entry is GithubActionsLabEnvironmentEntry => !!entry);
211
+ }
212
+
213
+ function parseWorkspaceEnvironment(
214
+ input: unknown,
215
+ ): GithubActionsLabEnvironment | undefined {
216
+ if (!input || typeof input !== "object") return undefined;
217
+ const candidate = input as Record<string, unknown>;
218
+ const variables = parseEnvironmentEntries(
219
+ candidate.variables,
220
+ "GitHub Actions variable",
221
+ );
222
+ const secrets = parseEnvironmentEntries(
223
+ candidate.secrets,
224
+ "GitHub Actions secret",
225
+ );
226
+ const env = parseEnvironmentEntries(candidate.env, "GitHub Actions env");
227
+ if (variables.length === 0 && secrets.length === 0 && env.length === 0) {
228
+ return undefined;
229
+ }
230
+ return {
231
+ ...(variables.length ? { variables } : {}),
232
+ ...(secrets.length ? { secrets } : {}),
233
+ ...(env.length ? { env } : {}),
234
+ };
235
+ }
236
+
158
237
  function parseWorkspace(input: unknown): GithubActionsLabWorkspace {
159
238
  if (!input || typeof input !== "object") {
160
239
  throw new Error("workspace payload is required");
@@ -188,6 +267,7 @@ function parseWorkspace(input: unknown): GithubActionsLabWorkspace {
188
267
  if (totalBytes > MAX_TOTAL_SOURCE_BYTES) {
189
268
  throw new Error("workspace source exceeds the allowed size limit");
190
269
  }
270
+ const environment = parseWorkspaceEnvironment(candidate.environment);
191
271
  return {
192
272
  version: 1,
193
273
  label:
@@ -207,6 +287,7 @@ function parseWorkspace(input: unknown): GithubActionsLabWorkspace {
207
287
  typeof candidate.defaultWorkflow === "string"
208
288
  ? candidate.defaultWorkflow
209
289
  : undefined,
290
+ ...(environment ? { environment } : {}),
210
291
  files,
211
292
  };
212
293
  }
@@ -273,6 +354,72 @@ async function syncWorkspaceToSession(
273
354
  return workspaceDir;
274
355
  }
275
356
 
357
+ function escapeDotenvValue(value: string): string {
358
+ return `"${value
359
+ .replace(/\\/g, "\\\\")
360
+ .replace(/\r/g, "")
361
+ .replace(/\n/g, "\\n")
362
+ .replace(/"/g, '\\"')}"`;
363
+ }
364
+
365
+ function formatEnvironmentFile(
366
+ entries: GithubActionsLabEnvironmentEntry[],
367
+ ): string {
368
+ return `${entries
369
+ .filter((entry) => entry.enabled !== false)
370
+ .map((entry) => `${entry.name}=${escapeDotenvValue(entry.value)}`)
371
+ .join("\n")}\n`;
372
+ }
373
+
374
+ async function buildActEnvironmentArgs(
375
+ runDir: string,
376
+ workspace: GithubActionsLabWorkspace,
377
+ ): Promise<{ args: string[]; summary: string[] }> {
378
+ const args: string[] = [];
379
+ const summary: string[] = [];
380
+ const specs: Array<{
381
+ key: keyof GithubActionsLabEnvironment;
382
+ flag: string;
383
+ fileName: string;
384
+ label: string;
385
+ }> = [
386
+ {
387
+ key: "variables",
388
+ flag: "--var-file",
389
+ fileName: "act-vars.env",
390
+ label: "variable",
391
+ },
392
+ {
393
+ key: "secrets",
394
+ flag: "--secret-file",
395
+ fileName: "act-secrets.env",
396
+ label: "secret",
397
+ },
398
+ {
399
+ key: "env",
400
+ flag: "--env-file",
401
+ fileName: "act-env.env",
402
+ label: "runner env",
403
+ },
404
+ ];
405
+
406
+ for (const spec of specs) {
407
+ const entries = (workspace.environment?.[spec.key] ?? []).filter(
408
+ (entry) => entry.enabled !== false,
409
+ );
410
+ if (entries.length === 0) continue;
411
+
412
+ const filePath = path.join(runDir, spec.fileName);
413
+ await fs.writeFile(filePath, formatEnvironmentFile(entries), "utf8");
414
+ args.push(spec.flag, filePath);
415
+ summary.push(
416
+ `${entries.length} ${spec.label}${entries.length === 1 ? "" : "s"}`,
417
+ );
418
+ }
419
+
420
+ return { args, summary };
421
+ }
422
+
276
423
  // ─── Command parsing ────────────────────────────────────────────────────
277
424
 
278
425
  function splitCommand(command: string): string[] {
@@ -722,6 +869,8 @@ export async function streamGhaCommand(
722
869
  const runDir = path.join(getGhaRunsDir(), runId);
723
870
  const workspaceDir = await syncWorkspaceToSession(sessionKey, workspace);
724
871
  await fs.mkdir(runDir, { recursive: true });
872
+ const environmentArgs = await buildActEnvironmentArgs(runDir, workspace);
873
+ const actArgs = [...platformArgs.args, ...environmentArgs.args];
725
874
 
726
875
  let logs = "";
727
876
  let status: "completed" | "failed" = "completed";
@@ -737,12 +886,19 @@ export async function streamGhaCommand(
737
886
  text: "[info] Using default Medium act runner image mappings. Pass -P/--platform to override.\n",
738
887
  });
739
888
  }
889
+ if (environmentArgs.summary.length > 0) {
890
+ emit({
891
+ type: "output",
892
+ kind: "info",
893
+ text: `[info] Injected ${environmentArgs.summary.join(", ")} into act via temporary files.\n`,
894
+ });
895
+ }
740
896
 
741
897
  // Track per-job status from act's prefixed stdout/stderr lines so the
742
898
  // client can render a live DAG in addition to the raw console.
743
899
  const tracker = new JobTracker((job) => emit({ type: "job", job }));
744
900
 
745
- const child = spawn("act", platformArgs.args, {
901
+ const child = spawn("act", actArgs, {
746
902
  cwd: workspaceDir,
747
903
  env: {
748
904
  ...process.env,
@@ -1163,6 +1163,34 @@ async function getOrCreateFolder(
1163
1163
  return folderId;
1164
1164
  }
1165
1165
 
1166
+ async function resolveExportFolderId(
1167
+ drive: drive_v3.Drive,
1168
+ ws: storage.WorkspaceMeta,
1169
+ targetFolderId?: string,
1170
+ ): Promise<string> {
1171
+ const requestedTarget = targetFolderId?.trim();
1172
+ if (requestedTarget) return requestedTarget;
1173
+
1174
+ // If the workspace is currently scoped to a Drive subfolder (for example
1175
+ // Shared Questions/SBD), push back into that same folder. Falling back to
1176
+ // _export here made topic-level pushes from a selected subfolder land in
1177
+ // Shared Questions/_export instead of beside the original topic folders.
1178
+ const selectedSubfolder = ws.driveConfig?.subFolderId?.trim();
1179
+ if (selectedSubfolder) return selectedSubfolder;
1180
+
1181
+ if (!ws.driveConfig?.folderId) {
1182
+ throw new Error("No Drive folder linked to this workspace");
1183
+ }
1184
+
1185
+ // Local workspaces do not have a Drive-navigation state, so a missing
1186
+ // target should mean "the linked folder itself" rather than silently
1187
+ // creating/using _export. The UI now prompts for a destination, but this
1188
+ // server-side fallback protects older/stale clients too.
1189
+ if (ws.type === "local") return ws.driveConfig.folderId;
1190
+
1191
+ return getOrCreateFolder(drive, ws.driveConfig.folderId, EXPORT_FOLDER_NAME);
1192
+ }
1193
+
1166
1194
  async function uploadFileToFolder(
1167
1195
  drive: drive_v3.Drive,
1168
1196
  folderId: string,
@@ -1342,7 +1370,6 @@ export async function exportWorkspace(
1342
1370
  }
1343
1371
 
1344
1372
  const drive = await getExportDriveClient();
1345
- const { folderId } = ws.driveConfig;
1346
1373
  const result: ExportResult = {
1347
1374
  topicsExported: 0,
1348
1375
  questionsExported: 0,
@@ -1350,10 +1377,9 @@ export async function exportWorkspace(
1350
1377
  errors: [],
1351
1378
  };
1352
1379
 
1353
- // Use the chosen subfolder directly, or fall back to an "_export" subfolder
1354
- const exportFolderId =
1355
- targetFolderId ??
1356
- (await getOrCreateFolder(drive, folderId, EXPORT_FOLDER_NAME));
1380
+ // Use the chosen subfolder directly. If none was passed, use the workspace's
1381
+ // selected Drive subfolder before falling back to root/_export.
1382
+ const exportFolderId = await resolveExportFolderId(drive, ws, targetFolderId);
1357
1383
  const topics = await storage.getTopicsForWorkspace(workspaceId);
1358
1384
  const ctxDir = storage.getContextFilesDirForWorkspace(workspaceId);
1359
1385
 
@@ -1415,10 +1441,7 @@ export async function exportTopic(
1415
1441
  }
1416
1442
 
1417
1443
  const drive = await getExportDriveClient();
1418
- const { folderId } = ws.driveConfig;
1419
- const exportFolderId =
1420
- targetFolderId ??
1421
- (await getOrCreateFolder(drive, folderId, EXPORT_FOLDER_NAME));
1444
+ const exportFolderId = await resolveExportFolderId(drive, ws, targetFolderId);
1422
1445
  const topic = (await storage.getTopicsForWorkspace(workspaceId)).find(
1423
1446
  (t) => t.id === topicId,
1424
1447
  );