@learnpack/learnpack 5.0.354 → 5.0.357

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 (120) hide show
  1. package/lib/commands/serve.d.ts +0 -17
  2. package/lib/commands/serve.js +160 -130
  3. package/lib/creatorDist/assets/{index-DnthLsvb.js → index-D6pmbMe9.js} +14030 -13903
  4. package/lib/creatorDist/assets/index-zrPponAn.css +1701 -0
  5. package/lib/creatorDist/index.html +2 -2
  6. package/lib/models/creator.d.ts +1 -1
  7. package/lib/services/ingest/adapters/gcsCourseStorage.d.ts +13 -0
  8. package/lib/services/ingest/adapters/gcsCourseStorage.js +58 -0
  9. package/lib/services/ingest/adapters/rigoUserTokenAuth.d.ts +15 -0
  10. package/lib/services/ingest/adapters/rigoUserTokenAuth.js +46 -0
  11. package/lib/services/ingest/adapters/rigobotPackageRegistry.d.ts +7 -0
  12. package/lib/services/ingest/adapters/rigobotPackageRegistry.js +119 -0
  13. package/lib/services/ingest/core/buildIngestConfig.d.ts +27 -0
  14. package/lib/services/ingest/core/buildIngestConfig.js +40 -0
  15. package/lib/services/ingest/core/buildInitialSyllabus.d.ts +24 -0
  16. package/lib/services/ingest/core/buildInitialSyllabus.js +104 -0
  17. package/lib/services/ingest/core/buildSidebar.d.ts +13 -0
  18. package/lib/services/ingest/core/buildSidebar.js +28 -0
  19. package/lib/services/ingest/core/coursePaths.d.ts +31 -0
  20. package/lib/services/ingest/core/coursePaths.js +50 -0
  21. package/lib/services/ingest/core/ingestCoursePackage.d.ts +52 -0
  22. package/lib/services/ingest/core/ingestCoursePackage.js +217 -0
  23. package/lib/services/ingest/core/normalizeLearnJson.d.ts +25 -0
  24. package/lib/services/ingest/core/normalizeLearnJson.js +29 -0
  25. package/lib/services/ingest/core/pairLessons.d.ts +29 -0
  26. package/lib/services/ingest/core/pairLessons.js +102 -0
  27. package/lib/services/ingest/core/planWrites.d.ts +44 -0
  28. package/lib/services/ingest/core/planWrites.js +59 -0
  29. package/lib/services/ingest/core/resolveSlug.d.ts +53 -0
  30. package/lib/services/ingest/core/resolveSlug.js +93 -0
  31. package/lib/services/ingest/core/types.d.ts +53 -0
  32. package/lib/services/ingest/core/types.js +2 -0
  33. package/lib/services/ingest/core/validatePackage.d.ts +38 -0
  34. package/lib/services/ingest/core/validatePackage.js +97 -0
  35. package/lib/services/ingest/createIngestService.d.ts +34 -0
  36. package/lib/services/ingest/createIngestService.js +38 -0
  37. package/lib/services/ingest/errors.d.ts +22 -0
  38. package/lib/services/ingest/errors.js +34 -0
  39. package/lib/services/ingest/http/errorMapping.d.ts +8 -0
  40. package/lib/services/ingest/http/errorMapping.js +40 -0
  41. package/lib/services/ingest/http/router.d.ts +32 -0
  42. package/lib/services/ingest/http/router.js +124 -0
  43. package/lib/services/ingest/http/zipPackageReader.d.ts +26 -0
  44. package/lib/services/ingest/http/zipPackageReader.js +62 -0
  45. package/lib/services/ingest/ports/courseStorage.d.ts +25 -0
  46. package/lib/services/ingest/ports/courseStorage.js +2 -0
  47. package/lib/services/ingest/ports/packageRegistry.d.ts +58 -0
  48. package/lib/services/ingest/ports/packageRegistry.js +2 -0
  49. package/lib/services/ingest/ports/requestAuthenticator.d.ts +25 -0
  50. package/lib/services/ingest/ports/requestAuthenticator.js +2 -0
  51. package/lib/utils/coursePackage/bucketIo.d.ts +23 -0
  52. package/lib/utils/coursePackage/bucketIo.js +36 -0
  53. package/lib/utils/coursePackage/learnJson.d.ts +26 -0
  54. package/lib/utils/coursePackage/learnJson.js +36 -0
  55. package/lib/utils/coursePackage/sidebar.d.ts +15 -0
  56. package/lib/utils/coursePackage/sidebar.js +26 -0
  57. package/lib/utils/creatorSocket.js +15 -21
  58. package/lib/utils/gcpCredentials.d.ts +18 -0
  59. package/lib/utils/gcpCredentials.js +27 -0
  60. package/lib/utils/rigoActions.d.ts +16 -0
  61. package/lib/utils/rigoActions.js +102 -1
  62. package/lib/utils/socketRegistry.d.ts +46 -0
  63. package/lib/utils/socketRegistry.js +83 -0
  64. package/package.json +1 -1
  65. package/src/commands/serve.ts +163 -115
  66. package/src/creator/README.md +63 -51
  67. package/src/creator/package-lock.json +1188 -8
  68. package/src/creator/package.json +5 -1
  69. package/src/creator/src/App.tsx +120 -54
  70. package/src/creator/src/components/FileUploader.tsx +1 -12
  71. package/src/creator/src/components/NotificationListener.tsx +1 -8
  72. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +101 -36
  73. package/src/creator/src/locales/en.json +8 -0
  74. package/src/creator/src/locales/es.json +8 -0
  75. package/src/creator/src/utils/completionResult.test.ts +144 -0
  76. package/src/creator/src/utils/completionResult.ts +154 -0
  77. package/src/creator/src/utils/constants.ts +17 -4
  78. package/src/creator/src/utils/socket.test.ts +113 -0
  79. package/src/creator/src/utils/socket.ts +60 -37
  80. package/src/creator/src/utils/store.ts +84 -67
  81. package/src/creator/src/utils/useCompletionWatchdog.test.ts +99 -0
  82. package/src/creator/src/utils/useCompletionWatchdog.ts +37 -0
  83. package/src/creator/vitest.config.ts +16 -0
  84. package/src/creatorDist/assets/{index-DnthLsvb.js → index-D6pmbMe9.js} +14030 -13903
  85. package/src/creatorDist/assets/index-zrPponAn.css +1701 -0
  86. package/src/creatorDist/index.html +2 -2
  87. package/src/models/creator.ts +1 -1
  88. package/src/services/ingest/adapters/gcsCourseStorage.ts +70 -0
  89. package/src/services/ingest/adapters/rigoUserTokenAuth.ts +65 -0
  90. package/src/services/ingest/adapters/rigobotPackageRegistry.ts +161 -0
  91. package/src/services/ingest/core/buildIngestConfig.ts +51 -0
  92. package/src/services/ingest/core/buildInitialSyllabus.ts +122 -0
  93. package/src/services/ingest/core/buildSidebar.ts +32 -0
  94. package/src/services/ingest/core/coursePaths.ts +59 -0
  95. package/src/services/ingest/core/ingestCoursePackage.ts +343 -0
  96. package/src/services/ingest/core/normalizeLearnJson.ts +47 -0
  97. package/src/services/ingest/core/pairLessons.ts +132 -0
  98. package/src/services/ingest/core/planWrites.ts +91 -0
  99. package/src/services/ingest/core/resolveSlug.ts +119 -0
  100. package/src/services/ingest/core/types.ts +57 -0
  101. package/src/services/ingest/core/validatePackage.ts +132 -0
  102. package/src/services/ingest/createIngestService.ts +66 -0
  103. package/src/services/ingest/errors.ts +45 -0
  104. package/src/services/ingest/http/errorMapping.ts +49 -0
  105. package/src/services/ingest/http/router.ts +156 -0
  106. package/src/services/ingest/http/zipPackageReader.ts +81 -0
  107. package/src/services/ingest/ports/courseStorage.ts +25 -0
  108. package/src/services/ingest/ports/packageRegistry.ts +60 -0
  109. package/src/services/ingest/ports/requestAuthenticator.ts +28 -0
  110. package/src/ui/_app/app.js +325 -325
  111. package/src/ui/app.tar.gz +0 -0
  112. package/src/utils/coursePackage/bucketIo.ts +46 -0
  113. package/src/utils/coursePackage/learnJson.ts +37 -0
  114. package/src/utils/coursePackage/sidebar.ts +28 -0
  115. package/src/utils/creatorSocket.ts +16 -22
  116. package/src/utils/gcpCredentials.ts +38 -0
  117. package/src/utils/rigoActions.ts +163 -0
  118. package/src/utils/socketRegistry.ts +85 -0
  119. package/lib/creatorDist/assets/index-CjddKHB_.css +0 -1
  120. package/src/creatorDist/assets/index-CjddKHB_.css +0 -1
