@learnpack/learnpack 5.0.270 → 5.0.274

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.
Files changed (77) hide show
  1. package/README.md +409 -409
  2. package/lib/commands/audit.js +15 -15
  3. package/lib/commands/breakToken.js +19 -19
  4. package/lib/commands/clean.js +3 -3
  5. package/lib/commands/init.js +41 -41
  6. package/lib/commands/logout.js +3 -3
  7. package/lib/commands/serve.js +48 -20
  8. package/lib/creatorDist/assets/{index-CQXTTbaZ.js → index-BfLyIQVh.js} +11535 -11409
  9. package/lib/creatorDist/assets/{index-B4khtb0r.css → index-C39zeF3W.css} +3 -3
  10. package/lib/creatorDist/index.html +2 -2
  11. package/lib/managers/config/index.js +77 -77
  12. package/lib/models/creator.d.ts +1 -0
  13. package/lib/utils/api.js +1 -0
  14. package/lib/utils/creatorUtilities.js +14 -14
  15. package/package.json +1 -1
  16. package/src/commands/audit.ts +487 -487
  17. package/src/commands/breakToken.ts +67 -67
  18. package/src/commands/clean.ts +30 -30
  19. package/src/commands/init.ts +650 -650
  20. package/src/commands/logout.ts +38 -38
  21. package/src/commands/publish.ts +522 -522
  22. package/src/commands/serve.ts +64 -33
  23. package/src/commands/start.ts +333 -333
  24. package/src/commands/translate.ts +123 -123
  25. package/src/creator/README.md +54 -54
  26. package/src/creator/eslint.config.js +28 -28
  27. package/src/creator/src/components/syllabus/ContentIndex.tsx +1 -1
  28. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +3 -1
  29. package/src/creator/src/i18n.ts +28 -28
  30. package/src/creator/src/index.css +217 -217
  31. package/src/creator/src/locales/en.json +1 -0
  32. package/src/creator/src/locales/es.json +1 -0
  33. package/src/creator/src/utils/configTypes.ts +122 -122
  34. package/src/creator/src/utils/constants.ts +13 -13
  35. package/src/creator/src/utils/creatorUtils.ts +46 -46
  36. package/src/creator/src/utils/eventBus.ts +2 -2
  37. package/src/creator/src/utils/lib.ts +468 -468
  38. package/src/creator/src/utils/rigo.ts +26 -26
  39. package/src/creator/src/utils/socket.ts +61 -61
  40. package/src/creator/src/utils/store.ts +222 -222
  41. package/src/creator/src/vite-env.d.ts +1 -1
  42. package/src/creator/vite.config.ts +13 -13
  43. package/src/creatorDist/assets/{index-CQXTTbaZ.js → index-BfLyIQVh.js} +11535 -11409
  44. package/src/creatorDist/assets/{index-B4khtb0r.css → index-C39zeF3W.css} +3 -3
  45. package/src/creatorDist/index.html +2 -2
  46. package/src/managers/config/defaults.ts +49 -49
  47. package/src/managers/config/exercise.ts +364 -364
  48. package/src/managers/config/index.ts +775 -775
  49. package/src/managers/file.ts +236 -236
  50. package/src/managers/server/routes.ts +554 -554
  51. package/src/managers/session.ts +182 -182
  52. package/src/managers/telemetry.ts +188 -188
  53. package/src/models/action.ts +13 -13
  54. package/src/models/config-manager.ts +28 -28
  55. package/src/models/config.ts +106 -106
  56. package/src/models/creator.ts +40 -39
  57. package/src/models/exercise-obj.ts +30 -30
  58. package/src/models/session.ts +39 -39
  59. package/src/models/socket.ts +61 -61
  60. package/src/models/status.ts +16 -16
  61. package/src/ui/_app/app.css +1 -1
  62. package/src/ui/_app/app.js +435 -414
  63. package/src/ui/_app/learnpack.svg +7 -7
  64. package/src/ui/app.tar.gz +0 -0
  65. package/src/utils/BaseCommand.ts +56 -56
  66. package/src/utils/api.ts +31 -30
  67. package/src/utils/audit.ts +392 -392
  68. package/src/utils/checkNotInstalled.ts +267 -267
  69. package/src/utils/configBuilder.ts +82 -82
  70. package/src/utils/convertCreds.js +34 -34
  71. package/src/utils/creatorUtilities.ts +504 -504
  72. package/src/utils/incrementVersion.js +74 -74
  73. package/src/utils/misc.ts +58 -58
  74. package/src/utils/rigoActions.ts +500 -500
  75. package/src/utils/sidebarGenerator.ts +195 -195
  76. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  77. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
