@learnpack/learnpack 5.0.344 → 5.0.347

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.
@@ -1,8 +1,14 @@
1
1
  import SessionCommand from "../utils/SessionCommand";
2
+ type ExistingAssetInfo = {
3
+ lang: string;
4
+ slug: string;
5
+ exists: boolean;
6
+ academyId?: number;
7
+ };
2
8
  export declare const handleAssetCreation: (sessionPayload: {
3
9
  token: string;
4
10
  rigobotToken: string;
5
- }, learnJson: any, selectedLang: string, learnpackDeployUrl: string, b64IndexReadme: string, academyId: number | undefined, 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>;
6
12
  declare class BuildCommand extends SessionCommand {
7
13
  static description: string;
8
14
  static flags: {
@@ -45,7 +45,33 @@ const getLocalizedValue = (translations, lang, fallbackLangs = ["en", "us"]) =>
45
45
  const first = firstKey ? translations[firstKey] : "";
46
46
  return typeof first === "string" ? first : "";
47
47
  };
48
- const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, learnpackDeployUrl, b64IndexReadme, academyId, all_translations = []) => {
48
+ const getExistingAssetsInfo = async (token, learnJson) => {
49
+ const availableLangs = getAvailableLangs(learnJson);
50
+ const results = [];
51
+ for (const lang of availableLangs) {
52
+ const assetTitle = getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, lang);
53
+ if (!assetTitle)
54
+ continue;
55
+ let slug = (0, creatorUtilities_1.slugify)(assetTitle).slice(0, 47);
56
+ slug = `${slug}-${lang}`;
57
+ // eslint-disable-next-line no-await-in-loop
58
+ const { exists, academyId } = await api_1.default.doesAssetExists(token, slug);
59
+ results.push({ lang, slug, exists, academyId });
60
+ }
61
+ return results;
62
+ };
63
+ const determinePublishAcademyMode = (existingAssets) => {
64
+ const academyIds = existingAssets
65
+ .filter((a) => a.exists && a.academyId !== undefined)
66
+ .map((a) => a.academyId);
67
+ const unique = [...new Set(academyIds)];
68
+ if (unique.length === 0)
69
+ return { type: "select" };
70
+ if (unique.length === 1)
71
+ return { type: "locked", academyId: unique[0] };
72
+ return { type: "conflict", academies: unique };
73
+ };
74
+ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, learnpackDeployUrl, b64IndexReadme, academyId, learnpackId, preflightInfo, all_translations = []) => {
49
75
  const category = "uncategorized";
50
76
  try {
51
77
  const user = await api_1.default.validateToken(sessionPayload.token);
@@ -56,15 +82,8 @@ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, lear
56
82
  }
57
83
  let slug = (0, creatorUtilities_1.slugify)(assetTitle).slice(0, 47);
58
84
  slug = `${slug}-${selectedLang}`;
59
- const { exists, academyId: existingAcademyId } = await api_1.default.doesAssetExists(sessionPayload.token, slug);
60
- // Compare academy IDs if asset exists and academyId is provided
61
- if (exists &&
62
- existingAcademyId !== undefined &&
63
- academyId !== undefined &&
64
- existingAcademyId !== academyId) {
65
- console_1.default.warning(`Asset exists in academy ${existingAcademyId}, but attempting to publish to academy ${academyId}. ` +
66
- `The asset will be updated in its current academy (${existingAcademyId}).`);
67
- }
85
+ // Use pre-flight info when available to avoid an extra GET request
86
+ const { exists, academyId: existingAcademyId } = preflightInfo !== null && preflightInfo !== void 0 ? preflightInfo : (await api_1.default.doesAssetExists(sessionPayload.token, slug));
68
87
  // const technologies: unknown[] = Array.isArray(learnJson?.technologies) ?
69
88
  // learnJson.technologies :
70
89
  // []
@@ -88,6 +107,7 @@ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, lear
88
107
  preview: learnJson.preview,
89
108
  readme_raw: b64IndexReadme,
90
109
  all_translations,
110
+ learnpack_id: learnpackId,
91
111
  };
92
112
  if (academyId !== undefined) {
93
113
  assetPayload.academy_id = academyId;
@@ -105,14 +125,20 @@ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, lear
105
125
  return asset;
106
126
  }