@@ -1,21 +1,4 @@
1
1
  import SessionCommand from "../utils/SessionCommand";
2
- import { FormState } from "../models/creator";
3
- export declare const createLearnJson: (courseInfo: FormState) => {
4
- slug: string;
5
- title: {
6
- [x: string]: string;
7
- };
8
- technologies: string[];
9
- difficulty: string;
10
- description: {
11
- [x: string]: string;
12
- };
13
- grading: string;
14
- telemetry: {
15
- batch: string;
16
- };
17
- preview: string;
18
- };
19
2
  export declare const processImage: (url: string, description: string, rigoToken: string, courseSlug: string) => Promise<boolean>;
20
3
  declare class ServeCommand extends SessionCommand {
21
4
  static description: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processImage = exports.createLearnJson = void 0;
3
+ exports.processImage = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const command_1 = require("@oclif/command");
6
6
  const buffer_1 = require("buffer");
@@ -33,6 +33,10 @@ const creatorUtilities_2 = require("../utils/creatorUtilities");
33
33
  const sidebarGenerator_1 = require("../utils/sidebarGenerator");
34
34
  const publish_1 = require("./publish");
35
35
  const export_1 = require("../utils/export");
36
+ const router_1 = require("../services/ingest/http/router");
37
+ const bucketIo_1 = require("../utils/coursePackage/bucketIo");
38
+ const learnJson_1 = require("../utils/coursePackage/learnJson");
39
+ const sidebar_1 = require("../utils/coursePackage/sidebar");
36
40
  const packageManifest_1 = require("../utils/packageManifest");
37
41
  const gcsBucketName_1 = require("../utils/gcsBucketName");
38
42
  const syllabusSync_1 = require("../utils/syllabusSync");
@@ -86,39 +90,6 @@ async function serializeByCourse(courseSlug, fn) {
86
90
  courseQueues.set(courseSlug, next);
87
91
  return next;
88
92
  }
89
- const createLearnJson = (courseInfo) => {
90
- // console.log("courseInfo to create learn json", courseInfo)
91
- const expectedPreviewUrl = `https://${courseInfo.slug}.learn-pack.com/preview.png`;
92
- const language = courseInfo.language || "en";
93
- const learnJson = {
94
- slug: courseInfo.slug,
95
- title: {
96
- [language]: courseInfo.title,
97
- },
98
- technologies: courseInfo.technologies || [],
99
- difficulty: "beginner",
100
- description: {
101
- [language]: courseInfo.description,
102
- },
103
- grading: "isolated",
104
- telemetry: {
105
- batch: api_1.BREATHECODE_TELEMETRY_URL,
106
- },
107
- preview: expectedPreviewUrl,
108
- };
109
- return learnJson;
110
- };
111
- exports.createLearnJson = createLearnJson;
112
- const uploadFileToBucket = async (bucket, file, path) => {
113
- const isReadme = /readme(\.\w+)?\.md$/i.test(path);
114
- const content = isReadme ? (0, readmeSanitizer_1.sanitizeReadmeNewlines)(file) : file;
115
- const fileRef = bucket.file(path);
116
- await fileRef.save(buffer_1.Buffer.from(content, "utf8"));
117
- };
118
- const uploadBinaryToBucket = async (bucket, buffer, path, contentType) => {
119
- const fileRef = bucket.file(path);
120
- await fileRef.save(buffer, Object.assign({ resumable: false }, (contentType && { contentType })));
121
- };
122
93
  const getGithubCredentials = () => {
123
94
  var _a, _b;
124
95
  const token = (_a = process.env.GITHUB_TOKEN) === null || _a === void 0 ? void 0 : _a.trim();
@@ -331,15 +302,6 @@ const processImage = async (url, description, rigoToken, courseSlug) => {
331
302
  }
332
303
  };
333
304
  exports.processImage = processImage;
334
- const createInitialSidebar = async (slugs, initialLanguage = "en") => {
335
- const sidebar = {};
336
- for (const slug of slugs) {
337
- sidebar[slug] = {
338
- [initialLanguage]: slug,
339
- };
340
- }
341
- return sidebar;
342
- };
343
305
  const uploadInitialReadme = async (bucket, exSlug, targetDir, packageContext) => {
344
306
  const isGeneratingText = `
345
307
  \`\`\`loader slug="${exSlug}"
@@ -347,7 +309,7 @@ const uploadInitialReadme = async (bucket, exSlug, targetDir, packageContext) =>
347
309
  \`\`\`
348
310
  `;
349
311
  const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(packageContext.language || "en")}`;
350
- await uploadFileToBucket(bucket, isGeneratingText, `${targetDir}/${readmeFilename}`);
312
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, isGeneratingText, `${targetDir}/${readmeFilename}`);
351
313
  };
352
314
  const cleanFormState = (formState) => {
353
315
  const { description, technologies, purpose, hasContentIndex, duration, isCompleted, variables, currentStep, language } = formState, rest = tslib_1.__rest(formState, ["description", "technologies", "purpose", "hasContentIndex", "duration", "isCompleted", "variables", "currentStep", "language"]);
@@ -581,7 +543,7 @@ function sanitizeSyllabusFromUnknown(syllabus) {
581
543
  }
582
544
  async function saveSyllabus(courseSlug, syllabus, bucket) {
583
545
  sanitizeSyllabusFromUnknown(syllabus);
584
- await uploadFileToBucket(bucket, JSON.stringify(syllabus), `courses/${courseSlug}/.learn/initialSyllabus.json`);
546
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(syllabus), `courses/${courseSlug}/.learn/initialSyllabus.json`);
585
547
  }
586
548
  // Instrumented syllabus operations for race condition observability
587
549
  async function getSyllabusWithLog(courseSlug, bucket, context) {
@@ -786,29 +748,68 @@ const getTitleFromHTML = (html) => {
786
748
  const titleMatch = html.match(titleRegex);
787
749
  return titleMatch ? titleMatch[1] : null;
788
750
  };
751
+ /** How often the notification is touched while a translation is running. */
752
+ const SYNC_HEARTBEAT_INTERVAL_MS = 60 * 1000;
753
+ /**
754
+ * A synchronization with no news for this long is considered stuck: the GET
755
+ * endpoint flags it as failed and it can be accepted again.
756
+ */
757
+ const SYNC_PROCESSING_TIMEOUT_MS = 3 * 60 * 1000;
758
+ // True when a processing notification stopped reporting progress.
759
+ function isSyncProcessingStale(notification) {
760
+ const lastUpdate = notification.processingLastUpdate || notification.updatedAt;
761
+ return Date.now() - lastUpdate > SYNC_PROCESSING_TIMEOUT_MS;
762
+ }
763
+ function findSyncNotification(syllabus, exerciseSlug, notificationId) {
764
+ var _a;
765
+ const lesson = syllabus.lessons.find(lesson => (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title) === exerciseSlug);
766
+ return (_a = lesson === null || lesson === void 0 ? void 0 : lesson.syncNotifications) === null || _a === void 0 ? void 0 : _a.find(n => n.id === notificationId);
767
+ }
789
768
  async function processSyncTranslationsSequentially(courseSlug, exerciseSlug, notificationId, sourceReadmeContent, targetLanguages, rigoToken, bucket, historyManager) {
790
- var _a, _b, _c, _d, _e, _f;
769
+ var _a, _b, _c, _d, _e, _f, _g;
791
770
  try {
792
771
  const sanitizedSource = (0, readmeSanitizer_1.sanitizeReadmeNewlines)(sourceReadmeContent);
772
+ let lastHeartbeatAt = Date.now();
773
+ // Keeps `processingLastUpdate` fresh while a language is being translated,
774
+ // otherwise the PROCESSING_TIMEOUT in GET /sync-notifications can flag a
775
+ // running synchronization as failed. Throttled, since every beat rewrites
776
+ // the syllabus in the bucket.
777
+ const heartbeat = async (targetLang) => {
778
+ if (Date.now() - lastHeartbeatAt < SYNC_HEARTBEAT_INTERVAL_MS) {
779
+ return;
780
+ }
781
+ lastHeartbeatAt = Date.now();
782
+ try {
783
+ await serializeByCourse(courseSlug, async () => {
784
+ const syllabus = await getSyllabus(courseSlug, bucket);
785
+ const notification = findSyncNotification(syllabus, exerciseSlug, notificationId);
786
+ if (!notification) {
787
+ return;
788
+ }
789
+ notification.processingLastUpdate = Date.now();
790
+ if (notification.syncProgress) {
791
+ notification.syncProgress.currentLanguage = targetLang;
792
+ }
793
+ await saveSyllabus(courseSlug, syllabus, bucket);
794
+ });
795
+ }
796
+ catch (error) {
797
+ console.warn(`🔄 SYNC: heartbeat failed for ${exerciseSlug}:`, error.message);
798
+ }
799
+ };
793
800
  // Process translations sequentially (no race conditions)
794
801
  for (const targetLang of targetLanguages) {
795
802
  try {
796
- // Call Rigobot directly with synchronous execution (no webhook needed)
803
+ const languageStartedAt = Date.now();
804
+ // The translation runs as an asynchronous Rigobot job that we poll:
805
+ // waiting for it inside a single request takes 28-45s for a full
806
+ // README and Heroku kills anything above 30s.
797
807
  // eslint-disable-next-line no-await-in-loop
798
- const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/translate-asset-markdown/`, {
799
- inputs: {
800
- text_to_translate: sanitizedSource,
801
- output_language: targetLang,
802
- },
803
- include_purpose_objective: false,
804
- execute_async: false, // Synchronous execution
805
- }, {
806
- headers: {
807
- "Content-Type": "application/json",
808
- Authorization: "Token " + rigoToken,
809
- },
810
- });
811
- const translationResult = response.data;
808
+ const translationResult = await (0, rigoActions_1.translateExerciseAndWait)(rigoToken, {
809
+ text_to_translate: sanitizedSource,
810
+ output_language: targetLang,
811
+ }, { onPoll: () => heartbeat(targetLang) });
812
+ console.log(`🔄 SYNC: ${targetLang} translated in ${Math.round((Date.now() - languageStartedAt) / 1000)}s (job ${translationResult.id})`);
812
813
  // Check if translation was successful
