create-interview-cockpit 0.18.0 → 0.20.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.
- package/package.json +1 -1
- package/template/client/src/api.ts +101 -0
- package/template/client/src/components/GhaHistoryPanel.tsx +194 -0
- package/template/client/src/components/GhaJobsPanel.tsx +432 -0
- package/template/client/src/components/GithubActionsLabModal.tsx +583 -76
- package/template/client/src/components/LabsPanel.tsx +11 -1
- package/template/client/src/components/Sidebar.tsx +216 -59
- package/template/client/src/githubActionsLab.ts +239 -2
- package/template/client/src/store.ts +47 -0
- package/template/client/src/types.ts +6 -0
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/gha-runner.ts +327 -1
- package/template/server/src/google-drive.ts +507 -125
- package/template/server/src/index.ts +87 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useStore } from "../store";
|
|
3
3
|
import { DOCKER_DEEP_DIVE_LAB, parseInfraLabWorkspace } from "../infraLab";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_GHA_LAB,
|
|
6
|
+
parseGhaLabWorkspace,
|
|
7
|
+
REACT_VITE_TYPESCRIPT_GHA_LAB,
|
|
8
|
+
} from "../githubActionsLab";
|
|
5
9
|
import { ENTERPRISE_LOCAL_AUTH_LAB } from "../enterpriseLocalLab";
|
|
6
10
|
import {
|
|
7
11
|
parseFrontendLabWorkspace,
|
|
@@ -656,6 +660,12 @@ export default function LabsPanel() {
|
|
|
656
660
|
origin="github-actions"
|
|
657
661
|
emptyText="Save a GitHub Actions lab to reopen it here"
|
|
658
662
|
newLabMenu={[
|
|
663
|
+
{
|
|
664
|
+
label: "React Vite TypeScript Starter",
|
|
665
|
+
description:
|
|
666
|
+
"Scaffolded React app with an empty .github/workflows/ci.yml",
|
|
667
|
+
onClick: () => openGhaLab(REACT_VITE_TYPESCRIPT_GHA_LAB),
|
|
668
|
+
},
|
|
659
669
|
{
|
|
660
670
|
label: "Workflows + Composite Action",
|
|
661
671
|
description:
|
|
@@ -119,7 +119,9 @@ export default function Sidebar() {
|
|
|
119
119
|
selectDriveSubfolder,
|
|
120
120
|
clearDriveSubfolder,
|
|
121
121
|
syncWorkspace,
|
|
122
|
+
syncTopic,
|
|
122
123
|
exportWorkspace,
|
|
124
|
+
exportTopic,
|
|
123
125
|
workspaceFiles,
|
|
124
126
|
uploadWorkspaceFiles,
|
|
125
127
|
removeWorkspaceFile,
|
|
@@ -162,6 +164,7 @@ export default function Sidebar() {
|
|
|
162
164
|
const [openMenuQuestionId, setOpenMenuQuestionId] = useState<string | null>(
|
|
163
165
|
null,
|
|
164
166
|
);
|
|
167
|
+
const [openMenuTopicId, setOpenMenuTopicId] = useState<string | null>(null);
|
|
165
168
|
const [openTopicPrompts, setOpenTopicPrompts] = useState<Set<string>>(
|
|
166
169
|
new Set(),
|
|
167
170
|
);
|
|
@@ -216,6 +219,11 @@ export default function Sidebar() {
|
|
|
216
219
|
const [navigating, setNavigating] = useState(false);
|
|
217
220
|
const [syncing, setSyncing] = useState(false);
|
|
218
221
|
const [pushing, setPushing] = useState(false);
|
|
222
|
+
const [topicSyncingId, setTopicSyncingId] = useState<string | null>(null);
|
|
223
|
+
const [topicPushingId, setTopicPushingId] = useState<string | null>(null);
|
|
224
|
+
const [topicDriveStatus, setTopicDriveStatus] = useState<
|
|
225
|
+
Record<string, string>
|
|
226
|
+
>({});
|
|
219
227
|
const [wsFilesExpanded, setWsFilesExpanded] = useState(true);
|
|
220
228
|
const [driveFileSyncStatus, setDriveFileSyncStatus] = useState<string | null>(
|
|
221
229
|
null,
|
|
@@ -294,6 +302,61 @@ export default function Sidebar() {
|
|
|
294
302
|
}
|
|
295
303
|
};
|
|
296
304
|
|
|
305
|
+
const setTopicStatus = (topicId: string, value: string) => {
|
|
306
|
+
setTopicDriveStatus((prev) => ({ ...prev, [topicId]: value }));
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const handlePullTopicFromDrive = async (topicId: string) => {
|
|
310
|
+
if (!activeWorkspaceId || !activeWs?.driveConfig?.folderId) return;
|
|
311
|
+
setTopicSyncingId(topicId);
|
|
312
|
+
setTopicStatus(topicId, "Pulling topic from Drive…");
|
|
313
|
+
try {
|
|
314
|
+
const result = await syncTopic(activeWorkspaceId, topicId);
|
|
315
|
+
if ("needsAuth" in result && result.needsAuth) {
|
|
316
|
+
window.location.href = result.authUrl;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const firstError = result.errors[0];
|
|
320
|
+
setTopicStatus(
|
|
321
|
+
topicId,
|
|
322
|
+
result.errors.length > 0
|
|
323
|
+
? `Topic pull finished with ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}. ${firstError ? `First: ${firstError}` : ""}`
|
|
324
|
+
: `Pulled ${result.filesImported} file${result.filesImported === 1 ? "" : "s"} into this topic.`,
|
|
325
|
+
);
|
|
326
|
+
} catch (err: any) {
|
|
327
|
+
setTopicStatus(topicId, err?.message || "Topic pull failed.");
|
|
328
|
+
} finally {
|
|
329
|
+
setTopicSyncingId(null);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const handlePushTopicToDrive = async (topicId: string) => {
|
|
334
|
+
if (!activeWorkspaceId || !activeWs?.driveConfig?.folderId) return;
|
|
335
|
+
setTopicPushingId(topicId);
|
|
336
|
+
setTopicStatus(topicId, "Pushing topic to Drive…");
|
|
337
|
+
try {
|
|
338
|
+
const result = await exportTopic(
|
|
339
|
+
activeWorkspaceId,
|
|
340
|
+
topicId,
|
|
341
|
+
activeWs.driveConfig.subFolderId,
|
|
342
|
+
);
|
|
343
|
+
if ("needsAuth" in result && result.needsAuth) {
|
|
344
|
+
window.location.href = result.authUrl;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
setTopicStatus(
|
|
348
|
+
topicId,
|
|
349
|
+
result.errors.length > 0
|
|
350
|
+
? `Topic push finished with ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}.`
|
|
351
|
+
: `Pushed ${result.questionsExported} question${result.questionsExported === 1 ? "" : "s"} and ${result.filesExported} file${result.filesExported === 1 ? "" : "s"} to Drive.`,
|
|
352
|
+
);
|
|
353
|
+
} catch (err: any) {
|
|
354
|
+
setTopicStatus(topicId, err?.message || "Topic push failed.");
|
|
355
|
+
} finally {
|
|
356
|
+
setTopicPushingId(null);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
297
360
|
useEffect(() => {
|
|
298
361
|
if (editingTopicId || editingQuestionId) {
|
|
299
362
|
editInputRef.current?.select();
|
|
@@ -1194,6 +1257,9 @@ export default function Sidebar() {
|
|
|
1194
1257
|
sensitivity: "base",
|
|
1195
1258
|
}),
|
|
1196
1259
|
);
|
|
1260
|
+
const isTopicMenuOpen = openMenuTopicId === topic.id;
|
|
1261
|
+
const topicBusy =
|
|
1262
|
+
topicSyncingId === topic.id || topicPushingId === topic.id;
|
|
1197
1263
|
|
|
1198
1264
|
return (
|
|
1199
1265
|
<div key={topic.id}>
|
|
@@ -1238,69 +1304,160 @@ export default function Sidebar() {
|
|
|
1238
1304
|
</span>
|
|
1239
1305
|
</button>
|
|
1240
1306
|
{editingTopicId !== topic.id && (
|
|
1241
|
-
<
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
setEditingTopicId(topic.id);
|
|
1245
|
-
setEditingTopicName(topic.name);
|
|
1246
|
-
}}
|
|
1247
|
-
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
|
|
1248
|
-
title="Rename"
|
|
1307
|
+
<div
|
|
1308
|
+
className="relative shrink-0 flex items-center"
|
|
1309
|
+
onClick={(e) => e.stopPropagation()}
|
|
1249
1310
|
>
|
|
1250
|
-
|
|
1251
|
-
|
|
1311
|
+
{topicBusy && !isTopicMenuOpen ? (
|
|
1312
|
+
<Loader2 className="w-3 h-3 animate-spin text-cyan-400" />
|
|
1313
|
+
) : (
|
|
1314
|
+
<button
|
|
1315
|
+
onClick={() =>
|
|
1316
|
+
setOpenMenuTopicId(
|
|
1317
|
+
isTopicMenuOpen ? null : topic.id,
|
|
1318
|
+
)
|
|
1319
|
+
}
|
|
1320
|
+
className={`p-0.5 rounded transition-all ${
|
|
1321
|
+
isTopicMenuOpen
|
|
1322
|
+
? "text-cyan-400"
|
|
1323
|
+
: "opacity-0 group-hover:opacity-100 text-slate-600 hover:text-slate-300"
|
|
1324
|
+
}`}
|
|
1325
|
+
title="Topic options"
|
|
1326
|
+
>
|
|
1327
|
+
<MoreHorizontal className="w-3.5 h-3.5" />
|
|
1328
|
+
</button>
|
|
1329
|
+
)}
|
|
1330
|
+
|
|
1331
|
+
{isTopicMenuOpen && (
|
|
1332
|
+
<>
|
|
1333
|
+
<div
|
|
1334
|
+
className="fixed inset-0 z-40"
|
|
1335
|
+
onClick={() => setOpenMenuTopicId(null)}
|
|
1336
|
+
/>
|
|
1337
|
+
<div className="absolute right-0 top-full mt-0.5 z-50 bg-slate-800 border border-slate-700 rounded-md shadow-xl min-w-[170px] py-0.5">
|
|
1338
|
+
<button
|
|
1339
|
+
onClick={() => {
|
|
1340
|
+
setOpenMenuTopicId(null);
|
|
1341
|
+
setEditingTopicId(topic.id);
|
|
1342
|
+
setEditingTopicName(topic.name);
|
|
1343
|
+
}}
|
|
1344
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
|
|
1345
|
+
>
|
|
1346
|
+
<Pencil className="w-3 h-3" /> Rename
|
|
1347
|
+
</button>
|
|
1348
|
+
<button
|
|
1349
|
+
onClick={() => {
|
|
1350
|
+
setOpenMenuTopicId(null);
|
|
1351
|
+
setAddingQuestionTo(topic.id);
|
|
1352
|
+
setNewQuestionTitle("");
|
|
1353
|
+
if (!isExpanded) toggleTopic(topic.id);
|
|
1354
|
+
}}
|
|
1355
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
|
|
1356
|
+
>
|
|
1357
|
+
<Plus className="w-3 h-3" /> Add question
|
|
1358
|
+
</button>
|
|
1359
|
+
<button
|
|
1360
|
+
onClick={async () => {
|
|
1361
|
+
setOpenMenuTopicId(null);
|
|
1362
|
+
let topicQuestions =
|
|
1363
|
+
questionsByTopic[topic.id] ?? [];
|
|
1364
|
+
try {
|
|
1365
|
+
topicQuestions = await api.fetchQuestions(
|
|
1366
|
+
topic.id,
|
|
1367
|
+
);
|
|
1368
|
+
} catch {
|
|
1369
|
+
// Fall back to the already-loaded sidebar snapshot.
|
|
1370
|
+
}
|
|
1371
|
+
const rootQuestions = topicQuestions.filter(
|
|
1372
|
+
(q) => !q.parentQuestionId,
|
|
1373
|
+
);
|
|
1374
|
+
const exportedQuestions = await Promise.all(
|
|
1375
|
+
rootQuestions.map((q) =>
|
|
1376
|
+
buildQuestionExport(q, topicQuestions),
|
|
1377
|
+
),
|
|
1378
|
+
);
|
|
1379
|
+
downloadJson(
|
|
1380
|
+
{
|
|
1381
|
+
id: topic.id,
|
|
1382
|
+
name: topic.name,
|
|
1383
|
+
systemContext: topic.systemContext ?? "",
|
|
1384
|
+
contextFiles: topic.contextFiles,
|
|
1385
|
+
createdAt: topic.createdAt,
|
|
1386
|
+
questions: exportedQuestions,
|
|
1387
|
+
},
|
|
1388
|
+
`${topic.name.replace(/[^a-z0-9]+/gi, "-")}.json`,
|
|
1389
|
+
);
|
|
1390
|
+
}}
|
|
1391
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-white transition-colors"
|
|
1392
|
+
>
|
|
1393
|
+
<Download className="w-3 h-3" /> Download
|
|
1394
|
+
</button>
|
|
1395
|
+
|
|
1396
|
+
{canSyncDriveFolder && (
|
|
1397
|
+
<>
|
|
1398
|
+
<div className="border-t border-slate-700 my-0.5" />
|
|
1399
|
+
<button
|
|
1400
|
+
onClick={() => {
|
|
1401
|
+
setOpenMenuTopicId(null);
|
|
1402
|
+
void handlePushTopicToDrive(topic.id);
|
|
1403
|
+
}}
|
|
1404
|
+
disabled={topicBusy || pushing || syncing}
|
|
1405
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-cyan-300 hover:bg-slate-700 hover:text-cyan-200 disabled:opacity-50 transition-colors"
|
|
1406
|
+
>
|
|
1407
|
+
{topicPushingId === topic.id ? (
|
|
1408
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
1409
|
+
) : (
|
|
1410
|
+
<Upload className="w-3 h-3" />
|
|
1411
|
+
)}
|
|
1412
|
+
Push topic
|
|
1413
|
+
</button>
|
|
1414
|
+
<button
|
|
1415
|
+
onClick={() => {
|
|
1416
|
+
setOpenMenuTopicId(null);
|
|
1417
|
+
void handlePullTopicFromDrive(topic.id);
|
|
1418
|
+
}}
|
|
1419
|
+
disabled={topicBusy || pushing || syncing}
|
|
1420
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700 hover:text-cyan-300 disabled:opacity-50 transition-colors"
|
|
1421
|
+
>
|
|
1422
|
+
{topicSyncingId === topic.id ? (
|
|
1423
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
1424
|
+
) : (
|
|
1425
|
+
<RefreshCw className="w-3 h-3" />
|
|
1426
|
+
)}
|
|
1427
|
+
Pull topic
|
|
1428
|
+
</button>
|
|
1429
|
+
</>
|
|
1430
|
+
)}
|
|
1431
|
+
|
|
1432
|
+
<div className="border-t border-slate-700 my-0.5" />
|
|
1433
|
+
<button
|
|
1434
|
+
onClick={() => {
|
|
1435
|
+
setOpenMenuTopicId(null);
|
|
1436
|
+
if (
|
|
1437
|
+
confirm(
|
|
1438
|
+
`Delete topic "${topic.name}" and all its questions?`,
|
|
1439
|
+
)
|
|
1440
|
+
) {
|
|
1441
|
+
removeTopic(topic.id);
|
|
1442
|
+
}
|
|
1443
|
+
}}
|
|
1444
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-slate-700 hover:text-red-300 transition-colors"
|
|
1445
|
+
>
|
|
1446
|
+
<Trash2 className="w-3 h-3" /> Delete
|
|
1447
|
+
</button>
|
|
1448
|
+
</div>
|
|
1449
|
+
</>
|
|
1450
|
+
)}
|
|
1451
|
+
</div>
|
|
1252
1452
|
)}
|
|
1253
|
-
<button
|
|
1254
|
-
onClick={(e) => {
|
|
1255
|
-
e.stopPropagation();
|
|
1256
|
-
if (
|
|
1257
|
-
confirm(
|
|
1258
|
-
`Delete topic "${topic.name}" and all its questions?`,
|
|
1259
|
-
)
|
|
1260
|
-
) {
|
|
1261
|
-
removeTopic(topic.id);
|
|
1262
|
-
}
|
|
1263
|
-
}}
|
|
1264
|
-
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all"
|
|
1265
|
-
>
|
|
1266
|
-
<Trash2 className="w-3 h-3" />
|
|
1267
|
-
</button>
|
|
1268
|
-
<button
|
|
1269
|
-
onClick={async (e) => {
|
|
1270
|
-
e.stopPropagation();
|
|
1271
|
-
let topicQuestions = questionsByTopic[topic.id] ?? [];
|
|
1272
|
-
try {
|
|
1273
|
-
topicQuestions = await api.fetchQuestions(topic.id);
|
|
1274
|
-
} catch {
|
|
1275
|
-
// Fall back to the already-loaded sidebar snapshot.
|
|
1276
|
-
}
|
|
1277
|
-
const rootQuestions = topicQuestions.filter(
|
|
1278
|
-
(q) => !q.parentQuestionId,
|
|
1279
|
-
);
|
|
1280
|
-
const exportedQuestions = await Promise.all(
|
|
1281
|
-
rootQuestions.map((q) =>
|
|
1282
|
-
buildQuestionExport(q, topicQuestions),
|
|
1283
|
-
),
|
|
1284
|
-
);
|
|
1285
|
-
downloadJson(
|
|
1286
|
-
{
|
|
1287
|
-
id: topic.id,
|
|
1288
|
-
name: topic.name,
|
|
1289
|
-
systemContext: topic.systemContext ?? "",
|
|
1290
|
-
contextFiles: topic.contextFiles,
|
|
1291
|
-
createdAt: topic.createdAt,
|
|
1292
|
-
questions: exportedQuestions,
|
|
1293
|
-
},
|
|
1294
|
-
`${topic.name.replace(/[^a-z0-9]+/gi, "-")}.json`,
|
|
1295
|
-
);
|
|
1296
|
-
}}
|
|
1297
|
-
className="p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-cyan-400 transition-all"
|
|
1298
|
-
title="Download topic as JSON"
|
|
1299
|
-
>
|
|
1300
|
-
<Download className="w-3 h-3" />
|
|
1301
|
-
</button>
|
|
1302
1453
|
</div>
|
|
1303
1454
|
|
|
1455
|
+
{topicDriveStatus[topic.id] && (
|
|
1456
|
+
<p className="px-3 pb-1 text-[10px] leading-relaxed text-slate-500">
|
|
1457
|
+
{topicDriveStatus[topic.id]}
|
|
1458
|
+
</p>
|
|
1459
|
+
)}
|
|
1460
|
+
|
|
1304
1461
|
{/* Questions list */}
|
|
1305
1462
|
{isExpanded && (
|
|
1306
1463
|
<div className="ml-3 border-l border-slate-800">
|
|
@@ -164,6 +164,223 @@ require("fs").readdirSync(".").forEach((f) => console.log(" -", f));
|
|
|
164
164
|
`,
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
const REACT_VITE_TYPESCRIPT_FILES: Record<string, string> = {
|
|
168
|
+
"README.md": `# React Vite TypeScript GitHub Actions Lab
|
|
169
|
+
|
|
170
|
+
This template starts with a small React + Vite + TypeScript application and an intentionally empty workflow file.
|
|
171
|
+
|
|
172
|
+
## App files
|
|
173
|
+
|
|
174
|
+
- \`package.json\` defines the Vite scripts and React dependencies.
|
|
175
|
+
- \`index.html\` mounts the app.
|
|
176
|
+
- \`src/main.tsx\` boots React.
|
|
177
|
+
- \`src/App.tsx\` contains the starter UI.
|
|
178
|
+
- \`.github/workflows/ci.yml\` is blank on purpose so you can write the CI workflow from scratch.
|
|
179
|
+
|
|
180
|
+
## Local app commands
|
|
181
|
+
|
|
182
|
+
npm install
|
|
183
|
+
npm run dev
|
|
184
|
+
npm run build
|
|
185
|
+
|
|
186
|
+
## GitHub Actions task
|
|
187
|
+
|
|
188
|
+
Open \`.github/workflows/ci.yml\`, add your workflow, then run it with the lab controls.
|
|
189
|
+
`,
|
|
190
|
+
|
|
191
|
+
".github/workflows/ci.yml": "",
|
|
192
|
+
|
|
193
|
+
".gitignore": `node_modules
|
|
194
|
+
dist
|
|
195
|
+
.DS_Store
|
|
196
|
+
*.local
|
|
197
|
+
`,
|
|
198
|
+
|
|
199
|
+
"package.json": `{
|
|
200
|
+
"name": "gha-react-vite-typescript-app",
|
|
201
|
+
"private": true,
|
|
202
|
+
"version": "0.0.0",
|
|
203
|
+
"type": "module",
|
|
204
|
+
"scripts": {
|
|
205
|
+
"dev": "vite --host 0.0.0.0",
|
|
206
|
+
"build": "tsc --noEmit && vite build",
|
|
207
|
+
"preview": "vite preview --host 0.0.0.0"
|
|
208
|
+
},
|
|
209
|
+
"dependencies": {
|
|
210
|
+
"react": "^19.0.0",
|
|
211
|
+
"react-dom": "^19.0.0"
|
|
212
|
+
},
|
|
213
|
+
"devDependencies": {
|
|
214
|
+
"@types/react": "^19.0.0",
|
|
215
|
+
"@types/react-dom": "^19.0.0",
|
|
216
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
217
|
+
"typescript": "^5.9.3",
|
|
218
|
+
"vite": "^6.0.0"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`,
|
|
222
|
+
|
|
223
|
+
"index.html": `<!doctype html>
|
|
224
|
+
<html lang="en">
|
|
225
|
+
<head>
|
|
226
|
+
<meta charset="UTF-8" />
|
|
227
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
228
|
+
<title>React Vite TypeScript App</title>
|
|
229
|
+
</head>
|
|
230
|
+
<body>
|
|
231
|
+
<div id="root"></div>
|
|
232
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
233
|
+
</body>
|
|
234
|
+
</html>
|
|
235
|
+
`,
|
|
236
|
+
|
|
237
|
+
"tsconfig.json": `{
|
|
238
|
+
"compilerOptions": {
|
|
239
|
+
"target": "ES2020",
|
|
240
|
+
"useDefineForClassFields": true,
|
|
241
|
+
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
|
242
|
+
"allowJs": false,
|
|
243
|
+
"skipLibCheck": true,
|
|
244
|
+
"esModuleInterop": true,
|
|
245
|
+
"allowSyntheticDefaultImports": true,
|
|
246
|
+
"strict": true,
|
|
247
|
+
"forceConsistentCasingInFileNames": true,
|
|
248
|
+
"module": "ESNext",
|
|
249
|
+
"moduleResolution": "Node",
|
|
250
|
+
"resolveJsonModule": true,
|
|
251
|
+
"isolatedModules": true,
|
|
252
|
+
"noEmit": true,
|
|
253
|
+
"jsx": "react-jsx"
|
|
254
|
+
},
|
|
255
|
+
"include": ["src"]
|
|
256
|
+
}
|
|
257
|
+
`,
|
|
258
|
+
|
|
259
|
+
"vite.config.ts": `import { defineConfig } from "vite";
|
|
260
|
+
import react from "@vitejs/plugin-react";
|
|
261
|
+
|
|
262
|
+
export default defineConfig({
|
|
263
|
+
plugins: [react()],
|
|
264
|
+
});
|
|
265
|
+
`,
|
|
266
|
+
|
|
267
|
+
"src/main.tsx": `import { StrictMode } from "react";
|
|
268
|
+
import { createRoot } from "react-dom/client";
|
|
269
|
+
import App from "./App";
|
|
270
|
+
import "./index.css";
|
|
271
|
+
|
|
272
|
+
createRoot(document.getElementById("root")!).render(
|
|
273
|
+
<StrictMode>
|
|
274
|
+
<App />
|
|
275
|
+
</StrictMode>,
|
|
276
|
+
);
|
|
277
|
+
`,
|
|
278
|
+
|
|
279
|
+
"src/App.tsx": `const checks = [
|
|
280
|
+
"Edit .github/workflows/ci.yml from a clean slate",
|
|
281
|
+
"Install dependencies with npm install",
|
|
282
|
+
"Run npm run build in your workflow",
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
export default function App() {
|
|
286
|
+
return (
|
|
287
|
+
<main className="app-shell">
|
|
288
|
+
<section className="hero-card">
|
|
289
|
+
<p className="eyebrow">GitHub Actions Practice</p>
|
|
290
|
+
<h1>React + Vite + TypeScript</h1>
|
|
291
|
+
<p className="lead">
|
|
292
|
+
The application is ready. Your CI workflow starts empty so you can
|
|
293
|
+
build it one line at a time.
|
|
294
|
+
</p>
|
|
295
|
+
<ul>
|
|
296
|
+
{checks.map((check) => (
|
|
297
|
+
<li key={check}>{check}</li>
|
|
298
|
+
))}
|
|
299
|
+
</ul>
|
|
300
|
+
</section>
|
|
301
|
+
</main>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
`,
|
|
305
|
+
|
|
306
|
+
"src/index.css": `:root {
|
|
307
|
+
color: #e5eefb;
|
|
308
|
+
background: #0f172a;
|
|
309
|
+
font-family:
|
|
310
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
311
|
+
sans-serif;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
* {
|
|
315
|
+
box-sizing: border-box;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
body {
|
|
319
|
+
margin: 0;
|
|
320
|
+
min-width: 320px;
|
|
321
|
+
min-height: 100vh;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.app-shell {
|
|
325
|
+
display: grid;
|
|
326
|
+
min-height: 100vh;
|
|
327
|
+
place-items: center;
|
|
328
|
+
padding: 2rem;
|
|
329
|
+
background:
|
|
330
|
+
radial-gradient(circle at top left, rgba(59, 130, 246, 0.35), transparent 32rem),
|
|
331
|
+
linear-gradient(135deg, #0f172a 0%, #111827 100%);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.hero-card {
|
|
335
|
+
width: min(100%, 720px);
|
|
336
|
+
padding: 2rem;
|
|
337
|
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
338
|
+
border-radius: 24px;
|
|
339
|
+
background: rgba(15, 23, 42, 0.78);
|
|
340
|
+
box-shadow: 0 24px 80px rgba(2, 6, 23, 0.45);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.eyebrow {
|
|
344
|
+
margin: 0 0 0.75rem;
|
|
345
|
+
color: #fbbf24;
|
|
346
|
+
font-size: 0.78rem;
|
|
347
|
+
font-weight: 700;
|
|
348
|
+
letter-spacing: 0.16em;
|
|
349
|
+
text-transform: uppercase;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
h1 {
|
|
353
|
+
margin: 0;
|
|
354
|
+
font-size: clamp(2.5rem, 8vw, 5rem);
|
|
355
|
+
line-height: 0.95;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.lead {
|
|
359
|
+
color: #cbd5e1;
|
|
360
|
+
font-size: 1.1rem;
|
|
361
|
+
line-height: 1.7;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
ul {
|
|
365
|
+
display: grid;
|
|
366
|
+
gap: 0.75rem;
|
|
367
|
+
margin: 1.5rem 0 0;
|
|
368
|
+
padding: 0;
|
|
369
|
+
list-style: none;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
li {
|
|
373
|
+
padding: 0.85rem 1rem;
|
|
374
|
+
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
375
|
+
border-radius: 14px;
|
|
376
|
+
background: rgba(30, 41, 59, 0.72);
|
|
377
|
+
}
|
|
378
|
+
`,
|
|
379
|
+
|
|
380
|
+
"src/vite-env.d.ts": `/// <reference types="vite/client" />
|
|
381
|
+
`,
|
|
382
|
+
};
|
|
383
|
+
|
|
167
384
|
export const DEFAULT_GHA_LAB: GithubActionsLabWorkspace = {
|
|
168
385
|
version: 1,
|
|
169
386
|
label: "GitHub Actions Playground",
|
|
@@ -173,6 +390,15 @@ export const DEFAULT_GHA_LAB: GithubActionsLabWorkspace = {
|
|
|
173
390
|
files: DEFAULT_FILES,
|
|
174
391
|
};
|
|
175
392
|
|
|
393
|
+
export const REACT_VITE_TYPESCRIPT_GHA_LAB: GithubActionsLabWorkspace = {
|
|
394
|
+
version: 1,
|
|
395
|
+
label: "React Vite TypeScript Starter",
|
|
396
|
+
activeFile: ".github/workflows/ci.yml",
|
|
397
|
+
defaultEvent: "push",
|
|
398
|
+
defaultWorkflow: ".github/workflows/ci.yml",
|
|
399
|
+
files: REACT_VITE_TYPESCRIPT_FILES,
|
|
400
|
+
};
|
|
401
|
+
|
|
176
402
|
// ─── Helpers (mirror infraLab.ts API surface) ────────────────────────────
|
|
177
403
|
|
|
178
404
|
export function cloneGhaLabWorkspace(
|
|
@@ -183,7 +409,10 @@ export function cloneGhaLabWorkspace(
|
|
|
183
409
|
source.files && Object.keys(source.files).length > 0
|
|
184
410
|
? { ...source.files }
|
|
185
411
|
: { ...DEFAULT_FILES };
|
|
186
|
-
const activeFile =
|
|
412
|
+
const activeFile = Object.prototype.hasOwnProperty.call(
|
|
413
|
+
sourceFiles,
|
|
414
|
+
source.activeFile,
|
|
415
|
+
)
|
|
187
416
|
? source.activeFile
|
|
188
417
|
: (Object.keys(sourceFiles)[0] ?? ".github/workflows/ci.yml");
|
|
189
418
|
|
|
@@ -193,12 +422,17 @@ export function cloneGhaLabWorkspace(
|
|
|
193
422
|
activeFile,
|
|
194
423
|
defaultEvent: source.defaultEvent || "push",
|
|
195
424
|
defaultWorkflow:
|
|
196
|
-
source.defaultWorkflow &&
|
|
425
|
+
source.defaultWorkflow &&
|
|
426
|
+
Object.prototype.hasOwnProperty.call(sourceFiles, source.defaultWorkflow)
|
|
197
427
|
? source.defaultWorkflow
|
|
198
428
|
: Object.keys(sourceFiles).find((f) =>
|
|
199
429
|
f.startsWith(".github/workflows/"),
|
|
200
430
|
) || ".github/workflows/ci.yml",
|
|
201
431
|
files: sourceFiles,
|
|
432
|
+
// Preserve optional UX flags so they round-trip through save/load.
|
|
433
|
+
...(source.includeRunHistoryInContext
|
|
434
|
+
? { includeRunHistoryInContext: true }
|
|
435
|
+
: {}),
|
|
202
436
|
};
|
|
203
437
|
}
|
|
204
438
|
|
|
@@ -280,6 +514,9 @@ export function parseGhaLabWorkspace(
|
|
|
280
514
|
? parsed.defaultWorkflow
|
|
281
515
|
: DEFAULT_GHA_LAB.defaultWorkflow,
|
|
282
516
|
files,
|
|
517
|
+
...(parsed.includeRunHistoryInContext === true
|
|
518
|
+
? { includeRunHistoryInContext: true }
|
|
519
|
+
: {}),
|
|
283
520
|
});
|
|
284
521
|
} catch {
|
|
285
522
|
return null;
|