epicshop 6.74.2 โ†’ 6.75.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/README.md CHANGED
@@ -41,8 +41,8 @@ epicshop start <workshop>
41
41
 
42
42
  ### Helpful commands
43
43
 
44
- - `epicshop add <repo-name> [destination]`: clone a workshop from the
45
- `epicweb-dev` GitHub org
44
+ - `epicshop add <repo-name[#ref]> [destination]`: clone a workshop from the
45
+ `epicweb-dev` GitHub org (use `#` to pin a tag, branch, or commit)
46
46
  - `epicshop list`: list your workshops
47
47
  - `epicshop open`: open a workshop in your editor
48
48
  - `epicshop update`: pull the latest workshop changes
package/dist/cli.js CHANGED
@@ -158,7 +158,7 @@ const cli = yargs(args)
158
158
  .command('add [repo-name] [destination]', 'Add a workshop by cloning from epicweb-dev GitHub org', (yargs) => {
159
159
  return yargs
160
160
  .positional('repo-name', {
161
- describe: 'Repository name from epicweb-dev org (optional, shows list if omitted)',
161
+ describe: 'Repository name from epicweb-dev org (optional, shows list if omitted). Use <repo>#<tag|branch|commit> to pin a ref.',
162
162
  type: 'string',
163
163
  })
164
164
  .positional('destination', {
@@ -179,7 +179,8 @@ const cli = yargs(args)
179
179
  .example('$0 add', 'Show available workshops to add')
180
180
  .example('$0 add full-stack-foundations', 'Clone and set up the full-stack-foundations workshop')
181
181
  .example('$0 add web-forms --directory ~/my-workshops', 'Clone workshop to a custom directory')
182
- .example('$0 add react-fundamentals ~/Desktop/react-fundamentals', 'Clone workshop to a specific destination directory');
182
+ .example('$0 add react-fundamentals ~/Desktop/react-fundamentals', 'Clone workshop to a specific destination directory')
183
+ .example('$0 add react-fundamentals#v1.2.0', 'Clone a workshop at a specific tag, branch, or commit');
183
184
  }, async (argv) => {
184
185
  const { add } = await import("./commands/workshops.js");
185
186
  const result = await add({
@@ -17,6 +17,7 @@ export type WorkshopsResult = {
17
17
  };
18
18
  export type AddOptions = {
19
19
  repoName?: string;
20
+ repoRef?: string;
20
21
  directory?: string;
21
22
  destination?: string;
22
23
  silent?: boolean;
@@ -99,6 +99,19 @@ function resolvePathWithTilde(inputPath) {
99
99
  }
100
100
  return trimmed;
101
101
  }
102
+ function parseRepoSpecifier(value) {
103
+ const trimmed = value.trim();
104
+ const hashIndex = trimmed.indexOf('#');
105
+ if (hashIndex === -1) {
106
+ return { repoName: trimmed };
107
+ }
108
+ const repoName = trimmed.slice(0, hashIndex).trim();
109
+ const repoRef = trimmed.slice(hashIndex + 1).trim();
110
+ if (!repoName || !repoRef) {
111
+ throw new Error('Invalid repo specifier. Use the format: <repo-name>#<tag|branch|commit>.');
112
+ }
113
+ return { repoName, repoRef };
114
+ }
102
115
  function getGitHubHeaders() {
103
116
  const headers = {
104
117
  Accept: 'application/vnd.github.v3+json',
@@ -258,7 +271,7 @@ async function checkWorkshopAccess(workshops) {
258
271
  * This handles the actual cloning and setup logic
259
272
  */
260
273
  async function addSingleWorkshop(repoName, options) {
261
- const { silent = false } = options;
274
+ const { silent = false, repoRef } = options;
262
275
  const hasExplicitCloneDestination = Boolean(options.destination?.trim() || options.directory?.trim());
263
276
  // Ensure config is set up first (only when using the managed repos directory)
264
277
  if (!hasExplicitCloneDestination) {
@@ -344,7 +357,8 @@ async function addSingleWorkshop(repoName, options) {
344
357
  }
345
358
  const repoUrl = `https://github.com/${GITHUB_ORG}/${repoName}.git`;
346
359
  if (!silent) {
347
- console.log(chalk.cyan(`๐Ÿ“ฆ Cloning ${repoUrl}...`));
360
+ const refHint = repoRef ? ` (${repoRef})` : '';
361
+ console.log(chalk.cyan(`๐Ÿ“ฆ Cloning ${repoUrl}${refHint}...`));
348
362
  }
349
363
  // Clone the repository
350
364
  const cloneArgs = cloneIntoExistingDir
@@ -362,6 +376,31 @@ async function addSingleWorkshop(repoName, options) {
362
376
  error: cloneResult.error,
363
377
  };
364
378
  }
379
+ if (repoRef) {
380
+ if (!silent) {
381
+ console.log(chalk.cyan(`๐Ÿ”€ Checking out ${repoRef}...`));
382
+ }
383
+ const checkoutResult = await runCommand('git', ['checkout', repoRef], {
384
+ cwd: workshopPath,
385
+ silent,
386
+ });
387
+ if (!checkoutResult.success) {
388
+ if (!silent) {
389
+ console.log(chalk.yellow(`๐Ÿงน Cleaning up cloned directory...`));
390
+ }
391
+ try {
392
+ await fs.promises.rm(workshopPath, { recursive: true, force: true });
393
+ }
394
+ catch {
395
+ // Ignore cleanup errors
396
+ }
397
+ return {
398
+ success: false,
399
+ message: `Failed to check out ${repoRef}: ${checkoutResult.message}`,
400
+ error: checkoutResult.error,
401
+ };
402
+ }
403
+ }
365
404
  const setupResult = await setup({ cwd: workshopPath, silent });
366
405
  if (!setupResult.success) {
367
406
  // Clean up the cloned directory on setup failure
@@ -380,7 +419,8 @@ async function addSingleWorkshop(repoName, options) {
380
419
  error: setupResult.error,
381
420
  };
382
421
  }
383
- const message = `Workshop "${repoName}" cloned successfully to ${workshopPath}`;
422
+ const refSuffix = repoRef ? ` (${repoRef})` : '';
423
+ const message = `Workshop "${repoName}"${refSuffix} cloned successfully to ${workshopPath}`;
384
424
  if (!silent) {
385
425
  console.log(chalk.green(`โœ… ${message}`));
386
426
  }
@@ -392,6 +432,25 @@ async function addSingleWorkshop(repoName, options) {
392
432
  export async function add(options) {
393
433
  const { silent = false } = options;
394
434
  let { repoName } = options;
435
+ let repoRef = options.repoRef;
436
+ if (repoName) {
437
+ try {
438
+ const parsed = parseRepoSpecifier(repoName);
439
+ repoName = parsed.repoName;
440
+ repoRef = parsed.repoRef ?? repoRef;
441
+ }
442
+ catch (error) {
443
+ const message = error instanceof Error ? error.message : String(error);
444
+ if (!silent) {
445
+ console.error(chalk.red(`โŒ ${message}`));
446
+ }
447
+ return {
448
+ success: false,
449
+ message,
450
+ error: error instanceof Error ? error : new Error(message),
451
+ };
452
+ }
453
+ }
395
454
  try {
396
455
  // If no repo name provided, fetch available workshops and let user select
397
456
  if (!repoName) {
@@ -406,6 +465,7 @@ export async function add(options) {
406
465
  hints: [
407
466
  'Provide the repo name: npx epicshop add <repo-name>',
408
467
  'Example: npx epicshop add react-fundamentals',
468
+ 'Pin to a tag/branch/commit: npx epicshop add react-fundamentals#v1.2.0',
409
469
  ],
410
470
  });
411
471
  const spinner = ora('Fetching available workshops...').start();
@@ -667,7 +727,10 @@ export async function add(options) {
667
727
  for (const selectedRepo of selectedRepoNames) {
668
728
  const displayName = getDisplayName(selectedRepo);
669
729
  console.log(chalk.cyan(`๐ŸŽ๏ธ Setting up ${chalk.bold(displayName)}...\n`));
670
- const result = await addSingleWorkshop(selectedRepo, options);
730
+ const result = await addSingleWorkshop(selectedRepo, {
731
+ ...options,
732
+ repoRef,
733
+ });
671
734
  if (result.success) {
672
735
  successCount++;
673
736
  console.log(chalk.green(`๐Ÿ Finished setting up ${chalk.bold(displayName)}\n`));
@@ -706,7 +769,7 @@ export async function add(options) {
706
769
  }
707
770
  const displayName = getDisplayName(repoName);
708
771
  console.log(chalk.cyan(`๐ŸŽ๏ธ Setting up ${chalk.bold(displayName)}...\n`));
709
- const result = await addSingleWorkshop(repoName, options);
772
+ const result = await addSingleWorkshop(repoName, { ...options, repoRef });
710
773
  if (result.success) {
711
774
  console.log(chalk.green(`๐Ÿ Finished setting up ${chalk.bold(displayName)}\n`));
712
775
  console.log(chalk.white('Run:'));
@@ -724,7 +787,7 @@ export async function add(options) {
724
787
  if (!silent) {
725
788
  console.log(chalk.cyan(`๐ŸŽ๏ธ Setting up ${chalk.bold(repoName)}...\n`));
726
789
  }
727
- const result = await addSingleWorkshop(repoName, options);
790
+ const result = await addSingleWorkshop(repoName, { ...options, repoRef });
728
791
  if (result.success && !silent) {
729
792
  console.log(chalk.green(`๐Ÿ Finished setting up ${chalk.bold(repoName)}\n`));
730
793
  console.log(chalk.white('Run:'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epicshop",
3
- "version": "6.74.2",
3
+ "version": "6.75.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -99,7 +99,7 @@
99
99
  "build:watch": "nx watch --projects=epicshop -- nx run \\$NX_PROJECT_NAME:build"
100
100
  },
101
101
  "dependencies": {
102
- "@epic-web/workshop-utils": "6.74.2",
102
+ "@epic-web/workshop-utils": "6.75.0",
103
103
  "@inquirer/prompts": "^8.2.0",
104
104
  "@sentry/node": "^10.36.0",
105
105
  "chalk": "^5.6.2",