@learnpack/learnpack 5.0.358 → 5.0.360

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 (47) hide show
  1. package/lib/commands/publish.d.ts +0 -10
  2. package/lib/commands/publish.js +52 -168
  3. package/lib/commands/serve.js +52 -116
  4. package/lib/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
  5. package/lib/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
  6. package/lib/creatorDist/index.html +2 -2
  7. package/lib/utils/api.d.ts +62 -15
  8. package/lib/utils/api.js +124 -45
  9. package/lib/utils/assetResolver.d.ts +116 -0
  10. package/lib/utils/assetResolver.js +268 -0
  11. package/lib/utils/assetSync.d.ts +48 -0
  12. package/lib/utils/assetSync.js +267 -0
  13. package/lib/utils/creatorUtilities.d.ts +1 -0
  14. package/lib/utils/creatorUtilities.js +17 -1
  15. package/package.json +1 -1
  16. package/src/commands/publish.ts +61 -255
  17. package/src/commands/serve.ts +65 -140
  18. package/src/creator/src/App.test.tsx +338 -0
  19. package/src/creator/src/App.tsx +152 -26
  20. package/src/creator/src/components/CourseLanguagePicker.tsx +51 -0
  21. package/src/creator/src/components/LanguageDetectionModal.tsx +29 -9
  22. package/src/creator/src/components/StepWizard.tsx +11 -3
  23. package/src/creator/src/components/Uploader.test.tsx +65 -0
  24. package/src/creator/src/components/Uploader.tsx +9 -3
  25. package/src/creator/src/locales/en.json +7 -0
  26. package/src/creator/src/locales/es.json +7 -0
  27. package/src/creator/src/utils/courseLanguage.test.ts +215 -0
  28. package/src/creator/src/utils/courseLanguage.ts +173 -0
  29. package/src/creator/src/utils/creationRequest.test.ts +104 -0
  30. package/src/creator/src/utils/creationRequest.ts +63 -0
  31. package/src/creator/src/utils/store.ts +7 -0
  32. package/src/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
  33. package/src/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
  34. package/src/creatorDist/index.html +2 -2
  35. package/src/ui/_app/app.css +1 -1
  36. package/src/ui/_app/app.js +2105 -2105
  37. package/src/ui/_app/sw.js +13 -0
  38. package/src/ui/app.tar.gz +0 -0
  39. package/src/utils/api.ts +157 -42
  40. package/src/utils/assetResolver.ts +368 -0
  41. package/src/utils/assetSync.ts +343 -0
  42. package/src/utils/awsCredentials.ts +57 -57
  43. package/src/utils/creatorUtilities.ts +16 -0
  44. package/src/utils/descriptions/publishStage.ts +394 -394
  45. package/src/utils/descriptions/resumePublication.ts +217 -217
  46. package/src/utils/descriptions/s3Storage.ts +214 -214
  47. package/src/utils/s3/packageManifestBackfill.ts +771 -771
@@ -19,282 +19,78 @@ import api, { getConsumable, RIGOBOT_HOST, TAcademy } from "../utils/api"
19
19
  import * as prompts from "prompts"
20
20
  import { isValidRigoToken } from "../utils/rigoActions"
21
21
  import { minutesToISO8601Duration } from "../utils/misc"
22
- import { getReadmeExtension, slugify } from "../utils/creatorUtilities"
22
+ import { getReadmeExtension } from "../utils/creatorUtilities"
23
23
  import { getFallbackPreviewUrl } from "../utils/branding"
24
+ import {
25
+ determineAcademyMode,
26
+ getAvailableLangs,
27
+ PackageAssetsResolution,
28
+ resolvePackageAssets,
29
+ } from "../utils/assetResolver"
30
+ import { getLocalizedValue, syncPackageAssets } from "../utils/assetSync"
24
31
 
25
32
  const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"
26
33
 
27
- const getAvailableLangs = (learnJson: any): string[] => {
28
- const langs = Object.keys(learnJson?.title || {})
29
- return langs.filter((l) => typeof l === "string" && l.length > 0)
30
- }
31
-
32
34
  const getDefaultLang = (learnJson: any): string => {
33
35
  const availableLangs = getAvailableLangs(learnJson)
34
36
  if (availableLangs.length === 0) return "en"
35
37
  return availableLangs.includes("en") ? "en" : availableLangs[0]
36
38
  }
