@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.
@@ -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.358",
4
+ "version": "5.0.359",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -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, slugify } from "../utils/creatorUtilities"
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 getLocalizedValue = (
39
- translations: Record<string, any> | undefined,
40
- lang: string,
41
- fallbackLangs: string[] = ["en", "us"]
42
- ): string => {
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
- const user = await api.validateToken(sessionPayload.token)
121
-
122
- const assetTitle = getLocalizedValue(learnJson?.title, selectedLang)
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 updating or creating asset:", error)
224
- throw error
50
+ Console.error("Error reading index readme:", error)
51
+ return ""
225
52
  }
226
53
  }
227
54
 
228
- const createMultiLangAssetFromDisk = async (
55
+ const syncAssetsFromDisk = async (
229
56
  sessionPayload: { token: string; rigobotToken: string },
230
57
  learnJson: any,
231
58
  deployUrl: string,
232
- selectedAcademyId?: number,
233
- existingAssetsInfo: ExistingAssetInfo[] = []
59
+ selectedAcademyId: number | undefined,
60
+ resolution: PackageAssetsResolution
234
61
  ) => {
235
- const availableLangs = getAvailableLangs(learnJson)
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
- const learnpackId = await api.resolveLearnpackPackageId(
245
- sessionPayload.rigobotToken,
246
- learnJson.slug
247
- )
248
- if (learnpackId === null) {
249
- Console.warning(
250
- "Breathecode assets skipped: could not resolve Learnpack package id"
251
- )
252
- return
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 all_translations: string[] = []
256
- for (const lang of availableLangs) {
257
- const readmePath = path.join(
258
- process.cwd(),
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
- let indexReadmeString = ""
263
- try {
264
- if (fs.existsSync(readmePath)) {
265
- indexReadmeString = fs.readFileSync(readmePath, "utf-8")
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 existingAssetsInfo = await getExistingAssetsInfo(
492
- sessionPayload.token,
493
- learnJson
494
- )
495
- const academyMode = determinePublishAcademyMode(existingAssetsInfo)
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 createMultiLangAssetFromDisk(
497
+ await syncAssetsFromDisk(
692
498
  { token: sessionPayload.token, rigobotToken: rigoToken },
693
499
  learnJson,
694
500
  res.data.url,
695
501
  selectedAcademyId,
696
- existingAssetsInfo
502
+ resolution
697
503
  )
698
504
  } catch (error) {
699
505
  if (axios.isAxiosError(error)) {