epicshop 6.82.1 → 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 CHANGED
@@ -1034,7 +1034,7 @@ const cli = yargs(args)
1034
1034
  .command('exercises [exercise] [step]', 'List exercises or show exercise details (context-aware)', (yargs) => {
1035
1035
  return yargs
1036
1036
  .positional('exercise', {
1037
- describe: 'Exercise number to show details for (e.g., 1 or 01)',
1037
+ describe: 'Exercise number or "context" to export all workshop context',
1038
1038
  type: 'string',
1039
1039
  })
1040
1040
  .positional('step', {
@@ -1056,8 +1056,15 @@ const cli = yargs(args)
1056
1056
  alias: 'w',
1057
1057
  type: 'string',
1058
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)',
1059
1064
  })
1060
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')
1061
1068
  .example('$0 exercises 1', 'Show details for exercise 1')
1062
1069
  .example('$0 exercises 1 2', 'Show details for exercise 1 step 2')
1063
1070
  .example('$0 exercises --json', 'Output exercises as JSON');
@@ -1071,11 +1078,22 @@ const cli = yargs(args)
1071
1078
  const originalCwd = process.cwd();
1072
1079
  process.chdir(workshopRoot);
1073
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
+ }
1074
1092
  const { list, showExercise } = await import("./commands/exercises.js");
1075
1093
  if (argv.exercise) {
1076
1094
  const exerciseNumber = parseInt(argv.exercise, 10);
1077
1095
  if (isNaN(exerciseNumber)) {
1078
- 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".`));
1079
1097
  process.exit(1);
1080
1098
  }
1081
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.82.1",
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.82.1",
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",