813
814
  if (!((_a = translationResult.parsed) === null || _a === void 0 ? void 0 : _a.translation)) {
814
815
  throw new Error("Translation result is empty");
@@ -897,10 +898,20 @@ async function processSyncTranslationsSequentially(courseSlug, exerciseSlug, not
897
898
  const totalProcessed = notification.syncProgress.completedLanguages.length +
898
899
  (((_e = notification.syncProgress.failedLanguages) === null || _e === void 0 ? void 0 : _e.length) || 0);
899
900
  if (totalProcessed === notification.syncProgress.totalLanguages) {
900
- notification.status =
901
- notification.syncProgress.completedLanguages.length === 0 ?
902
- "error" :
903
- "completed";
901
+ const completedCount = notification.syncProgress.completedLanguages.length;
902
+ const failedCount = ((_f = notification.syncProgress.failedLanguages) === null || _f === void 0 ? void 0 : _f.length) || 0;
903
+ // A partial result stays actionable: the notification remains
904
+ // visible so the user can retry just the languages that failed.
905
+ if (completedCount === 0) {
906
+ notification.status = "error";
907
+ }
908
+ else if (failedCount > 0) {
909
+ notification.status = "partial";
910
+ }
911
+ else {
912
+ notification.status = "completed";
913
+ }
914
+ notification.updatedAt = Date.now();
904
915
  await saveSyllabus(courseSlug, syllabus, bucket);
905
916
  console.log(`🔄 SYNC: ✅ All translations completed for ${exerciseSlug} - Status: ${notification.status}`);
906
917
  (0, creatorSocket_1.emitToCourse)(courseSlug, "sync-notification-completed", {
@@ -908,7 +919,7 @@ async function processSyncTranslationsSequentially(courseSlug, exerciseSlug, not
908
919
  notificationId,
909
920
  status: notification.status,
910
921
  completed: notification.syncProgress.completedLanguages.length,
911
- failed: ((_f = notification.syncProgress.failedLanguages) === null || _f === void 0 ? void 0 : _f.length) || 0,
922
+ failed: ((_g = notification.syncProgress.failedLanguages) === null || _g === void 0 ? void 0 : _g.length) || 0,
912
923
  });
913
924
  }
914
925
  }
@@ -1021,6 +1032,7 @@ class ServeCommand extends SessionCommand_1.default {
1021
1032
  // app.use(express.static(distPath))
1022
1033
  app.use(express.json({ limit: "20mb" }));
1023
1034
  app.use(cors());
1035
+ app.use("/generation", (0, router_1.createIngestRouter)());
1024
1036
  const appPath = path.resolve(__dirname, "../ui/_app");
1025
1037
  const tarPath = path.resolve(__dirname, "../ui/app.tar.gz");
1026
1038
  if (fs.existsSync(appPath)) {
@@ -1207,7 +1219,7 @@ class ServeCommand extends SessionCommand_1.default {
1207
1219
  });
1208
1220
  }
1209
1221
  const filePath = `courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(langCode)}`;
1210
- await uploadFileToBucket(bucket, content, filePath);
1222
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, content, filePath);
1211
1223
  res.json({ status: "SUCCESS" });
1212
1224
  });