37
39
 
38
- const getLocalizedValue = (
39
- translations: Record<string, any> | undefined,
40
- lang: string,
41
- fallbackLangs: string[] = ["en", "us"]
42
- ): string => {
43
- if (!translations || typeof translations !== "object") return ""
44
-
45
- const direct = translations[lang]
46
- if (typeof direct === "string" && direct.trim().length > 0) return direct
47
-
48
- for (const fb of fallbackLangs) {
49
- const v = translations[fb]
50
- if (typeof v === "string" && v.trim().length > 0) return v
51
- }
52
-
53
- const firstKey = Object.keys(translations)[0]
54
- const first = firstKey ? translations[firstKey] : ""
55
- return typeof first === "string" ? first : ""
56
- }
57
-
58
- type ExistingAssetInfo = {
59
- lang: string;
60
- slug: string;
61
- exists: boolean;
62
- academyId?: number;
63
- };
64
-
65
- type AcademyMode =
66
- | { type: "select" }
67
- | { type: "locked"; academyId: number }
68
- | { type: "conflict"; academies: number[] };
69
-
70
- const getExistingAssetsInfo = async (
71
- token: string,
72
- learnJson: any
73
- ): Promise<ExistingAssetInfo[]> => {
74
- const availableLangs = getAvailableLangs(learnJson)
75
- const results: ExistingAssetInfo[] = []
76
-
77
- for (const lang of availableLangs) {
78
- const assetTitle = getLocalizedValue(learnJson?.title, lang)
79
- if (!assetTitle) continue
80
-
81
- let slug = slugify(assetTitle).slice(0, 47)
82
- slug = `${slug}-${lang}`
83
-
84
- // eslint-disable-next-line no-await-in-loop
85
- const { exists, academyId } = await api.doesAssetExists(token, slug)
86
- results.push({ lang, slug, exists, academyId })
87
- }
88
-
89
- return results
90
- }
91
-
92
- const determinePublishAcademyMode = (
93
- existingAssets: ExistingAssetInfo[]
94
- ): AcademyMode => {
95
- const academyIds = existingAssets
96
- .filter((a) => a.exists && a.academyId !== undefined)
97
- .map((a) => a.academyId as number)
98
-
99
- const unique = [...new Set(academyIds)]
100
-
101
- if (unique.length === 0) return { type: "select" }
102
- if (unique.length === 1) return { type: "locked", academyId: unique[0] }
103
- return { type: "conflict", academies: unique }
104
- }
105
-
106
- export const handleAssetCreation = async (
107
- sessionPayload: { token: string; rigobotToken: string },
108
- learnJson: any,
109
- selectedLang: string,
110
- learnpackDeployUrl: string,
111
- b64IndexReadme: string,
112
- academyId: number | undefined,
113
- learnpackId: number,
114
- preflightInfo?: ExistingAssetInfo,
115
- all_translations: string[] = []
116
- ) => {
117
- const category = "uncategorized"
118
-
40
+ const readReadmeFromDisk = async (lang: string): Promise<string> => {
41
+ const readmePath = path.join(
42
+ process.cwd(),
43
+ `README${getReadmeExtension(lang)}`
44
+ )
119
45
  try {
120
- const user = await api.validateToken(sessionPayload.token)
121
-
122
- const assetTitle = getLocalizedValue(learnJson?.title, selectedLang)
123
- const assetDescription = getLocalizedValue(
124
- learnJson?.description,
125
- selectedLang
126
- )
127
-
128
- if (!assetTitle) {
129
- throw new Error(
130
- `Missing learn.json title for language "${selectedLang}"`
131
- )
132
- }
133
-
134
- let slug = slugify(assetTitle).slice(0, 47)
135
- slug = `${slug}-${selectedLang}`
136
-
137
- // Use pre-flight info when available to avoid an extra GET request
138
- const { exists, academyId: existingAcademyId } =
139
- preflightInfo ?? (await api.doesAssetExists(sessionPayload.token, slug))
140
-
141
- // const technologies: unknown[] = Array.isArray(learnJson?.technologies) ?
142
- // learnJson.technologies :
143
- // []
144
-
145
- if (!exists) {
146
- Console.info("Asset does not exist in this academy, creating it")
147
- const assetPayload: any = {
148
- slug: slug,
149
- title: assetTitle,
150
- lang: selectedLang,
151
- graded: true,
152
- description: assetDescription,
153
- learnpack_deploy_url: learnpackDeployUrl,
154
- // technologies: technologies.map((tech: unknown) =>
155
- // String(tech).toLowerCase().replace(/\s+/g, "-")
156
- // ),
157
- technologies: [],
158
- url: learnpackDeployUrl,
159
- category: category,
160
- owner: user.id,
161
- author: user.id,
162
- preview: learnJson.preview,
163
- readme_raw: b64IndexReadme,
164
- all_translations,
165
- learnpack_id: learnpackId,
166
- }
167
- if (academyId !== undefined) {
168
- assetPayload.academy_id = academyId
169
- }
170
-
171
- const asset = await api.createAsset(sessionPayload.token, assetPayload)
172
- try {
173
- await api.updateRigoPackage(
174
- sessionPayload.rigobotToken.trim(),
175
- learnJson.slug,
176
- {
177
- asset_id: asset.id,
178
- }
179
- )
180
- } catch (error) {
181
- Console.error("Error updating Rigo package:", error)
182
- }
183
-
184
- Console.info("Asset created with id", asset.id)
185
- return asset
186
- }
187
-
188
- Console.info("Asset exists, updating it")
189
- const updatePayload: any = {
190
- graded: true,
191
- learnpack_deploy_url: learnpackDeployUrl,
192
- title: assetTitle,
193
- category: category,
194
- description: assetDescription,
195
- all_translations,
196
- learnpack_id: learnpackId,
197
- }
198
- // Only set academy when the asset has none yet and the user selected one
199
- if (existingAcademyId === undefined && academyId !== undefined) {
200
- updatePayload.academy_id = academyId
201
- }
202
-
203
- const asset = await api.updateAsset(
204
- sessionPayload.token,
205
- slug,
206
- updatePayload
207
- )
208
- try {
209
- await api.updateRigoPackage(
210
- sessionPayload.rigobotToken.trim(),
211
- learnJson.slug,
212
- {
213
- asset_id: asset.id,
214
- }
215
- )
216
- } catch (error) {
217
- Console.error("Error updating Rigo package:", error)
218
- }
219
-
220
- Console.info("Asset updated with id", asset.id)
221
- return asset
46
+ return fs.existsSync(readmePath) ?
47
+ fs.readFileSync(readmePath, "utf-8") :
48
+ ""
222
49
  } catch (error) {
223
- Console.error("Error updating or creating asset:", error)
224
- throw error
50
+ Console.error("Error reading index readme:", error)
51
+ return ""
225
52
  }
226
53
  }
227
54
 
228
- const createMultiLangAssetFromDisk = async (
55
+ const syncAssetsFromDisk = async (
229
56
  sessionPayload: { token: string; rigobotToken: string },
230
57
  learnJson: any,
231
58
  deployUrl: string,
232
- selectedAcademyId?: number,
233
- existingAssetsInfo: ExistingAssetInfo[] = []
59
+ selectedAcademyId: number | undefined,
60
+ resolution: PackageAssetsResolution
234
61
  ) => {
235
- const availableLangs = getAvailableLangs(learnJson)
236
-
237
- if (availableLangs.length === 0) {
62
+ if (getAvailableLangs(learnJson).length === 0) {
238
63
  Console.error(
239
64
  "No languages found in learn.json.title. Add at least one language (e.g. title.en)."
240
65
  )
241
66
  return
242
67
  }
243
68
 
244
- const learnpackId = await api.resolveLearnpackPackageId(
245
- sessionPayload.rigobotToken,
246
- learnJson.slug
247
- )
248
- if (learnpackId === null) {
249
- Console.warning(
250
- "Breathecode assets skipped: could not resolve Learnpack package id"
251
- )
252
- return
253
- }
69
+ // The pre-upload resolution is reused unless the package did not exist in
70
+ // Rigobot yet (first publish): the upload has just created it.
71
+ const result = await syncPackageAssets({
72
+ bcToken: sessionPayload.token,
73
+ rigoToken: sessionPayload.rigobotToken,
74
+ packageSlug: learnJson.slug,
75
+ learnJson,
76
+ deployUrl,
77
+ academyId: selectedAcademyId,
78
+ readReadme: readReadmeFromDisk,
79
+ resolution: resolution.learnpackId === null ? undefined : resolution,
80
+ })
254
81
 
255
- const all_translations: string[] = []
256
- for (const lang of availableLangs) {
257
- const readmePath = path.join(
258
- process.cwd(),
259
- `README${getReadmeExtension(lang)}`
82
+ for (const warning of result.warnings) Console.warning(warning)
83
+ for (const synced of result.synced) {
84
+ Console.info(
85
+ `Asset ${synced.action} for "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`
260
86
  )
87
+ }
261
88
 
262
- let indexReadmeString = ""
263
- try {
264
- if (fs.existsSync(readmePath)) {
265
- indexReadmeString = fs.readFileSync(readmePath, "utf-8")
266
- }
267
- } catch (error) {
268
- Console.error("Error reading index readme:", error)
269
- indexReadmeString = ""
270
- }
271
-
272
- const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
273
- const preflightInfo = existingAssetsInfo.find((a) => a.lang === lang)
274
-
275
- try {
276
- // eslint-disable-next-line no-await-in-loop
277
- const asset = await handleAssetCreation(
278
- sessionPayload,
279
- learnJson,
280
- lang,
281
- deployUrl,
282
- b64IndexReadme,
283
- selectedAcademyId,
284
- learnpackId,
285
- preflightInfo,
286
- all_translations
287
- )
288
-
289
- if (!asset) {
290
- Console.debug("Could not create/update asset for lang", lang)
291
- continue
292
- }
293
-
294
- all_translations.push(asset.slug)
295
- } catch (error) {
296
- Console.error("Error creating asset for language", lang, error)
297
- // Continue with other languages
89
+ for (const error of result.errors) {
90
+ if (error.kind === "lang_error") {
91
+ Console.error(`Asset for "${error.lang}": ${error.error.detail}`)
92
+ } else {
93
+ Console.error(error.error.detail)
298
94
  }
299
95
  }
300
96
  }
@@ -488,11 +284,21 @@ class BuildCommand extends SessionCommand {
488
284
  }
489
285
 
490
286
  Console.info("Checking existing assets...")
491
- const existingAssetsInfo = await getExistingAssetsInfo(
492
- sessionPayload.token,
493
- learnJson
494
- )
495
- const academyMode = determinePublishAcademyMode(existingAssetsInfo)
287
+ const resolution = await resolvePackageAssets({
288
+ bcToken: sessionPayload.token,
289
+ rigoToken: rigoToken,
290
+ packageSlug: learnJson.slug,
291
+ learnJson,
292
+ })
293
+ for (const warning of resolution.warnings) Console.warning(warning)
294
+ if (resolution.packageState === "unknown") {
295
+ Console.error(
296
+ `${resolution.packageError}. Existing assets cannot be checked, so publishing now could create duplicates. Try again later.`
297
+ )
298
+ process.exit(1)
299
+ }
300
+
301
+ const academyMode = determineAcademyMode(resolution)
496
302
 
497
303
  let selectedAcademyId: number | undefined
498
304
 
@@ -688,12 +494,12 @@ class BuildCommand extends SessionCommand {
688
494
  fs.unlinkSync(zipFilePath)
689
495
  this.removeDirectory(buildDir)
690
496
 
691
- await createMultiLangAssetFromDisk(
497
+ await syncAssetsFromDisk(
692
498
  { token: sessionPayload.token, rigobotToken: rigoToken },
693
499
  learnJson,
694
500
  res.data.url,
695
501
  selectedAcademyId,
696
- existingAssetsInfo
502
+ resolution
697
503
  )
698
504
  } catch (error) {
699
505
  if (axios.isAxiosError(error)) {
@@ -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)