epicshop 6.50.13 → 6.50.14
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/dist/commands/workshops.js +476 -207
- package/package.json +2 -2
|
@@ -234,6 +234,134 @@ async function checkWorkshopAccess(workshops) {
|
|
|
234
234
|
hasAccess: accessResults[index],
|
|
235
235
|
}));
|
|
236
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Helper function to add a single workshop by repo name
|
|
239
|
+
* This handles the actual cloning and setup logic
|
|
240
|
+
*/
|
|
241
|
+
async function addSingleWorkshop(repoName, options) {
|
|
242
|
+
const { silent = false } = options;
|
|
243
|
+
const hasExplicitCloneDestination = Boolean(options.destination?.trim() || options.directory?.trim());
|
|
244
|
+
// Ensure config is set up first (only when using the managed repos directory)
|
|
245
|
+
if (!hasExplicitCloneDestination) {
|
|
246
|
+
if (!(await ensureConfigured())) {
|
|
247
|
+
return { success: false, message: 'Setup cancelled' };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const { getReposDirectory, workshopExists } = await import('@epic-web/workshop-utils/workshops.server');
|
|
251
|
+
// Check if workshop already exists (only meaningful for managed repos directory)
|
|
252
|
+
if (!hasExplicitCloneDestination) {
|
|
253
|
+
if (await workshopExists(repoName)) {
|
|
254
|
+
const message = `Workshop "${repoName}" already exists`;
|
|
255
|
+
if (!silent) {
|
|
256
|
+
const { getWorkshop } = await import('@epic-web/workshop-utils/workshops.server');
|
|
257
|
+
const reposDir = await getReposDirectory();
|
|
258
|
+
const workshop = await getWorkshop(repoName);
|
|
259
|
+
const workshopPath = workshop?.path ?? path.join(reposDir, repoName);
|
|
260
|
+
const workshopRepoName = workshop?.repoName ?? repoName;
|
|
261
|
+
const openCommand = `npx epicshop open ${workshopRepoName}`;
|
|
262
|
+
const startCommand = `npx epicshop start ${workshopRepoName}`;
|
|
263
|
+
console.log(chalk.yellow(`⚠️ ${message}`));
|
|
264
|
+
console.log(chalk.gray(` Location on disk: ${workshopPath}`));
|
|
265
|
+
console.log(chalk.gray(` You can run:`));
|
|
266
|
+
console.log(chalk.white.bold(` ${openCommand}`));
|
|
267
|
+
console.log(chalk.white.bold(` ${startCommand}`));
|
|
268
|
+
}
|
|
269
|
+
return { success: false, message };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
let reposDir;
|
|
273
|
+
let workshopPath;
|
|
274
|
+
if (options.destination?.trim()) {
|
|
275
|
+
// destination is always treated as a parent directory
|
|
276
|
+
// - if it exists and is a directory: clone into <destination>/<repoName>
|
|
277
|
+
// - if it doesn't exist: create it and clone into <destination>/<repoName>
|
|
278
|
+
// This ensures consistent behavior for both single and multiple workshop setups
|
|
279
|
+
const resolvedDestination = path.resolve(resolvePathWithTilde(options.destination));
|
|
280
|
+
try {
|
|
281
|
+
const stat = await fs.promises.stat(resolvedDestination);
|
|
282
|
+
if (stat.isDirectory()) {
|
|
283
|
+
reposDir = resolvedDestination;
|
|
284
|
+
workshopPath = path.join(reposDir, repoName);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
return {
|
|
288
|
+
success: false,
|
|
289
|
+
message: `Destination is not a directory: ${resolvedDestination}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Destination doesn't exist. Create it as a parent directory and clone inside.
|
|
295
|
+
reposDir = resolvedDestination;
|
|
296
|
+
workshopPath = path.join(reposDir, repoName);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
reposDir = options.directory?.trim()
|
|
301
|
+
? path.resolve(resolvePathWithTilde(options.directory))
|
|
302
|
+
: await getReposDirectory();
|
|
303
|
+
workshopPath = path.join(reposDir, repoName);
|
|
304
|
+
}
|
|
305
|
+
// Ensure the repos directory exists
|
|
306
|
+
await fs.promises.mkdir(reposDir, { recursive: true });
|
|
307
|
+
// Check if directory already exists
|
|
308
|
+
try {
|
|
309
|
+
await fs.promises.access(workshopPath);
|
|
310
|
+
const message = `Directory already exists: ${workshopPath}`;
|
|
311
|
+
if (!silent)
|
|
312
|
+
console.log(chalk.yellow(`⚠️ ${message}`));
|
|
313
|
+
return { success: false, message };
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Directory doesn't exist, which is what we want
|
|
317
|
+
}
|
|
318
|
+
const repoUrl = `https://github.com/${GITHUB_ORG}/${repoName}.git`;
|
|
319
|
+
if (!silent) {
|
|
320
|
+
console.log(chalk.cyan(`📦 Cloning ${repoUrl}...`));
|
|
321
|
+
}
|
|
322
|
+
// Clone the repository
|
|
323
|
+
const cloneResult = await runCommand('git', ['clone', repoUrl, workshopPath], {
|
|
324
|
+
cwd: reposDir,
|
|
325
|
+
silent,
|
|
326
|
+
});
|
|
327
|
+
if (!cloneResult.success) {
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
message: `Failed to clone repository: ${cloneResult.message}`,
|
|
331
|
+
error: cloneResult.error,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (!silent) {
|
|
335
|
+
console.log(chalk.cyan(`🔧 Running npm run setup...`));
|
|
336
|
+
}
|
|
337
|
+
// Run npm run setup
|
|
338
|
+
const setupResult = await runCommand('npm', ['run', 'setup'], {
|
|
339
|
+
cwd: workshopPath,
|
|
340
|
+
silent,
|
|
341
|
+
});
|
|
342
|
+
if (!setupResult.success) {
|
|
343
|
+
// Clean up the cloned directory on setup failure
|
|
344
|
+
if (!silent) {
|
|
345
|
+
console.log(chalk.yellow(`🧹 Cleaning up cloned directory...`));
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
await fs.promises.rm(workshopPath, { recursive: true, force: true });
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// Ignore cleanup errors
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
success: false,
|
|
355
|
+
message: `Failed to run setup: ${setupResult.message}`,
|
|
356
|
+
error: setupResult.error,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const message = `Workshop "${repoName}" cloned successfully to ${workshopPath}`;
|
|
360
|
+
if (!silent) {
|
|
361
|
+
console.log(chalk.green(`✅ ${message}`));
|
|
362
|
+
}
|
|
363
|
+
return { success: true, message };
|
|
364
|
+
}
|
|
237
365
|
/**
|
|
238
366
|
* Add a workshop by cloning from epicweb-dev GitHub org and running setup
|
|
239
367
|
*/
|
|
@@ -325,7 +453,7 @@ export async function add(options) {
|
|
|
325
453
|
error: error instanceof Error ? error : new Error(message),
|
|
326
454
|
};
|
|
327
455
|
}
|
|
328
|
-
const { search } = await import('@inquirer/prompts');
|
|
456
|
+
const { search, select, checkbox } = await import('@inquirer/prompts');
|
|
329
457
|
console.log();
|
|
330
458
|
console.log(chalk.bold.cyan('📚 Available Workshops\n'));
|
|
331
459
|
console.log(chalk.gray('Icon Key:'));
|
|
@@ -335,176 +463,246 @@ export async function add(options) {
|
|
|
335
463
|
console.log(chalk.gray(` 🔑 You have access to this workshop`));
|
|
336
464
|
console.log(chalk.gray(` ✔︎ Already downloaded on your machine`));
|
|
337
465
|
console.log();
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
]
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
name
|
|
359
|
-
value:
|
|
360
|
-
description
|
|
361
|
-
|
|
466
|
+
// Filter workshops for quick-select options (has access, not downloaded, has product)
|
|
467
|
+
const quickSelectCandidates = enrichedWorkshops.filter((w) => w.hasAccess === true &&
|
|
468
|
+
w.isDownloaded !== true &&
|
|
469
|
+
w.productHost &&
|
|
470
|
+
w.name !== TUTORIAL_REPO);
|
|
471
|
+
// Check if we have enough candidates for quick-select options
|
|
472
|
+
const hasQuickSelectOptions = quickSelectCandidates.length > 1;
|
|
473
|
+
let selectedRepoNames = [];
|
|
474
|
+
if (hasQuickSelectOptions) {
|
|
475
|
+
// Group workshops by product for quick-select options
|
|
476
|
+
const workshopsByProduct = new Map();
|
|
477
|
+
for (const w of quickSelectCandidates) {
|
|
478
|
+
const host = w.productHost;
|
|
479
|
+
const existing = workshopsByProduct.get(host) || [];
|
|
480
|
+
existing.push(w.name);
|
|
481
|
+
workshopsByProduct.set(host, existing);
|
|
482
|
+
}
|
|
483
|
+
const selectionMethodChoices = [];
|
|
484
|
+
// Add "All My Workshops" option
|
|
485
|
+
selectionMethodChoices.push({
|
|
486
|
+
name: `⭐ All My Workshops`,
|
|
487
|
+
value: '__ALL_MY__',
|
|
488
|
+
description: `Set up all ${quickSelectCandidates.length} workshops you have access to`,
|
|
489
|
+
});
|
|
490
|
+
// Add per-product options for products with multiple workshops
|
|
491
|
+
const productDisplayNames = {
|
|
492
|
+
'www.epicreact.dev': '🚀 All Epic React workshops',
|
|
493
|
+
'www.epicweb.dev': '🌌 All Epic Web workshops',
|
|
494
|
+
'www.epicai.pro': '⚡ All Epic AI workshops',
|
|
362
495
|
};
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
496
|
+
for (const [host, workshops] of workshopsByProduct) {
|
|
497
|
+
if (workshops.length > 1 && productDisplayNames[host]) {
|
|
498
|
+
selectionMethodChoices.push({
|
|
499
|
+
name: productDisplayNames[host],
|
|
500
|
+
value: `__PRODUCT__${host}`,
|
|
501
|
+
description: `Set up all ${workshops.length} workshops from this product`,
|
|
502
|
+
});
|
|
369
503
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
504
|
+
}
|
|
505
|
+
// Add "Choose individually" option
|
|
506
|
+
selectionMethodChoices.push({
|
|
507
|
+
name: '📋 Choose individually',
|
|
508
|
+
value: '__INDIVIDUAL__',
|
|
509
|
+
description: 'Select specific workshops from a list',
|
|
510
|
+
});
|
|
511
|
+
// Add "Browse all" option to see all workshops including ones without access
|
|
512
|
+
selectionMethodChoices.push({
|
|
513
|
+
name: '🔍 Browse all workshops',
|
|
514
|
+
value: '__BROWSE_ALL__',
|
|
515
|
+
description: 'Search through all available workshops',
|
|
516
|
+
});
|
|
517
|
+
const selectionMethod = await select({
|
|
518
|
+
message: 'How would you like to select workshops?',
|
|
519
|
+
choices: selectionMethodChoices,
|
|
520
|
+
});
|
|
521
|
+
if (selectionMethod === '__ALL_MY__') {
|
|
522
|
+
selectedRepoNames = quickSelectCandidates.map((w) => w.name);
|
|
523
|
+
console.log(chalk.cyan(`\n✓ Selected all ${selectedRepoNames.length} workshops you have access to\n`));
|
|
524
|
+
}
|
|
525
|
+
else if (selectionMethod.startsWith('__PRODUCT__')) {
|
|
526
|
+
const host = selectionMethod.replace('__PRODUCT__', '');
|
|
527
|
+
selectedRepoNames = workshopsByProduct.get(host) || [];
|
|
528
|
+
const productName = productDisplayNames[host]?.replace(/^[^\s]+\s/, '') || host;
|
|
529
|
+
console.log(chalk.cyan(`\n✓ Selected ${selectedRepoNames.length} ${productName.replace('All ', '')}\n`));
|
|
530
|
+
}
|
|
531
|
+
else if (selectionMethod === '__INDIVIDUAL__') {
|
|
532
|
+
// Show checkbox for individual selection from accessible workshops
|
|
533
|
+
const individualChoices = quickSelectCandidates.map((w) => {
|
|
534
|
+
const productIcon = w.productHost
|
|
535
|
+
? PRODUCT_ICONS[w.productHost] || ''
|
|
536
|
+
: '';
|
|
537
|
+
const accessIcon = chalk.yellow('🔑');
|
|
538
|
+
const name = [productIcon, w.title || w.name, accessIcon]
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.join(' ');
|
|
541
|
+
const descriptionParts = [
|
|
542
|
+
w.instructorName ? `by ${w.instructorName}` : null,
|
|
543
|
+
w.productDisplayName || w.productHost,
|
|
544
|
+
w.description,
|
|
545
|
+
].filter(Boolean);
|
|
546
|
+
const description = descriptionParts.join(' • ') || undefined;
|
|
547
|
+
return {
|
|
548
|
+
name,
|
|
549
|
+
value: w.name,
|
|
550
|
+
description,
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
console.log(chalk.gray('\n Use space to select, enter to confirm your selection.\n'));
|
|
554
|
+
selectedRepoNames = await checkbox({
|
|
555
|
+
message: 'Select workshops to set up:',
|
|
556
|
+
choices: individualChoices,
|
|
384
557
|
});
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
const hasExplicitCloneDestination = Boolean(options.destination?.trim() || options.directory?.trim());
|
|
389
|
-
// Ensure config is set up first (only when using the managed repos directory)
|
|
390
|
-
if (!hasExplicitCloneDestination) {
|
|
391
|
-
if (!(await ensureConfigured())) {
|
|
392
|
-
return { success: false, message: 'Setup cancelled' };
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
const { getReposDirectory, workshopExists } = await import('@epic-web/workshop-utils/workshops.server');
|
|
396
|
-
// Check if workshop already exists (only meaningful for managed repos directory)
|
|
397
|
-
if (!hasExplicitCloneDestination) {
|
|
398
|
-
if (await workshopExists(repoName)) {
|
|
399
|
-
const message = `Workshop "${repoName}" already exists`;
|
|
400
|
-
if (!silent) {
|
|
401
|
-
const { getWorkshop } = await import('@epic-web/workshop-utils/workshops.server');
|
|
402
|
-
const reposDir = await getReposDirectory();
|
|
403
|
-
const workshop = await getWorkshop(repoName);
|
|
404
|
-
const workshopPath = workshop?.path ?? path.join(reposDir, repoName);
|
|
405
|
-
const workshopRepoName = workshop?.repoName ?? repoName;
|
|
406
|
-
const openCommand = `npx epicshop open ${workshopRepoName}`;
|
|
407
|
-
const startCommand = `npx epicshop start ${workshopRepoName}`;
|
|
408
|
-
console.log(chalk.yellow(`⚠️ ${message}`));
|
|
409
|
-
console.log(chalk.gray(` Location on disk: ${workshopPath}`));
|
|
410
|
-
console.log(chalk.gray(` You can run:`));
|
|
411
|
-
console.log(chalk.white.bold(` ${openCommand}`));
|
|
412
|
-
console.log(chalk.white.bold(` ${startCommand}`));
|
|
413
558
|
}
|
|
414
|
-
|
|
559
|
+
// For __BROWSE_ALL__, selectedRepoNames stays empty and falls through to search
|
|
415
560
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
561
|
+
// If no quick-select was made, show the full search interface
|
|
562
|
+
if (selectedRepoNames.length === 0) {
|
|
563
|
+
const allChoices = enrichedWorkshops.map((w) => {
|
|
564
|
+
const productIcon = w.productHost
|
|
565
|
+
? PRODUCT_ICONS[w.productHost] || ''
|
|
566
|
+
: '';
|
|
567
|
+
const accessIcon = w.hasAccess === true ? chalk.yellow('🔑') : '';
|
|
568
|
+
const downloadedIcon = w.isDownloaded === true ? chalk.green('✔︎') : '';
|
|
569
|
+
const nameParts = [
|
|
570
|
+
productIcon,
|
|
571
|
+
w.title || w.name,
|
|
572
|
+
accessIcon,
|
|
573
|
+
downloadedIcon,
|
|
574
|
+
].filter(Boolean);
|
|
575
|
+
const name = nameParts.join(' ');
|
|
576
|
+
const descriptionParts = [
|
|
577
|
+
w.instructorName ? `by ${w.instructorName}` : null,
|
|
578
|
+
w.productDisplayName || w.productHost,
|
|
579
|
+
w.description,
|
|
580
|
+
].filter(Boolean);
|
|
581
|
+
const description = descriptionParts.join(' • ') || undefined;
|
|
582
|
+
return {
|
|
583
|
+
name,
|
|
584
|
+
value: w.name,
|
|
585
|
+
description,
|
|
586
|
+
workshop: w,
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
repoName = await search({
|
|
590
|
+
message: 'Select a workshop to add:',
|
|
591
|
+
source: async (input) => {
|
|
592
|
+
if (!input) {
|
|
593
|
+
return allChoices;
|
|
594
|
+
}
|
|
595
|
+
return matchSorter(allChoices, input, {
|
|
596
|
+
keys: [
|
|
597
|
+
{ key: 'name', threshold: rankings.CONTAINS },
|
|
598
|
+
{ key: 'value', threshold: rankings.CONTAINS },
|
|
599
|
+
{
|
|
600
|
+
key: 'workshop.productDisplayName',
|
|
601
|
+
threshold: rankings.CONTAINS,
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
key: 'workshop.instructorName',
|
|
605
|
+
threshold: rankings.CONTAINS,
|
|
606
|
+
},
|
|
607
|
+
{ key: 'description', threshold: rankings.WORD_STARTS_WITH },
|
|
608
|
+
],
|
|
609
|
+
});
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
selectedRepoNames = [repoName];
|
|
613
|
+
}
|
|
614
|
+
// Create a map from repo name to workshop title for nice display
|
|
615
|
+
const repoToTitle = new Map();
|
|
616
|
+
for (const w of enrichedWorkshops) {
|
|
617
|
+
repoToTitle.set(w.name, w.title || w.name);
|
|
618
|
+
}
|
|
619
|
+
// Helper to get display name for a repo
|
|
620
|
+
const getDisplayName = (repo) => repoToTitle.get(repo) || repo;
|
|
621
|
+
// Set up selected workshops
|
|
622
|
+
if (selectedRepoNames.length > 1) {
|
|
623
|
+
// Multiple workshops selected - confirm before proceeding
|
|
624
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
625
|
+
console.log();
|
|
626
|
+
const shouldProceed = await confirm({
|
|
627
|
+
message: `You've selected to set up ${selectedRepoNames.length} workshops. This may take some time. Continue?`,
|
|
628
|
+
default: true,
|
|
629
|
+
});
|
|
630
|
+
if (!shouldProceed) {
|
|
631
|
+
console.log(chalk.gray('\nSetup cancelled.\n'));
|
|
632
|
+
return { success: false, message: 'Setup cancelled by user' };
|
|
633
|
+
}
|
|
634
|
+
console.log();
|
|
635
|
+
let successCount = 0;
|
|
636
|
+
let failCount = 0;
|
|
637
|
+
for (const selectedRepo of selectedRepoNames) {
|
|
638
|
+
const displayName = getDisplayName(selectedRepo);
|
|
639
|
+
console.log(chalk.cyan(`🏎️ Setting up ${chalk.bold(displayName)}...\n`));
|
|
640
|
+
const result = await addSingleWorkshop(selectedRepo, options);
|
|
641
|
+
if (result.success) {
|
|
642
|
+
successCount++;
|
|
643
|
+
console.log(chalk.green(`🏁 Finished setting up ${chalk.bold(displayName)}\n`));
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
failCount++;
|
|
647
|
+
console.log(chalk.yellow(`⚠️ Failed to set up ${displayName}. You can retry later with \`npx epicshop add ${selectedRepo}\`.`));
|
|
648
|
+
if (result.message)
|
|
649
|
+
console.log(chalk.gray(` ${result.message}`));
|
|
650
|
+
console.log();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// Final summary
|
|
654
|
+
if (successCount > 0) {
|
|
655
|
+
console.log(chalk.green.bold(`🏁 🏁 Finished setting up all ${successCount} workshop${successCount > 1 ? 's' : ''}${failCount > 0 ? ` (${failCount} failed)` : ''}.\n`));
|
|
656
|
+
console.log(chalk.white('Run:'));
|
|
657
|
+
console.log(chalk.white(` ${chalk.cyan('npx epicshop open')} - open a workshop in your editor`));
|
|
658
|
+
console.log(chalk.white(` ${chalk.cyan('npx epicshop start')} - start a workshop`));
|
|
659
|
+
console.log();
|
|
660
|
+
return {
|
|
661
|
+
success: true,
|
|
662
|
+
message: `Successfully set up ${successCount} workshop(s)${failCount > 0 ? `, ${failCount} failed` : ''}`,
|
|
663
|
+
};
|
|
429
664
|
}
|
|
430
665
|
else {
|
|
431
666
|
return {
|
|
432
667
|
success: false,
|
|
433
|
-
message: `
|
|
668
|
+
message: `Failed to set up any workshops`,
|
|
434
669
|
};
|
|
435
670
|
}
|
|
436
671
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
672
|
+
// Single workshop selected
|
|
673
|
+
repoName = selectedRepoNames[0];
|
|
674
|
+
if (!repoName) {
|
|
675
|
+
return { success: false, message: 'No workshop selected' };
|
|
441
676
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// Check if directory already exists
|
|
452
|
-
try {
|
|
453
|
-
await fs.promises.access(workshopPath);
|
|
454
|
-
const message = `Directory already exists: ${workshopPath}`;
|
|
455
|
-
if (!silent)
|
|
456
|
-
console.log(chalk.yellow(`⚠️ ${message}`));
|
|
457
|
-
return { success: false, message };
|
|
458
|
-
}
|
|
459
|
-
catch {
|
|
460
|
-
// Directory doesn't exist, which is what we want
|
|
461
|
-
}
|
|
462
|
-
const repoUrl = `https://github.com/${GITHUB_ORG}/${repoName}.git`;
|
|
463
|
-
if (!silent) {
|
|
464
|
-
console.log(chalk.cyan(`📦 Cloning ${repoUrl}...`));
|
|
465
|
-
}
|
|
466
|
-
// Clone the repository
|
|
467
|
-
const cloneResult = await runCommand('git', ['clone', repoUrl, workshopPath], {
|
|
468
|
-
cwd: reposDir,
|
|
469
|
-
silent,
|
|
470
|
-
});
|
|
471
|
-
if (!cloneResult.success) {
|
|
472
|
-
return {
|
|
473
|
-
success: false,
|
|
474
|
-
message: `Failed to clone repository: ${cloneResult.message}`,
|
|
475
|
-
error: cloneResult.error,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
if (!silent) {
|
|
479
|
-
console.log(chalk.cyan(`🔧 Running npm run setup...`));
|
|
480
|
-
}
|
|
481
|
-
// Run npm run setup
|
|
482
|
-
const setupResult = await runCommand('npm', ['run', 'setup'], {
|
|
483
|
-
cwd: workshopPath,
|
|
484
|
-
silent,
|
|
485
|
-
});
|
|
486
|
-
if (!setupResult.success) {
|
|
487
|
-
// Clean up the cloned directory on setup failure
|
|
488
|
-
if (!silent) {
|
|
489
|
-
console.log(chalk.yellow(`🧹 Cleaning up cloned directory...`));
|
|
490
|
-
}
|
|
491
|
-
try {
|
|
492
|
-
await fs.promises.rm(workshopPath, { recursive: true, force: true });
|
|
493
|
-
}
|
|
494
|
-
catch {
|
|
495
|
-
// Ignore cleanup errors
|
|
677
|
+
const displayName = getDisplayName(repoName);
|
|
678
|
+
console.log(chalk.cyan(`🏎️ Setting up ${chalk.bold(displayName)}...\n`));
|
|
679
|
+
const result = await addSingleWorkshop(repoName, options);
|
|
680
|
+
if (result.success) {
|
|
681
|
+
console.log(chalk.green(`🏁 Finished setting up ${chalk.bold(displayName)}\n`));
|
|
682
|
+
console.log(chalk.white('Run:'));
|
|
683
|
+
console.log(chalk.white(` ${chalk.cyan('npx epicshop open')} - open a workshop in your editor`));
|
|
684
|
+
console.log(chalk.white(` ${chalk.cyan('npx epicshop start')} - start a workshop`));
|
|
685
|
+
console.log();
|
|
496
686
|
}
|
|
497
|
-
return
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
};
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
689
|
+
// Ensure we have a repo name at this point
|
|
690
|
+
if (!repoName) {
|
|
691
|
+
return { success: false, message: 'No workshop selected' };
|
|
502
692
|
}
|
|
503
|
-
|
|
693
|
+
// Use the helper to set up the single workshop (when repo was provided via CLI args)
|
|
504
694
|
if (!silent) {
|
|
505
|
-
console.log(chalk.
|
|
695
|
+
console.log(chalk.cyan(`🏎️ Setting up ${chalk.bold(repoName)}...\n`));
|
|
696
|
+
}
|
|
697
|
+
const result = await addSingleWorkshop(repoName, options);
|
|
698
|
+
if (result.success && !silent) {
|
|
699
|
+
console.log(chalk.green(`🏁 Finished setting up ${chalk.bold(repoName)}\n`));
|
|
700
|
+
console.log(chalk.white('Run:'));
|
|
701
|
+
console.log(chalk.white(` ${chalk.cyan('npx epicshop open')} - open a workshop in your editor`));
|
|
702
|
+
console.log(chalk.white(` ${chalk.cyan('npx epicshop start')} - start a workshop`));
|
|
703
|
+
console.log();
|
|
506
704
|
}
|
|
507
|
-
return
|
|
705
|
+
return result;
|
|
508
706
|
}
|
|
509
707
|
catch (error) {
|
|
510
708
|
if (error.message === 'USER_QUIT') {
|
|
@@ -1511,7 +1709,7 @@ async function promptAndSetupAccessibleWorkshops() {
|
|
|
1511
1709
|
'Skip this step by not running onboarding, and add workshops directly: npx epicshop add <repo-name>',
|
|
1512
1710
|
],
|
|
1513
1711
|
});
|
|
1514
|
-
const {
|
|
1712
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
1515
1713
|
const spinner = ora('Fetching available workshops...').start();
|
|
1516
1714
|
let enrichedWorkshops;
|
|
1517
1715
|
try {
|
|
@@ -1566,11 +1764,77 @@ async function promptAndSetupAccessibleWorkshops() {
|
|
|
1566
1764
|
console.log(chalk.gray(` ⚡ EpicAI.pro`));
|
|
1567
1765
|
console.log(chalk.gray(` 🔑 You have access to this workshop`));
|
|
1568
1766
|
console.log();
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1767
|
+
// Filter workshops that have a product configured (for "All My Workshops" option)
|
|
1768
|
+
const workshopsWithProduct = candidates.filter((w) => w.productHost);
|
|
1769
|
+
// Group workshops by product for quick-select options
|
|
1770
|
+
const workshopsByProduct = new Map();
|
|
1771
|
+
for (const w of workshopsWithProduct) {
|
|
1772
|
+
const host = w.productHost;
|
|
1773
|
+
const existing = workshopsByProduct.get(host) || [];
|
|
1774
|
+
existing.push(w.name);
|
|
1775
|
+
workshopsByProduct.set(host, existing);
|
|
1776
|
+
}
|
|
1777
|
+
const selectionMethodChoices = [];
|
|
1778
|
+
// Add "All My Workshops" option if there are multiple workshops with products
|
|
1779
|
+
if (workshopsWithProduct.length > 1) {
|
|
1780
|
+
selectionMethodChoices.push({
|
|
1781
|
+
name: `⭐ All My Workshops`,
|
|
1782
|
+
value: '__ALL_MY__',
|
|
1783
|
+
description: `Set up all ${workshopsWithProduct.length} workshops you have access to`,
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
// Add per-product options for products with multiple workshops
|
|
1787
|
+
const productDisplayNames = {
|
|
1788
|
+
'www.epicreact.dev': '🚀 All Epic React workshops',
|
|
1789
|
+
'www.epicweb.dev': '🌌 All Epic Web workshops',
|
|
1790
|
+
'www.epicai.pro': '⚡ All Epic AI workshops',
|
|
1791
|
+
};
|
|
1792
|
+
for (const [host, workshops] of workshopsByProduct) {
|
|
1793
|
+
if (workshops.length > 1 && productDisplayNames[host]) {
|
|
1794
|
+
selectionMethodChoices.push({
|
|
1795
|
+
name: productDisplayNames[host],
|
|
1796
|
+
value: `__PRODUCT__${host}`,
|
|
1797
|
+
description: `Set up all ${workshops.length} workshops from this product`,
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
// Always add the "Choose individually" option
|
|
1802
|
+
selectionMethodChoices.push({
|
|
1803
|
+
name: '📋 Choose individually',
|
|
1804
|
+
value: '__INDIVIDUAL__',
|
|
1805
|
+
description: 'Select specific workshops from a list',
|
|
1806
|
+
});
|
|
1807
|
+
// Add skip option
|
|
1808
|
+
selectionMethodChoices.push({
|
|
1809
|
+
name: '⏭️ Skip for now',
|
|
1810
|
+
value: '__SKIP__',
|
|
1811
|
+
description: 'Continue without setting up additional workshops',
|
|
1812
|
+
});
|
|
1813
|
+
const { select } = await import('@inquirer/prompts');
|
|
1814
|
+
const selectionMethod = await select({
|
|
1815
|
+
message: 'How would you like to select workshops?',
|
|
1816
|
+
choices: selectionMethodChoices,
|
|
1817
|
+
});
|
|
1818
|
+
if (selectionMethod === '__SKIP__') {
|
|
1819
|
+
console.log(chalk.gray('\nSkipping workshop setup. Continuing...\n'));
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
let selectedWorkshops;
|
|
1823
|
+
if (selectionMethod === '__ALL_MY__') {
|
|
1824
|
+
// Select all workshops with products (that the user has access to)
|
|
1825
|
+
selectedWorkshops = workshopsWithProduct.map((w) => w.name);
|
|
1826
|
+
console.log(chalk.cyan(`\n✓ Selected all ${selectedWorkshops.length} workshops you have access to\n`));
|
|
1827
|
+
}
|
|
1828
|
+
else if (selectionMethod.startsWith('__PRODUCT__')) {
|
|
1829
|
+
// Select all workshops for this product
|
|
1830
|
+
const host = selectionMethod.replace('__PRODUCT__', '');
|
|
1831
|
+
selectedWorkshops = workshopsByProduct.get(host) || [];
|
|
1832
|
+
const productName = productDisplayNames[host]?.replace(/^[^\s]+\s/, '') || host;
|
|
1833
|
+
console.log(chalk.cyan(`\n✓ Selected ${selectedWorkshops.length} ${productName.replace('All ', '')}\n`));
|
|
1834
|
+
}
|
|
1835
|
+
else {
|
|
1836
|
+
// Show checkbox for individual selection
|
|
1837
|
+
const individualChoices = candidates.map((w) => {
|
|
1574
1838
|
const productIcon = w.productHost
|
|
1575
1839
|
? PRODUCT_ICONS[w.productHost] || ''
|
|
1576
1840
|
: '';
|
|
@@ -1588,65 +1852,70 @@ async function promptAndSetupAccessibleWorkshops() {
|
|
|
1588
1852
|
name,
|
|
1589
1853
|
value: w.name,
|
|
1590
1854
|
description,
|
|
1591
|
-
workshop: w,
|
|
1592
1855
|
};
|
|
1593
1856
|
});
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
value: 'done',
|
|
1599
|
-
description: 'Continue to the tutorial setup',
|
|
1600
|
-
},
|
|
1601
|
-
{
|
|
1602
|
-
name: 'Skip workshop setup',
|
|
1603
|
-
value: 'skip',
|
|
1604
|
-
description: 'Continue without setting up more workshops',
|
|
1605
|
-
},
|
|
1606
|
-
];
|
|
1607
|
-
};
|
|
1608
|
-
while (true) {
|
|
1609
|
-
const choices = await buildChoices();
|
|
1610
|
-
// If there are no remaining workshops, stop
|
|
1611
|
-
if (choices.length <= 2)
|
|
1612
|
-
return;
|
|
1613
|
-
const selection = await search({
|
|
1614
|
-
message: 'Select a workshop to set up (you can pick multiple):',
|
|
1615
|
-
source: async (input) => {
|
|
1616
|
-
if (!input)
|
|
1617
|
-
return choices;
|
|
1618
|
-
return matchSorter(choices, input, {
|
|
1619
|
-
keys: [
|
|
1620
|
-
{ key: 'name', threshold: rankings.CONTAINS },
|
|
1621
|
-
{ key: 'value', threshold: rankings.CONTAINS },
|
|
1622
|
-
{
|
|
1623
|
-
key: 'workshop.productDisplayName',
|
|
1624
|
-
threshold: rankings.CONTAINS,
|
|
1625
|
-
},
|
|
1626
|
-
{ key: 'workshop.instructorName', threshold: rankings.CONTAINS },
|
|
1627
|
-
{ key: 'description', threshold: rankings.WORD_STARTS_WITH },
|
|
1628
|
-
],
|
|
1629
|
-
});
|
|
1630
|
-
},
|
|
1857
|
+
console.log(chalk.gray('\n Use space to select, enter to confirm your selection.\n'));
|
|
1858
|
+
selectedWorkshops = await checkbox({
|
|
1859
|
+
message: 'Select workshops to set up:',
|
|
1860
|
+
choices: individualChoices,
|
|
1631
1861
|
});
|
|
1632
|
-
|
|
1633
|
-
|
|
1862
|
+
}
|
|
1863
|
+
if (selectedWorkshops.length === 0) {
|
|
1864
|
+
console.log(chalk.gray('\nNo workshops selected. Continuing...\n'));
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
// Create a map from repo name to workshop title for nice display
|
|
1868
|
+
const repoToTitle = new Map();
|
|
1869
|
+
for (const w of candidates) {
|
|
1870
|
+
repoToTitle.set(w.name, w.title || w.name);
|
|
1871
|
+
}
|
|
1872
|
+
const getDisplayName = (repo) => repoToTitle.get(repo) || repo;
|
|
1873
|
+
// Confirm before setting up multiple workshops
|
|
1874
|
+
if (selectedWorkshops.length > 1) {
|
|
1875
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
1876
|
+
console.log();
|
|
1877
|
+
const shouldProceed = await confirm({
|
|
1878
|
+
message: `You've selected to set up ${selectedWorkshops.length} workshops. This may take some time. Continue?`,
|
|
1879
|
+
default: true,
|
|
1880
|
+
});
|
|
1881
|
+
if (!shouldProceed) {
|
|
1882
|
+
console.log(chalk.gray('\nSetup cancelled. Continuing...\n'));
|
|
1634
1883
|
return;
|
|
1635
1884
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1885
|
+
}
|
|
1886
|
+
console.log();
|
|
1887
|
+
let successCount = 0;
|
|
1888
|
+
let failCount = 0;
|
|
1889
|
+
// Set up each selected workshop
|
|
1890
|
+
for (const repoName of selectedWorkshops) {
|
|
1891
|
+
const displayName = getDisplayName(repoName);
|
|
1892
|
+
// If already present, don't treat that as an error
|
|
1893
|
+
if (await workshopExists(repoName)) {
|
|
1894
|
+
console.log(chalk.gray(`• ${displayName} (already set up)`));
|
|
1639
1895
|
continue;
|
|
1640
1896
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1897
|
+
console.log(chalk.cyan(`🏎️ Setting up ${chalk.bold(displayName)}...\n`));
|
|
1898
|
+
const result = await add({ repoName, silent: true });
|
|
1899
|
+
if (result.success) {
|
|
1900
|
+
successCount++;
|
|
1901
|
+
console.log(chalk.green(`🏁 Finished setting up ${chalk.bold(displayName)}\n`));
|
|
1902
|
+
}
|
|
1903
|
+
else {
|
|
1904
|
+
failCount++;
|
|
1905
|
+
console.log(chalk.yellow(`⚠️ Failed to set up ${displayName}. You can retry later with \`npx epicshop add ${repoName}\`.`));
|
|
1644
1906
|
if (result.message)
|
|
1645
1907
|
console.log(chalk.gray(` ${result.message}`));
|
|
1646
1908
|
console.log();
|
|
1647
|
-
continue;
|
|
1648
1909
|
}
|
|
1649
1910
|
}
|
|
1911
|
+
// Final summary for multiple workshops
|
|
1912
|
+
if (selectedWorkshops.length > 1 && successCount > 0) {
|
|
1913
|
+
console.log(chalk.green.bold(`🏁 🏁 Finished setting up all ${successCount} workshop${successCount > 1 ? 's' : ''}${failCount > 0 ? ` (${failCount} failed)` : ''}.\n`));
|
|
1914
|
+
console.log(chalk.white('Run:'));
|
|
1915
|
+
console.log(chalk.white(` ${chalk.cyan('npx epicshop open')} - open a workshop in your editor`));
|
|
1916
|
+
console.log(chalk.white(` ${chalk.cyan('npx epicshop start')} - start a workshop`));
|
|
1917
|
+
console.log();
|
|
1918
|
+
}
|
|
1650
1919
|
}
|
|
1651
1920
|
/**
|
|
1652
1921
|
* Ensure the tutorial workshop exists and start it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epicshop",
|
|
3
|
-
"version": "6.50.
|
|
3
|
+
"version": "6.50.14",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"build:watch": "nx watch --projects=epicshop -- nx run \\$NX_PROJECT_NAME:build"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@epic-web/workshop-utils": "6.50.
|
|
78
|
+
"@epic-web/workshop-utils": "6.50.14",
|
|
79
79
|
"@inquirer/prompts": "^7.5.1",
|
|
80
80
|
"chalk": "^5.6.2",
|
|
81
81
|
"close-with-grace": "^2.3.0",
|