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 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 to show details for (e.g., 1 or 01)',
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.82.1",
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.82.1",
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",