@learnpack/learnpack 5.0.346 → 5.0.348

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.
@@ -55,6 +55,8 @@ import api, {
55
55
  RIGOBOT_REALTIME_HOST,
56
56
  listUserAcademies,
57
57
  doesAssetExists,
58
+ type AssetSyncError,
59
+ resolveLearnpackPackageId,
58
60
  } from "../utils/api"
59
61
  import {
60
62
  createUploadMiddleware,
@@ -583,6 +585,16 @@ const getLocalizedValue = (
583
585
  return typeof first === "string" ? first : ""
584
586
  }
585
587
 
588
+ function assetSyncErrorDetail(err: any): string {
589
+ if (typeof err?.detail === "string") return err.detail
590
+ if (typeof err?.message === "string") return err.message
591
+ try {
592
+ return JSON.stringify(err)
593
+ } catch {
594
+ return String(err)
595
+ }
596
+ }
597
+
586
598
  const createMultiLangAsset = async (
587
599
  bucket: Bucket,
588
600
  rigoToken: string,
@@ -591,12 +603,27 @@ const createMultiLangAsset = async (
591
603
  courseJson: any,
592
604
  deployUrl: string,
593
605
  academyId?: number
594
- ): Promise<{ errors: Array<{ lang: string; error: any }> }> => {
606
+ ): Promise<{ errors: AssetSyncError[] }> => {
607
+ const learnpackId = await resolveLearnpackPackageId(rigoToken, courseSlug)
608
+ if (learnpackId === null) {
609
+ return {
610
+ errors: [
611
+ {
612
+ kind: "package_error",
613
+ error: {
614
+ detail:
615
+ "Could not resolve Learnpack package id; assets not synced to Breathecode.",
616
+ },
617
+ },
618
+ ],
619
+ }
620
+ }
621
+
595
622
  const availableLangs = Object.keys(courseJson.title)
596
623
  console.log("AVAILABLE LANGUAGES to upload asset", availableLangs)
597
624
 
598
625
  const all_translations: string[] = []
599
- const errors: Array<{ lang: string; error: any }> = []
626
+ const errors: AssetSyncError[] = []
600
627
 
601
628
  for (const lang of availableLangs) {
602
629
  // eslint-disable-next-line no-await-in-loop
@@ -625,14 +652,16 @@ const createMultiLangAsset = async (
625
652
  deployUrl,
626
653
  b64IndexReadme,
627
654
  academyId,
655
+ learnpackId,
628
656
  undefined,
629
657
  all_translations
630
658
  )
631
659
 
632
660
  if (!asset) {
633
661
  errors.push({
662
+ kind: "lang_error",
634
663
  lang,
635
- error: { detail: "Failed to create asset", status_code: 500 },
664
+ error: { detail: "Failed to create asset" },
636
665
  })
637
666
  console.log("No se pudo crear el asset, saltando idioma", lang)
638
667
  continue
@@ -644,7 +673,11 @@ const createMultiLangAsset = async (
644
673
  error && typeof error === "object" && "response" in error ?
645
674
  (error as any).response?.data || error :
646
675
  error
647
- errors.push({ lang, error: errorData })
676
+ errors.push({
677
+ kind: "lang_error",
678
+ lang,
679
+ error: { detail: assetSyncErrorDetail(errorData) },
680
+ })
648
681
  console.error(`Error creating asset for language ${lang}:`, error)
649
682
  }
650
683
  }
@@ -4708,6 +4741,7 @@ class ServeCommand extends SessionCommand {
4708
4741
  }> = []
4709
4742
  let repairedTranslationsInLessons = 0
4710
4743
  let repairedTranslationEntries = 0
4744
+ let fixedLessons = 0
4711
4745
 
4712
4746
  console.log(
4713
4747
  `📋 Checking ${syllabus.lessons.length} lessons in syllabus...`
@@ -4950,6 +4984,35 @@ class ServeCommand extends SessionCommand {
4950
4984
  repairedTranslationsInLessons += 1
4951
4985
  }
4952
4986
  }
4987
+
4988
+ // Fifth pass: fix lessons stuck in GENERATING or ERROR when the file exists in the bucket
4989
+ const primaryLanguage = syllabus.courseInfo.language || "en"
4990
+ for (const lesson of syllabus.lessons) {
4991
+ if (lesson.generated !== false) continue
4992
+ if (lesson.status !== "GENERATING" && lesson.status !== "ERROR")
4993
+ continue
4994
+
4995
+ const candidateSlugs = [
4996
+ slugify(lesson.id + "-" + lesson.title),
4997
+ lesson.uid,
4998
+ ].filter(Boolean) as string[]
4999
+
5000
+ const matchedSlug = candidateSlugs.find(s =>
5001
+ translationsBySlug.has(s)
5002
+ )
5003
+ if (!matchedSlug) continue
5004
+
5005
+ const langs = translationsBySlug.get(matchedSlug) || []
5006
+ if (!langs.includes(primaryLanguage)) continue
5007
+
5008
+ const prevStatus = lesson.status
5009
+ lesson.generated = true
5010
+ lesson.status = "DONE"
5011
+ fixedLessons += 1
5012
+ console.log(
5013
+ `🔧 Fixed lesson: ${lesson.id} - "${lesson.title}" (was generated:false status:${prevStatus}, primary language "${primaryLanguage}" exists in bucket)`
5014
+ )
5015
+ }
4953
5016
  } catch (error) {
4954
5017
  console.error(
4955
5018
  "⚠️ Could not reconcile lesson translations during syllabus sync:",
@@ -4960,11 +5023,12 @@ class ServeCommand extends SessionCommand {
4960
5023
  if (
4961
5024
  totalRemoved > 0 ||
4962
5025
  addedLessons.length > 0 ||
4963
- repairedTranslationsInLessons > 0
5026
+ repairedTranslationsInLessons > 0 ||
5027
+ fixedLessons > 0
4964
5028
  ) {
4965
5029
  await saveSyllabus(courseSlug, syllabus, bucket)
4966
5030
  console.log(
4967
- `✅ Syllabus synchronized. Removed ${removedLessons.length} non-existent, ${duplicatesRemoved.length} duplicate(s); added ${addedLessons.length} from bucket; repaired translations in ${repairedTranslationsInLessons} lesson(s).`
5031
+ `✅ Syllabus synchronized. Removed ${removedLessons.length} non-existent, ${duplicatesRemoved.length} duplicate(s); added ${addedLessons.length} from bucket; repaired translations in ${repairedTranslationsInLessons} lesson(s); fixed ${fixedLessons} stuck lesson(s).`
4968
5032
  )
4969
5033
  } else {
4970
5034
  console.log(`✅ Syllabus is already in sync. No changes.`)
@@ -4978,6 +5042,7 @@ class ServeCommand extends SessionCommand {
4978
5042
  removedLessons: removedLessons.length,
4979
5043
  duplicatesResolved: duplicatesRemoved.length,
4980
5044
  addedLessons: addedLessons.length,
5045
+ fixedLessons,
4981
5046
  repairedTranslationsInLessons,
4982
5047
  repairedTranslationEntries,
4983
5048
  removed: removedLessons,
@@ -5620,38 +5685,76 @@ class ServeCommand extends SessionCommand {
5620
5685
  const archive = archiver("zip", { zlib: { level: 9 } })
5621
5686
 
5622
5687
  output.on("close", async () => {
5623
- // 10) Subir ZIP a RigoBot
5624
- const form = new FormData()
5625
- form.append("file", fs.createReadStream(zipPath))
5626
- form.append("config", JSON.stringify(config))
5627
-
5628
- const rigoRes = await axios.post(
5629
- `${RIGOBOT_HOST}/v1/learnpack/upload`,
5630
- form,
5631
- {
5632
- headers: {
5633
- ...form.getHeaders(),
5634
- Authorization: "Token " + rigoToken.trim(),
5635
- },
5636
- }
5637
- )
5688
+ let rigoPublishUrl: string | undefined
5689
+ try {
5690
+ // 10) Subir ZIP a RigoBot
5691
+ const form = new FormData()
5692
+ form.append("file", fs.createReadStream(zipPath))
5693
+ form.append("config", JSON.stringify(config))
5694
+
5695
+ const rigoRes = await axios.post(
5696
+ `${RIGOBOT_HOST}/v1/learnpack/upload`,
5697
+ form,
5698
+ {
5699
+ headers: {
5700
+ ...form.getHeaders(),
5701
+ Authorization: "Token " + rigoToken.trim(),
5702
+ },
5703
+ }
5704
+ )
5705
+ rigoPublishUrl = rigoRes.data.url
5638
5706
 
5639
- const assetResults = await createMultiLangAsset(
5640
- bucket,
5641
- rigoToken,
5642
- bcToken,
5643
- slug,
5644
- fullConfig.config,
5645
- rigoRes.data.url,
5646
- academyId
5647
- )
5707
+ let errors: AssetSyncError[]
5708
+ try {
5709
+ const assetResults = await createMultiLangAsset(
5710
+ bucket,
5711
+ rigoToken,
5712
+ bcToken,
5713
+ slug,
5714
+ fullConfig.config,
5715
+ rigoRes.data.url,
5716
+ academyId
5717
+ )
5718
+ errors = assetResults.errors
5719
+ } catch (error) {
5720
+ console.error("Asset sync failed unexpectedly:", error)
5721
+ errors = [
5722
+ {
5723
+ kind: "package_error",
5724
+ error: { detail: "Asset sync failed unexpectedly." },
5725
+ },
5726
+ ]
5727
+ }
5648
5728
 
5649
- rimraf.sync(tmpRoot)
5650
- console.log("RigoRes", rigoRes.data)
5651
- return res.json({
5652
- url: rigoRes.data.url,
5653
- errors: assetResults.errors,
5654
- })
5729
+ if (res.headersSent) return
5730
+ console.log("RigoRes", rigoRes.data)
5731
+ res.json({
5732
+ url: rigoPublishUrl,
5733
+ errors,
5734
+ })
5735
+ } catch (error) {
5736
+ console.error(error)
5737
+ if (res.headersSent) return
5738
+ if (rigoPublishUrl !== undefined) {
5739
+ res.json({
5740
+ url: rigoPublishUrl,
5741
+ errors: [
5742
+ {
5743
+ kind: "package_error",
5744
+ error: { detail: "Asset sync failed unexpectedly." },
5745
+ },
5746
+ ],
5747
+ })
5748
+ } else {
5749
+ res.status(500).json({ error: (error as Error).message })
5750
+ }
5751
+ } finally {
5752
+ try {
5753
+ rimraf.sync(tmpRoot)
5754
+ } catch (error) {
5755
+ console.error("rimraf tmpRoot:", error)
5756
+ }
5757
+ }
5655
5758
  })
5656
5759
 
5657
5760
  archive.on("error", err => {