@learnpack/learnpack 5.0.344 → 5.0.346
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 +64 -21
- package/lib/commands/serve.js +35 -12
- package/lib/utils/api.js +37 -17
- package/package.json +1 -1
- package/src/commands/publish.ts +107 -30
- package/src/commands/serve.ts +41 -13
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +519 -520
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +50 -19
|
@@ -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, 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, 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
|
// []
|
|
@@ -105,14 +124,19 @@ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, lear
|
|
|
105
124
|
return asset;
|
|
106
125
|
}
|
|
107
126
|
console_1.default.info("Asset exists, updating it");
|
|
108
|
-
const
|
|
127
|
+
const updatePayload = {
|
|
109
128
|
graded: true,
|
|
110
129
|
learnpack_deploy_url: learnpackDeployUrl,
|
|
111
130
|
title: assetTitle,
|
|
112
131
|
category: category,
|
|
113
132
|
description: assetDescription,
|
|
114
133
|
all_translations,
|
|
115
|
-
}
|
|
134
|
+
};
|
|
135
|
+
// Only set academy when the asset has none yet and the user selected one
|
|
136
|
+
if (existingAcademyId === undefined && academyId !== undefined) {
|
|
137
|
+
updatePayload.academy_id = academyId;
|
|
138
|
+
}
|
|
139
|
+
const asset = await api_1.default.updateAsset(sessionPayload.token, slug, updatePayload);
|
|
116
140
|
try {
|
|
117
141
|
await api_1.default.updateRigoPackage(sessionPayload.rigobotToken.trim(), learnJson.slug, {
|
|
118
142
|
asset_id: asset.id,
|
|
@@ -130,7 +154,7 @@ const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, lear
|
|
|
130
154
|
}
|
|
131
155
|
};
|
|
132
156
|
exports.handleAssetCreation = handleAssetCreation;
|
|
133
|
-
const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl) => {
|
|
157
|
+
const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl, selectedAcademyId, existingAssetsInfo = []) => {
|
|
134
158
|
const availableLangs = getAvailableLangs(learnJson);
|
|
135
159
|
if (availableLangs.length === 0) {
|
|
136
160
|
console_1.default.error("No languages found in learn.json.title. Add at least one language (e.g. title.en).");
|
|
@@ -150,9 +174,10 @@ const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl
|
|
|
150
174
|
indexReadmeString = "";
|
|
151
175
|
}
|
|
152
176
|
const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64");
|
|
177
|
+
const preflightInfo = existingAssetsInfo.find((a) => a.lang === lang);
|
|
153
178
|
try {
|
|
154
179
|
// eslint-disable-next-line no-await-in-loop
|
|
155
|
-
const asset = await (0, exports.handleAssetCreation)(sessionPayload, learnJson, lang, deployUrl, b64IndexReadme,
|
|
180
|
+
const asset = await (0, exports.handleAssetCreation)(sessionPayload, learnJson, lang, deployUrl, b64IndexReadme, selectedAcademyId, preflightInfo, all_translations);
|
|
156
181
|
if (!asset) {
|
|
157
182
|
console_1.default.debug("Could not create/update asset for lang", lang);
|
|
158
183
|
continue;
|
|
@@ -230,7 +255,7 @@ class BuildCommand extends SessionCommand_1.default {
|
|
|
230
255
|
await this.initSession(flags);
|
|
231
256
|
}
|
|
232
257
|
async run() {
|
|
233
|
-
var _a, _b, _c;
|
|
258
|
+
var _a, _b, _c, _d;
|
|
234
259
|
const buildDir = path.join(process.cwd(), "build");
|
|
235
260
|
const { flags } = this.parse(BuildCommand);
|
|
236
261
|
const strict = flags.strict;
|
|
@@ -269,17 +294,35 @@ class BuildCommand extends SessionCommand_1.default {
|
|
|
269
294
|
console_1.default.debug("Building exercises");
|
|
270
295
|
(_c = this.configManager) === null || _c === void 0 ? void 0 : _c.buildIndex();
|
|
271
296
|
}
|
|
297
|
+
const learnJsonPath = path.join(process.cwd(), "learn.json");
|
|
298
|
+
if (!fs.existsSync(learnJsonPath)) {
|
|
299
|
+
this.error("learn.json not found");
|
|
300
|
+
}
|
|
301
|
+
const learnJson = JSON.parse(fs.readFileSync(learnJsonPath, "utf-8"));
|
|
272
302
|
const academies = await api_1.default.listUserAcademies(sessionPayload.token);
|
|
273
303
|
if (academies.length === 0) {
|
|
274
304
|
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
305
|
process.exit(1);
|
|
276
306
|
}
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
307
|
+
console_1.default.info("Checking existing assets...");
|
|
308
|
+
const existingAssetsInfo = await getExistingAssetsInfo(sessionPayload.token, learnJson);
|
|
309
|
+
const academyMode = determinePublishAcademyMode(existingAssetsInfo);
|
|
310
|
+
let selectedAcademyId;
|
|
311
|
+
if (academyMode.type === "conflict") {
|
|
312
|
+
console_1.default.warning(`Some of your assets are associated with different academies ` +
|
|
313
|
+
`(${academyMode.academies.join(", ")}). ` +
|
|
314
|
+
`Academy assignment will be skipped to avoid conflicts.`);
|
|
315
|
+
}
|
|
316
|
+
else if (academyMode.type === "locked") {
|
|
317
|
+
const lockedAcademy = academies.find((a) => a.id === academyMode.academyId);
|
|
318
|
+
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.`);
|
|
319
|
+
selectedAcademyId = academyMode.academyId;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// mode === "select": all existing assets have no academy, user picks one
|
|
323
|
+
const { academy } = await selectAcademy(academies, sessionPayload.token);
|
|
324
|
+
selectedAcademyId = academy === null || academy === void 0 ? void 0 : academy.id;
|
|
281
325
|
}
|
|
282
|
-
const learnJson = JSON.parse(fs.readFileSync(learnJsonPath, "utf-8"));
|
|
283
326
|
const zipFilePath = path.join(process.cwd(), `${learnJson.slug}.zip`);
|
|
284
327
|
// Ensure build directory exists
|
|
285
328
|
if (!fs.existsSync(buildDir)) {
|
|
@@ -405,7 +448,7 @@ class BuildCommand extends SessionCommand_1.default {
|
|
|
405
448
|
console.log(res.data);
|
|
406
449
|
fs.unlinkSync(zipFilePath);
|
|
407
450
|
this.removeDirectory(buildDir);
|
|
408
|
-
await createMultiLangAssetFromDisk({ token: sessionPayload.token, rigobotToken: rigoToken }, learnJson, res.data.url);
|
|
451
|
+
await createMultiLangAssetFromDisk({ token: sessionPayload.token, rigobotToken: rigoToken }, learnJson, res.data.url, selectedAcademyId, existingAssetsInfo);
|
|
409
452
|
}
|
|
410
453
|
catch (error) {
|
|
411
454
|
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");
|
|
@@ -387,7 +388,7 @@ const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, cour
|
|
|
387
388
|
const b64IndexReadme = buffer_1.Buffer.from(indexReadmeString).toString("base64");
|
|
388
389
|
try {
|
|
389
390
|
// 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);
|
|
391
|
+
const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, academyId, undefined, all_translations);
|
|
391
392
|
if (!asset) {
|
|
392
393
|
errors.push({
|
|
393
394
|
lang,
|
|
@@ -3780,26 +3781,48 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
3780
3781
|
const configJson = JSON.parse(configContent.toString());
|
|
3781
3782
|
const { config } = configJson;
|
|
3782
3783
|
const availableLangs = Object.keys(config.title || {});
|
|
3783
|
-
let academyId = null;
|
|
3784
3784
|
let isPublished = false;
|
|
3785
|
-
|
|
3785
|
+
// Collect academy ids from all existing assets
|
|
3786
|
+
const foundAcademyIds = [];
|
|
3787
|
+
const slugsToCheck = availableLangs
|
|
3788
|
+
.map(lang => {
|
|
3786
3789
|
const assetTitle = getLocalizedValue(config.title, lang);
|
|
3787
3790
|
if (!assetTitle)
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3791
|
+
return null;
|
|
3792
|
+
const assetSlug = (0, creatorUtilities_2.slugify)(assetTitle).slice(0, 47);
|
|
3793
|
+
return `${assetSlug}-${lang}`;
|
|
3794
|
+
})
|
|
3795
|
+
.filter((slug) => slug !== null);
|
|
3796
|
+
const assetChecks = await Promise.all(slugsToCheck.map(slug => (0, api_1.doesAssetExists)(bcToken, slug)));
|
|
3797
|
+
for (const { exists, academyId: existingAcademyId } of assetChecks) {
|
|
3794
3798
|
if (exists) {
|
|
3795
3799
|
isPublished = true;
|
|
3796
3800
|
if (existingAcademyId !== undefined) {
|
|
3797
|
-
|
|
3798
|
-
break;
|
|
3801
|
+
foundAcademyIds.push(existingAcademyId);
|
|
3799
3802
|
}
|
|
3800
3803
|
}
|
|
3801
3804
|
}
|
|
3802
|
-
|
|
3805
|
+
const uniqueAcademies = [...new Set(foundAcademyIds)];
|
|
3806
|
+
let mode;
|
|
3807
|
+
let lockedAcademyId;
|
|
3808
|
+
let conflictAcademies;
|
|
3809
|
+
if (uniqueAcademies.length === 0) {
|
|
3810
|
+
mode = "select";
|
|
3811
|
+
}
|
|
3812
|
+
else if (uniqueAcademies.length === 1) {
|
|
3813
|
+
mode = "locked";
|
|
3814
|
+
lockedAcademyId = uniqueAcademies[0];
|
|
3815
|
+
}
|
|
3816
|
+
else {
|
|
3817
|
+
mode = "conflict";
|
|
3818
|
+
conflictAcademies = uniqueAcademies;
|
|
3819
|
+
}
|
|
3820
|
+
return res.json({
|
|
3821
|
+
isPublished,
|
|
3822
|
+
mode,
|
|
3823
|
+
lockedAcademyId,
|
|
3824
|
+
conflictAcademies,
|
|
3825
|
+
});
|
|
3803
3826
|
}
|
|
3804
3827
|
catch (error) {
|
|
3805
3828
|
console.error("Error fetching package academy:", error);
|
package/lib/utils/api.js
CHANGED
|
@@ -278,36 +278,48 @@ const getConsumable = async (token, consumableSlug = "ai-generation") => {
|
|
|
278
278
|
}
|
|
279
279
|
};
|
|
280
280
|
exports.getConsumable = getConsumable;
|
|
281
|
-
const
|
|
282
|
-
"add_asset",
|
|
283
|
-
"change_asset",
|
|
284
|
-
"view_asset",
|
|
285
|
-
"delete_asset",
|
|
286
|
-
];
|
|
281
|
+
const CRUD_ASSET_CAPABILITY_URL = `${HOST}/v1/auth/user/me/capability/crud_asset`;
|
|
287
282
|
const listUserAcademies = async (breathecodeToken) => {
|
|
288
|
-
const
|
|
283
|
+
const meUrl = `${HOST}/v1/auth/user/me`;
|
|
289
284
|
try {
|
|
290
|
-
const response = await axios_1.default.get(
|
|
285
|
+
const response = await axios_1.default.get(meUrl, {
|
|
291
286
|
headers: {
|
|
292
287
|
Authorization: `Token ${breathecodeToken}`,
|
|
293
288
|
},
|
|
294
289
|
});
|
|
295
290
|
const data = response.data;
|
|
296
291
|
const academiesMap = new Map();
|
|
292
|
+
if (!Array.isArray(data.roles)) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
297
295
|
for (const role of data.roles) {
|
|
298
296
|
const academy = role.academy;
|
|
297
|
+
if (!academy)
|
|
298
|
+
continue;
|
|
299
299
|
if (!academiesMap.has(academy.id)) {
|
|
300
300
|
academiesMap.set(academy.id, academy);
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
const academies = [...academiesMap.values()];
|
|
304
|
+
const allowed = await Promise.all(academies.map(async (academy) => {
|
|
305
|
+
const capResponse = await axios_1.default.get(CRUD_ASSET_CAPABILITY_URL, {
|
|
306
|
+
headers: {
|
|
307
|
+
Authorization: `Token ${breathecodeToken}`,
|
|
308
|
+
Academy: academy.id,
|
|
309
|
+
},
|
|
310
|
+
validateStatus: () => true,
|
|
311
|
+
});
|
|
312
|
+
if (capResponse.status === 200) {
|
|
313
|
+
return academy;
|
|
314
|
+
}
|
|
315
|
+
if (capResponse.status !== 403) {
|
|
316
|
+
console.warn(`listUserAcademies: unexpected status ${capResponse.status} for academy ${academy.id}`);
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}));
|
|
320
|
+
const filtered = allowed.filter((a) => a !== null);
|
|
321
|
+
filtered.sort((a, b) => a.name.localeCompare(b.name));
|
|
322
|
+
return filtered;
|
|
311
323
|
}
|
|
312
324
|
catch (error) {
|
|
313
325
|
console.error("Failed to fetch user academies:", error);
|
|
@@ -330,6 +342,7 @@ const validateToken = async (token) => {
|
|
|
330
342
|
};
|
|
331
343
|
exports.validateToken = validateToken;
|
|
332
344
|
const createAsset = async (token, asset) => {
|
|
345
|
+
var _a;
|
|
333
346
|
const body = {
|
|
334
347
|
slug: asset.slug,
|
|
335
348
|
title: asset.title,
|
|
@@ -367,6 +380,7 @@ const createAsset = async (token, asset) => {
|
|
|
367
380
|
url = `https://breathecode.herokuapp.com/v1/registry/academy/asset`;
|
|
368
381
|
headers.Academy = String(asset.academy_id);
|
|
369
382
|
}
|
|
383
|
+
console.log("[BC] POST", url, "| academy_id:", (_a = asset.academy_id) !== null && _a !== void 0 ? _a : "none", "| slug:", asset.slug);
|
|
370
384
|
try {
|
|
371
385
|
const response = await axios_1.default.post(url, body, { headers });
|
|
372
386
|
return response.data;
|
|
@@ -398,10 +412,12 @@ const doesAssetExists = async (token, assetSlug) => {
|
|
|
398
412
|
};
|
|
399
413
|
exports.doesAssetExists = doesAssetExists;
|
|
400
414
|
const updateAsset = async (token, assetSlug, asset) => {
|
|
415
|
+
var _a;
|
|
401
416
|
const url = `https://breathecode.herokuapp.com/v1/registry/asset/me/${assetSlug}`;
|
|
402
417
|
const headers = {
|
|
403
418
|
Authorization: `Token ${token}`,
|
|
404
419
|
};
|
|
420
|
+
console.log("[BC] PUT", url, "| academy_id:", (_a = asset.academy_id) !== null && _a !== void 0 ? _a : "none");
|
|
405
421
|
try {
|
|
406
422
|
const response = await axios_1.default.put(url, asset, { headers });
|
|
407
423
|
return response.data;
|
|
@@ -460,7 +476,11 @@ const createRigoPackage = async (token, slug, config) => {
|
|
|
460
476
|
}
|
|
461
477
|
};
|
|
462
478
|
let technologiesCache = [];
|
|
463
|
-
/**
|
|
479
|
+
/**
|
|
480
|
+
* Strip .env comments (e.g. "token # comment") so the token is sent without spaces.
|
|
481
|
+
* @param token - Raw token value from env, possibly with trailing comment.
|
|
482
|
+
* @returns Trimmed token with inline `#` comments removed, or empty string if missing.
|
|
483
|
+
*/
|
|
464
484
|
function sanitizeToken(token) {
|
|
465
485
|
if (!token)
|
|
466
486
|
return "";
|
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.346",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|
package/src/commands/publish.ts
CHANGED
|
@@ -54,6 +54,54 @@ const getLocalizedValue = (
|
|
|
54
54
|
return typeof first === "string" ? first : ""
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
type ExistingAssetInfo = {
|
|
58
|
+
lang: string;
|
|
59
|
+
slug: string;
|
|
60
|
+
exists: boolean;
|
|
61
|
+
academyId?: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type AcademyMode =
|
|
65
|
+
| { type: "select" }
|
|
66
|
+
| { type: "locked"; academyId: number }
|
|
67
|
+
| { type: "conflict"; academies: number[] };
|
|
68
|
+
|
|
69
|
+
const getExistingAssetsInfo = async (
|
|
70
|
+
token: string,
|
|
71
|
+
learnJson: any
|
|
72
|
+
): Promise<ExistingAssetInfo[]> => {
|
|
73
|
+
const availableLangs = getAvailableLangs(learnJson)
|
|
74
|
+
const results: ExistingAssetInfo[] = []
|
|
75
|
+
|
|
76
|
+
for (const lang of availableLangs) {
|
|
77
|
+
const assetTitle = getLocalizedValue(learnJson?.title, lang)
|
|
78
|
+
if (!assetTitle) continue
|
|
79
|
+
|
|
80
|
+
let slug = slugify(assetTitle).slice(0, 47)
|
|
81
|
+
slug = `${slug}-${lang}`
|
|
82
|
+
|
|
83
|
+
// eslint-disable-next-line no-await-in-loop
|
|
84
|
+
const { exists, academyId } = await api.doesAssetExists(token, slug)
|
|
85
|
+
results.push({ lang, slug, exists, academyId })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return results
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const determinePublishAcademyMode = (
|
|
92
|
+
existingAssets: ExistingAssetInfo[]
|
|
93
|
+
): AcademyMode => {
|
|
94
|
+
const academyIds = existingAssets
|
|
95
|
+
.filter((a) => a.exists && a.academyId !== undefined)
|
|
96
|
+
.map((a) => a.academyId as number)
|
|
97
|
+
|
|
98
|
+
const unique = [...new Set(academyIds)]
|
|
99
|
+
|
|
100
|
+
if (unique.length === 0) return { type: "select" }
|
|
101
|
+
if (unique.length === 1) return { type: "locked", academyId: unique[0] }
|
|
102
|
+
return { type: "conflict", academies: unique }
|
|
103
|
+
}
|
|
104
|
+
|
|
57
105
|
export const handleAssetCreation = async (
|
|
58
106
|
sessionPayload: { token: string; rigobotToken: string },
|
|
59
107
|
learnJson: any,
|
|
@@ -61,6 +109,7 @@ export const handleAssetCreation = async (
|
|
|
61
109
|
learnpackDeployUrl: string,
|
|
62
110
|
b64IndexReadme: string,
|
|
63
111
|
academyId: number | undefined,
|
|
112
|
+
preflightInfo?: ExistingAssetInfo,
|
|
64
113
|
all_translations: string[] = []
|
|
65
114
|
) => {
|
|
66
115
|
const category = "uncategorized"
|
|
@@ -83,23 +132,9 @@ export const handleAssetCreation = async (
|
|
|
83
132
|
let slug = slugify(assetTitle).slice(0, 47)
|
|
84
133
|
slug = `${slug}-${selectedLang}`
|
|
85
134
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
slug
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
// Compare academy IDs if asset exists and academyId is provided
|
|
92
|
-
if (
|
|
93
|
-
exists &&
|
|
94
|
-
existingAcademyId !== undefined &&
|
|
95
|
-
academyId !== undefined &&
|
|
96
|
-
existingAcademyId !== academyId
|
|
97
|
-
) {
|
|
98
|
-
Console.warning(
|
|
99
|
-
`Asset exists in academy ${existingAcademyId}, but attempting to publish to academy ${academyId}. ` +
|
|
100
|
-
`The asset will be updated in its current academy (${existingAcademyId}).`
|
|
101
|
-
)
|
|
102
|
-
}
|
|
135
|
+
// Use pre-flight info when available to avoid an extra GET request
|
|
136
|
+
const { exists, academyId: existingAcademyId } =
|
|
137
|
+
preflightInfo ?? (await api.doesAssetExists(sessionPayload.token, slug))
|
|
103
138
|
|
|
104
139
|
// const technologies: unknown[] = Array.isArray(learnJson?.technologies) ?
|
|
105
140
|
// learnJson.technologies :
|
|
@@ -148,14 +183,24 @@ export const handleAssetCreation = async (
|
|
|
148
183
|
}
|
|
149
184
|
|
|
150
185
|
Console.info("Asset exists, updating it")
|
|
151
|
-
const
|
|
186
|
+
const updatePayload: any = {
|
|
152
187
|
graded: true,
|
|
153
188
|
learnpack_deploy_url: learnpackDeployUrl,
|
|
154
189
|
title: assetTitle,
|
|
155
190
|
category: category,
|
|
156
191
|
description: assetDescription,
|
|
157
192
|
all_translations,
|
|
158
|
-
}
|
|
193
|
+
}
|
|
194
|
+
// Only set academy when the asset has none yet and the user selected one
|
|
195
|
+
if (existingAcademyId === undefined && academyId !== undefined) {
|
|
196
|
+
updatePayload.academy_id = academyId
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const asset = await api.updateAsset(
|
|
200
|
+
sessionPayload.token,
|
|
201
|
+
slug,
|
|
202
|
+
updatePayload
|
|
203
|
+
)
|
|
159
204
|
try {
|
|
160
205
|
await api.updateRigoPackage(
|
|
161
206
|
sessionPayload.rigobotToken.trim(),
|
|
@@ -179,7 +224,9 @@ export const handleAssetCreation = async (
|
|
|
179
224
|
const createMultiLangAssetFromDisk = async (
|
|
180
225
|
sessionPayload: { token: string; rigobotToken: string },
|
|
181
226
|
learnJson: any,
|
|
182
|
-
deployUrl: string
|
|
227
|
+
deployUrl: string,
|
|
228
|
+
selectedAcademyId?: number,
|
|
229
|
+
existingAssetsInfo: ExistingAssetInfo[] = []
|
|
183
230
|
) => {
|
|
184
231
|
const availableLangs = getAvailableLangs(learnJson)
|
|
185
232
|
|
|
@@ -208,6 +255,7 @@ const createMultiLangAssetFromDisk = async (
|
|
|
208
255
|
}
|
|
209
256
|
|
|
210
257
|
const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
|
|
258
|
+
const preflightInfo = existingAssetsInfo.find((a) => a.lang === lang)
|
|
211
259
|
|
|
212
260
|
try {
|
|
213
261
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -217,7 +265,8 @@ const createMultiLangAssetFromDisk = async (
|
|
|
217
265
|
lang,
|
|
218
266
|
deployUrl,
|
|
219
267
|
b64IndexReadme,
|
|
220
|
-
|
|
268
|
+
selectedAcademyId,
|
|
269
|
+
preflightInfo,
|
|
221
270
|
all_translations
|
|
222
271
|
)
|
|
223
272
|
|
|
@@ -406,6 +455,13 @@ class BuildCommand extends SessionCommand {
|
|
|
406
455
|
this.configManager?.buildIndex()
|
|
407
456
|
}
|
|
408
457
|
|
|
458
|
+
const learnJsonPath = path.join(process.cwd(), "learn.json")
|
|
459
|
+
if (!fs.existsSync(learnJsonPath)) {
|
|
460
|
+
this.error("learn.json not found")
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const learnJson = JSON.parse(fs.readFileSync(learnJsonPath, "utf-8"))
|
|
464
|
+
|
|
409
465
|
const academies = await api.listUserAcademies(sessionPayload.token)
|
|
410
466
|
|
|
411
467
|
if (academies.length === 0) {
|
|
@@ -415,17 +471,36 @@ class BuildCommand extends SessionCommand {
|
|
|
415
471
|
process.exit(1)
|
|
416
472
|
}
|
|
417
473
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
sessionPayload.token
|
|
474
|
+
Console.info("Checking existing assets...")
|
|
475
|
+
const existingAssetsInfo = await getExistingAssetsInfo(
|
|
476
|
+
sessionPayload.token,
|
|
477
|
+
learnJson
|
|
421
478
|
)
|
|
479
|
+
const academyMode = determinePublishAcademyMode(existingAssetsInfo)
|
|
422
480
|
|
|
423
|
-
|
|
424
|
-
if (!fs.existsSync(learnJsonPath)) {
|
|
425
|
-
this.error("learn.json not found")
|
|
426
|
-
}
|
|
481
|
+
let selectedAcademyId: number | undefined
|
|
427
482
|
|
|
428
|
-
|
|
483
|
+
if (academyMode.type === "conflict") {
|
|
484
|
+
Console.warning(
|
|
485
|
+
`Some of your assets are associated with different academies ` +
|
|
486
|
+
`(${academyMode.academies.join(", ")}). ` +
|
|
487
|
+
`Academy assignment will be skipped to avoid conflicts.`
|
|
488
|
+
)
|
|
489
|
+
} else if (academyMode.type === "locked") {
|
|
490
|
+
const lockedAcademy = academies.find(
|
|
491
|
+
(a) => a.id === academyMode.academyId
|
|
492
|
+
)
|
|
493
|
+
Console.info(
|
|
494
|
+
`This package is associated with academy: ${
|
|
495
|
+
lockedAcademy?.name ?? academyMode.academyId
|
|
496
|
+
}. Academy cannot be changed.`
|
|
497
|
+
)
|
|
498
|
+
selectedAcademyId = academyMode.academyId
|
|
499
|
+
} else {
|
|
500
|
+
// mode === "select": all existing assets have no academy, user picks one
|
|
501
|
+
const { academy } = await selectAcademy(academies, sessionPayload.token)
|
|
502
|
+
selectedAcademyId = academy?.id
|
|
503
|
+
}
|
|
429
504
|
|
|
430
505
|
const zipFilePath = path.join(process.cwd(), `${learnJson.slug}.zip`)
|
|
431
506
|
|
|
@@ -595,7 +670,9 @@ class BuildCommand extends SessionCommand {
|
|
|
595
670
|
await createMultiLangAssetFromDisk(
|
|
596
671
|
{ token: sessionPayload.token, rigobotToken: rigoToken },
|
|
597
672
|
learnJson,
|
|
598
|
-
res.data.url
|
|
673
|
+
res.data.url,
|
|
674
|
+
selectedAcademyId,
|
|
675
|
+
existingAssetsInfo
|
|
599
676
|
)
|
|
600
677
|
} catch (error) {
|
|
601
678
|
if (axios.isAxiosError(error)) {
|
package/src/commands/serve.ts
CHANGED
|
@@ -87,7 +87,8 @@ import { HistoryManager } from "../managers/historyManager"
|
|
|
87
87
|
import { ReadmeHistoryService } from "../managers/readmeHistoryService"
|
|
88
88
|
import { sanitizeReadmeNewlines } from "../utils/readmeSanitizer"
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// eslint-disable-next-line
|
|
91
|
+
const frontMatter = require("front-matter");
|
|
91
92
|
|
|
92
93
|
if (process.env.NEW_RELIC_ENABLED === "true") {
|
|
93
94
|
require("newrelic")
|
|
@@ -624,6 +625,7 @@ const createMultiLangAsset = async (
|
|
|
624
625
|
deployUrl,
|
|
625
626
|
b64IndexReadme,
|
|
626
627
|
academyId,
|
|
628
|
+
undefined,
|
|
627
629
|
all_translations
|
|
628
630
|
)
|
|
629
631
|
|
|
@@ -5451,30 +5453,56 @@ class ServeCommand extends SessionCommand {
|
|
|
5451
5453
|
const { config } = configJson
|
|
5452
5454
|
|
|
5453
5455
|
const availableLangs = Object.keys(config.title || {})
|
|
5454
|
-
let academyId: number | null = null
|
|
5455
5456
|
let isPublished = false
|
|
5456
5457
|
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
if (!assetTitle) continue
|
|
5458
|
+
// Collect academy ids from all existing assets
|
|
5459
|
+
const foundAcademyIds: number[] = []
|
|
5460
5460
|
|
|
5461
|
-
|
|
5462
|
-
|
|
5461
|
+
const slugsToCheck = availableLangs
|
|
5462
|
+
.map(lang => {
|
|
5463
|
+
const assetTitle = getLocalizedValue(config.title, lang)
|
|
5464
|
+
if (!assetTitle) return null
|
|
5465
|
+
const assetSlug = slugify(assetTitle).slice(0, 47)
|
|
5466
|
+
return `${assetSlug}-${lang}`
|
|
5467
|
+
})
|
|
5468
|
+
.filter((slug): slug is string => slug !== null)
|
|
5463
5469
|
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5470
|
+
const assetChecks = await Promise.all(
|
|
5471
|
+
slugsToCheck.map(slug => doesAssetExists(bcToken, slug))
|
|
5472
|
+
)
|
|
5467
5473
|
|
|
5474
|
+
for (const { exists, academyId: existingAcademyId } of assetChecks) {
|
|
5468
5475
|
if (exists) {
|
|
5469
5476
|
isPublished = true
|
|
5470
5477
|
if (existingAcademyId !== undefined) {
|
|
5471
|
-
|
|
5472
|
-
break
|
|
5478
|
+
foundAcademyIds.push(existingAcademyId)
|
|
5473
5479
|
}
|
|
5474
5480
|
}
|
|
5475
5481
|
}
|
|
5476
5482
|
|
|
5477
|
-
|
|
5483
|
+
const uniqueAcademies = [...new Set(foundAcademyIds)]
|
|
5484
|
+
|
|
5485
|
+
type AcademyMode = "select" | "locked" | "conflict";
|
|
5486
|
+
let mode: AcademyMode
|
|
5487
|
+
let lockedAcademyId: number | undefined
|
|
5488
|
+
let conflictAcademies: number[] | undefined
|
|
5489
|
+
|
|
5490
|
+
if (uniqueAcademies.length === 0) {
|
|
5491
|
+
mode = "select"
|
|
5492
|
+
} else if (uniqueAcademies.length === 1) {
|
|
5493
|
+
mode = "locked"
|
|
5494
|
+
lockedAcademyId = uniqueAcademies[0]
|
|
5495
|
+
} else {
|
|
5496
|
+
mode = "conflict"
|
|
5497
|
+
conflictAcademies = uniqueAcademies
|
|
5498
|
+
}
|
|
5499
|
+
|
|
5500
|
+
return res.json({
|
|
5501
|
+
isPublished,
|
|
5502
|
+
mode,
|
|
5503
|
+
lockedAcademyId,
|
|
5504
|
+
conflictAcademies,
|
|
5505
|
+
})
|
|
5478
5506
|
} catch (error) {
|
|
5479
5507
|
console.error("Error fetching package academy:", error)
|
|
5480
5508
|
return res.status(500).json({ error: (error as Error).message })
|