epicshop 6.74.3 โ†’ 6.75.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/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({
@@ -758,7 +759,7 @@ const cli = yargs(args)
758
759
  })
759
760
  .example('$0 playground', 'Show current playground status')
760
761
  .example('$0 playground show', 'Show current playground status')
761
- .example('$0 playground set', 'Set playground (auto-detect next)')
762
+ .example('$0 playground set', 'Select a playground app to set (defaults to next step)')
762
763
  .example('$0 playground set 1.2.problem', 'Set to specific step')
763
764
  .example('$0 playground set --exercise 1 --step 2', 'Set with options')
764
765
  .example('$0 playground saved', 'Interactively select a saved playground')
@@ -797,18 +798,11 @@ const cli = yargs(args)
797
798
  stepNumber = parsed.stepNumber ?? stepNumber;
798
799
  type = parsed.type ?? type;
799
800
  }
800
- // If no specific target, check if we should show interactive or auto
801
+ // If no specific target, prompt for selection
801
802
  if (!exerciseNumber && !stepNumber && !type) {
802
- // Auto-detect next step or show interactive
803
- const result = await set({ silent: argv.silent });
804
- if (!result.success) {
805
- // If auto-detect fails, try interactive
806
- const interactiveResult = await selectAndSet({
807
- silent: argv.silent,
808
- });
809
- if (!interactiveResult.success)
810
- process.exit(1);
811
- }
803
+ const result = await selectAndSet({ silent: argv.silent });
804
+ if (!result.success)
805
+ process.exit(1);
812
806
  }