@@ -369,21 +369,21 @@ class AuditCommand extends SessionCommand_1.default {
369
369
  }
370
370
  }
371
371
  }
372
- AuditCommand.description = `learnpack audit is the command in charge of creating an auditory of the repository
373
- ...
374
- learnpack audit checks for the following information in a repository:
375
- 1. The configuration object has slug, repository and description. (Error)
376
- 2. The command learnpack clean has been run. (Error)
377
- 3. If a markdown or test file doesn't have any content. (Error)
378
- 4. The links are accessing to valid servers. (Error)
379
- 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)
380
- 6. The external images are working (If they are pointing to a valid server). (Error)
381
- 7. The exercises directory names are valid. (Error)
382
- 8. If an exercise doesn't have a README file. (Error)
383
- 9. The exercises array (Of the config file) has content. (Error)
384
- 10. The exercses have the same translations. (Warning)
385
- 11. The .gitignore file exists. (Warning)
386
- 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
372
+ AuditCommand.description = `learnpack audit is the command in charge of creating an auditory of the repository
373
+ ...
374
+ learnpack audit checks for the following information in a repository:
375
+ 1. The configuration object has slug, repository and description. (Error)
376
+ 2. The command learnpack clean has been run. (Error)
377
+ 3. If a markdown or test file doesn't have any content. (Error)
378
+ 4. The links are accessing to valid servers. (Error)
379
+ 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)
380
+ 6. The external images are working (If they are pointing to a valid server). (Error)
381
+ 7. The exercises directory names are valid. (Error)
382
+ 8. If an exercise doesn't have a README file. (Error)
383
+ 9. The exercises array (Of the config file) has content. (Error)
384
+ 10. The exercses have the same translations. (Warning)
385
+ 11. The .gitignore file exists. (Warning)
386
+ 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
387
387
  `;
388
388
  AuditCommand.flags = {
389
389
  strict: command_1.flags.boolean({
@@ -4,25 +4,25 @@ const command_1 = require("@oclif/command");
4
4
  const BaseCommand_1 = require("../utils/BaseCommand");
5
5
  const console_1 = require("../utils/console");
6
6
  const creatorUtilities_1 = require("../utils/creatorUtilities");
7
- const exampleMd = `# How to Install Node.js
8
-
9
- Node.js lets you run JavaScript outside a web browser.
10
-
11
- ## Step 1: Download Node.js
12
-
13
- Get the Node.js installer from the [official site](https://nodejs.org/en/download/).
14
-
15
- ## Step 2: Install Node.js
16
-
17
- Open the installer and follow the steps to finish.
18
-
19
- ## Step 3: Verify the Installation
20
-
21
- Open a terminal and type:
22
-
23
- \`\`\`bash
24
- node -v
25
- \`\`\`
7
+ const exampleMd = `# How to Install Node.js
8
+
9
+ Node.js lets you run JavaScript outside a web browser.
10
+
11
+ ## Step 1: Download Node.js
12
+
13
+ Get the Node.js installer from the [official site](https://nodejs.org/en/download/).
14
+
15
+ ## Step 2: Install Node.js
16
+
17
+ Open the installer and follow the steps to finish.
18
+
19
+ ## Step 3: Verify the Installation
20
+
21
+ Open a terminal and type:
22
+
23
+ \`\`\`bash
24
+ node -v
25
+ \`\`\`
26
26
  `;
27
27
  class BreakTokenCommand extends BaseCommand_1.default {
28
28
  async run() {
@@ -16,9 +16,9 @@ class CleanCommand extends SessionCommand_1.default {
16
16
  console_1.default.success("Package cleaned successfully, ready to publish");
17
17
  }
18
18
  }
19
- CleanCommand.description = `Clean the configuration object
20
- ...
21
- Extra documentation goes here
19
+ CleanCommand.description = `Clean the configuration object
20
+ ...
21
+ Extra documentation goes here
22
22
  `;
23
23
  CleanCommand.flags = {
24
24
  // name: flags.string({char: 'n', description: 'name to print'}),
@@ -107,9 +107,9 @@ const initializeInteractiveCreation = async (rigoToken, courseInfo) => {
107
107
  while (!isReady) {
108
108
  const spinner = (0, ora_1.default)("Thinking...").start();
109
109
  let wholeInfo = courseInfo;
110
- wholeInfo += `
111
- Current title: ${currentTitle}
112
- Current description: ${currentDescription}
110
+ wholeInfo += `
111
+ Current title: ${currentTitle}
112
+ Current description: ${currentDescription}
113
113
  `;
114
114
  // eslint-disable-next-line
115
115
  const res = await (0, rigoActions_1.interactiveCreation)(rigoToken, {
@@ -200,56 +200,56 @@ const handleAILogic = async (tutorialDir, packageInfo) => {
200
200
  fs.mkdirSync(rulesDir, { recursive: true });
201
201
  fs.writeFileSync(path.join(rulesDir, "airules.txt"), airules);
202
202
  }
203
- let packageContext = `
204
- \n
205
- Title: "${packageInfo.title.us}"
206
- Description: "${packageInfo.description.us}"
207
- Target Audience: "${targetAudience}"
208
- Estimated Duration: "${estimatedDuration} minutes"
209
-
203
+ let packageContext = `
204
+ \n
205
+ Title: "${packageInfo.title.us}"
206
+ Description: "${packageInfo.description.us}"
207
+ Target Audience: "${targetAudience}"
208
+ Estimated Duration: "${estimatedDuration} minutes"
209
+
210
210
  ${contentIndex ?
211
- `Content Index submitted by the user, use this to guide your creation. Keep in mind that your tutorial should contain these topics:
212
- ---
213
- ${contentIndex}
214
- ---
211
+ `Content Index submitted by the user, use this to guide your creation. Keep in mind that your tutorial should contain these topics:
212
+ ---
213
+ ${contentIndex}
214
+ ---
215
215
  ` :
216
- ""}
217
-
218
- This is the duration for each type of step, use it to estimate the number of steps to create:
216
+ ""}
217
+
218
+ This is the duration for each type of step, use it to estimate the number of steps to create:
219
219
  ${Object.entries(durationByKind)
220
220
  .map(([key, value]) => `${key}: ${value} minutes`)
221
- .join("\n")}
222
-
223
-
224
- Within the estimated duration, is possible to have the following activities:
225
- Format=
226
- Activity: Maximum number of steps for duration
227
-
228
- Estimated activities:
229
- ${JSON.stringify(estimateActivities(estimatedDuration))}
230
-
231
- You should create a tutorial that is engaging and fun to follow.
232
-
233
-
221
+ .join("\n")}
222
+
223
+
224
+ Within the estimated duration, is possible to have the following activities:
225
+ Format=
226
+ Activity: Maximum number of steps for duration
227
+
228
+ Estimated activities:
229
+ ${JSON.stringify(estimateActivities(estimatedDuration))}
230
+
231
+ You should create a tutorial that is engaging and fun to follow.
232
+
233
+
234
234
  ${airules ?
235
- `
236
- This is a list of rules you need to follow when creating the tutorial:
237
- ${airules}
235
+ `
236
+ This is a list of rules you need to follow when creating the tutorial:
237
+ ${airules}
238
238
  ` :
239
- ""}
239
+ ""}
240
240
  `;
241
241
  const { steps, title, description, duration, difficulty } = await initializeInteractiveCreation(rigoToken, packageContext);
242
242
  packageInfo.title.us = title;
243
243
  packageInfo.description.us = description;
244
244
  packageInfo.duration = duration;
245
245
  packageInfo.difficulty = difficulty;
246
- packageContext = `
247
- Title: "${title}"
248
- Description: "${description}"
249
- Target Audience: "${targetAudience}"
250
- List of exercises: ${steps.join(", ")}
251
-
252
- AI Rules: ${airules}
246
+ packageContext = `
247
+ Title: "${title}"
248
+ Description: "${description}"
249
+ Target Audience: "${targetAudience}"
250
+ List of exercises: ${steps.join(", ")}
251
+
252
+ AI Rules: ${airules}
253
253
  `;
254
254
  const exercisesDir = path.join(tutorialDir, "exercises");
255
255
  fs.ensureDirSync(exercisesDir);
@@ -14,9 +14,9 @@ class LogoutCommand extends SessionCommand_1.default {
14
14
  session_1.default.destroy();
15
15
  }
16
16
  }
17
- LogoutCommand.description = `Describe the command here
18
- ...
19
- Extra documentation goes here
17
+ LogoutCommand.description = `Describe the command here
18
+ ...
19
+ Extra documentation goes here
20
20
  `;
21
21
  LogoutCommand.flags = {
22
22
  // name: flags.string({char: 'n', description: 'name to print'}),
@@ -105,9 +105,7 @@ const uploadInitialReadme = async (bucket, exSlug, targetDir, packageContext) =>
105
105
  :rigo
106
106
  \`\`\`
107
107
  `;
108
- const readmeFilename = `README.${packageContext.language && packageContext.language !== "en" ?
109
- `${packageContext.language}.` :
110
- ""}md`;
108
+ const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(packageContext.language || "en")}`;
111
109
  await uploadFileToBucket(bucket, isGeneratingText, `${targetDir}/${readmeFilename}`);
112
110
  };
113
111
  const cleanFormState = (formState) => {
@@ -134,12 +132,12 @@ const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, cour
134
132
  all_translations.push(asset.slug);
135
133
  }
136
134
  };
137
- async function startExerciseGeneration(bucket, rigoToken, steps, packageContext, exercise, tutorialDir, courseSlug, purposeSlug, lastLesson = "") {
135
+ async function startExerciseGeneration(rigoToken, steps, packageContext, exercise, courseSlug, purposeSlug, lastLesson = "") {
138
136
  const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
139
137
  console.log("Starting generation of", exSlug);
140
138
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/exercise-processor/${exercise.id}/${rigoToken}`;
141
139
  console.log("WEBHOOK URL", webhookUrl);
142
- await (0, rigoActions_1.readmeCreator)(rigoToken, {
140
+ const res = await (0, rigoActions_1.readmeCreator)(rigoToken, {
143
141
  title: `${exercise.id} - ${exercise.title}`,
144
142
  output_lang: packageContext.language || "en",
145
143
  list_of_exercises: JSON.stringify(steps.map(step => step.id + "-" + step.title)),
@@ -148,6 +146,7 @@ async function startExerciseGeneration(bucket, rigoToken, steps, packageContext,
148
146
  kind: exercise.type.toLowerCase(),
149
147
  last_lesson: lastLesson,
150
148
  }, purposeSlug, webhookUrl);
149
+ console.log("README CREATOR RES", res);
151
150
  }
152
151
  async function createInitialReadme(tutorialInfo, tutorialSlug, rigoToken) {
153
152
  const webhookUrl = `${process.env.HOST}/webhooks/${tutorialSlug}/initial-readme-processor`;
@@ -356,7 +355,6 @@ class ServeCommand extends SessionCommand_1.default {
356
355
  });
357
356
  return res.json({ status: "ERROR" });
358
357
  }
359
- fs.writeFileSync(`image-${imageId}.json`, JSON.stringify(body, null, 2));
360
358
  const imageUrl = body.image_url;
361
359
  const format = body.format;
362
360
  const imagePath = `courses/${courseSlug}/.learn/assets/${imageId}`;
@@ -434,9 +432,19 @@ class ServeCommand extends SessionCommand_1.default {
434
432
  previousReadme = content.toString();
435
433
  }
436
434
  }
437
- await startExerciseGeneration(bucket, rigoToken, syllabusJson.lessons, syllabusJson.courseInfo, exercise, `courses/${courseSlug}`, courseSlug, syllabusJson.courseInfo.purpose, previousReadme +
435
+ await startExerciseGeneration(rigoToken, syllabusJson.lessons, syllabusJson.courseInfo, exercise, courseSlug, syllabusJson.courseInfo.purpose, previousReadme +
438
436
  "\n\nThe user provided the following feedback related to the content of the course so far: \n\n" +
439
437
  feedback);
438
+ syllabusJson.lessons[parseInt(position)].status = "GENERATING";
439
+ if (syllabusJson.feedback &&
440
+ typeof syllabusJson.feedback === "string") {
441
+ syllabusJson.feedback += "\n\n" + feedback;
442
+ }
443
+ else {
444
+ syllabusJson.feedback = feedback;
445
+ }
446
+ await uploadFileToBucket(bucket, JSON.stringify(syllabusJson), `courses/${courseSlug}/.learn/initialSyllabus.json`);
447
+ res.json({ status: "SUCCESS" });
440
448
  });
441
449
  app.post("/webhooks/:courseSlug/exercise-processor/:lessonID/:rigoToken", async (req, res) => {
442
450
  // console.log("Receiving a webhook to exercise processor")
@@ -493,7 +501,11 @@ class ServeCommand extends SessionCommand_1.default {
493
501
  let nextStarted = false;
494
502
  if (nextExercise &&
495
503
  (exerciseIndex === 0 || !(exerciseIndex % 3 === 0))) {
496
- startExerciseGeneration(bucket, rigoToken, syllabusJson.lessons, syllabusJson.courseInfo, nextExercise, `courses/${courseSlug}`, courseSlug, syllabusJson.courseInfo.purpose, readme.parsed.content);
504
+ let feedback = "";
505
+ if (syllabusJson.feedback) {
506
+ feedback = `\n\nThe user added the following feedback with relation to the previous generations: ${syllabusJson.feedback}`;
507
+ }
508
+ startExerciseGeneration(rigoToken, syllabusJson.lessons, syllabusJson.courseInfo, nextExercise, courseSlug, syllabusJson.courseInfo.purpose, readme.parsed.content + "\n\n" + feedback);
497
509
  nextStarted = true;
498
510
  }
499
511
  else {
@@ -511,8 +523,8 @@ class ServeCommand extends SessionCommand_1.default {
511
523
  await (0, exports.processImage)(image.url, image.alt, rigoToken, courseSlug);
512
524
  }
513
525
  }
514
- const newSyllabus = Object.assign(Object.assign({}, syllabusJson), { lessons: syllabusJson.lessons.map(lesson => {
515
- if (lesson.id === exercise.id) {
526
+ const newSyllabus = Object.assign(Object.assign({}, syllabusJson), { lessons: syllabusJson.lessons.map((lesson, index) => {
527
+ if (index === exerciseIndex) {
516
528
  return Object.assign(Object.assign({}, lesson), { generated: true, status: "DONE" });
517
529
  }
518
530
  if (nextExercise && nextExercise.id === lesson.id && nextStarted) {
@@ -924,7 +936,7 @@ class ServeCommand extends SessionCommand_1.default {
924
936
  await uploadFileToBucket(bucket, JSON.stringify(sidebar), `${tutorialDir}/.learn/sidebar.json`);
925
937
  const firstLesson = syllabus.lessons[0];
926
938
  const lastResult = "---";
927
- await startExerciseGeneration(bucket, rigoToken, syllabus.lessons, syllabus.courseInfo, firstLesson, tutorialDir, courseSlug, syllabus.courseInfo.purpose, lastResult);
939
+ await startExerciseGeneration(rigoToken, syllabus.lessons, syllabus.courseInfo, firstLesson, courseSlug, syllabus.courseInfo.purpose, lastResult);
928
940
  await createInitialReadme(JSON.stringify(syllabus.courseInfo), courseSlug, rigoToken);
929
941
  return res.json({
930
942
  message: "Course generation started",
@@ -961,7 +973,7 @@ class ServeCommand extends SessionCommand_1.default {
961
973
  const lastGeneratedLesson = findLast(syllabusJson.lessons, lesson => { var _a; return (_a = lesson.generated) !== null && _a !== void 0 ? _a : false; });
962
974
  console.log("ABout to generate", notGeneratedLessons.length, "lessons");
963
975
  const firstLessonToGenerate = notGeneratedLessons[0];
964
- await startExerciseGeneration(bucket, rigoToken, syllabusJson.lessons, syllabusJson.courseInfo, firstLessonToGenerate, `courses/${courseSlug}`, courseSlug, syllabusJson.courseInfo.purpose, JSON.stringify(lastGeneratedLesson) +
976
+ await startExerciseGeneration(rigoToken, syllabusJson.lessons, syllabusJson.courseInfo, firstLessonToGenerate, courseSlug, syllabusJson.courseInfo.purpose, JSON.stringify(lastGeneratedLesson) +
965
977
  `\n\nThe user provided this feedback in relation to the course: ${feedback}`);
966
978
  return res.json({
967
979
  message: "Course continued",
@@ -1124,7 +1136,9 @@ class ServeCommand extends SessionCommand_1.default {
1124
1136
  console.error("❌ Error fetching file:", error);
1125
1137
  return res
1126
1138
  .status(500)
1127
- .json({ error: error.message || "Unable to fetch file" });
1139
+ .json({
1140
+ error: error.message || "Unable to fetch file",
1141
+ });
1128
1142
  }
1129
1143
  });
1130
1144
  const YT_REGEX = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{11})/;
@@ -1136,20 +1150,32 @@ class ServeCommand extends SessionCommand_1.default {
1136
1150
  const ytMatch = decoded.match(YT_REGEX);
1137
1151
  if (ytMatch) {
1138
1152
  const videoId = ytMatch[1];
1153
+ console.log("VIDEO ID", videoId);
1139
1154
  // fetch metadata
1140
1155
  const items = await youtube_transcript_1.YoutubeTranscript.fetchTranscript(videoId);
1156
+ console.log("ITEMS FROM YOUTUBE TRANSCRIPT", items);
1141
1157
  const transcript = items.map(i => i.text).join(" ");
1142
- const { data: meta } = await axios_1.default.get("https://www.youtube.com/oembed", {
1143
- params: { url: decoded, format: "json" },
1144
- });
1158
+ let meta = null;
1159
+ try {
1160
+ const { data: meta } = await axios_1.default.get("https://www.youtube.com/oembed", {
1161
+ params: { url: decoded, format: "json" },
1162
+ });
1163
+ console.log("META", meta);
1164
+ }
1165
+ catch (error) {
1166
+ console.error("ERROR FETCHING META", error);
1167
+ meta = null;
1168
+ }
1169
+ throw new Error("test");
1145
1170
  return res.json({
1146
1171
  url: decoded,
1147
- title: meta.title,
1148
- author: meta.author_name,
1149
- thumbnail: meta.thumbnail_url,
1172
+ title: (meta === null || meta === void 0 ? void 0 : meta.title) || null,
1173
+ author: (meta === null || meta === void 0 ? void 0 : meta.author_name) || null,
1174
+ thumbnail: (meta === null || meta === void 0 ? void 0 : meta.thumbnail_url) || null,
1150
1175
  transcript,
1151
1176
  });
1152
1177
  }
1178
+ console.log("NOT A YOUTUBE LINK", decoded);
1153
1179
  const response = await axios_1.default.get(decoded, { responseType: "text" });
1154
1180
  const html = response.data;
1155
1181
  const title = getTitleFromHTML(html);
@@ -1163,7 +1189,9 @@ class ServeCommand extends SessionCommand_1.default {
1163
1189
  }
1164
1190
  catch (error) {
1165
1191
  console.error("❌ /actions/fetch error:", error.message || error);
1166
- res.status(500).json({ error: error.message || "Failed to fetch link" });
1192
+ res
1193
+ .status(500)
1194
+ .json({ error: error.message || "Failed to fetch link" });
1167
1195
  }
1168
1196
  });
1169
1197
  app.delete("/packages/:slug", async (req, res) => {