epicshop 6.82.1 → 6.84.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 +24 -2
- package/dist/commands/exercises.d.ts +9 -0
- package/dist/commands/exercises.js +141 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import '@epic-web/workshop-utils/init-env';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import { matchSorter } from 'match-sorter';
|
|
@@ -1034,7 +1035,7 @@ const cli = yargs(args)
|
|
|
1034
1035
|
.command('exercises [exercise] [step]', 'List exercises or show exercise details (context-aware)', (yargs) => {
|
|
1035
1036
|
return yargs
|
|
1036
1037
|
.positional('exercise', {
|
|
1037
|
-
describe: 'Exercise number
|
|
1038
|
+
describe: 'Exercise number or "context" to export all workshop context',
|
|
1038
1039
|
type: 'string',
|
|
1039
1040
|
})
|
|
1040
1041
|
.positional('step', {
|
|
@@ -1056,8 +1057,15 @@ const cli = yargs(args)
|
|
|
1056
1057
|
alias: 'w',
|
|
1057
1058
|
type: 'string',
|
|
1058
1059
|
description: 'Path to a workshop directory to use as context (instead of the current working directory)',
|
|
1060
|
+
})
|
|
1061
|
+
.option('output', {
|
|
1062
|
+
alias: 'o',
|
|
1063
|
+
type: 'string',
|
|
1064
|
+
description: 'Write context output to file (for exercises context subcommand)',
|
|
1059
1065
|
})
|
|
1060
1066
|
.example('$0 exercises', 'List all exercises with progress')
|
|
1067
|
+
.example('$0 exercises context', 'Export all workshop context as JSON')
|
|
1068
|
+
.example('$0 exercises context -o context.json', 'Export context to file')
|
|
1061
1069
|
.example('$0 exercises 1', 'Show details for exercise 1')
|
|
1062
1070
|
.example('$0 exercises 1 2', 'Show details for exercise 1 step 2')
|
|
1063
1071
|
.example('$0 exercises --json', 'Output exercises as JSON');
|
|
@@ -1071,11 +1079,25 @@ const cli = yargs(args)
|
|
|
1071
1079
|
const originalCwd = process.cwd();
|
|
1072
1080
|
process.chdir(workshopRoot);
|
|
1073
1081
|
try {
|
|
1082
|
+
// "context" subcommand: export all workshop context
|
|
1083
|
+
if (argv.exercise === 'context') {
|
|
1084
|
+
const { exportContext } = await import("./commands/exercises.js");
|
|
1085
|
+
const outputPath = argv.output
|
|
1086
|
+
? path.resolve(originalCwd, argv.output)
|
|
1087
|
+
: undefined;
|
|
1088
|
+
const result = await exportContext({
|
|
1089
|
+
silent: argv.silent,
|
|
1090
|
+
output: outputPath,
|
|
1091
|
+
});
|
|
1092
|
+
if (!result.success)
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1074
1096
|
const { list, showExercise } = await import("./commands/exercises.js");
|
|
1075
1097
|
if (argv.exercise) {
|
|
1076
1098
|
const exerciseNumber = parseInt(argv.exercise, 10);
|
|
1077
1099
|
if (isNaN(exerciseNumber)) {
|
|
1078
|
-
console.error(chalk.red(`❌ Invalid exercise number: "${argv.exercise}". Expected a number.`));
|
|
1100
|
+
console.error(chalk.red(`❌ Invalid exercise number: "${argv.exercise}". Expected a number or "context".`));
|
|
1079
1101
|
process.exit(1);
|
|
1080
1102
|
}
|
|
1081
1103
|
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,145 @@
|
|
|
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)) ||
|
|
77
|
+
null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
steps.push({
|
|
81
|
+
stepNumber: step.stepNumber,
|
|
82
|
+
title: stepTitle,
|
|
83
|
+
problem: problemData,
|
|
84
|
+
solution: solutionData,
|
|
85
|
+
diff: diffOutput,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
output.exercises.push({
|
|
89
|
+
exerciseNumber: exercise.exerciseNumber,
|
|
90
|
+
title: exercise.title,
|
|
91
|
+
instructions: { content: exerciseInstructions },
|
|
92
|
+
finishedInstructions: { content: exerciseFinished },
|
|
93
|
+
steps,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const jsonOutput = JSON.stringify(output, null, 2);
|
|
97
|
+
if (outputPath) {
|
|
98
|
+
await fs.writeFile(outputPath, jsonOutput, 'utf-8');
|
|
99
|
+
if (!silent) {
|
|
100
|
+
console.error(chalk.green(`✓ Context written to ${outputPath}`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(jsonOutput);
|
|
105
|
+
}
|
|
106
|
+
return { success: true };
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
if (!silent) {
|
|
111
|
+
console.error(chalk.red(`❌ Failed to export context: ${message}`));
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
message,
|
|
116
|
+
error: error instanceof Error ? error : new Error(message),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function buildTranscriptsForExport(embeds, videoInfos) {
|
|
121
|
+
if (!embeds.length)
|
|
122
|
+
return [];
|
|
123
|
+
return embeds.map((embed) => {
|
|
124
|
+
const info = videoInfos[embed];
|
|
125
|
+
if (info && info.transcript) {
|
|
126
|
+
return { embed, transcript: info.transcript, status: 'success' };
|
|
127
|
+
}
|
|
128
|
+
if (info && info.status === 'error') {
|
|
129
|
+
const err = info;
|
|
130
|
+
return {
|
|
131
|
+
embed,
|
|
132
|
+
status: 'error',
|
|
133
|
+
message: err.message ?? err.type ?? 'Unknown error',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
embed,
|
|
138
|
+
status: 'error',
|
|
139
|
+
message: 'No transcript found',
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
2
143
|
/**
|
|
3
144
|
* List all exercises with progress
|
|
4
145
|
*/
|
|
@@ -121,7 +262,6 @@ export async function showExercise(options = {}) {
|
|
|
121
262
|
try {
|
|
122
263
|
const { init, getExercise, getPlaygroundApp, extractNumbersAndTypeFromAppNameOrPath, } = await import('@epic-web/workshop-utils/apps.server');
|
|
123
264
|
const { getProgress } = await import('@epic-web/workshop-utils/epic-api.server');
|
|
124
|
-
const path = await import('node:path');
|
|
125
265
|
await init();
|
|
126
266
|
const playgroundApp = await getPlaygroundApp();
|
|
127
267
|
let targetExercise = exerciseNumber;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epicshop",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.84.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.84.0",
|
|
103
103
|
"@inquirer/prompts": "^8.2.0",
|
|
104
104
|
"@sentry/node": "^10.38.0",
|
|
105
105
|
"chalk": "^5.6.2",
|