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 +2 -2
- package/dist/cli.js +14 -20
- package/dist/commands/diff.d.ts +6 -0
- package/dist/commands/diff.js +115 -0
- package/dist/commands/playground.js +73 -50
- package/dist/commands/workshops.d.ts +1 -0
- package/dist/commands/workshops.js +69 -6
- package/package.json +2 -2
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', '
|
|
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,
|
|
801
|
+
// If no specific target, prompt for selection
|
|
801
802
|
if (!exerciseNumber && !stepNumber && !type) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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,
|
|
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', '
|
|
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 {
|
|
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
|
|
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 {
|
|
1556
|
-
const result = await
|
|
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
|
}
|
package/dist/commands/diff.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/commands/diff.js
CHANGED
|
@@ -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,
|
|
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
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
258
|
+
return orderedChoices;
|
|
236
259
|
return matchSorter(choices, input, {
|
|
237
260
|
keys: ['name', 'value.name'],
|
|
238
261
|
});
|
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
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",
|