@learnpack/learnpack 5.0.358 → 5.0.359
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 +0 -10
- package/lib/commands/publish.js +52 -168
- package/lib/commands/serve.js +52 -116
- package/lib/utils/api.d.ts +62 -15
- package/lib/utils/api.js +124 -45
- package/lib/utils/assetResolver.d.ts +116 -0
- package/lib/utils/assetResolver.js +268 -0
- package/lib/utils/assetSync.d.ts +48 -0
- package/lib/utils/assetSync.js +267 -0
- package/lib/utils/creatorUtilities.d.ts +1 -0
- package/lib/utils/creatorUtilities.js +17 -1
- package/package.json +1 -1
- package/src/commands/publish.ts +61 -255
- package/src/commands/serve.ts +65 -140
- package/src/ui/_app/app.js +1784 -1784
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +157 -42
- package/src/utils/assetResolver.ts +368 -0
- package/src/utils/assetSync.ts +343 -0
- package/src/utils/awsCredentials.ts +57 -57
- package/src/utils/creatorUtilities.ts +16 -0
- package/src/utils/descriptions/publishStage.ts +394 -394
- package/src/utils/descriptions/resumePublication.ts +217 -217
- package/src/utils/descriptions/s3Storage.ts +214 -214
- package/src/utils/s3/packageManifestBackfill.ts +771 -771
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLocalizedValue = void 0;
|
|
4
|
+
exports.syncPackageAssets = syncPackageAssets;
|
|
5
|
+
/**
|
|
6
|
+
* Creates or updates the Breathecode assets of a published package, one per
|
|
7
|
+
* language, and writes the resulting ids back to the Rigobot package.
|
|
8
|
+
*
|
|
9
|
+
* Kept free of fs, oclif and express so the whole decision table can be
|
|
10
|
+
* exercised with HTTP mocks alone; `publish` and `serve` only supply the
|
|
11
|
+
* readme of each language and the tokens.
|
|
12
|
+
*/
|
|
13
|
+
const api_1 = require("./api");
|
|
14
|
+
const console_1 = require("./console");
|
|
15
|
+
const creatorUtilities_1 = require("./creatorUtilities");
|
|
16
|
+
const assetResolver_1 = require("./assetResolver");
|
|
17
|
+
/**
|
|
18
|
+
* Localized value with the same fallbacks the publish flow always used.
|
|
19
|
+
* @param translations - A learn.json map such as `title` or `description`.
|
|
20
|
+
* @param lang - Preferred language key.
|
|
21
|
+
* @returns The value for `lang`, else "en"/"us", else the first one, else "".
|
|
22
|
+
*/
|
|
23
|
+
const getLocalizedValue = (translations, lang) => {
|
|
24
|
+
if (!translations || typeof translations !== "object")
|
|
25
|
+
return "";
|
|
26
|
+
const direct = translations[lang];
|
|
27
|
+
if (typeof direct === "string" && direct.trim().length > 0)
|
|
28
|
+
return direct;
|
|
29
|
+
for (const fb of ["en", "us"]) {
|
|
30
|
+
const v = translations[fb];
|
|
31
|
+
if (typeof v === "string" && v.trim().length > 0)
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
const firstKey = Object.keys(translations)[0];
|
|
35
|
+
const first = firstKey ? translations[firstKey] : "";
|
|
36
|
+
return typeof first === "string" ? first : "";
|
|
37
|
+
};
|
|
38
|
+
exports.getLocalizedValue = getLocalizedValue;
|
|
39
|
+
const packageError = (detail) => ({
|
|
40
|
+
kind: "package_error",
|
|
41
|
+
error: { detail },
|
|
42
|
+
});
|
|
43
|
+
const langError = (lang, detail) => ({
|
|
44
|
+
kind: "lang_error",
|
|
45
|
+
lang,
|
|
46
|
+
error: { detail },
|
|
47
|
+
});
|
|
48
|
+
const errorDetail = (error) => {
|
|
49
|
+
const err = error;
|
|
50
|
+
if (typeof (err === null || err === void 0 ? void 0 : err.detail) === "string")
|
|
51
|
+
return err.detail;
|
|
52
|
+
if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string")
|
|
53
|
+
return err.message;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(err);
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
return String(err);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Synchronises every language of a package with Breathecode.
|
|
63
|
+
* @param input - Tokens, package data, deploy url and readme provider.
|
|
64
|
+
* @returns Per-language errors (the publish itself is never blocked), warnings worth logging, and what was written.
|
|
65
|
+
*/
|
|
66
|
+
async function syncPackageAssets(input) {
|
|
67
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
68
|
+
const warnings = [];
|
|
69
|
+
const errors = [];
|
|
70
|
+
const synced = [];
|
|
71
|
+
const user = await api_1.default.validateToken(input.bcToken);
|
|
72
|
+
if (!user || typeof user.id !== "number") {
|
|
73
|
+
return {
|
|
74
|
+
errors: [packageError("Invalid Breathecode token; assets not synced.")],
|
|
75
|
+
warnings,
|
|
76
|
+
synced,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const resolution = (_a = input.resolution) !== null && _a !== void 0 ? _a : (await (0, assetResolver_1.resolvePackageAssets)({
|
|
80
|
+
bcToken: input.bcToken,
|
|
81
|
+
rigoToken: input.rigoToken,
|
|
82
|
+
packageSlug: input.packageSlug,
|
|
83
|
+
learnJson: input.learnJson,
|
|
84
|
+
}));
|
|
85
|
+
warnings.push(...resolution.warnings);
|
|
86
|
+
if (resolution.packageState === "unknown") {
|
|
87
|
+
return {
|
|
88
|
+
errors: [
|
|
89
|
+
packageError(`${(_b = resolution.packageError) !== null && _b !== void 0 ? _b : "Existing assets could not be checked"}; nothing was created or updated to avoid duplicates.`),
|
|
90
|
+
],
|
|
91
|
+
warnings,
|
|
92
|
+
synced,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const learnpackId = resolution.learnpackId;
|
|
96
|
+
if (learnpackId === null) {
|
|
97
|
+
return {
|
|
98
|
+
errors: [
|
|
99
|
+
packageError("Could not resolve Learnpack package id; assets not synced to Breathecode."),
|
|
100
|
+
],
|
|
101
|
+
warnings,
|
|
102
|
+
synced,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Decide every language before touching anything, so all_translations can
|
|
106
|
+
// name the full set up front (the PUT replaces the relation, it does not add).
|
|
107
|
+
const langs = (0, assetResolver_1.dedupeLangs)((0, assetResolver_1.getAvailableLangs)(input.learnJson));
|
|
108
|
+
const plans = [];
|
|
109
|
+
for (const lang of langs) {
|
|
110
|
+
const title = (0, exports.getLocalizedValue)((_c = input.learnJson) === null || _c === void 0 ? void 0 : _c.title, lang);
|
|
111
|
+
if (!title) {
|
|
112
|
+
plans.push({
|
|
113
|
+
lang,
|
|
114
|
+
kind: "skip",
|
|
115
|
+
detail: `Missing learn.json title for language "${lang}"`,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const entry = resolution.byLang.get((0, assetResolver_1.canonicalLang)(lang));
|
|
120
|
+
if (!entry || entry.status === "unknown") {
|
|
121
|
+
plans.push({
|
|
122
|
+
lang,
|
|
123
|
+
kind: "skip",
|
|
124
|
+
detail: `${(entry === null || entry === void 0 ? void 0 : entry.status) === "unknown" ?
|
|
125
|
+
entry.error :
|
|
126
|
+
"Existing asset could not be checked"}; nothing was created or updated for this language to avoid duplicates.`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else if (entry.status === "ambiguous") {
|
|
130
|
+
plans.push({
|
|
131
|
+
lang,
|
|
132
|
+
kind: "skip",
|
|
133
|
+
detail: (0, assetResolver_1.describeAmbiguity)(lang, entry.candidates),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else if (entry.status === "found") {
|
|
137
|
+
if (entry.asset.ownerId !== null && entry.asset.ownerId !== user.id) {
|
|
138
|
+
plans.push({
|
|
139
|
+
lang,
|
|
140
|
+
kind: "skip",
|
|
141
|
+
detail: `Asset #${entry.asset.id} ${entry.asset.slug} belongs to another user (owner ${entry.asset.ownerId}); it was not updated and no duplicate was created.`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
plans.push({
|
|
146
|
+
lang,
|
|
147
|
+
kind: "update",
|
|
148
|
+
asset: entry.asset,
|
|
149
|
+
slug: entry.asset.slug,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
plans.push({ lang, kind: "create", slug: (0, creatorUtilities_1.buildAssetSlug)(title, lang) });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const allTranslations = plans
|
|
158
|
+
.filter(p => p.kind !== "skip")
|
|
159
|
+
.map(p => p.slug);
|
|
160
|
+
const rigoToken = input.rigoToken.trim();
|
|
161
|
+
const createdOrUpdatedIds = [];
|
|
162
|
+
for (const plan of plans) {
|
|
163
|
+
if (plan.kind === "skip") {
|
|
164
|
+
errors.push(langError(plan.lang, plan.detail));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const title = (0, exports.getLocalizedValue)((_d = input.learnJson) === null || _d === void 0 ? void 0 : _d.title, plan.lang);
|
|
168
|
+
const description = (0, exports.getLocalizedValue)((_e = input.learnJson) === null || _e === void 0 ? void 0 : _e.description, plan.lang);
|
|
169
|
+
try {
|
|
170
|
+
if (plan.kind === "update") {
|
|
171
|
+
// Never send lang or slug: the language of an existing asset is not
|
|
172
|
+
// ours to change, and a slug would rename it.
|
|
173
|
+
const updatePayload = {
|
|
174
|
+
graded: true,
|
|
175
|
+
learnpack_deploy_url: input.deployUrl,
|
|
176
|
+
title,
|
|
177
|
+
category: "uncategorized",
|
|
178
|
+
description,
|
|
179
|
+
all_translations: allTranslations,
|
|
180
|
+
learnpack_id: learnpackId,
|
|
181
|
+
};
|
|
182
|
+
if (plan.asset.academyId === null && input.academyId !== undefined) {
|
|
183
|
+
updatePayload.academy_id = input.academyId;
|
|
184
|
+
}
|
|
185
|
+
console_1.default.info(`Asset exists for "${plan.lang}" (#${plan.asset.id} ${plan.slug}), updating it`);
|
|
186
|
+
// eslint-disable-next-line no-await-in-loop
|
|
187
|
+
const asset = await api_1.default.updateAsset(input.bcToken, plan.asset.id, updatePayload);
|
|
188
|
+
const id = typeof (asset === null || asset === void 0 ? void 0 : asset.id) === "number" ? asset.id : plan.asset.id;
|
|
189
|
+
createdOrUpdatedIds.push(id);
|
|
190
|
+
synced.push({
|
|
191
|
+
lang: plan.lang,
|
|
192
|
+
action: "updated",
|
|
193
|
+
asset: { id, slug: (_f = asset === null || asset === void 0 ? void 0 : asset.slug) !== null && _f !== void 0 ? _f : plan.slug },
|
|
194
|
+
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// eslint-disable-next-line no-await-in-loop
|
|
198
|
+
const readme = await input.readReadme(plan.lang);
|
|
199
|
+
const assetPayload = {
|
|
200
|
+
slug: plan.slug,
|
|
201
|
+
title,
|
|
202
|
+
lang: plan.lang,
|
|
203
|
+
graded: true,
|
|
204
|
+
description,
|
|
205
|
+
learnpack_deploy_url: input.deployUrl,
|
|
206
|
+
technologies: [],
|
|
207
|
+
url: input.deployUrl,
|
|
208
|
+
category: "uncategorized",
|
|
209
|
+
owner: user.id,
|
|
210
|
+
author: user.id,
|
|
211
|
+
preview: (_g = input.learnJson) === null || _g === void 0 ? void 0 : _g.preview,
|
|
212
|
+
readme_raw: Buffer.from(readme).toString("base64"),
|
|
213
|
+
all_translations: allTranslations,
|
|
214
|
+
learnpack_id: learnpackId,
|
|
215
|
+
};
|
|
216
|
+
if (input.academyId !== undefined) {
|
|
217
|
+
assetPayload.academy_id = input.academyId;
|
|
218
|
+
}
|
|
219
|
+
console_1.default.info(`No asset for "${plan.lang}", creating ${plan.slug}`);
|
|
220
|
+
// eslint-disable-next-line no-await-in-loop
|
|
221
|
+
const asset = await api_1.default.createAsset(input.bcToken, assetPayload);
|
|
222
|
+
createdOrUpdatedIds.push(asset.id);
|
|
223
|
+
synced.push({
|
|
224
|
+
lang: plan.lang,
|
|
225
|
+
action: "created",
|
|
226
|
+
asset: { id: asset.id, slug: (_h = asset.slug) !== null && _h !== void 0 ? _h : plan.slug },
|
|
227
|
+
});
|
|
228
|
+
// Record the id right away: a stale Breathecode cache could hide the
|
|
229
|
+
// new slug from the next publish, but the id never lies.
|
|
230
|
+
try {
|
|
231
|
+
// eslint-disable-next-line no-await-in-loop
|
|
232
|
+
await api_1.default.updateRigoPackage(rigoToken, input.packageSlug, {
|
|
233
|
+
asset_id: asset.id,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
warnings.push(`Could not record asset #${asset.id} in the Rigobot package: ${errorDetail(error)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const errorData = error && typeof error === "object" && "response" in error ?
|
|
242
|
+
((_j = error.response) === null || _j === void 0 ? void 0 : _j.data) || error :
|
|
243
|
+
error;
|
|
244
|
+
console_1.default.error(`Error syncing asset for language ${plan.lang}:`, error);
|
|
245
|
+
errors.push(langError(plan.lang, errorDetail(errorData)));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Final list: everything Rigobot knew that still exists and is ours, plus
|
|
249
|
+
// whatever this run touched. Ids that 404 or belong to another package are
|
|
250
|
+
// the only ones that fall off; duplicates awaiting a manual decision stay.
|
|
251
|
+
const finalIds = [
|
|
252
|
+
...new Set([
|
|
253
|
+
...resolution.knownAssetIds,
|
|
254
|
+
...resolution.orphanIds,
|
|
255
|
+
...createdOrUpdatedIds,
|
|
256
|
+
]),
|
|
257
|
+
];
|
|
258
|
+
try {
|
|
259
|
+
await api_1.default.updateRigoPackage(rigoToken, input.packageSlug, {
|
|
260
|
+
asset_ids: finalIds,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
warnings.push(`Could not write asset_ids ${JSON.stringify(finalIds)} to the Rigobot package: ${errorDetail(error)}`);
|
|
265
|
+
}
|
|
266
|
+
return { errors, warnings, synced };
|
|
267
|
+
}
|
|
@@ -27,6 +27,7 @@ export declare function checkReadability(markdown: string, wordsPerMinute?: numb
|
|
|
27
27
|
fkglResult: TFKGLResult;
|
|
28
28
|
};
|
|
29
29
|
export declare const slugify: (text: string) => string;
|
|
30
|
+
export declare const buildAssetSlug: (title: string, lang: string) => string;
|
|
30
31
|
export declare const getExInfo: (title: string) => {
|
|
31
32
|
exNumber: string;
|
|
32
33
|
kind: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getReadmeExtension = exports.appendAIRules = exports.appendContentIndex = exports.saveTranslatedReadme = exports.makePackageInfo = exports.getExInfo = exports.slugify = exports.estimateReadingTime = void 0;
|
|
3
|
+
exports.getReadmeExtension = exports.appendAIRules = exports.appendContentIndex = exports.saveTranslatedReadme = exports.makePackageInfo = exports.getExInfo = exports.buildAssetSlug = exports.slugify = exports.estimateReadingTime = void 0;
|
|
4
4
|
exports.checkReadability = checkReadability;
|
|
5
5
|
exports.extractImagesFromMarkdown = extractImagesFromMarkdown;
|
|
6
6
|
exports.getFilenameFromUrl = getFilenameFromUrl;
|
|
@@ -98,6 +98,22 @@ const slugify = (text) => {
|
|
|
98
98
|
.replace(/^-+|-+$/g, ""); // Trim hyphens from start/end
|
|
99
99
|
};
|
|
100
100
|
exports.slugify = slugify;
|
|
101
|
+
// Builds the slug of the Breathecode asset for a given title and language.
|
|
102
|
+
// The backend only accepts letters, numbers, underscores and hyphens, so the
|
|
103
|
+
// periods that slugify() keeps (e.g. "Next.js") are turned into hyphens here.
|
|
104
|
+
// Hyphens are collapsed and trimmed BEFORE the 47-char cut on purpose: for a
|
|
105
|
+
// title without periods this must yield exactly slugify(title).slice(0, 47),
|
|
106
|
+
// which is the slug every existing asset was created with (a trailing hyphen
|
|
107
|
+
// left by the cut is part of that historical slug and is kept).
|
|
108
|
+
const buildAssetSlug = (title, lang) => {
|
|
109
|
+
const slug = (0, exports.slugify)(title)
|
|
110
|
+
.replace(/\./g, "-")
|
|
111
|
+
.replace(/-+/g, "-")
|
|
112
|
+
.replace(/^-+|-+$/g, "")
|
|
113
|
+
.slice(0, 47);
|
|
114
|
+
return `${slug}-${lang}`;
|
|
115
|
+
};
|
|
116
|
+
exports.buildAssetSlug = buildAssetSlug;
|
|
101
117
|
const getExInfo = (title) => {
|
|
102
118
|
// Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
|
|
103
119
|
let [exNumber, exTitle] = title.split(" - ");
|
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.359",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|
package/src/commands/publish.ts
CHANGED
|
@@ -19,282 +19,78 @@ import api, { getConsumable, RIGOBOT_HOST, TAcademy } from "../utils/api"
|
|
|
19
19
|
import * as prompts from "prompts"
|
|
20
20
|
import { isValidRigoToken } from "../utils/rigoActions"
|
|
21
21
|
import { minutesToISO8601Duration } from "../utils/misc"
|
|
22
|
-
import { getReadmeExtension
|
|
22
|
+
import { getReadmeExtension } from "../utils/creatorUtilities"
|
|
23
23
|
import { getFallbackPreviewUrl } from "../utils/branding"
|
|
24
|
+
import {
|
|
25
|
+
determineAcademyMode,
|
|
26
|
+
getAvailableLangs,
|
|
27
|
+
PackageAssetsResolution,
|
|
28
|
+
resolvePackageAssets,
|
|
29
|
+
} from "../utils/assetResolver"
|
|
30
|
+
import { getLocalizedValue, syncPackageAssets } from "../utils/assetSync"
|
|
24
31
|
|
|
25
32
|
const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"
|
|
26
33
|
|
|
27
|
-
const getAvailableLangs = (learnJson: any): string[] => {
|
|
28
|
-
const langs = Object.keys(learnJson?.title || {})
|
|
29
|
-
return langs.filter((l) => typeof l === "string" && l.length > 0)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
34
|
const getDefaultLang = (learnJson: any): string => {
|
|
33
35
|
const availableLangs = getAvailableLangs(learnJson)
|
|
34
36
|
if (availableLangs.length === 0) return "en"
|
|
35
37
|
return availableLangs.includes("en") ? "en" : availableLangs[0]
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
if (!translations || typeof translations !== "object") return ""
|
|
44
|
-
|
|
45
|
-
const direct = translations[lang]
|
|
46
|
-
if (typeof direct === "string" && direct.trim().length > 0) return direct
|
|
47
|
-
|
|
48
|
-
for (const fb of fallbackLangs) {
|
|
49
|
-
const v = translations[fb]
|
|
50
|
-
if (typeof v === "string" && v.trim().length > 0) return v
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const firstKey = Object.keys(translations)[0]
|
|
54
|
-
const first = firstKey ? translations[firstKey] : ""
|
|
55
|
-
return typeof first === "string" ? first : ""
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
type ExistingAssetInfo = {
|
|
59
|
-
lang: string;
|
|
60
|
-
slug: string;
|
|
61
|
-
exists: boolean;
|
|
62
|
-
academyId?: number;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
type AcademyMode =
|
|
66
|
-
| { type: "select" }
|
|
67
|
-
| { type: "locked"; academyId: number }
|
|
68
|
-
| { type: "conflict"; academies: number[] };
|
|
69
|
-
|
|
70
|
-
const getExistingAssetsInfo = async (
|
|
71
|
-
token: string,
|
|
72
|
-
learnJson: any
|
|
73
|
-
): Promise<ExistingAssetInfo[]> => {
|
|
74
|
-
const availableLangs = getAvailableLangs(learnJson)
|
|
75
|
-
const results: ExistingAssetInfo[] = []
|
|
76
|
-
|
|
77
|
-
for (const lang of availableLangs) {
|
|
78
|
-
const assetTitle = getLocalizedValue(learnJson?.title, lang)
|
|
79
|
-
if (!assetTitle) continue
|
|
80
|
-
|
|
81
|
-
let slug = slugify(assetTitle).slice(0, 47)
|
|
82
|
-
slug = `${slug}-${lang}`
|
|
83
|
-
|
|
84
|
-
// eslint-disable-next-line no-await-in-loop
|
|
85
|
-
const { exists, academyId } = await api.doesAssetExists(token, slug)
|
|
86
|
-
results.push({ lang, slug, exists, academyId })
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return results
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const determinePublishAcademyMode = (
|
|
93
|
-
existingAssets: ExistingAssetInfo[]
|
|
94
|
-
): AcademyMode => {
|
|
95
|
-
const academyIds = existingAssets
|
|
96
|
-
.filter((a) => a.exists && a.academyId !== undefined)
|
|
97
|
-
.map((a) => a.academyId as number)
|
|
98
|
-
|
|
99
|
-
const unique = [...new Set(academyIds)]
|
|
100
|
-
|
|
101
|
-
if (unique.length === 0) return { type: "select" }
|
|
102
|
-
if (unique.length === 1) return { type: "locked", academyId: unique[0] }
|
|
103
|
-
return { type: "conflict", academies: unique }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export const handleAssetCreation = async (
|
|
107
|
-
sessionPayload: { token: string; rigobotToken: string },
|
|
108
|
-
learnJson: any,
|
|
109
|
-
selectedLang: string,
|
|
110
|
-
learnpackDeployUrl: string,
|
|
111
|
-
b64IndexReadme: string,
|
|
112
|
-
academyId: number | undefined,
|
|
113
|
-
learnpackId: number,
|
|
114
|
-
preflightInfo?: ExistingAssetInfo,
|
|
115
|
-
all_translations: string[] = []
|
|
116
|
-
) => {
|
|
117
|
-
const category = "uncategorized"
|
|
118
|
-
|
|
40
|
+
const readReadmeFromDisk = async (lang: string): Promise<string> => {
|
|
41
|
+
const readmePath = path.join(
|
|
42
|
+
process.cwd(),
|
|
43
|
+
`README${getReadmeExtension(lang)}`
|
|
44
|
+
)
|
|
119
45
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const assetDescription = getLocalizedValue(
|
|
124
|
-
learnJson?.description,
|
|
125
|
-
selectedLang
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
if (!assetTitle) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`Missing learn.json title for language "${selectedLang}"`
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let slug = slugify(assetTitle).slice(0, 47)
|
|
135
|
-
slug = `${slug}-${selectedLang}`
|
|
136
|
-
|
|
137
|
-
// Use pre-flight info when available to avoid an extra GET request
|
|
138
|
-
const { exists, academyId: existingAcademyId } =
|
|
139
|
-
preflightInfo ?? (await api.doesAssetExists(sessionPayload.token, slug))
|
|
140
|
-
|
|
141
|
-
// const technologies: unknown[] = Array.isArray(learnJson?.technologies) ?
|
|
142
|
-
// learnJson.technologies :
|
|
143
|
-
// []
|
|
144
|
-
|
|
145
|
-
if (!exists) {
|
|
146
|
-
Console.info("Asset does not exist in this academy, creating it")
|
|
147
|
-
const assetPayload: any = {
|
|
148
|
-
slug: slug,
|
|
149
|
-
title: assetTitle,
|
|
150
|
-
lang: selectedLang,
|
|
151
|
-
graded: true,
|
|
152
|
-
description: assetDescription,
|
|
153
|
-
learnpack_deploy_url: learnpackDeployUrl,
|
|
154
|
-
// technologies: technologies.map((tech: unknown) =>
|
|
155
|
-
// String(tech).toLowerCase().replace(/\s+/g, "-")
|
|
156
|
-
// ),
|
|
157
|
-
technologies: [],
|
|
158
|
-
url: learnpackDeployUrl,
|
|
159
|
-
category: category,
|
|
160
|
-
owner: user.id,
|
|
161
|
-
author: user.id,
|
|
162
|
-
preview: learnJson.preview,
|
|
163
|
-
readme_raw: b64IndexReadme,
|
|
164
|
-
all_translations,
|
|
165
|
-
learnpack_id: learnpackId,
|
|
166
|
-
}
|
|
167
|
-
if (academyId !== undefined) {
|
|
168
|
-
assetPayload.academy_id = academyId
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const asset = await api.createAsset(sessionPayload.token, assetPayload)
|
|
172
|
-
try {
|
|
173
|
-
await api.updateRigoPackage(
|
|
174
|
-
sessionPayload.rigobotToken.trim(),
|
|
175
|
-
learnJson.slug,
|
|
176
|
-
{
|
|
177
|
-
asset_id: asset.id,
|
|
178
|
-
}
|
|
179
|
-
)
|
|
180
|
-
} catch (error) {
|
|
181
|
-
Console.error("Error updating Rigo package:", error)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
Console.info("Asset created with id", asset.id)
|
|
185
|
-
return asset
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
Console.info("Asset exists, updating it")
|
|
189
|
-
const updatePayload: any = {
|
|
190
|
-
graded: true,
|
|
191
|
-
learnpack_deploy_url: learnpackDeployUrl,
|
|
192
|
-
title: assetTitle,
|
|
193
|
-
category: category,
|
|
194
|
-
description: assetDescription,
|
|
195
|
-
all_translations,
|
|
196
|
-
learnpack_id: learnpackId,
|
|
197
|
-
}
|
|
198
|
-
// Only set academy when the asset has none yet and the user selected one
|
|
199
|
-
if (existingAcademyId === undefined && academyId !== undefined) {
|
|
200
|
-
updatePayload.academy_id = academyId
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const asset = await api.updateAsset(
|
|
204
|
-
sessionPayload.token,
|
|
205
|
-
slug,
|
|
206
|
-
updatePayload
|
|
207
|
-
)
|
|
208
|
-
try {
|
|
209
|
-
await api.updateRigoPackage(
|
|
210
|
-
sessionPayload.rigobotToken.trim(),
|
|
211
|
-
learnJson.slug,
|
|
212
|
-
{
|
|
213
|
-
asset_id: asset.id,
|
|
214
|
-
}
|
|
215
|
-
)
|
|
216
|
-
} catch (error) {
|
|
217
|
-
Console.error("Error updating Rigo package:", error)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
Console.info("Asset updated with id", asset.id)
|
|
221
|
-
return asset
|
|
46
|
+
return fs.existsSync(readmePath) ?
|
|
47
|
+
fs.readFileSync(readmePath, "utf-8") :
|
|
48
|
+
""
|
|
222
49
|
} catch (error) {
|
|
223
|
-
Console.error("Error
|
|
224
|
-
|
|
50
|
+
Console.error("Error reading index readme:", error)
|
|
51
|
+
return ""
|
|
225
52
|
}
|
|
226
53
|
}
|
|
227
54
|
|
|
228
|
-
const
|
|
55
|
+
const syncAssetsFromDisk = async (
|
|
229
56
|
sessionPayload: { token: string; rigobotToken: string },
|
|
230
57
|
learnJson: any,
|
|
231
58
|
deployUrl: string,
|
|
232
|
-
selectedAcademyId
|
|
233
|
-
|
|
59
|
+
selectedAcademyId: number | undefined,
|
|
60
|
+
resolution: PackageAssetsResolution
|
|
234
61
|
) => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (availableLangs.length === 0) {
|
|
62
|
+
if (getAvailableLangs(learnJson).length === 0) {
|
|
238
63
|
Console.error(
|
|
239
64
|
"No languages found in learn.json.title. Add at least one language (e.g. title.en)."
|
|
240
65
|
)
|
|
241
66
|
return
|
|
242
67
|
}
|
|
243
68
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
69
|
+
// The pre-upload resolution is reused unless the package did not exist in
|
|
70
|
+
// Rigobot yet (first publish): the upload has just created it.
|
|
71
|
+
const result = await syncPackageAssets({
|
|
72
|
+
bcToken: sessionPayload.token,
|
|
73
|
+
rigoToken: sessionPayload.rigobotToken,
|
|
74
|
+
packageSlug: learnJson.slug,
|
|
75
|
+
learnJson,
|
|
76
|
+
deployUrl,
|
|
77
|
+
academyId: selectedAcademyId,
|
|
78
|
+
readReadme: readReadmeFromDisk,
|
|
79
|
+
resolution: resolution.learnpackId === null ? undefined : resolution,
|
|
80
|
+
})
|
|
254
81
|
|
|
255
|
-
const
|
|
256
|
-
for (const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
`README${getReadmeExtension(lang)}`
|
|
82
|
+
for (const warning of result.warnings) Console.warning(warning)
|
|
83
|
+
for (const synced of result.synced) {
|
|
84
|
+
Console.info(
|
|
85
|
+
`Asset ${synced.action} for "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`
|
|
260
86
|
)
|
|
87
|
+
}
|
|
261
88
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
} catch (error) {
|
|
268
|
-
Console.error("Error reading index readme:", error)
|
|
269
|
-
indexReadmeString = ""
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
|
|
273
|
-
const preflightInfo = existingAssetsInfo.find((a) => a.lang === lang)
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
// eslint-disable-next-line no-await-in-loop
|
|
277
|
-
const asset = await handleAssetCreation(
|
|
278
|
-
sessionPayload,
|
|
279
|
-
learnJson,
|
|
280
|
-
lang,
|
|
281
|
-
deployUrl,
|
|
282
|
-
b64IndexReadme,
|
|
283
|
-
selectedAcademyId,
|
|
284
|
-
learnpackId,
|
|
285
|
-
preflightInfo,
|
|
286
|
-
all_translations
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
if (!asset) {
|
|
290
|
-
Console.debug("Could not create/update asset for lang", lang)
|
|
291
|
-
continue
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
all_translations.push(asset.slug)
|
|
295
|
-
} catch (error) {
|
|
296
|
-
Console.error("Error creating asset for language", lang, error)
|
|
297
|
-
// Continue with other languages
|
|
89
|
+
for (const error of result.errors) {
|
|
90
|
+
if (error.kind === "lang_error") {
|
|
91
|
+
Console.error(`Asset for "${error.lang}": ${error.error.detail}`)
|
|
92
|
+
} else {
|
|
93
|
+
Console.error(error.error.detail)
|
|
298
94
|
}
|
|
299
95
|
}
|
|
300
96
|
}
|
|
@@ -488,11 +284,21 @@ class BuildCommand extends SessionCommand {
|
|
|
488
284
|
}
|
|
489
285
|
|
|
490
286
|
Console.info("Checking existing assets...")
|
|
491
|
-
const
|
|
492
|
-
sessionPayload.token,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
287
|
+
const resolution = await resolvePackageAssets({
|
|
288
|
+
bcToken: sessionPayload.token,
|
|
289
|
+
rigoToken: rigoToken,
|
|
290
|
+
packageSlug: learnJson.slug,
|
|
291
|
+
learnJson,
|
|
292
|
+
})
|
|
293
|
+
for (const warning of resolution.warnings) Console.warning(warning)
|
|
294
|
+
if (resolution.packageState === "unknown") {
|
|
295
|
+
Console.error(
|
|
296
|
+
`${resolution.packageError}. Existing assets cannot be checked, so publishing now could create duplicates. Try again later.`
|
|
297
|
+
)
|
|
298
|
+
process.exit(1)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const academyMode = determineAcademyMode(resolution)
|
|
496
302
|
|
|
497
303
|
let selectedAcademyId: number | undefined
|
|
498
304
|
|
|
@@ -688,12 +494,12 @@ class BuildCommand extends SessionCommand {
|
|
|
688
494
|
fs.unlinkSync(zipFilePath)
|
|
689
495
|
this.removeDirectory(buildDir)
|
|
690
496
|
|
|
691
|
-
await
|
|
497
|
+
await syncAssetsFromDisk(
|
|
692
498
|
{ token: sessionPayload.token, rigobotToken: rigoToken },
|
|
693
499
|
learnJson,
|
|
694
500
|
res.data.url,
|
|
695
501
|
selectedAcademyId,
|
|
696
|
-
|
|
502
|
+
resolution
|
|
697
503
|
)
|
|
698
504
|
} catch (error) {
|
|
699
505
|
if (axios.isAxiosError(error)) {
|