813
807
  else {
814
808
  const result = await set({
@@ -947,7 +941,7 @@ const cli = yargs(args)
947
941
  .command('diff [app1] [app2]', 'Show differences between apps or playground vs solution (context-aware)', (yargs) => {
948
942
  return yargs
949
943
  .positional('app1', {
950
- describe: 'First app identifier (e.g., 01.02.problem). If omitted, shows playground vs solution.',
944
+ describe: 'First app identifier (e.g., 01.02.problem). If omitted, prompts with playground vs solution as the default.',
951
945
  type: 'string',
952
946
  })
953
947
  .positional('app2', {
@@ -960,7 +954,7 @@ const cli = yargs(args)
960
954
  description: 'Run without output logs',
961
955
  default: false,
962
956
  })
963
- .example('$0 diff', 'Show diff between playground and solution')
957
+ .example('$0 diff', 'Select apps to diff (defaults to playground vs solution)')
964
958
  .example('$0 diff 01.02.problem 01.02.solution', 'Show diff between two apps');
965
959
  }, async (argv) => {
966
960
  const { findWorkshopRoot } = await import("./commands/workshops.js");
@@ -972,7 +966,7 @@ const cli = yargs(args)
972
966
  const originalCwd = process.cwd();
973
967
  process.chdir(workshopRoot);
974
968
  try {
975
- const { showProgressDiff, showDiffBetweenApps } = await import("./commands/diff.js");
969
+ const { showDiffBetweenApps, selectAndShowDiff } = await import("./commands/diff.js");
976
970
  if (argv.app1 && argv.app2) {
977
971
  const result = await showDiffBetweenApps({
978
972
  app1: argv.app1,
@@ -987,7 +981,7 @@ const cli = yargs(args)
987
981
  process.exit(1);
988
982
  }
989
983
  else {
990
- const result = await showProgressDiff({ silent: argv.silent });
984
+ const result = await selectAndShowDiff({ silent: argv.silent });
991
985
  if (!result.success)
992
986
  process.exit(1);
993
987
  }
@@ -1552,8 +1546,8 @@ try {
1552
1546
  const originalCwd = process.cwd();
1553
1547
  process.chdir(wsRoot);
1554
1548
  try {
1555
- const { showProgressDiff } = await import("./commands/diff.js");
1556
- const result = await showProgressDiff({});
1549
+ const { selectAndShowDiff } = await import("./commands/diff.js");
1550
+ const result = await selectAndShowDiff({});
1557
1551
  if (!result.success)
1558
1552
  process.exit(1);
1559
1553
  }
@@ -15,6 +15,12 @@ export type DiffOptions = {
15
15
  export declare function showProgressDiff(options?: {
16
16
  silent?: boolean;
17
17
  }): Promise<DiffResult>;
18
+ /**
19
+ * Interactive diff selection between two apps
20
+ */
21
+ export declare function selectAndShowDiff(options?: {
22
+ silent?: boolean;
23
+ }): Promise<DiffResult>;
18
24
  /**
19
25
  * Show diff between two specific apps
20
26
  */
@@ -1,4 +1,6 @@
1
1
  import chalk from 'chalk';
2
+ import { matchSorter } from 'match-sorter';
3
+ import { assertCanPrompt } from "../utils/cli-runtime.js";
2
4
  /**
3
5
  * Show diff between the current playground and its solution
4
6
  */
@@ -47,6 +49,119 @@ export async function showProgressDiff(options = {}) {
47
49
  };
48
50
  }
49
51
  }
52
+ /**
53
+ * Interactive diff selection between two apps
54
+ */
55
+ export async function selectAndShowDiff(options = {}) {
56
+ const { silent = false } = options;
57
+ try {
58
+ const { findSolutionDir, getAppDisplayName, getApps, getFullPathFromAppName, init, isPlaygroundApp, } = await import('@epic-web/workshop-utils/apps.server');
59
+ const { getDiffOutputWithRelativePaths } = await import('@epic-web/workshop-utils/diff.server');
60
+ await init();
61
+ assertCanPrompt({
62
+ reason: 'select apps to diff',
63
+ hints: [
64
+ 'Provide the apps directly: npx epicshop diff 01.02.problem 01.02.solution',
65
+ ],
66
+ });
67
+ const { search } = await import('@inquirer/prompts');
68
+ const apps = await getApps();
69
+ if (apps.length < 2) {
70
+ throw new Error('Need at least two apps to diff');
71
+ }
72
+ const playgroundApp = apps.find(isPlaygroundApp);
73
+ let defaultFirstApp = playgroundApp;
74
+ let defaultSecondApp = undefined;
75
+ if (playgroundApp) {
76
+ try {
77
+ const solutionDir = await findSolutionDir({
78
+ fullPath: await getFullPathFromAppName(playgroundApp.appName),
79
+ });
80
+ defaultSecondApp = apps.find((app) => app.fullPath === solutionDir);
81
+ }
82
+ catch {
83
+ // Ignore default selection errors; still allow interactive selection.
84
+ }
85
+ }
86
+ const choices = apps.map((app) => ({
87
+ name: getAppDisplayName(app, apps),
88
+ value: app,
89
+ description: app.fullPath,
90
+ }));
91
+ const orderChoices = (allChoices, preferredName) => {
92
+ if (!preferredName)
93
+ return allChoices;
94
+ const preferred = allChoices.find((choice) => choice.value.name === preferredName);
95
+ if (!preferred)
96
+ return allChoices;
97
+ return [
98
+ preferred,
99
+ ...allChoices.filter((choice) => choice.value.name !== preferredName),
100
+ ];
101
+ };
102
+ const selectApp = async (message, options = {}) => {
103
+ const { excludeName, preferredName } = options;
104
+ const availableChoices = excludeName
105
+ ? choices.filter((choice) => choice.value.name !== excludeName)
106
+ : choices;
107
+ const orderedChoices = orderChoices(availableChoices, preferredName);
108
+ return search({
109
+ message,
110
+ source: async (input) => {
111
+ if (!input)
112
+ return orderedChoices;
113
+ return matchSorter(availableChoices, input, {
114
+ keys: ['name', 'value.name', 'description'],
115
+ });
116
+ },
117
+ });
118
+ };
119
+ try {
120
+ const app1 = await selectApp('Select the first app to diff:', {
121
+ preferredName: defaultFirstApp?.name,
122
+ });
123
+ const app2 = await selectApp('Select the second app to diff:', {
124
+ excludeName: app1.name,
125
+ preferredName: defaultSecondApp?.name === app1.name
126
+ ? undefined
127
+ : defaultSecondApp?.name,
128
+ });
129
+ const diffCode = await getDiffOutputWithRelativePaths(app1, app2);
130
+ const app1Label = getAppDisplayName(app1, apps);
131
+ const app2Label = getAppDisplayName(app2, apps);
132
+ if (!diffCode) {
133
+ if (!silent) {
134
+ console.log(chalk.green('โœ“ No differences between the apps!'));
135
+ }
136
+ return { success: true, message: 'No changes', diff: '' };
137
+ }
138
+ if (!silent) {
139
+ console.log(chalk.bold.cyan(`\n๐Ÿ“‹ Diff: ${app1Label} vs ${app2Label}\n`));
140
+ console.log(chalk.gray(`Lines starting with - are in ${app1Label} but not ${app2Label}`));
141
+ console.log(chalk.gray(`Lines starting with + are in ${app2Label} but not ${app1Label}\n`));
142
+ console.log(formatDiff(diffCode));
143
+ }
144
+ return { success: true, diff: diffCode };
145
+ }
146
+ catch (error) {
147
+ if (error.message === 'USER_QUIT') {
148
+ return { success: false, message: 'Cancelled' };
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+ catch (error) {
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ if (!silent) {
156
+ console.error(chalk.red(`โŒ Failed to generate diff: ${message}`));
157
+ }
158
+ return {
159
+ success: false,
160
+ message,
161
+ error: error instanceof Error ? error : new Error(message),
162
+ };
163
+ }
164
+ }
50
165
  /**
51
166
  * Show diff between two specific apps
52
167
  */
@@ -1,6 +1,48 @@
1
1
  import chalk from 'chalk';
2
2
  import { matchSorter } from 'match-sorter';
3
3
  import { assertCanPrompt } from "../utils/cli-runtime.js";
4
+ /**
5
+ * Score progress items for sorting to find the next incomplete step
6
+ */
7
+ function scoreProgress(a) {
8
+ if (a.type === 'workshop-instructions')
9
+ return 0;
10
+ if (a.type === 'workshop-finished')
11
+ return 10000;
12
+ if (a.type === 'instructions')
13
+ return a.exerciseNumber * 100;
14
+ if (a.type === 'step')
15
+ return a.exerciseNumber * 100 + a.stepNumber;
16
+ if (a.type === 'finished')
17
+ return a.exerciseNumber * 100 + 100;
18
+ if (a.type === 'unknown')
19
+ return 100000;
20
+ return -1;
21
+ }
22
+ /**
23
+ * Find the default playground app based on progress or next step
24
+ */
25
+ async function findDefaultPlaygroundApp(params) {
26
+ const { exerciseStepApps, progress, authInfo, getPlaygroundAppName, isProblemApp, } = params;
27
+ // If authenticated, try to find the next incomplete step based on progress
28
+ if (authInfo) {
29
+ const sortedProgress = [...progress].sort((a, b) => {
30
+ return scoreProgress(a) - scoreProgress(b);
31
+ });
32
+ const nextProgress = sortedProgress.find((p) => !p.epicCompletedAt);
33
+ if (nextProgress && nextProgress.type === 'step') {
34
+ const app = exerciseStepApps.find((a) => a.exerciseNumber === nextProgress.exerciseNumber &&
35
+ a.stepNumber === nextProgress.stepNumber &&
36
+ a.type === 'problem');
37
+ if (app)
38
+ return app;
39
+ }
40
+ }
41
+ // Otherwise, find the next step from current
42
+ const playgroundAppName = await getPlaygroundAppName();
43
+ const currentIndex = exerciseStepApps.findIndex((a) => a.name === playgroundAppName);
44
+ return exerciseStepApps.slice(currentIndex + 1).find(isProblemApp);
45
+ }
4
46
  async function getSavedPlaygroundEntries() {
5
47
  const { init, getApps, getAppDisplayName, getSavedPlaygrounds } = await import('@epic-web/workshop-utils/apps.server');
6
48
  const { getPreferences } = await import('@epic-web/workshop-utils/db.server');
@@ -84,62 +126,23 @@ export async function show(options = {}) {
84
126
  export async function set(options = {}) {
85
127
  const { exerciseNumber, stepNumber, type, silent = false } = options;
86
128
  try {
87
- const { init, getApps, getExerciseApp, getPlaygroundAppName, isExerciseStepApp, isProblemApp, setPlayground, } = await import('@epic-web/workshop-utils/apps.server');
129
+ const { init, getApps, getPlaygroundAppName, isExerciseStepApp, isProblemApp, setPlayground, } = await import('@epic-web/workshop-utils/apps.server');
88
130
  const { getAuthInfo } = await import('@epic-web/workshop-utils/db.server');
89
131
  const { getProgress } = await import('@epic-web/workshop-utils/epic-api.server');
90
132
  await init();
91
133
  const authInfo = await getAuthInfo();
92
134
  // If no arguments provided, try to set based on progress or next step
93
135
  if (!exerciseNumber && !stepNumber && !type) {
94
- if (authInfo) {
95
- // Try to find the next incomplete step based on progress
96
- const progress = await getProgress();
97
- const scoreProgress = (a) => {
98
- if (a.type === 'workshop-instructions')
99
- return 0;
100
- if (a.type === 'workshop-finished')
101
- return 10000;
102
- if (a.type === 'instructions')
103
- return a.exerciseNumber * 100;
104
- if (a.type === 'step')
105
- return a.exerciseNumber * 100 + a.stepNumber;
106
- if (a.type === 'finished')
107
- return a.exerciseNumber * 100 + 100;
108
- if (a.type === 'unknown')
109
- return 100000;
110
- return -1;
111
- };
112
- const sortedProgress = progress.sort((a, b) => {
113
- return scoreProgress(a) - scoreProgress(b);
114
- });
115
- const nextProgress = sortedProgress.find((p) => !p.epicCompletedAt);
116
- if (nextProgress && nextProgress.type === 'step') {
117
- const exerciseApp = await getExerciseApp({
118
- exerciseNumber: nextProgress.exerciseNumber.toString(),
119
- stepNumber: nextProgress.stepNumber.toString(),
120
- type: 'problem',
121
- });
122
- if (!exerciseApp) {
123
- throw new Error('No exercise app found');
124
- }
125
- await setPlayground(exerciseApp.fullPath);
126
- if (!silent) {
127
- console.log(chalk.green(`โœ… Playground set to ${exerciseApp.exerciseNumber}.${exerciseApp.stepNumber}.${exerciseApp.type}`));
128
- }
129
- return {
130
- success: true,
131
- message: `Playground set to ${exerciseApp.exerciseNumber}.${exerciseApp.stepNumber}.${exerciseApp.type}`,
132
- };
133
- }
134
- }
135
- // Otherwise, find the next step from current
136
136
  const apps = await getApps();
137
137
  const exerciseStepApps = apps.filter(isExerciseStepApp);
138
- const playgroundAppName = await getPlaygroundAppName();
139
- const currentIndex = exerciseStepApps.findIndex((a) => a.name === playgroundAppName);
140
- const desiredApp = exerciseStepApps
141
- .slice(currentIndex + 1)
142
- .find(isProblemApp);
138
+ const progress = await getProgress();
139
+ const desiredApp = await findDefaultPlaygroundApp({
140
+ exerciseStepApps,
141
+ progress,
142
+ authInfo,
143
+ getPlaygroundAppName,
144
+ isProblemApp,
145
+ });
143
146
  if (!desiredApp) {
144
147
  const message = 'No next problem app found. You may be at the end of the workshop!';
145
148
  if (!silent) {
@@ -199,7 +202,8 @@ export async function set(options = {}) {
199
202
  export async function selectAndSet(options = {}) {
200
203
  const { silent = false } = options;
201
204
  try {
202
- const { init, getApps, isExerciseStepApp, setPlayground } = await import('@epic-web/workshop-utils/apps.server');
205
+ const { init, getApps, getPlaygroundAppName, isExerciseStepApp, isProblemApp, setPlayground, } = await import('@epic-web/workshop-utils/apps.server');
206
+ const { getAuthInfo } = await import('@epic-web/workshop-utils/db.server');
203
207
  const { getProgress } = await import('@epic-web/workshop-utils/epic-api.server');
204
208
  await init();
205
209
  assertCanPrompt({
@@ -213,6 +217,14 @@ export async function selectAndSet(options = {}) {
213
217
  const apps = await getApps();
214
218
  const exerciseStepApps = apps.filter(isExerciseStepApp);
215
219
  const progress = await getProgress();
220
+ const authInfo = await getAuthInfo();
221
+ const defaultApp = await findDefaultPlaygroundApp({
222
+ exerciseStepApps,
223
+ progress,
224
+ authInfo,
225
+ getPlaygroundAppName,
226
+ isProblemApp,
227
+ });
216
228
  const choices = exerciseStepApps.map((app) => {
217
229
  const ex = app.exerciseNumber.toString().padStart(2, '0');
218
230
  const st = app.stepNumber.toString().padStart(2, '0');
@@ -227,12 +239,23 @@ export async function selectAndSet(options = {}) {
227
239
  description: app.fullPath,
228
240
  };
229
241
  });
242
+ const orderedChoices = defaultApp
243
+ ? (() => {
244
+ const preferred = choices.find((choice) => choice.value.name === defaultApp?.name);
245
+ if (!preferred)
246
+ return choices;
247
+ return [
248
+ preferred,
249
+ ...choices.filter((choice) => choice.value.name !== defaultApp?.name),
250
+ ];
251
+ })()
252
+ : choices;
230
253
  try {
231
254
  const selectedApp = await search({
232
255
  message: 'Select an exercise step to set as playground:',
233
256
  source: async (input) => {
234
257
  if (!input)
235
- return choices;
258
+ return orderedChoices;
236
259
  return matchSorter(choices, input, {
237
260
  keys: ['name', 'value.name'],
238
261
  });
@@ -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.3",
3
+ "version": "6.75.1",
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.3",
102
+ "@epic-web/workshop-utils": "6.75.1",
103
103
  "@inquirer/prompts": "^8.2.0",
104
104
  "@sentry/node": "^10.36.0",
105
105
  "chalk": "^5.6.2",