107
127
  console_1.default.info("Asset exists, updating it");
108
- const asset = await api_1.default.updateAsset(sessionPayload.token, slug, {
128
+ const updatePayload = {
109
129
  graded: true,
110
130
  learnpack_deploy_url: learnpackDeployUrl,
111
131
  title: assetTitle,
112
132
  category: category,
113
133
  description: assetDescription,
114
134
  all_translations,
115
- });
135
+ learnpack_id: learnpackId,
136
+ };
137
+ // Only set academy when the asset has none yet and the user selected one
138
+ if (existingAcademyId === undefined && academyId !== undefined) {
139
+ updatePayload.academy_id = academyId;
140
+ }
141
+ const asset = await api_1.default.updateAsset(sessionPayload.token, slug, updatePayload);
116
142
  try {
117
143
  await api_1.default.updateRigoPackage(sessionPayload.rigobotToken.trim(), learnJson.slug, {
118
144
  asset_id: asset.id,
@@ -130,12 +156,17 @@ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, lear
130
156
  }
131
157
  };
132
158
  exports.handleAssetCreation = handleAssetCreation;
133
- const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl) => {
159
+ const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl, selectedAcademyId, existingAssetsInfo = []) => {
134
160
  const availableLangs = getAvailableLangs(learnJson);
135
161
  if (availableLangs.length === 0) {
136
162
  console_1.default.error("No languages found in learn.json.title. Add at least one language (e.g. title.en).");
137
163
  return;
138
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
+ }
139
170
  const all_translations = [];
140
171
  for (const lang of availableLangs) {
141
172
  const readmePath = path.join(process.cwd(), `README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
@@ -150,9 +181,10 @@ const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl
150
181
  indexReadmeString = "";
151
182
  }
152
183
  const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64");
184
+ const preflightInfo = existingAssetsInfo.find((a) => a.lang === lang);
153
185
  try {
154
186
  // eslint-disable-next-line no-await-in-loop
155
- const asset = await (0, exports.handleAssetCreation)(sessionPayload, learnJson, lang, deployUrl, b64IndexReadme, undefined, all_translations);
187
+ const asset = await (0, exports.handleAssetCreation)(sessionPayload, learnJson, lang, deployUrl, b64IndexReadme, selectedAcademyId, learnpackId, preflightInfo, all_translations);
156
188
  if (!asset) {
157
189
  console_1.default.debug("Could not create/update asset for lang", lang);
158
190
  continue;
@@ -230,7 +262,7 @@ class BuildCommand extends SessionCommand_1.default {
230
262
  await this.initSession(flags);
231
263
  }
232
264
  async run() {
233
- var _a, _b, _c;
265
+ var _a, _b, _c, _d;
234
266
  const buildDir = path.join(process.cwd(), "build");
235
267
  const { flags } = this.parse(BuildCommand);
236
268
  const strict = flags.strict;
@@ -269,17 +301,35 @@ class BuildCommand extends SessionCommand_1.default {
269
301
  console_1.default.debug("Building exercises");
270
302
  (_c = this.configManager) === null || _c === void 0 ? void 0 : _c.buildIndex();
271
303
  }
304
+ const learnJsonPath = path.join(process.cwd(), "learn.json");
305
+ if (!fs.existsSync(learnJsonPath)) {
306
+ this.error("learn.json not found");
307
+ }
308
+ const learnJson = JSON.parse(fs.readFileSync(learnJsonPath, "utf-8"));
272
309
  const academies = await api_1.default.listUserAcademies(sessionPayload.token);
273
310
  if (academies.length === 0) {
274
311
  console_1.default.error("It seems you cannot publish tutorials. Make sure you creator subscription is up to date here: https://4geeks.com/profile/subscriptions. If you believe there is an issue you can always contact support@4geeks.com");
275
312
  process.exit(1);
276
313
  }
277
- const { academy, category } = await selectAcademy(academies, sessionPayload.token);
278
- const learnJsonPath = path.join(process.cwd(), "learn.json");
279
- if (!fs.existsSync(learnJsonPath)) {
280
- this.error("learn.json not found");
314
+ console_1.default.info("Checking existing assets...");
315
+ const existingAssetsInfo = await getExistingAssetsInfo(sessionPayload.token, learnJson);
316
+ const academyMode = determinePublishAcademyMode(existingAssetsInfo);
317
+ let selectedAcademyId;
318
+ if (academyMode.type === "conflict") {
319
+ console_1.default.warning(`Some of your assets are associated with different academies ` +
320
+ `(${academyMode.academies.join(", ")}). ` +
321
+ `Academy assignment will be skipped to avoid conflicts.`);
322
+ }
323
+ else if (academyMode.type === "locked") {
324
+ const lockedAcademy = academies.find((a) => a.id === academyMode.academyId);
325
+ console_1.default.info(`This package is associated with academy: ${(_d = lockedAcademy === null || lockedAcademy === void 0 ? void 0 : lockedAcademy.name) !== null && _d !== void 0 ? _d : academyMode.academyId}. Academy cannot be changed.`);
326
+ selectedAcademyId = academyMode.academyId;
327
+ }
328
+ else {
329
+ // mode === "select": all existing assets have no academy, user picks one
330
+ const { academy } = await selectAcademy(academies, sessionPayload.token);
331
+ selectedAcademyId = academy === null || academy === void 0 ? void 0 : academy.id;
281
332
  }
282
- const learnJson = JSON.parse(fs.readFileSync(learnJsonPath, "utf-8"));
283
333
  const zipFilePath = path.join(process.cwd(), `${learnJson.slug}.zip`);
284
334
  // Ensure build directory exists
285
335
  if (!fs.existsSync(buildDir)) {
@@ -405,7 +455,7 @@ class BuildCommand extends SessionCommand_1.default {
405
455
  console.log(res.data);
406
456
  fs.unlinkSync(zipFilePath);
407
457
  this.removeDirectory(buildDir);
408
- await createMultiLangAssetFromDisk({ token: sessionPayload.token, rigobotToken: rigoToken }, learnJson, res.data.url);
458
+ await createMultiLangAssetFromDisk({ token: sessionPayload.token, rigobotToken: rigoToken }, learnJson, res.data.url, selectedAcademyId, existingAssetsInfo);
409
459
  }
410
460
  catch (error) {
411
461
  if (axios_1.default.isAxiosError(error)) {
@@ -39,6 +39,7 @@ const redis_1 = require("redis");
39
39
  const historyManager_1 = require("../managers/historyManager");
40
40
  const readmeHistoryService_1 = require("../managers/readmeHistoryService");
41
41
  const readmeSanitizer_1 = require("../utils/readmeSanitizer");
42
+ // eslint-disable-next-line
42
43
  const frontMatter = require("front-matter");
43
44
  if (process.env.NEW_RELIC_ENABLED === "true") {
44
45
  require("newrelic");
@@ -364,8 +365,33 @@ const getLocalizedValue = (translations, lang, fallbackLangs = ["en", "us"]) =>
364
365
  const first = firstKey ? translations[firstKey] : "";
365
366
  return typeof first === "string" ? first : "";
366
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
+ }
367
380
  const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, courseJson, deployUrl, academyId) => {
368
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
+ }
369
395
  const availableLangs = Object.keys(courseJson.title);
370
396
  console.log("AVAILABLE LANGUAGES to upload asset", availableLangs);
371
397
  const all_translations = [];
@@ -387,11 +413,12 @@ const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, cour
387
413
  const b64IndexReadme = buffer_1.Buffer.from(indexReadmeString).toString("base64");
388
414
  try {
389
415
  // eslint-disable-next-line no-await-in-loop
390
- const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, academyId, all_translations);
416
+ const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, academyId, learnpackId, undefined, all_translations);
391
417
  if (!asset) {
392
418
  errors.push({
419
+ kind: "lang_error",
393
420
  lang,
394
- error: { detail: "Failed to create asset", status_code: 500 },
421
+ error: { detail: "Failed to create asset" },
395
422
  });
396
423
  console.log("No se pudo crear el asset, saltando idioma", lang);
397
424
  continue;
@@ -402,7 +429,11 @@ const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, cour
402
429
  const errorData = error && typeof error === "object" && "response" in error ?
403
430
  ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error :
404
431
  error;
405
- errors.push({ lang, error: errorData });
432
+ errors.push({
433
+ kind: "lang_error",
434
+ lang,
435
+ error: { detail: assetSyncErrorDetail(errorData) },
436
+ });
406
437
  console.error(`Error creating asset for language ${lang}:`, error);
407
438
  }
408
439
  }
@@ -3780,26 +3811,48 @@ class ServeCommand extends SessionCommand_1.default {
3780
3811
  const configJson = JSON.parse(configContent.toString());
3781
3812
  const { config } = configJson;
3782
3813
  const availableLangs = Object.keys(config.title || {});
3783
- let academyId = null;
3784
3814
  let isPublished = false;
3785
- for (const lang of availableLangs) {
3815
+ // Collect academy ids from all existing assets
3816
+ const foundAcademyIds = [];
3817
+ const slugsToCheck = availableLangs
3818
+ .map(lang => {
3786
3819
  const assetTitle = getLocalizedValue(config.title, lang);
3787
3820
  if (!assetTitle)
3788
- continue;
3789
- let assetSlug = (0, creatorUtilities_2.slugify)(assetTitle).slice(0, 47);
3790
- assetSlug = `${assetSlug}-${lang}`;
3791
- const { exists, academyId: existingAcademyId } =
3792
- // eslint-disable-next-line no-await-in-loop
3793
- await (0, api_1.doesAssetExists)(bcToken, assetSlug);
3821
+ return null;
3822
+ const assetSlug = (0, creatorUtilities_2.slugify)(assetTitle).slice(0, 47);
3823
+ return `${assetSlug}-${lang}`;
3824
+ })
3825
+ .filter((slug) => slug !== null);
3826
+ const assetChecks = await Promise.all(slugsToCheck.map(slug => (0, api_1.doesAssetExists)(bcToken, slug)));
3827
+ for (const { exists, academyId: existingAcademyId } of assetChecks) {
3794
3828
  if (exists) {
3795
3829
  isPublished = true;
3796
3830
  if (existingAcademyId !== undefined) {
3797
- academyId = existingAcademyId;
3798
- break;
3831
+ foundAcademyIds.push(existingAcademyId);
3799
3832
  }
3800
3833
  }
3801
3834
  }
3802
- return res.json({ academyId, isPublished });
3835
+ const uniqueAcademies = [...new Set(foundAcademyIds)];
3836
+ let mode;
3837
+ let lockedAcademyId;
3838
+ let conflictAcademies;
3839
+ if (uniqueAcademies.length === 0) {
3840
+ mode = "select";
3841
+ }
3842
+ else if (uniqueAcademies.length === 1) {
3843
+ mode = "locked";
3844
+ lockedAcademyId = uniqueAcademies[0];
3845
+ }
3846
+ else {
3847
+ mode = "conflict";
3848
+ conflictAcademies = uniqueAcademies;
3849
+ }
3850
+ return res.json({
3851
+ isPublished,
3852
+ mode,
3853
+ lockedAcademyId,
3854
+ conflictAcademies,
3855
+ });
3803
3856
  }
3804
3857
  catch (error) {
3805
3858
  console.error("Error fetching package academy:", error);
@@ -3890,20 +3943,65 @@ class ServeCommand extends SessionCommand_1.default {
3890
3943
  const output = fs.createWriteStream(zipPath);
3891
3944
  const archive = archiver("zip", { zlib: { level: 9 } });
3892
3945
  output.on("close", async () => {
3893
- // 10) Subir ZIP a RigoBot
3894
- const form = new FormData();
3895
- form.append("file", fs.createReadStream(zipPath));
3896
- form.append("config", JSON.stringify(config));
3897
- const rigoRes = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/learnpack/upload`, form, {
3898
- headers: Object.assign(Object.assign({}, form.getHeaders()), { Authorization: "Token " + rigoToken.trim() }),
3899
- });
3900
- const assetResults = await createMultiLangAsset(bucket, rigoToken, bcToken, slug, fullConfig.config, rigoRes.data.url, academyId);
3901
- rimraf.sync(tmpRoot);
3902
- console.log("RigoRes", rigoRes.data);
3903
- return res.json({
3904
- url: rigoRes.data.url,
3905
- errors: assetResults.errors,
3906
- });
3946
+ let rigoPublishUrl;
3947
+ try {
3948
+ // 10) Subir ZIP a RigoBot
3949
+ const form = new FormData();
3950
+ form.append("file", fs.createReadStream(zipPath));
3951
+ form.append("config", JSON.stringify(config));
3952
+ const rigoRes = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/learnpack/upload`, form, {
3953
+ headers: Object.assign(Object.assign({}, form.getHeaders()), { Authorization: "Token " + rigoToken.trim() }),
3954
+ });
3955
+ rigoPublishUrl = rigoRes.data.url;
3956
+ let errors;
3957
+ try {
3958
+ const assetResults = await createMultiLangAsset(bucket, rigoToken, bcToken, slug, fullConfig.config, rigoRes.data.url, academyId);
3959
+ errors = assetResults.errors;
3960
+ }
3961
+ catch (error) {
3962
+ console.error("Asset sync failed unexpectedly:", error);
3963
+ errors = [
3964
+ {
3965
+ kind: "package_error",
3966
+ error: { detail: "Asset sync failed unexpectedly." },
3967
+ },
3968
+ ];
3969
+ }
3970
+ if (res.headersSent)
3971
+ return;
3972
+ console.log("RigoRes", rigoRes.data);
3973
+ res.json({
3974
+ url: rigoPublishUrl,
3975
+ errors,
3976
+ });
3977
+ }
3978
+ catch (error) {
3979
+ console.error(error);
3980
+ if (res.headersSent)
3981
+ return;
3982
+ if (rigoPublishUrl !== undefined) {
3983
+ res.json({
3984
+ url: rigoPublishUrl,
3985
+ errors: [
3986
+ {
3987
+ kind: "package_error",
3988
+ error: { detail: "Asset sync failed unexpectedly." },
3989
+ },
3990
+ ],
3991
+ });
3992
+ }
3993
+ else {
3994
+ res.status(500).json({ error: error.message });
3995
+ }
3996
+ }
3997
+ finally {
3998
+ try {
3999
+ rimraf.sync(tmpRoot);
4000
+ }
4001
+ catch (error) {
4002
+ console.error("rimraf tmpRoot:", error);
4003
+ }
4004
+ }
3907
4005
  });
3908
4006
  archive.on("error", err => {
3909
4007
  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");
@@ -278,36 +279,48 @@ const getConsumable = async (token, consumableSlug = "ai-generation") => {
278
279
  }
279
280
  };
280
281
  exports.getConsumable = getConsumable;
281
- const neededPermissions = [
282
- "add_asset",
283
- "change_asset",
284
- "view_asset",
285
- "delete_asset",
286
- ];
282
+ const CRUD_ASSET_CAPABILITY_URL = `${HOST}/v1/auth/user/me/capability/crud_asset`;
287
283
  const listUserAcademies = async (breathecodeToken) => {
288
- const url = "https://breathecode.herokuapp.com/v1/auth/user/me";
284
+ const meUrl = `${HOST}/v1/auth/user/me`;
289
285
  try {
290
- const response = await axios_1.default.get(url, {
286
+ const response = await axios_1.default.get(meUrl, {
291
287
  headers: {
292
288
  Authorization: `Token ${breathecodeToken}`,
293
289
  },
294
290
  });
295
291
  const data = response.data;
296
292
  const academiesMap = new Map();
293
+ if (!Array.isArray(data.roles)) {
294
+ return [];
295
+ }
297
296
  for (const role of data.roles) {
298
297
  const academy = role.academy;
298
+ if (!academy)
299
+ continue;
299
300
  if (!academiesMap.has(academy.id)) {
300
301
  academiesMap.set(academy.id, academy);
301
302
  }
302
303
  }
303
- const permissions = new Set(data.permissions.map((p) => p.codename));
304
- // Validate if the user has ALL the needed permissions
305
- const hasAllPermissions = neededPermissions.every(permission => permissions.has(permission));
306
- if (!hasAllPermissions) {
307
- // The user does not have all the needed permissions
308
- return [];
309
- }
310
- return [...academiesMap.values()];
304
+ const academies = [...academiesMap.values()];
305
+ const allowed = await Promise.all(academies.map(async (academy) => {
306
+ const capResponse = await axios_1.default.get(CRUD_ASSET_CAPABILITY_URL, {
307
+ headers: {
308
+ Authorization: `Token ${breathecodeToken}`,
309
+ Academy: academy.id,
310
+ },
311
+ validateStatus: () => true,
312
+ });
313
+ if (capResponse.status === 200) {
314
+ return academy;
315
+ }
316
+ if (capResponse.status !== 403) {
317
+ console.warn(`listUserAcademies: unexpected status ${capResponse.status} for academy ${academy.id}`);
318
+ }
319
+ return null;
320
+ }));
321
+ const filtered = allowed.filter((a) => a !== null);
322
+ filtered.sort((a, b) => a.name.localeCompare(b.name));
323
+ return filtered;
311
324
  }
312
325
  catch (error) {
313
326
  console.error("Failed to fetch user academies:", error);
@@ -329,7 +342,37 @@ const validateToken = async (token) => {
329
342
  }
330
343
  };
331
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
+ }
332
374
  const createAsset = async (token, asset) => {
375
+ var _a;
333
376
  const body = {
334
377
  slug: asset.slug,
335
378
  title: asset.title,
@@ -354,6 +397,7 @@ const createAsset = async (token, asset) => {
354
397
  intro_video_url: null,
355
398
  translations: [asset.lang],
356
399
  learnpack_deploy_url: asset.learnpack_deploy_url,
400
+ learnpack_id: asset.learnpack_id,
357
401
  technologies: asset.technologies,
358
402
  readme_raw: asset.readme_raw,
359
403
  all_translations: asset.all_translations,
@@ -367,6 +411,7 @@ const createAsset = async (token, asset) => {
367
411
  url = `https://breathecode.herokuapp.com/v1/registry/academy/asset`;
368
412
  headers.Academy = String(asset.academy_id);
369
413
  }
414
+ console.log("[BC] POST", url, "| academy_id:", (_a = asset.academy_id) !== null && _a !== void 0 ? _a : "none", "| slug:", asset.slug);
370
415
  try {
371
416
  const response = await axios_1.default.post(url, body, { headers });
372
417
  return response.data;
@@ -398,12 +443,15 @@ const doesAssetExists = async (token, assetSlug) => {
398
443
  };
399
444
  exports.doesAssetExists = doesAssetExists;
400
445
  const updateAsset = async (token, assetSlug, asset) => {
446
+ var _a;
401
447
  const url = `https://breathecode.herokuapp.com/v1/registry/asset/me/${assetSlug}`;
402
448
  const headers = {
403
449
  Authorization: `Token ${token}`,
404
450
  };
451
+ const body = Object.assign(Object.assign({}, asset), { learnpack_id: asset.learnpack_id });
452
+ console.log("[BC] PUT", url, "| academy_id:", (_a = asset.academy_id) !== null && _a !== void 0 ? _a : "none");
405
453
  try {
406
- const response = await axios_1.default.put(url, asset, { headers });
454
+ const response = await axios_1.default.put(url, body, { headers });
407
455
  return response.data;
408
456
  }
409
457
  catch (error) {
@@ -460,7 +508,11 @@ const createRigoPackage = async (token, slug, config) => {
460
508
  }
461
509
  };
462
510
  let technologiesCache = [];
463
- /** Strip .env comments (e.g. "token # comment") so the token is sent without spaces. */
511
+ /**
512
+ * Strip .env comments (e.g. "token # comment") so the token is sent without spaces.
513
+ * @param token - Raw token value from env, possibly with trailing comment.
514
+ * @returns Trimmed token with inline `#` comments removed, or empty string if missing.
515
+ */
464
516
  function sanitizeToken(token) {
465
517
  if (!token)
466
518
  return "";
@@ -531,6 +583,7 @@ exports.default = {
531
583
  createAsset: exports.createAsset,
532
584
  doesAssetExists: exports.doesAssetExists,
533
585
  updateAsset,
586
+ resolveLearnpackPackageId,
534
587
  getCategories,
535
588
  updateRigoPackage,
536
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.344",
4
+ "version": "5.0.347",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {