epicshop 6.82.0 → 6.83.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/dist/cli.js +23 -3
- package/dist/commands/exercises.d.ts +9 -0
- package/dist/commands/exercises.js +140 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,9 @@ function formatHelp(helpText) {
|
|
|
29
29
|
.replace(/-\w(?=\s|,)/g, (match) => chalk.yellow(match));
|
|
30
30
|
}
|
|
31
31
|
function resolveWorkshopContextCwd(explicitWorkshopDir) {
|
|
32
|
-
const fromFlag = explicitWorkshopDir?.trim()
|
|
32
|
+
const fromFlag = explicitWorkshopDir?.trim()
|
|
33
|
+
? explicitWorkshopDir.trim()
|
|
34
|
+
: undefined;
|
|
33
35
|
if (fromFlag)
|
|
34
36
|
return fromFlag;
|
|
35
37
|
const fromEnv = process.env.EPICSHOP_CONTEXT_CWD;
|
|
@@ -1032,7 +1034,7 @@ const cli = yargs(args)
|
|
|
1032
1034
|
.command('exercises [exercise] [step]', 'List exercises or show exercise details (context-aware)', (yargs) => {
|
|
1033
1035
|
return yargs
|
|
1034
1036
|
.positional('exercise', {
|
|
1035
|
-
describe: 'Exercise number
|
|
1037
|
+
describe: 'Exercise number or "context" to export all workshop context',
|
|
1036
1038
|
type: 'string',
|
|
1037
1039
|
})
|
|
1038
1040
|
.positional('step', {
|
|
@@ -1054,8 +1056,15 @@ const cli = yargs(args)
|
|
|
1054
1056
|
alias: 'w',
|
|
1055
1057
|
type: 'string',
|
|
1056
1058
|
description: 'Path to a workshop directory to use as context (instead of the current working directory)',
|
|
1059
|
+
})
|
|
1060
|
+
.option('output', {
|
|
1061
|
+
alias: 'o',
|
|
1062
|
+
type: 'string',
|
|
1063
|
+
description: 'Write context output to file (for exercises context subcommand)',
|
|
1057
1064
|
})
|
|
1058
1065
|
.example('$0 exercises', 'List all exercises with progress')
|
|
1066
|
+
.example('$0 exercises context', 'Export all workshop context as JSON')
|
|
1067
|
+
.example('$0 exercises context -o context.json', 'Export context to file')
|
|
1059
1068
|
.example('$0 exercises 1', 'Show details for exercise 1')
|
|
1060
1069
|
.example('$0 exercises 1 2', 'Show details for exercise 1 step 2')
|
|
1061
1070
|
.example('$0 exercises --json', 'Output exercises as JSON');
|
|
@@ -1069,11 +1078,22 @@ const cli = yargs(args)
|
|
|
1069
1078
|
const originalCwd = process.cwd();
|
|
1070
1079
|
process.chdir(workshopRoot);
|
|
1071
1080
|
try {
|
|
1081
|
+
// "context" subcommand: export all workshop context
|
|
1082
|
+
if (argv.exercise === 'context') {
|
|
1083
|
+
const { exportContext } = await import("./commands/exercises.js");
|
|
1084
|
+
const result = await exportContext({
|
|
1085
|
+
silent: argv.silent,
|
|
1086
|
+
output: argv.output,
|
|
1087
|
+
});
|
|
1088
|
+
if (!result.success)
|
|
1089
|
+
process.exit(1);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1072
1092
|
const { list, showExercise } = await import("./commands/exercises.js");
|
|
1073
1093
|
if (argv.exercise) {
|
|
1074
1094
|
const exerciseNumber = parseInt(argv.exercise, 10);
|
|
1075
1095
|
if (isNaN(exerciseNumber)) {
|
|
1076
|
-
console.error(chalk.red(`❌ Invalid exercise number: "${argv.exercise}". Expected a number.`));
|
|
1096
|
+
console.error(chalk.red(`❌ Invalid exercise number: "${argv.exercise}". Expected a number or "context".`));
|
|
1077
1097
|
process.exit(1);
|
|
1078
1098
|
}
|
|
1079
1099
|
const stepNumber = argv.step ? parseInt(argv.step, 10) : undefined;
|
|
@@ -13,6 +13,15 @@ export type ExerciseContextOptions = {
|
|
|
13
13
|
silent?: boolean;
|
|
14
14
|
json?: boolean;
|
|
15
15
|
};
|
|
16
|
+
export type ExportContextOptions = {
|
|
17
|
+
silent?: boolean;
|
|
18
|
+
output?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Export all workshop context (instructions, diffs, transcripts) as JSON.
|
|
22
|
+
* Excludes user-specific data (progress, auth, playground state).
|
|
23
|
+
*/
|
|
24
|
+
export declare function exportContext(options?: ExportContextOptions): Promise<ExercisesResult>;
|
|
16
25
|
/**
|
|
17
26
|
* List all exercises with progress
|
|
18
27
|
*/
|
|
@@ -1,4 +1,144 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import chalk from 'chalk';
|
|
3
|
+
/**
|
|
4
|
+
* Export all workshop context (instructions, diffs, transcripts) as JSON.
|
|
5
|
+
* Excludes user-specific data (progress, auth, playground state).
|
|
6
|
+
*/
|
|
7
|
+
export async function exportContext(options = {}) {
|
|
8
|
+
const { silent = false, output: outputPath } = options;
|
|
9
|
+
try {
|
|
10
|
+
const { init, getExercises, getApps, getWorkshopRoot, isProblemApp, isSolutionApp, } = await import('@epic-web/workshop-utils/apps.server');
|
|
11
|
+
const { getWorkshopConfig } = await import('@epic-web/workshop-utils/config.server');
|
|
12
|
+
const { getDiffOutputWithRelativePaths } = await import('@epic-web/workshop-utils/diff.server');
|
|
13
|
+
const { getEpicVideoInfos } = await import('@epic-web/workshop-utils/epic-api.server');
|
|
14
|
+
const fs = await import('node:fs/promises');
|
|
15
|
+
await init();
|
|
16
|
+
const config = getWorkshopConfig();
|
|
17
|
+
const exercises = await getExercises();
|
|
18
|
+
const apps = await getApps();
|
|
19
|
+
// Workshop-level instructions (raw MDX)
|
|
20
|
+
const workshopRoot = getWorkshopRoot();
|
|
21
|
+
const instructionsContent = await safeReadFile(path.join(workshopRoot, 'exercises', 'README.mdx'));
|
|
22
|
+
const finishedInstructionsContent = await safeReadFile(path.join(workshopRoot, 'exercises', 'FINISHED.mdx'));
|
|
23
|
+
const output = {
|
|
24
|
+
workshop: {
|
|
25
|
+
title: config.title,
|
|
26
|
+
subtitle: config.subtitle,
|
|
27
|
+
},
|
|
28
|
+
instructions: { content: instructionsContent ?? null },
|
|
29
|
+
finishedInstructions: { content: finishedInstructionsContent ?? null },
|
|
30
|
+
exercises: [],
|
|
31
|
+
};
|
|
32
|
+
for (const exercise of exercises) {
|
|
33
|
+
const exerciseInstructions = await safeReadFile(path.join(exercise.fullPath, 'README.mdx'));
|
|
34
|
+
const exerciseFinished = await safeReadFile(path.join(exercise.fullPath, 'FINISHED.mdx'));
|
|
35
|
+
const steps = [];
|
|
36
|
+
for (const step of exercise.steps) {
|
|
37
|
+
const stepTitle = step.problem?.title ?? step.solution?.title ?? 'Untitled';
|
|
38
|
+
// Collect all embeds for this step and fetch once
|
|
39
|
+
const problemEmbeds = step.problem?.epicVideoEmbeds ?? [];
|
|
40
|
+
const solutionEmbeds = step.solution?.epicVideoEmbeds ?? [];
|
|
41
|
+
const allStepEmbeds = [
|
|
42
|
+
...new Set([...problemEmbeds, ...solutionEmbeds]),
|
|
43
|
+
];
|
|
44
|
+
const videoInfos = await getEpicVideoInfos(allStepEmbeds);
|
|
45
|
+
// Problem instructions and transcripts
|
|
46
|
+
let problemData = null;
|
|
47
|
+
if (step.problem) {
|
|
48
|
+
const problemInstructions = await safeReadFile(path.join(step.problem.fullPath, 'README.mdx'));
|
|
49
|
+
const problemTranscripts = buildTranscriptsForExport(problemEmbeds, videoInfos);
|
|
50
|
+
problemData = {
|
|
51
|
+
instructions: problemInstructions,
|
|
52
|
+
transcripts: problemTranscripts,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Solution instructions and transcripts
|
|
56
|
+
let solutionData = null;
|
|
57
|
+
if (step.solution) {
|
|
58
|
+
const solutionInstructions = await safeReadFile(path.join(step.solution.fullPath, 'README.mdx'));
|
|
59
|
+
const solutionTranscripts = buildTranscriptsForExport(solutionEmbeds, videoInfos);
|
|
60
|
+
solutionData = {
|
|
61
|
+
instructions: solutionInstructions,
|
|
62
|
+
transcripts: solutionTranscripts,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Diff (problem vs solution)
|
|
66
|
+
let diffOutput = null;
|
|
67
|
+
if (step.problem && step.solution) {
|
|
68
|
+
const problemApp = apps.find((a) => isProblemApp(a) &&
|
|
69
|
+
a.exerciseNumber === exercise.exerciseNumber &&
|
|
70
|
+
a.stepNumber === step.stepNumber);
|
|
71
|
+
const solutionApp = apps.find((a) => isSolutionApp(a) &&
|
|
72
|
+
a.exerciseNumber === exercise.exerciseNumber &&
|
|
73
|
+
a.stepNumber === step.stepNumber);
|
|
74
|
+
if (problemApp && solutionApp) {
|
|
75
|
+
diffOutput =
|
|
76
|
+
await getDiffOutputWithRelativePaths(problemApp, solutionApp) || null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
steps.push({
|
|
80
|
+
stepNumber: step.stepNumber,
|
|
81
|
+
title: stepTitle,
|
|
82
|
+
problem: problemData,
|
|
83
|
+
solution: solutionData,
|
|
84
|
+
diff: diffOutput,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
output.exercises.push({
|
|
88
|
+
exerciseNumber: exercise.exerciseNumber,
|
|
89
|
+
title: exercise.title,
|
|
90
|
+
instructions: { content: exerciseInstructions },
|
|
91
|
+
finishedInstructions: { content: exerciseFinished },
|
|
92
|
+
steps,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const jsonOutput = JSON.stringify(output, null, 2);
|
|
96
|
+
if (outputPath) {
|
|
97
|
+
await fs.writeFile(outputPath, jsonOutput, 'utf-8');
|
|
98
|
+
if (!silent) {
|
|
99
|
+
console.error(chalk.green(`✓ Context written to ${outputPath}`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log(jsonOutput);
|
|
104
|
+
}
|
|
105
|
+
return { success: true };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
if (!silent) {
|
|
110
|
+
console.error(chalk.red(`❌ Failed to export context: ${message}`));
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
message,
|
|
115
|
+
error: error instanceof Error ? error : new Error(message),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function buildTranscriptsForExport(embeds, videoInfos) {
|
|
120
|
+
if (!embeds.length)
|
|
121
|
+
return [];
|
|
122
|
+
return embeds.map((embed) => {
|
|
123
|
+
const info = videoInfos[embed];
|
|
124
|
+
if (info && info.transcript) {
|
|
125
|
+
return { embed, transcript: info.transcript, status: 'success' };
|
|
126
|
+
}
|
|
127
|
+
if (info && info.status === 'error') {
|
|
128
|
+
const err = info;
|
|
129
|
+
return {
|
|
130
|
+
embed,
|
|
131
|
+
status: 'error',
|
|
132
|
+
message: err.message ?? err.type ?? 'Unknown error',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
embed,
|
|
137
|
+
status: 'error',
|
|
138
|
+
message: 'No transcript found',
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
2
142
|
/**
|
|
3
143
|
* List all exercises with progress
|
|
4
144
|
*/
|
|
@@ -121,7 +261,6 @@ export async function showExercise(options = {}) {
|
|
|
121
261
|
try {
|
|
122
262
|
const { init, getExercise, getPlaygroundApp, extractNumbersAndTypeFromAppNameOrPath, } = await import('@epic-web/workshop-utils/apps.server');
|
|
123
263
|
const { getProgress } = await import('@epic-web/workshop-utils/epic-api.server');
|
|
124
|
-
const path = await import('node:path');
|
|
125
264
|
await init();
|
|
126
265
|
const playgroundApp = await getPlaygroundApp();
|
|
127
266
|
let targetExercise = exerciseNumber;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epicshop",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.83.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.
|
|
102
|
+
"@epic-web/workshop-utils": "6.83.0",
|
|
103
103
|
"@inquirer/prompts": "^8.2.0",
|
|
104
104
|
"@sentry/node": "^10.38.0",
|
|
105
105
|
"chalk": "^5.6.2",
|