1213
1225
  app.post("/webhooks/:courseSlug/images/:imageId", async (req, res) => {
@@ -1302,7 +1314,7 @@ class ServeCommand extends SessionCommand_1.default {
1302
1314
  if (fileObj.name && fileObj.content) {
1303
1315
  const filePath = `${exerciseDir}/${flatFileName}`;
1304
1316
  // eslint-disable-next-line no-await-in-loop
1305
- await uploadFileToBucket(bucket, fileObj.content, filePath);
1317
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, fileObj.content, filePath);
1306
1318
  console.log(`✅ Saved file: ${filePath}`);
1307
1319
  }
1308
1320
  // Save the solution file if it exists
@@ -1314,7 +1326,7 @@ class ServeCommand extends SessionCommand_1.default {
1314
1326
  const solutionFileName = `${baseName}.solution.hide.${extension}`;
1315
1327
  const solutionFilePath = `${exerciseDir}/${solutionFileName}`;
1316
1328
  // eslint-disable-next-line no-await-in-loop
1317
- await uploadFileToBucket(bucket, fileObj.solution, solutionFilePath);
1329
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, fileObj.solution, solutionFilePath);
1318
1330
  console.log(`✅ Saved solution file: ${solutionFilePath}`);
1319
1331
  }
1320
1332
  else {
@@ -1322,7 +1334,7 @@ class ServeCommand extends SessionCommand_1.default {
1322
1334
  const solutionFileName = `${flatFileName}.solution.hide`;
1323
1335
  const solutionFilePath = `${exerciseDir}/${solutionFileName}`;
1324
1336
  // eslint-disable-next-line no-await-in-loop
1325
- await uploadFileToBucket(bucket, fileObj.solution, solutionFilePath);
1337
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, fileObj.solution, solutionFilePath);
1326
1338
  console.log(`✅ Saved solution file: ${solutionFilePath}`);
1327
1339
  }
1328
1340
  }
