@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.
- package/lib/commands/publish.d.ts +7 -1
- package/lib/commands/publish.js +71 -21
- package/lib/commands/serve.js +126 -28
- package/lib/utils/api.d.ts +25 -1
- package/lib/utils/api.js +71 -18
- package/package.json +1 -1
- package/src/commands/publish.ts +122 -30
- package/src/commands/serve.ts +146 -47
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +511 -512
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +93 -21
|
@@ -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: {
|
package/lib/commands/publish.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
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)) {
|
package/lib/commands/serve.js
CHANGED
|
@@ -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"
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
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
|
-
|
|
3798
|
-
break;
|
|
3831
|
+
foundAcademyIds.push(existingAcademyId);
|
|
3799
3832
|
}
|
|
3800
3833
|
}
|
|
3801
3834
|
}
|
|
3802
|
-
|
|
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
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
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);
|
package/lib/utils/api.d.ts
CHANGED
|
@@ -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>
|
|
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
|
|
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
|
|
284
|
+
const meUrl = `${HOST}/v1/auth/user/me`;
|
|
289
285
|
try {
|
|
290
|
-
const response = await axios_1.default.get(
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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.
|
|
4
|
+
"version": "5.0.347",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|