@learnpack/learnpack 5.0.358 → 5.0.359

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,9 +55,7 @@ import api, {
55
55
  RIGOBOT_HOST,
56
56
  RIGOBOT_REALTIME_HOST,
57
57
  listUserAcademies,
58
- doesAssetExists,
59
58
  type AssetSyncError,
60
- resolveLearnpackPackageId,
61
59
  } from "../utils/api"
62
60
  import {
63
61
  createUploadMiddleware,
@@ -66,7 +64,12 @@ import {
66
64
  import { buildConfig } from "../utils/configBuilder"
67
65
  import { checkReadability, slugify } from "../utils/creatorUtilities"
68
66
  import { checkAndFixSidebarPure } from "../utils/sidebarGenerator"
69
- import { handleAssetCreation } from "./publish"
67
+ import {
68
+ determineAcademyMode,
69
+ hasPublishedAsset,
70
+ resolvePackageAssets,
71
+ } from "../utils/assetResolver"
72
+ import { syncPackageAssets } from "../utils/assetSync"
70
73
  import {
71
74
  FormState,
72
75
  Lesson,
@@ -538,36 +541,6 @@ const cleanFormStateForSyllabus = (formState: FormState) => {
538
541
  }
539
542
  }
540
543
 
541
- const getLocalizedValue = (
542
- translations: Record<string, any> | undefined,
543
- lang: string,
544
- fallbackLangs: string[] = ["en", "us"]
545
- ): string => {
546
- if (!translations || typeof translations !== "object") return ""
547
-
548
- const direct = translations[lang]
549
- if (typeof direct === "string" && direct.trim().length > 0) return direct
550
-
551
- for (const fb of fallbackLangs) {
552
- const v = translations[fb]
553
- if (typeof v === "string" && v.trim().length > 0) return v
554
- }
555
-
556
- const firstKey = Object.keys(translations)[0]
557
- const first = firstKey ? translations[firstKey] : ""
558
- return typeof first === "string" ? first : ""
559
- }
560
-
561
- function assetSyncErrorDetail(err: any): string {
562
- if (typeof err?.detail === "string") return err.detail
563
- if (typeof err?.message === "string") return err.message
564
- try {
565
- return JSON.stringify(err)
566
- } catch {
567
- return String(err)
568
- }
569
- }
570
-
571
544
  const createMultiLangAsset = async (
572
545
  bucket: Bucket,
573
546
  rigoToken: string,
@@ -577,85 +550,38 @@ const createMultiLangAsset = async (
577
550
  deployUrl: string,
578
551
  academyId?: number
579
552
  ): Promise<{ errors: AssetSyncError[] }> => {
580
- const learnpackId = await resolveLearnpackPackageId(rigoToken, courseSlug)
581
- if (learnpackId === null) {
582
- return {
583
- errors: [
584
- {
585
- kind: "package_error",
586
- error: {
587
- detail:
588
- "Could not resolve Learnpack package id; assets not synced to Breathecode.",
589
- },
590
- },
591
- ],
592
- }
593
- }
594
-
595
- const availableLangs = Object.keys(courseJson.title)
596
- console.log("AVAILABLE LANGUAGES to upload asset", availableLangs)
597
-
598
- const all_translations: string[] = []
599
- const errors: AssetSyncError[] = []
600
-
601
- for (const lang of availableLangs) {
602
- // eslint-disable-next-line no-await-in-loop
603
- const indexReadme = await bucket.file(
553
+ const readReadme = async (lang: string): Promise<string> => {
554
+ const indexReadme = bucket.file(
604
555
  `courses/${courseSlug}/README${getReadmeExtension(lang)}`
605
556
  )
606
- let indexReadmeString = ""
607
557
  try {
608
- // eslint-disable-next-line no-await-in-loop
609
- const [indexReadmeContent] = await indexReadme.download()
610
- indexReadmeString = indexReadmeContent.toString()
558
+ const [content] = await indexReadme.download()
559
+ return content.toString()
611
560
  } catch (error) {
612
561
  console.error("Error downloading index readme", error)
613
562
  // TODO: Trigger generation of the index readme
614
- indexReadmeString = ""
563
+ return ""
615
564
  }
565
+ }
616
566
 
617
- const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
618
-
619
- try {
620
- // eslint-disable-next-line no-await-in-loop
621
- const asset = await handleAssetCreation(
622
- { token: bcToken, rigobotToken: rigoToken.trim() },
623
- courseJson,
624
- lang,
625
- deployUrl,
626
- b64IndexReadme,
627
- academyId,
628
- learnpackId,
629
- undefined,
630
- all_translations
631
- )
632
-
633
- if (!asset) {
634
- errors.push({
635
- kind: "lang_error",
636
- lang,
637
- error: { detail: "Failed to create asset" },
638
- })
639
- console.log("No se pudo crear el asset, saltando idioma", lang)
640
- continue
641
- }
567
+ const result = await syncPackageAssets({
568
+ bcToken,
569
+ rigoToken,
570
+ packageSlug: courseSlug,
571
+ learnJson: courseJson,
572
+ deployUrl,
573
+ academyId,
574
+ readReadme,
575
+ })
642
576
 
643
- all_translations.push(asset.slug)
644
- } catch (error) {
645
- const errorData =
646
- error && typeof error === "object" && "response" in error ?
647
- (error as any).response?.data || error :
648
- error
649
- errors.push({
650
- kind: "lang_error",
651
- lang,
652
- error: { detail: assetSyncErrorDetail(errorData) },
653
- })
654
- console.error(`Error creating asset for language ${lang}:`, error)
655
- }
577
+ for (const warning of result.warnings) console.warn("[assets]", warning)
578
+ for (const synced of result.synced) {
579
+ console.log(
580
+ `[assets] ${synced.action} "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`
581
+ )
656
582
  }
657
583
 
658
- return { errors }
584
+ return { errors: result.errors }
659
585
  }
660
586
 
661
587
  const lessonCleaner = (lesson: Lesson) => {
@@ -5392,6 +5318,10 @@ class ServeCommand extends SessionCommand {
5392
5318
  })
5393
5319
  }
5394
5320
 
5321
+ // Older IDEs only send the Breathecode token; without the Rigobot
5322
+ // token the lookup falls back to the title-derived slugs.
5323
+ const rigoToken = req.header("x-rigo-token")
5324
+
5395
5325
  const configFile = await bucket.file(
5396
5326
  `courses/${slug}/.learn/config.json`
5397
5327
  )
@@ -5399,56 +5329,51 @@ class ServeCommand extends SessionCommand {
5399
5329
  const configJson = JSON.parse(configContent.toString())
5400
5330
  const { config } = configJson
5401
5331
 
5402
- const availableLangs = Object.keys(config.title || {})
5403
- let isPublished = false
5404
-
5405
- // Collect academy ids from all existing assets
5406
- const foundAcademyIds: number[] = []
5407
-
5408
- const slugsToCheck = availableLangs
5409
- .map(lang => {
5410
- const assetTitle = getLocalizedValue(config.title, lang)
5411
- if (!assetTitle) return null
5412
- const assetSlug = slugify(assetTitle).slice(0, 47)
5413
- return `${assetSlug}-${lang}`
5414
- })
5415
- .filter((slug): slug is string => slug !== null)
5332
+ const resolution = await resolvePackageAssets({
5333
+ bcToken,
5334
+ rigoToken,
5335
+ packageSlug: slug,
5336
+ learnJson: config,
5337
+ })
5338
+ for (const warning of resolution.warnings) {
5339
+ console.warn("[package-academy]", warning)
5340
+ }
5416
5341
 
5417
- const assetChecks = await Promise.all(
5418
- slugsToCheck.map(slug => doesAssetExists(bcToken, slug))
5419
- )
5342
+ const isPublished = hasPublishedAsset(resolution)
5343
+ const academyMode = determineAcademyMode(resolution)
5344
+ const warnings = [...resolution.warnings]
5345
+ if (resolution.packageState === "unknown") {
5346
+ warnings.push(
5347
+ `${resolution.packageError}; existing assets could not be checked.`
5348
+ )
5349
+ }
5420
5350
 
5421
- for (const { exists, academyId: existingAcademyId } of assetChecks) {
5422
- if (exists) {
5423
- isPublished = true
5424
- if (existingAcademyId !== undefined) {
5425
- foundAcademyIds.push(existingAcademyId)
5426
- }
5351
+ for (const [lang, entry] of resolution.byLang) {
5352
+ if (entry.status === "unknown") {
5353
+ warnings.push(`"${lang}": ${entry.error}`)
5354
+ } else if (entry.status === "ambiguous") {
5355
+ warnings.push(
5356
+ `"${lang}": ${
5357
+ entry.candidates.length
5358
+ } assets found (${entry.candidates
5359
+ .map(c => `#${c.id} ${c.slug}`)
5360
+ .join(", ")})`
5361
+ )
5427
5362
  }
5428
5363
  }
5429
5364
 
5430
- const uniqueAcademies = [...new Set(foundAcademyIds)]
5431
-
5432
- type AcademyMode = "select" | "locked" | "conflict";
5433
- let mode: AcademyMode
5434
- let lockedAcademyId: number | undefined
5435
- let conflictAcademies: number[] | undefined
5436
-
5437
- if (uniqueAcademies.length === 0) {
5438
- mode = "select"
5439
- } else if (uniqueAcademies.length === 1) {
5440
- mode = "locked"
5441
- lockedAcademyId = uniqueAcademies[0]
5442
- } else {
5443
- mode = "conflict"
5444
- conflictAcademies = uniqueAcademies
5445
- }
5365
+ const mode = academyMode.type
5366
+ const lockedAcademyId =
5367
+ academyMode.type === "locked" ? academyMode.academyId : undefined
5368
+ const conflictAcademies =
5369
+ academyMode.type === "conflict" ? academyMode.academies : undefined
5446
5370
 
5447
5371
  return res.json({
5448
5372
  isPublished,
5449
5373
  mode,
5450
5374
  lockedAcademyId,
5451
5375
  conflictAcademies,
5376
+ warnings,
5452
5377
  })
5453
5378
  } catch (error) {
5454
5379
  console.error("Error fetching package academy:", error)