@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.
@@ -8,7 +8,7 @@ type ExistingAssetInfo = {
8
8
  export declare const handleAssetCreation: (sessionPayload: {
9
9
  token: string;
10
10
  rigobotToken: string;
11
- }, learnJson: any, selectedLang: string, learnpackDeployUrl: string, b64IndexReadme: string, academyId: number | undefined, preflightInfo?: ExistingAssetInfo, all_translations?: string[]) => Promise<any>;
11
+ }, learnJson: any, selectedLang: string, learnpackDeployUrl: string, b64IndexReadme: string, academyId: number | undefined, learnpackId: number, preflightInfo?: ExistingAssetInfo, all_translations?: string[]) => Promise<any>;
12
12
  declare class BuildCommand extends SessionCommand {
13
13
  static description: string;
14
14
  static flags: {
@@ -71,7 +71,7 @@ const determinePublishAcademyMode = (existingAssets) => {
71
71
  return { type: "locked", academyId: unique[0] };
72
72
  return { type: "conflict", academies: unique };
73
73
  };
74
- const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, learnpackDeployUrl, b64IndexReadme, academyId, preflightInfo, all_translations = []) => {
74
+ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, learnpackDeployUrl, b64IndexReadme, academyId, learnpackId, preflightInfo, all_translations = []) => {
75
75
  const category = "uncategorized";
76
76
  try {
77
77
  const user = await api_1.default.validateToken(sessionPayload.token);
@@ -107,6 +107,7 @@ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, lear
107
107
  preview: learnJson.preview,
108
108
  readme_raw: b64IndexReadme,
109
109
  all_translations,
110
+ learnpack_id: learnpackId,
110
111
  };
111
112
  if (academyId !== undefined) {
112
113
  assetPayload.academy_id = academyId;
@@ -131,6 +132,7 @@ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, lear
131
132
  category: category,
132
133
  description: assetDescription,
133
134
  all_translations,
135
+ learnpack_id: learnpackId,
134
136
  };
135
137
  // Only set academy when the asset has none yet and the user selected one
136
138
  if (existingAcademyId === undefined && academyId !== undefined) {
@@ -160,6 +162,11 @@ const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl
160
162
  console_1.default.error("No languages found in learn.json.title. Add at least one language (e.g. title.en).");
161
163
  return;
162
164
  }
165
+ const learnpackId = await api_1.default.resolveLearnpackPackageId(sessionPayload.rigobotToken, learnJson.slug);
166
+ if (learnpackId === null) {
167
+ console_1.default.warning("Breathecode assets skipped: could not resolve Learnpack package id");
168
+ return;
169
+ }
163
170
  const all_translations = [];
164
171
  for (const lang of availableLangs) {
165
172
  const readmePath = path.join(process.cwd(), `README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
@@ -177,7 +184,7 @@ const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl
177
184
  const preflightInfo = existingAssetsInfo.find((a) => a.lang === lang);
178
185
  try {
179
186
  // eslint-disable-next-line no-await-in-loop
180
- const asset = await (0, exports.handleAssetCreation)(sessionPayload, learnJson, lang, deployUrl, b64IndexReadme, selectedAcademyId, preflightInfo, all_translations);
187
+ const asset = await (0, exports.handleAssetCreation)(sessionPayload, learnJson, lang, deployUrl, b64IndexReadme, selectedAcademyId, learnpackId, preflightInfo, all_translations);
181
188
  if (!asset) {
182
189
  console_1.default.debug("Could not create/update asset for lang", lang);
183
190
  continue;
@@ -365,8 +365,33 @@ const getLocalizedValue = (translations, lang, fallbackLangs = ["en", "us"]) =>
365
365
  const first = firstKey ? translations[firstKey] : "";
366
366
  return typeof first === "string" ? first : "";
367
367
  };
368
+ function assetSyncErrorDetail(err) {
369
+ if (typeof (err === null || err === void 0 ? void 0 : err.detail) === "string")
370
+ return err.detail;
371
+ if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string")
372
+ return err.message;
373
+ try {
374
+ return JSON.stringify(err);
375
+ }
376
+ catch (_a) {
377
+ return String(err);
378
+ }
379
+ }
368
380
  const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, courseJson, deployUrl, academyId) => {
369
381
  var _a;
382
+ const learnpackId = await (0, api_1.resolveLearnpackPackageId)(rigoToken, courseSlug);
383
+ if (learnpackId === null) {
384
+ return {
385
+ errors: [
386
+ {
387
+ kind: "package_error",
388
+ error: {
389
+ detail: "Could not resolve Learnpack package id; assets not synced to Breathecode.",
390
+ },
391
+ },
392
+ ],
393
+ };
394
+ }
370
395
  const availableLangs = Object.keys(courseJson.title);
371
396
  console.log("AVAILABLE LANGUAGES to upload asset", availableLangs);
372
397
  const all_translations = [];
@@ -388,11 +413,12 @@ const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, cour
388
413
  const b64IndexReadme = buffer_1.Buffer.from(indexReadmeString).toString("base64");
389
414
  try {
390
415
  // eslint-disable-next-line no-await-in-loop
391
- const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, academyId, undefined, all_translations);
416
+ const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, academyId, learnpackId, undefined, all_translations);
392
417
  if (!asset) {
393
418
  errors.push({
419
+ kind: "lang_error",
394
420
  lang,
395
- error: { detail: "Failed to create asset", status_code: 500 },
421
+ error: { detail: "Failed to create asset" },
396
422
  });
397
423
  console.log("No se pudo crear el asset, saltando idioma", lang);
398
424
  continue;
@@ -403,7 +429,11 @@ const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, cour
403
429
  const errorData = error && typeof error === "object" && "response" in error ?
404
430
  ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error :
405
431
  error;
406
- errors.push({ lang, error: errorData });
432
+ errors.push({
433
+ kind: "lang_error",
434
+ lang,
435
+ error: { detail: assetSyncErrorDetail(errorData) },
436
+ });
407
437
  console.error(`Error creating asset for language ${lang}:`, error);
408
438
  }
409
439
  }
@@ -3241,6 +3271,7 @@ class ServeCommand extends SessionCommand_1.default {
3241
3271
  const addedLessons = [];
3242
3272
  let repairedTranslationsInLessons = 0;
3243
3273
  let repairedTranslationEntries = 0;
3274
+ let fixedLessons = 0;
3244
3275
  console.log(`📋 Checking ${syllabus.lessons.length} lessons in syllabus...`);
3245
3276
  // First pass: Check each lesson to see if it exists in the bucket and count files.
3246
3277
  // We try two possible folder names because they can differ by source:
@@ -3438,15 +3469,39 @@ class ServeCommand extends SessionCommand_1.default {
3438
3469
  repairedTranslationsInLessons += 1;
3439
3470
  }
3440
3471
  }
3472
+ // Fifth pass: fix lessons stuck in GENERATING or ERROR when the file exists in the bucket
3473
+ const primaryLanguage = syllabus.courseInfo.language || "en";
3474
+ for (const lesson of syllabus.lessons) {
3475
+ if (lesson.generated !== false)
3476
+ continue;
3477
+ if (lesson.status !== "GENERATING" && lesson.status !== "ERROR")
3478
+ continue;
3479
+ const candidateSlugs = [
3480
+ (0, creatorUtilities_2.slugify)(lesson.id + "-" + lesson.title),
3481
+ lesson.uid,
3482
+ ].filter(Boolean);
3483
+ const matchedSlug = candidateSlugs.find(s => translationsBySlug.has(s));
3484
+ if (!matchedSlug)
3485
+ continue;
3486
+ const langs = translationsBySlug.get(matchedSlug) || [];
3487
+ if (!langs.includes(primaryLanguage))
3488
+ continue;
3489
+ const prevStatus = lesson.status;
3490
+ lesson.generated = true;
3491
+ lesson.status = "DONE";
3492
+ fixedLessons += 1;
3493
+ console.log(`🔧 Fixed lesson: ${lesson.id} - "${lesson.title}" (was generated:false status:${prevStatus}, primary language "${primaryLanguage}" exists in bucket)`);
3494
+ }
3441
3495
  }
3442
3496
  catch (error) {
3443
3497
  console.error("⚠️ Could not reconcile lesson translations during syllabus sync:", error);
3444
3498
  }
3445
3499
  if (totalRemoved > 0 ||
3446
3500
  addedLessons.length > 0 ||
3447
- repairedTranslationsInLessons > 0) {
3501
+ repairedTranslationsInLessons > 0 ||
3502
+ fixedLessons > 0) {
3448
3503
  await saveSyllabus(courseSlug, syllabus, bucket);
3449
- console.log(`✅ Syllabus synchronized. Removed ${removedLessons.length} non-existent, ${duplicatesRemoved.length} duplicate(s); added ${addedLessons.length} from bucket; repaired translations in ${repairedTranslationsInLessons} lesson(s).`);
3504
+ console.log(`✅ 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).`);
3450
3505
  }
3451
3506
  else {
3452
3507
  console.log(`✅ Syllabus is already in sync. No changes.`);
@@ -3459,6 +3514,7 @@ class ServeCommand extends SessionCommand_1.default {
3459
3514
  removedLessons: removedLessons.length,
3460
3515
  duplicatesResolved: duplicatesRemoved.length,
3461
3516
  addedLessons: addedLessons.length,
3517
+ fixedLessons,
3462
3518
  repairedTranslationsInLessons,
3463
3519
  repairedTranslationEntries,
3464
3520
  removed: removedLessons,
@@ -3913,20 +3969,65 @@ class ServeCommand extends SessionCommand_1.default {
3913
3969
  const output = fs.createWriteStream(zipPath);
3914
3970
  const archive = archiver("zip", { zlib: { level: 9 } });
3915
3971
  output.on("close", async () => {
3916
- // 10) Subir ZIP a RigoBot
3917
- const form = new FormData();
3918
- form.append("file", fs.createReadStream(zipPath));
3919
- form.append("config", JSON.stringify(config));
3920
- const rigoRes = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/learnpack/upload`, form, {
3921
- headers: Object.assign(Object.assign({}, form.getHeaders()), { Authorization: "Token " + rigoToken.trim() }),
3922
- });
3923
- const assetResults = await createMultiLangAsset(bucket, rigoToken, bcToken, slug, fullConfig.config, rigoRes.data.url, academyId);
3924
- rimraf.sync(tmpRoot);
3925
- console.log("RigoRes", rigoRes.data);
3926
- return res.json({
3927
- url: rigoRes.data.url,
3928
- errors: assetResults.errors,
3929
- });
3972
+ let rigoPublishUrl;
3973
+ try {
3974
+ // 10) Subir ZIP a RigoBot
3975
+ const form = new FormData();
3976
+ form.append("file", fs.createReadStream(zipPath));
3977
+ form.append("config", JSON.stringify(config));
3978
+ const rigoRes = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/learnpack/upload`, form, {
3979
+ headers: Object.assign(Object.assign({}, form.getHeaders()), { Authorization: "Token " + rigoToken.trim() }),
3980
+ });
3981
+ rigoPublishUrl = rigoRes.data.url;
3982
+ let errors;
3983
+ try {
3984
+ const assetResults = await createMultiLangAsset(bucket, rigoToken, bcToken, slug, fullConfig.config, rigoRes.data.url, academyId);
3985
+ errors = assetResults.errors;
3986
+ }
3987
+ catch (error) {
3988
+ console.error("Asset sync failed unexpectedly:", error);
3989
+ errors = [
3990
+ {
3991
+ kind: "package_error",
3992
+ error: { detail: "Asset sync failed unexpectedly." },
3993
+ },
3994
+ ];
3995
+ }
3996
+ if (res.headersSent)
3997
+ return;
3998
+ console.log("RigoRes", rigoRes.data);
3999
+ res.json({
4000
+ url: rigoPublishUrl,
4001
+ errors,
4002
+ });
4003
+ }
4004
+ catch (error) {
4005
+ console.error(error);
4006
+ if (res.headersSent)
4007
+ return;
4008
+ if (rigoPublishUrl !== undefined) {
4009
+ res.json({
4010
+ url: rigoPublishUrl,
4011
+ errors: [
4012
+ {
4013
+ kind: "package_error",
4014
+ error: { detail: "Asset sync failed unexpectedly." },
4015
+ },
4016
+ ],
4017
+ });
4018
+ }
4019
+ else {
4020
+ res.status(500).json({ error: error.message });
4021
+ }
4022
+ }
4023
+ finally {
4024
+ try {
4025
+ rimraf.sync(tmpRoot);
4026
+ }
4027
+ catch (error) {
4028
+ console.error("rimraf tmpRoot:", error);
4029
+ }
4030
+ }
3930
4031
  });
3931
4032
  archive.on("error", err => {
3932
4033
  console.error("ZIP Error:", err);
@@ -11,6 +11,26 @@ export interface TAcademy {
11
11
  }
12
12
  export declare const listUserAcademies: (breathecodeToken: string) => Promise<TAcademy[]>;
13
13
  export declare const validateToken: (token: string) => Promise<any>;
14
+ /** keep in sync with ide/src/components/Creator/PublishButton.tsx AssetSyncError */
15
+ export type AssetSyncError = {
16
+ kind: "lang_error";
17
+ lang: string;
18
+ error: {
19
+ detail: string;
20
+ };
21
+ } | {
22
+ kind: "package_error";
23
+ error: {
24
+ detail: string;
25
+ };
26
+ };
27
+ /**
28
+ * GET Rigobot package by slug after a successful upload. Does not throw.
29
+ * @param rigoToken Rigobot API token (Bearer-style `Token` header value).
30
+ * @param courseSlug Learnpack package slug used in the Rigobot URL path.
31
+ * @returns Resolved numeric package id from Rigobot, or `null` if the request fails, the id is missing, or it is not a finite integer.
32
+ */
33
+ export declare function resolveLearnpackPackageId(rigoToken: string, courseSlug: string): Promise<number | null>;
14
34
  type TAssetMissing = {
15
35
  slug: string;
16
36
  title: string;
@@ -27,6 +47,7 @@ type TAssetMissing = {
27
47
  readme_raw: string;
28
48
  all_translations: string[];
29
49
  academy_id?: number;
50
+ learnpack_id: number;
30
51
  };
31
52
  export declare const createAsset: (token: string, asset: TAssetMissing) => Promise<any>;
32
53
  export declare const doesAssetExists: (token: string, assetSlug: string) => Promise<{
@@ -59,7 +80,10 @@ declare const _default: {
59
80
  exists: boolean;
60
81
  academyId?: number;
61
82
  }>;
62
- updateAsset: (token: string, assetSlug: string, asset: Partial<TAssetMissing>) => Promise<any>;
83
+ updateAsset: (token: string, assetSlug: string, asset: Partial<TAssetMissing> & {
84
+ learnpack_id: number;
85
+ }) => Promise<any>;
86
+ resolveLearnpackPackageId: typeof resolveLearnpackPackageId;
63
87
  getCategories: (token: string) => Promise<any>;
64
88
  updateRigoPackage: (token: string, slug: string, updates: {
65
89
  asset_id?: number;
package/lib/utils/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getCurrentTechnologies = exports.fetchTechnologies = exports.doesAssetExists = exports.createAsset = exports.validateToken = exports.listUserAcademies = exports.getConsumable = exports.countConsumables = exports.RIGOBOT_REALTIME_HOST = exports.RIGOBOT_HOST = void 0;
4
+ exports.resolveLearnpackPackageId = resolveLearnpackPackageId;
4
5
  const console_1 = require("../utils/console");
5
6
  const storage = require("node-persist");
6
7
  const cli_ux_1 = require("cli-ux");
@@ -341,6 +342,35 @@ const validateToken = async (token) => {
341
342
  }
342
343
  };
343
344
  exports.validateToken = validateToken;
345
+ function parseLearnpackPackageId(raw) {
346
+ if (raw === undefined || raw === null)
347
+ return null;
348
+ const n = typeof raw === "number" ? raw : Number(raw);
349
+ if (!Number.isFinite(n) || !Number.isInteger(n))
350
+ return null;
351
+ return n;
352
+ }
353
+ /**
354
+ * GET Rigobot package by slug after a successful upload. Does not throw.
355
+ * @param rigoToken Rigobot API token (Bearer-style `Token` header value).
356
+ * @param courseSlug Learnpack package slug used in the Rigobot URL path.
357
+ * @returns Resolved numeric package id from Rigobot, or `null` if the request fails, the id is missing, or it is not a finite integer.
358
+ */
359
+ async function resolveLearnpackPackageId(rigoToken, courseSlug) {
360
+ var _a;
361
+ if (!(rigoToken === null || rigoToken === void 0 ? void 0 : rigoToken.trim()) || !courseSlug)
362
+ return null;
363
+ const url = `${exports.RIGOBOT_HOST}/v1/learnpack/package/${encodeURIComponent(courseSlug)}/`;
364
+ try {
365
+ const response = await axios_1.default.get(url, {
366
+ headers: { Authorization: `Token ${rigoToken.trim()}` },
367
+ });
368
+ return parseLearnpackPackageId((_a = response.data) === null || _a === void 0 ? void 0 : _a.id);
369
+ }
370
+ catch (_b) {
371
+ return null;
372
+ }
373
+ }
344
374
  const createAsset = async (token, asset) => {
345
375
  var _a;
346
376
  const body = {
@@ -367,6 +397,7 @@ const createAsset = async (token, asset) => {
367
397
  intro_video_url: null,
368
398
  translations: [asset.lang],
369
399
  learnpack_deploy_url: asset.learnpack_deploy_url,
400
+ learnpack_id: asset.learnpack_id,
370
401
  technologies: asset.technologies,
371
402
  readme_raw: asset.readme_raw,
372
403
  all_translations: asset.all_translations,
@@ -417,9 +448,10 @@ const updateAsset = async (token, assetSlug, asset) => {
417
448
  const headers = {
418
449
  Authorization: `Token ${token}`,
419
450
  };
451
+ const body = Object.assign(Object.assign({}, asset), { learnpack_id: asset.learnpack_id });
420
452
  console.log("[BC] PUT", url, "| academy_id:", (_a = asset.academy_id) !== null && _a !== void 0 ? _a : "none");
421
453
  try {
422
- const response = await axios_1.default.put(url, asset, { headers });
454
+ const response = await axios_1.default.put(url, body, { headers });
423
455
  return response.data;
424
456
  }
425
457
  catch (error) {
@@ -551,6 +583,7 @@ exports.default = {
551
583
  createAsset: exports.createAsset,
552
584
  doesAssetExists: exports.doesAssetExists,
553
585
  updateAsset,
586
+ resolveLearnpackPackageId,
554
587
  getCategories,
555
588
  updateRigoPackage,
556
589
  createRigoPackage,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.346",
4
+ "version": "5.0.348",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -109,6 +109,7 @@ export const handleAssetCreation = async (
109
109
  learnpackDeployUrl: string,
110
110
  b64IndexReadme: string,
111
111
  academyId: number | undefined,
112
+ learnpackId: number,
112
113
  preflightInfo?: ExistingAssetInfo,
113
114
  all_translations: string[] = []
114
115
  ) => {
@@ -160,6 +161,7 @@ export const handleAssetCreation = async (
160
161
  preview: learnJson.preview,
161
162
  readme_raw: b64IndexReadme,
162
163
  all_translations,
164
+ learnpack_id: learnpackId,
163
165
  }
164
166
  if (academyId !== undefined) {
165
167
  assetPayload.academy_id = academyId
@@ -190,6 +192,7 @@ export const handleAssetCreation = async (
190
192
  category: category,
191
193
  description: assetDescription,
192
194
  all_translations,
195
+ learnpack_id: learnpackId,
193
196
  }
194
197
  // Only set academy when the asset has none yet and the user selected one
195
198
  if (existingAcademyId === undefined && academyId !== undefined) {
@@ -237,6 +240,17 @@ const createMultiLangAssetFromDisk = async (
237
240
  return
238
241
  }
239
242
 
243
+ const learnpackId = await api.resolveLearnpackPackageId(
244
+ sessionPayload.rigobotToken,
245
+ learnJson.slug
246
+ )
247
+ if (learnpackId === null) {
248
+ Console.warning(
249
+ "Breathecode assets skipped: could not resolve Learnpack package id"
250
+ )
251
+ return
252
+ }
253
+
240
254
  const all_translations: string[] = []
241
255
  for (const lang of availableLangs) {
242
256
  const readmePath = path.join(
@@ -266,6 +280,7 @@ const createMultiLangAssetFromDisk = async (
266
280
  deployUrl,
267
281
  b64IndexReadme,
268
282
  selectedAcademyId,
283
+ learnpackId,
269
284
  preflightInfo,
270
285
  all_translations
271
286
  )