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 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() ? explicitWorkshopDir.trim() : undefined;
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 to show details for (e.g., 1 or 01)',
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.82.0",
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.0",
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",