@@ -1870,7 +1882,7 @@ class ServeCommand extends SessionCommand_1.default {
1870
1882
  const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(response.parsed.output_language ||
1871
1883
  syllabus.courseInfo.language ||
1872
1884
  "en")}`;
1873
- await uploadFileToBucket(bucket, readability.newMarkdown, `${targetDir}/${readmeFilename}`);
1885
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, readability.newMarkdown, `${targetDir}/${readmeFilename}`);
1874
1886
  // Update used components if provided by the AI
1875
1887
  if (response.parsed.used_components &&
1876
1888
  Array.isArray(response.parsed.used_components)) {
@@ -1994,7 +2006,7 @@ class ServeCommand extends SessionCommand_1.default {
1994
2006
  }
1995
2007
  // Translation successful
1996
2008
  const readmePath = `courses/${courseSlug}/exercises/${exSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(body.parsed.output_language_code)}`;
1997
- await uploadFileToBucket(bucket, body.parsed.translation, readmePath);
2009
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, body.parsed.translation, readmePath);
1998
2010
  // Verify file exists before updating syllabus (resilience: ensure file was actually saved)
1999
2011
  const [fileExists] = await bucket.file(readmePath).exists();
2000
2012
  // Update syllabus with completed status only if file was successfully saved
@@ -2132,7 +2144,7 @@ class ServeCommand extends SessionCommand_1.default {
2132
2144
  console.log("BODY", body);
2133
2145
  const readmePath = `courses/${courseSlug}/exercises/${exSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`;
2134
2146
  if (body.parsed.content) {
2135
- await uploadFileToBucket(bucket, body.parsed.content, readmePath);
2147
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, body.parsed.content, readmePath);
2136
2148
  }
2137
2149
  (0, creatorSocket_1.emitToNotification)(notificationId, {
2138
2150
  status: "SUCCESS",
@@ -2178,7 +2190,7 @@ class ServeCommand extends SessionCommand_1.default {
2178
2190
  const config = await mergeConfigPreservingGithub(bucket, courseSlug, builtConfig);
2179
2191
  res.set("X-Creator-Web", "true");
2180
2192
  res.set("Access-Control-Expose-Headers", "X-Creator-Web");
2181
- await uploadFileToBucket(bucket, JSON.stringify({ config, exercises }), `courses/${courseSlug}/.learn/config.json`);
2193
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify({ config, exercises }), `courses/${courseSlug}/.learn/config.json`);
2182
2194
  res.json({ config, exercises });
2183
2195
  }
2184
2196
  catch (error) {
@@ -2590,7 +2602,7 @@ class ServeCommand extends SessionCommand_1.default {
2590
2602
  completedAt: 0,
2591
2603
  },
2592
2604
  };
2593
- await uploadFileToBucket(bucket, JSON.stringify(Object.assign(Object.assign({}, initialSyllabus), { lessons: newLessons })), `courses/${courseSlug}/.learn/initialSyllabus.json`);
2605
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(Object.assign(Object.assign({}, initialSyllabus), { lessons: newLessons })), `courses/${courseSlug}/.learn/initialSyllabus.json`);
2594
2606
  const targetDir = `courses/${courseSlug}/exercises/${stepSlug}`;
2595
2607
  await uploadInitialReadme(bucket, stepSlug, targetDir, initialSyllabus.courseInfo);
2596
2608
  res.json({ status: "SUCCESS", message: "Exercise generati on started!" });
@@ -2633,20 +2645,20 @@ class ServeCommand extends SessionCommand_1.default {
2633
2645
  if (!learnJson.title)
2634
2646
  learnJson.title = {};
2635
2647
  learnJson.title[language] = title;
2636
- await uploadFileToBucket(bucket, JSON.stringify(learnJson), `courses/${courseSlug}/learn.json`);
2648
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(learnJson), `courses/${courseSlug}/learn.json`);
2637
2649
  const configFile = bucket.file(`courses/${courseSlug}/.learn/config.json`);
2638
2650
  const [configContent] = await configFile.download();
2639
2651
  const configJson = JSON.parse(configContent.toString());
2640
2652
  configJson.config = configJson.config || {};
2641
2653
  configJson.config.title = Object.assign(Object.assign({}, configJson.config.title), learnJson.title);
2642
- await uploadFileToBucket(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
2654
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
2643
2655
  try {
2644
2656
  const syllabusFile = bucket.file(`courses/${courseSlug}/.learn/initialSyllabus.json`);
2645
2657
  const [syllabusContent] = await syllabusFile.download();
2646
2658
  const syllabusJson = JSON.parse(syllabusContent.toString());
2647
2659
  if (syllabusJson === null || syllabusJson === void 0 ? void 0 : syllabusJson.courseInfo) {
2648
2660
  syllabusJson.courseInfo.title = title;
2649
- await uploadFileToBucket(bucket, JSON.stringify(syllabusJson), `courses/${courseSlug}/.learn/initialSyllabus.json`);
2661
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(syllabusJson), `courses/${courseSlug}/.learn/initialSyllabus.json`);
2650
2662
  }
2651
2663
  }
2652
2664
  catch (error) {
@@ -2664,9 +2676,9 @@ class ServeCommand extends SessionCommand_1.default {
2664
2676
  if ((_a = result === null || result === void 0 ? void 0 : result.parsed) === null || _a === void 0 ? void 0 : _a.title) {
2665
2677
  const translatedTitle = JSON.parse(result.parsed.title);
2666
2678
  learnJson.title = Object.assign(Object.assign(Object.assign({}, learnJson.title), translatedTitle), { [language]: title });
2667
- await uploadFileToBucket(bucket, JSON.stringify(learnJson), `courses/${courseSlug}/learn.json`);
2679
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(learnJson), `courses/${courseSlug}/learn.json`);
2668
2680
  configJson.config.title = Object.assign(Object.assign({}, configJson.config.title), learnJson.title);
2669
- await uploadFileToBucket(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
2681
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
2670
2682
  }
2671
2683
  }
2672
2684
  catch (translationError) {
@@ -2791,7 +2803,7 @@ class ServeCommand extends SessionCommand_1.default {
2791
2803
  const translatedDescription = JSON.parse(result.parsed.description);
2792
2804
  courseJson.title = translatedTitle;
2793
2805
  courseJson.description = translatedDescription;
2794
- await uploadFileToBucket(bucket, JSON.stringify(courseJson), `courses/${courseSlug}/learn.json`);
2806
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(courseJson), `courses/${courseSlug}/learn.json`);
2795
2807
  currentLanguages = Object.keys(courseJson.title);
2796
2808
  }
2797
2809
  const missingReadmeTranslations = [];
@@ -2989,37 +3001,35 @@ class ServeCommand extends SessionCommand_1.default {
2989
3001
  if (!syllabus) {
2990
3002
  return res.status(404).json({ error: "Syllabus not found" });
2991
3003
  }
2992
- const PROCESSING_TIMEOUT = 3 * 60 * 1000; // 3 minutes
2993
3004
  let modified = false;
2994
- // Collect active notifications (pending, processing, or error)
3005
+ // Collect active notifications (pending, processing, partial or error)
2995
3006
  const notifications = [];
2996
3007
  for (const lesson of syllabus.lessons) {
2997
3008
  if (lesson.syncNotifications && lesson.syncNotifications.length > 0) {
2998
3009
  for (const notification of lesson.syncNotifications) {
2999
3010
  // Check for timeout in processing notifications
3000
- if (notification.status === "processing") {
3001
- // Use processingLastUpdate if available, otherwise fallback to updatedAt
3002
- const processingLastUpdateTime = notification.processingLastUpdate || notification.updatedAt;
3003
- const timeSinceProcessingStarted = Date.now() - processingLastUpdateTime;
3004
- if (timeSinceProcessingStarted > PROCESSING_TIMEOUT) {
3005
- notification.status = "error";
3006
- notification.error = {
3007
- message: "Synchronization timeout - process took too long",
3008
- code: "PROCESSING_TIMEOUT",
3009
- timestamp: Date.now(),
3010
- };
3011
- modified = true;
3012
- // Emit error event
3013
- (0, creatorSocket_1.emitToCourse)(courseSlug, "sync-notification-error", {
3014
- exerciseSlug: (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title),
3015
- notificationId: notification.id,
3016
- error: "Processing timeout",
3017
- });
3018
- }
3011
+ if (notification.status === "processing" &&
3012
+ isSyncProcessingStale(notification)) {
3013
+ notification.status = "error";
3014
+ notification.error = {
3015
+ message: "Synchronization timeout - process took too long",
3016
+ code: "PROCESSING_TIMEOUT",
3017
+ timestamp: Date.now(),
3018
+ };
3019
+ modified = true;
3020
+ // Emit error event
3021
+ (0, creatorSocket_1.emitToCourse)(courseSlug, "sync-notification-error", {
3022
+ exerciseSlug: (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title),
3023
+ notificationId: notification.id,
3024
+ error: "Processing timeout",
3025
+ });
3019
3026
  }
3020
- // Include active notifications (pending, processing, or error)
3027
+ // Include active notifications (pending, processing, partial or
3028
+ // error): a partial sync is still actionable, the user can retry
3029
+ // the languages that failed.
3021
3030
  if (notification.status === "pending" ||
3022
3031
  notification.status === "processing" ||
3032
+ notification.status === "partial" ||
3023
3033
  notification.status === "error") {
3024
3034
  notifications.push(Object.assign(Object.assign({}, notification), { lessonSlug: (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title), lessonTitle: lesson.title }));
3025
3035
  }
@@ -3083,7 +3093,7 @@ class ServeCommand extends SessionCommand_1.default {
3083
3093
  });
3084
3094
  // Accept sync notification and start synchronization
3085
3095
  app.post("/courses/:courseSlug/lessons/:exerciseSlug/sync-notification/:notificationId/accept", express.json(), async (req, res) => {
3086
- var _a, _b;
3096
+ var _a, _b, _c;
3087
3097
  console.log("POST /courses/:courseSlug/lessons/:exerciseSlug/sync-notification/:notificationId/accept");
3088
3098
  const { courseSlug, exerciseSlug, notificationId } = req.params;
3089
3099
  const rigoToken = req.header("x-rigo-token");
@@ -3111,9 +3121,21 @@ class ServeCommand extends SessionCommand_1.default {
3111
3121
  if (!notification) {
3112
3122
  return res.status(404).json({ error: "Notification not found" });
3113
3123
  }
3114
- if (notification.status !== "pending") {
3124
+ // A synchronization can be accepted again unless it is actively
3125
+ // running: after a failure or a partial result this same
3126
+ // notification is what the Retry button acts on.
3127
+ if (notification.status === "processing" &&
3128
+ !isSyncProcessingStale(notification)) {
3129
+ return res.status(409).json({
3130
+ error: "Notification is already being synchronized",
3131
+ code: "ALREADY_PROCESSING",
3132
+ currentStatus: notification.status,
3133
+ });
3134
+ }
3135
+ if (notification.status === "completed") {
3115
3136
  return res.status(400).json({
3116
- error: "Notification is not pending",
3137
+ error: "Notification is already synchronized",
3138
+ code: "ALREADY_COMPLETED",
3117
3139
  currentStatus: notification.status,
3118
3140
  });
3119
3141
  }
@@ -3131,9 +3153,13 @@ class ServeCommand extends SessionCommand_1.default {
3131
3153
  code: "SOURCE_README_ERROR",
3132
3154
  });
3133
3155
  }
3134
- // Determine target languages
3156
+ // Determine target languages. On a retry only what is still missing
3157
+ // is translated again, so the languages that already succeeded are
3158
+ // neither paid for twice nor overwritten.
3159
+ const alreadyCompleted = ((_b = notification.syncProgress) === null || _b === void 0 ? void 0 : _b.completedLanguages) || [];
3135
3160
  const availableLanguages = Object.keys(lesson.translations || {});
3136
- const targetLanguages = availableLanguages.filter(lang => lang !== notification.sourceLanguage);
3161
+ const targetLanguages = availableLanguages.filter(lang => lang !== notification.sourceLanguage &&
3162
+ !alreadyCompleted.includes(lang));
3137
3163
  if (targetLanguages.length === 0) {
3138
3164
  return res.status(400).json({
3139
3165
  error: "No target languages found",
@@ -3142,10 +3168,14 @@ class ServeCommand extends SessionCommand_1.default {
3142
3168
  }
3143
3169
  // Remove all other notifications for this lesson
3144
3170
  lesson.syncNotifications =
3145
- ((_b = lesson.syncNotifications) === null || _b === void 0 ? void 0 : _b.filter(n => n.id === notificationId)) ||
3171
+ ((_c = lesson.syncNotifications) === null || _c === void 0 ? void 0 : _c.filter(n => n.id === notificationId)) ||
3146
3172
  [];
3173
+ // Start from a clean slate so a previous failure does not leak into
3174
+ // this run.
3147
3175
  notification.status = "processing";
3176
+ notification.updatedAt = Date.now();
3148
3177
  notification.processingLastUpdate = Date.now();
3178
+ delete notification.error;
3149
3179
  notification.syncProgress = {
3150
3180
  totalLanguages: targetLanguages.length,
3151
3181
  completedLanguages: [], // Array of completed language codes
@@ -3391,7 +3421,7 @@ class ServeCommand extends SessionCommand_1.default {
3391
3421
  const removedCount = initialCount - kept.length;
3392
3422
  exercise.files = kept;
3393
3423
  if (removedCount > 0) {
3394
- await uploadFileToBucket(bucket, JSON.stringify({ config: configJson.config, exercises }), `courses/${courseSlug}/.learn/config.json`);
3424
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify({ config: configJson.config, exercises }), `courses/${courseSlug}/.learn/config.json`);
3395
3425
  }
3396
3426
  if (movedCount > 0 || removedCount > 0) {
3397
3427
  console.log(`[sync-lesson-files] ${lessonSlug}: ${movedCount} moved from subdir, ${removedCount} removed from config`);
@@ -3427,7 +3457,7 @@ class ServeCommand extends SessionCommand_1.default {
3427
3457
  const { exercises } = await (0, configBuilder_1.buildConfig)(bucket, courseSlug);
3428
3458
  const { fixedSidebar, valid } = await (0, sidebarGenerator_1.checkAndFixSidebarPure)(sidebar, exercises, rigoToken);
3429
3459
  if (fixedSidebar) {
3430
- await uploadFileToBucket(bucket, JSON.stringify(fixedSidebar), `courses/${courseSlug}/.learn/sidebar.json`);
3460
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(fixedSidebar), `courses/${courseSlug}/.learn/sidebar.json`);
3431
3461
  return res.json(fixedSidebar);
3432
3462
  }
3433
3463
  }
@@ -3437,12 +3467,12 @@ class ServeCommand extends SessionCommand_1.default {
3437
3467
  if (error.code === 404) {
3438
3468
  const { exercises } = await (0, configBuilder_1.buildConfig)(bucket, courseSlug);
3439
3469
  const exerciseSlugsArray = exercises.map(exercise => exercise.slug);
3440
- const sidebar = await createInitialSidebar(exerciseSlugsArray);
3441
- await uploadFileToBucket(bucket, JSON.stringify(sidebar), `courses/${courseSlug}/.learn/sidebar.json`);
3470
+ const sidebar = await (0, sidebar_1.createInitialSidebar)(exerciseSlugsArray);
3471
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(sidebar), `courses/${courseSlug}/.learn/sidebar.json`);
3442
3472
  if (rigoToken) {
3443
3473
  const { fixedSidebar } = await (0, sidebarGenerator_1.checkAndFixSidebarPure)(sidebar, exercises, rigoToken);
3444
3474
  if (fixedSidebar) {
3445
- await uploadFileToBucket(bucket, JSON.stringify(fixedSidebar), `courses/${courseSlug}/.learn/sidebar.json`);
3475
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(fixedSidebar), `courses/${courseSlug}/.learn/sidebar.json`);
3446
3476
  }
3447
3477
  return res.status(200).json(fixedSidebar);
3448
3478
  }
@@ -3486,7 +3516,7 @@ class ServeCommand extends SessionCommand_1.default {
3486
3516
  }
3487
3517
  const courseSlug = syllabus.courseInfo.slug;
3488
3518
  const tutorialDir = `courses/${courseSlug}`;
3489
- const learnJson = (0, exports.createLearnJson)(syllabus.courseInfo);
3519
+ const learnJson = (0, learnJson_1.createLearnJson)(syllabus.courseInfo);
3490
3520
  try {
3491
3521
  await api_1.default.createRigoPackage(rigoToken, courseSlug, learnJson);
3492
3522
  }
@@ -3495,7 +3525,7 @@ class ServeCommand extends SessionCommand_1.default {
3495
3525
  return res.status(400).json({ error: "Failed to create Rigo package" });
3496
3526
  }
3497
3527
  try {
3498
- await uploadFileToBucket(bucket, JSON.stringify(learnJson), `${tutorialDir}/learn.json`);
3528
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(learnJson), `${tutorialDir}/learn.json`);
3499
3529
  }
3500
3530
  catch (error) {
3501
3531
  console.error("Failed to upload learn.json:", error);
@@ -3509,15 +3539,15 @@ class ServeCommand extends SessionCommand_1.default {
3509
3539
  // eslint-disable-next-line no-await-in-loop
3510
3540
  await uploadInitialReadme(bucket, exSlug, targetDir, syllabus.courseInfo);
3511
3541
  }
3512
- const sidebar = await createInitialSidebar(syllabus.lessons.map(lesson => (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title)), syllabus.courseInfo.language);
3542
+ const sidebar = await (0, sidebar_1.createInitialSidebar)(syllabus.lessons.map(lesson => (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title)), syllabus.courseInfo.language);
3513
3543
  const initialSyllabus = Object.assign(Object.assign({}, syllabus), { lessons: syllabus.lessons.map((lesson, index) => {
3514
3544
  if (index < 1) {
3515
3545
  return Object.assign(Object.assign({}, lesson), { generated: false, status: "GENERATING" });
3516
3546
  }
3517
3547
  return Object.assign(Object.assign({}, lesson), { generated: false, status: "PENDING" });
3518
3548
  }) });
3519
- await uploadFileToBucket(bucket, JSON.stringify(initialSyllabus), `${tutorialDir}/.learn/initialSyllabus.json`);
3520
- await uploadFileToBucket(bucket, JSON.stringify(sidebar), `${tutorialDir}/.learn/sidebar.json`);
3549
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(initialSyllabus), `${tutorialDir}/.learn/initialSyllabus.json`);
3550
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(sidebar), `${tutorialDir}/.learn/sidebar.json`);
3521
3551
  const firstLesson = syllabus.lessons[0];
3522
3552
  const lastResult = "---";
3523
3553
  // Use new two-phase generation workflow
@@ -4220,19 +4250,19 @@ class ServeCommand extends SessionCommand_1.default {
4220
4250
  const [learnJsonContent] = await learnJsonFile.download();
4221
4251
  const learnJson = JSON.parse(learnJsonContent.toString());
4222
4252
  learnJson.slug = newSlug;
4223
- await uploadFileToBucket(bucket, JSON.stringify(learnJson), `${newPrefix}learn.json`);
4253
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(learnJson), `${newPrefix}learn.json`);
4224
4254
  // Update initialSyllabus.json with new slug
4225
4255
  const syllabusFile = bucket.file(`${newPrefix}.learn/initialSyllabus.json`);
4226
4256
  const [syllabusContent] = await syllabusFile.download();
4227
4257
  const syllabus = JSON.parse(syllabusContent.toString());
4228
4258
  syllabus.courseInfo.slug = newSlug;
4229
- await uploadFileToBucket(bucket, JSON.stringify(syllabus), `${newPrefix}.learn/initialSyllabus.json`);
4259
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(syllabus), `${newPrefix}.learn/initialSyllabus.json`);
4230
4260
  // Update config.json with new slug
4231
4261
  const configFile = bucket.file(`${newPrefix}.learn/config.json`);
4232
4262
  const [configContent] = await configFile.download();
4233
4263
  const config = JSON.parse(configContent.toString());
4234
4264
  config.config.slug = newSlug;
4235
- await uploadFileToBucket(bucket, JSON.stringify(config), `${newPrefix}.learn/config.json`);
4265
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(config), `${newPrefix}.learn/config.json`);
4236
4266
  // Update Rigobot package slug
4237
4267
  const updateUrl = `${api_1.RIGOBOT_HOST}/v1/learnpack/package/${currentSlug}/`;
4238
4268
  await axios_1.default.put(updateUrl, { new_slug: newSlug }, {
@@ -4394,7 +4424,7 @@ class ServeCommand extends SessionCommand_1.default {
4394
4424
  defaultBranch,
4395
4425
  lastSyncSHA: commitRes.sha,
4396
4426
  };
4397
- await uploadFileToBucket(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
4427
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
4398
4428
  return res.json({
4399
4429
  success: true,
4400
4430
  repository,
@@ -4568,7 +4598,7 @@ class ServeCommand extends SessionCommand_1.default {
4568
4598
  compareRes.files.length === 0) {
4569
4599
  configJson.config = configJson.config || {};
4570
4600
  configJson.config.github = Object.assign(Object.assign({}, configJson.config.github), { lastSyncSHA: targetSHA });
4571
- await uploadFileToBucket(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
4601
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
4572
4602
  return res.json({
4573
4603
  success: true,
4574
4604
  syncedLessons: [],
@@ -4698,14 +4728,14 @@ class ServeCommand extends SessionCommand_1.default {
4698
4728
  continue;
4699
4729
  }
4700
4730
  const buffer = buffer_1.Buffer.from(content, "base64");
4701
- if (isImageFile(item.relativePath)) {
4731
+ if (item.isAsset || isImageFile(item.relativePath)) {
4702
4732
  // eslint-disable-next-line no-await-in-loop -- Sequential processing to avoid rate limits
4703
- await uploadBinaryToBucket(bucket, buffer, item.bucketPath, "application/octet-stream");
4733
+ await (0, bucketIo_1.uploadBinaryToBucket)(bucket, buffer, item.bucketPath, "application/octet-stream");
4704
4734
  }
4705
4735
  else {
4706
4736
  const text = buffer.toString("utf8");
4707
4737
  // eslint-disable-next-line no-await-in-loop -- Sequential processing to avoid rate limits
4708
- await uploadFileToBucket(bucket, text, item.bucketPath);
4738
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, text, item.bucketPath);
4709
4739
  }
4710
4740
  if (item.isAsset) {
4711
4741
  syncedAssetsUploaded++;
@@ -4718,7 +4748,7 @@ class ServeCommand extends SessionCommand_1.default {
4718
4748
  }
4719
4749
  configJson.config = configJson.config || {};
4720
4750
  configJson.config.github = Object.assign(Object.assign({}, configJson.config.github), { lastSyncSHA: targetSHA });
4721
- await uploadFileToBucket(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
4751
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
4722
4752
  return res.json({
4723
4753
  success: true,
4724
4754
  syncedLessons: [...syncedLessonSlugs],
@@ -4787,7 +4817,7 @@ class ServeCommand extends SessionCommand_1.default {
4787
4817
  });
4788
4818
  configJson.config = configJson.config || {};
4789
4819
  configJson.config.github = Object.assign(Object.assign({}, configJson.config.github), { lastSyncSHA: commitRes.sha });
4790
- await uploadFileToBucket(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
4820
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, JSON.stringify(configJson), `courses/${courseSlug}/.learn/config.json`);
4791
4821
  return res.json({
4792
4822
  success: true,
4793
4823
  repository,
@@ -4862,7 +4892,7 @@ class ServeCommand extends SessionCommand_1.default {
4862
4892
  }
4863
4893
  try {
4864
4894
  const memoryBankPath = `courses/${courseSlug}/.learn/memory_bank.txt`;
4865
- await uploadFileToBucket(bucket, content, memoryBankPath);
4895
+ await (0, bucketIo_1.uploadFileToBucket)(bucket, content, memoryBankPath);
4866
4896
  console.log(`✅ Memory bank updated for course: ${courseSlug}`);
4867
4897
  res.json({ message: "Memory bank updated successfully" });
4868
4898
  }