@learnpack/learnpack 5.0.89 → 5.0.94
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/README.md +13 -13
- package/lib/commands/publish.d.ts +2 -0
- package/lib/commands/publish.js +3 -1
- package/lib/commands/serve.js +168 -39
- package/lib/creatorDist/assets/{index-B48vF2UK.js → index-EA507Y8n.js} +36 -30
- package/lib/creatorDist/index.html +1 -1
- package/lib/utils/configBuilder.d.ts +21 -0
- package/lib/utils/configBuilder.js +49 -0
- package/oclif.manifest.json +1 -1
- package/package.json +4 -1
- package/src/commands/publish.ts +1 -1
- package/src/commands/serve.ts +216 -60
- package/src/creator/src/App.tsx +3 -1
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +2 -1
- package/src/creator/src/utils/store.ts +4 -0
- package/src/creatorDist/assets/{index-B48vF2UK.js → index-EA507Y8n.js} +36 -30
- package/src/creatorDist/index.html +1 -1
- package/src/utils/configBuilder.ts +73 -0
package/README.md
CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @learnpack/learnpack
|
|
21
21
|
$ learnpack COMMAND
|
22
22
|
running command...
|
23
23
|
$ learnpack (-v|--version|version)
|
24
|
-
@learnpack/learnpack/5.0.
|
24
|
+
@learnpack/learnpack/5.0.94 win32-x64 node-v22.15.0
|
25
25
|
$ learnpack --help [COMMAND]
|
26
26
|
USAGE
|
27
27
|
$ learnpack COMMAND
|
@@ -80,7 +80,7 @@ DESCRIPTION
|
|
80
80
|
12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
|
81
81
|
```
|
82
82
|
|
83
|
-
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
83
|
+
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\audit.ts)_
|
84
84
|
|
85
85
|
## `learnpack breakToken`
|
86
86
|
|
@@ -95,7 +95,7 @@ OPTIONS
|
|
95
95
|
-y, --yes Skip all prompts and initialize an empty project
|
96
96
|
```
|
97
97
|
|
98
|
-
_See code: [src\commands\breakToken.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
98
|
+
_See code: [src\commands\breakToken.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\breakToken.ts)_
|
99
99
|
|
100
100
|
## `learnpack clean`
|
101
101
|
|
@@ -110,7 +110,7 @@ DESCRIPTION
|
|
110
110
|
Extra documentation goes here
|
111
111
|
```
|
112
112
|
|
113
|
-
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
113
|
+
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\clean.ts)_
|
114
114
|
|
115
115
|
## `learnpack download [PACKAGE]`
|
116
116
|
|
@@ -128,7 +128,7 @@ DESCRIPTION
|
|
128
128
|
Extra documentation goes here
|
129
129
|
```
|
130
130
|
|
131
|
-
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
131
|
+
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\download.ts)_
|
132
132
|
|
133
133
|
## `learnpack help [COMMAND]`
|
134
134
|
|
@@ -160,7 +160,7 @@ OPTIONS
|
|
160
160
|
-y, --yes Skip all prompts and initialize an empty project
|
161
161
|
```
|
162
162
|
|
163
|
-
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
163
|
+
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\init.ts)_
|
164
164
|
|
165
165
|
## `learnpack login [PACKAGE]`
|
166
166
|
|
@@ -178,7 +178,7 @@ DESCRIPTION
|
|
178
178
|
Extra documentation goes here
|
179
179
|
```
|
180
180
|
|
181
|
-
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
181
|
+
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\login.ts)_
|
182
182
|
|
183
183
|
## `learnpack logout [PACKAGE]`
|
184
184
|
|
@@ -196,7 +196,7 @@ DESCRIPTION
|
|
196
196
|
Extra documentation goes here
|
197
197
|
```
|
198
198
|
|
199
|
-
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
199
|
+
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\logout.ts)_
|
200
200
|
|
201
201
|
## `learnpack plugins`
|
202
202
|
|
@@ -328,7 +328,7 @@ OPTIONS
|
|
328
328
|
-s, --strict strict mode
|
329
329
|
```
|
330
330
|
|
331
|
-
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
331
|
+
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\publish.ts)_
|
332
332
|
|
333
333
|
## `learnpack serve`
|
334
334
|
|
@@ -345,7 +345,7 @@ OPTIONS
|
|
345
345
|
-y, --yes Skip all prompts and initialize an empty project
|
346
346
|
```
|
347
347
|
|
348
|
-
_See code: [src\commands\serve.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
348
|
+
_See code: [src\commands\serve.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\serve.ts)_
|
349
349
|
|
350
350
|
## `learnpack start`
|
351
351
|
|
@@ -367,7 +367,7 @@ OPTIONS
|
|
367
367
|
-y, --yes Skip all prompts and initialize an empty project
|
368
368
|
```
|
369
369
|
|
370
|
-
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
370
|
+
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\start.ts)_
|
371
371
|
|
372
372
|
## `learnpack test [EXERCISESLUG]`
|
373
373
|
|
@@ -384,7 +384,7 @@ OPTIONS
|
|
384
384
|
-y, --yes Skip all prompts and initialize an empty project
|
385
385
|
```
|
386
386
|
|
387
|
-
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
387
|
+
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\test.ts)_
|
388
388
|
|
389
389
|
## `learnpack translate`
|
390
390
|
|
@@ -398,7 +398,7 @@ OPTIONS
|
|
398
398
|
-y, --yes Skip all prompts and initialize an empty project
|
399
399
|
```
|
400
400
|
|
401
|
-
_See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
401
|
+
_See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.94/src\commands\translate.ts)_
|
402
402
|
<!-- commandsstop -->
|
403
403
|
|
404
404
|
> > > > > > > 0cb3e56d84c197f9d008836bb573eade212b7e57
|
@@ -1,4 +1,6 @@
|
|
1
1
|
import SessionCommand from "../utils/SessionCommand";
|
2
|
+
import { TAcademy } from "../utils/api";
|
3
|
+
export declare const handleAssetCreation: (sessionPayload: any, academy: TAcademy, learnJson: any, learnpackDeployUrl: string, category: number) => Promise<void>;
|
2
4
|
declare class BuildCommand extends SessionCommand {
|
3
5
|
static description: string;
|
4
6
|
static flags: {
|
package/lib/commands/publish.js
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.handleAssetCreation = void 0;
|
3
4
|
/* eslint-disable arrow-parens */
|
4
5
|
/* eslint-disable unicorn/no-array-for-each */
|
5
6
|
const child_process_1 = require("child_process");
|
@@ -51,6 +52,7 @@ const handleAssetCreation = async (sessionPayload, academy, learnJson, learnpack
|
|
51
52
|
console_1.default.error("Error updating or creating asset:", error);
|
52
53
|
}
|
53
54
|
};
|
55
|
+
exports.handleAssetCreation = handleAssetCreation;
|
54
56
|
const runAudit = (strict) => {
|
55
57
|
try {
|
56
58
|
console_1.default.info("Running learnpack audit before publishing...");
|
@@ -282,7 +284,7 @@ class BuildCommand extends SessionCommand_1.default {
|
|
282
284
|
fs.unlinkSync(zipFilePath);
|
283
285
|
this.removeDirectory(buildDir);
|
284
286
|
if (academy) {
|
285
|
-
await handleAssetCreation(sessionPayload, academy, learnJson, res.data.url, category);
|
287
|
+
await (0, exports.handleAssetCreation)(sessionPayload, academy, learnJson, res.data.url, category);
|
286
288
|
}
|
287
289
|
}
|
288
290
|
catch (error) {
|
package/lib/commands/serve.js
CHANGED
@@ -4,12 +4,22 @@ const command_1 = require("@oclif/command");
|
|
4
4
|
const express = require("express");
|
5
5
|
const cors = require("cors");
|
6
6
|
const path = require("path");
|
7
|
+
const os = require("os");
|
8
|
+
const archiver = require("archiver");
|
9
|
+
const mkdirp = require("mkdirp");
|
10
|
+
const rimraf = require("rimraf");
|
7
11
|
const SessionCommand_1 = require("../utils/SessionCommand");
|
8
12
|
const storage_1 = require("@google-cloud/storage");
|
9
13
|
const file_1 = require("../managers/file");
|
10
14
|
const fs = require("fs");
|
11
15
|
const rigoActions_1 = require("../utils/rigoActions");
|
12
16
|
const dotenv = require("dotenv");
|
17
|
+
// import { handleAssetCreation } from "./publish"
|
18
|
+
const axios_1 = require("axios");
|
19
|
+
const FormData = require("form-data");
|
20
|
+
const api_1 = require("../utils/api");
|
21
|
+
const misc_1 = require("../utils/misc");
|
22
|
+
const configBuilder_1 = require("../utils/configBuilder");
|
13
23
|
dotenv.config();
|
14
24
|
const frontMatter = require("front-matter");
|
15
25
|
class ServeCommand extends SessionCommand_1.default {
|
@@ -127,46 +137,63 @@ class ServeCommand extends SessionCommand_1.default {
|
|
127
137
|
});
|
128
138
|
app.get("/config", async (req, res) => {
|
129
139
|
const courseSlug = req.query.slug;
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
if (!exerciseMap[slug]) {
|
143
|
-
exerciseMap[slug] = {
|
144
|
-
title: slug,
|
145
|
-
slug: slug,
|
146
|
-
graded: false,
|
147
|
-
files: [],
|
148
|
-
translations: {},
|
149
|
-
};
|
150
|
-
}
|
151
|
-
const fileName = pathParts.at(-1);
|
152
|
-
// Traducciones
|
153
|
-
const readmeMatch = fileName === null || fileName === void 0 ? void 0 : fileName.match(/^readme(?:\.([a-z]{2}))?\.md$/i);
|
154
|
-
if (readmeMatch) {
|
155
|
-
const lang = readmeMatch[1] || "us";
|
156
|
-
exerciseMap[slug].translations[lang] = fileName || "";
|
157
|
-
}
|
158
|
-
else {
|
159
|
-
exerciseMap[slug].files.push(fileName || "");
|
160
|
-
}
|
140
|
+
if (!courseSlug) {
|
141
|
+
return res.status(400).json({ error: "Course slug required" });
|
142
|
+
}
|
143
|
+
try {
|
144
|
+
const { config, exercises } = await (0, configBuilder_1.buildConfig)(bucket, courseSlug);
|
145
|
+
res.set("X-Creator-Web", "true");
|
146
|
+
res.set("Access-Control-Expose-Headers", "X-Creator-Web");
|
147
|
+
res.json({ config, exercises });
|
148
|
+
}
|
149
|
+
catch (error) {
|
150
|
+
console.error(error);
|
151
|
+
res.status(500).json({ error: error.message });
|
161
152
|
}
|
162
|
-
const exercises = Object.values(exerciseMap).map((ex, index) => (Object.assign(Object.assign({}, ex), { position: index })));
|
163
|
-
res.set("X-Creator-Web", "true");
|
164
|
-
res.set("Access-Control-Expose-Headers", "X-Creator-Web");
|
165
|
-
res.send({
|
166
|
-
config: Object.assign(Object.assign({}, learnJsonParsed), { title: { us: courseSlug } }),
|
167
|
-
exercises,
|
168
|
-
});
|
169
153
|
});
|
154
|
+
// app.get("/config", async (req, res) => {
|
155
|
+
// const courseSlug = req.query.slug
|
156
|
+
// const files = await listFilesWithPrefix(`courses/${courseSlug}`)
|
157
|
+
// const learnJson = files.find((file) => file.name.endsWith("learn.json"))
|
158
|
+
// const learnJsonContent = await learnJson?.download()
|
159
|
+
// const learnJsonParsed = JSON.parse(learnJsonContent?.toString() || "{}")
|
160
|
+
// const exerciseMap: ExerciseMap = {}
|
161
|
+
// // Agrupar archivos por ejercicio
|
162
|
+
// for (const file of files) {
|
163
|
+
// const pathParts = file.name.split("/")
|
164
|
+
// const isExercise = pathParts.includes("exercises")
|
165
|
+
// if (!isExercise) continue
|
166
|
+
// const slug = pathParts[pathParts.indexOf("exercises") + 1]
|
167
|
+
// if (!exerciseMap[slug]) {
|
168
|
+
// exerciseMap[slug] = {
|
169
|
+
// title: slug,
|
170
|
+
// slug: slug,
|
171
|
+
// graded: false,
|
172
|
+
// files: [],
|
173
|
+
// translations: {},
|
174
|
+
// }
|
175
|
+
// }
|
176
|
+
// const fileName = pathParts.at(-1)
|
177
|
+
// // Traducciones
|
178
|
+
// const readmeMatch = fileName?.match(/^readme(?:\.([a-z]{2}))?\.md$/i)
|
179
|
+
// if (readmeMatch) {
|
180
|
+
// const lang = readmeMatch[1] || "us"
|
181
|
+
// exerciseMap[slug].translations[lang] = fileName || ""
|
182
|
+
// } else {
|
183
|
+
// exerciseMap[slug].files.push(fileName || "")
|
184
|
+
// }
|
185
|
+
// }
|
186
|
+
// const exercises = Object.values(exerciseMap).map((ex, index) => ({
|
187
|
+
// ...ex,
|
188
|
+
// position: index,
|
189
|
+
// }))
|
190
|
+
// res.set("X-Creator-Web", "true")
|
191
|
+
// res.set("Access-Control-Expose-Headers", "X-Creator-Web")
|
192
|
+
// res.send({
|
193
|
+
// config: { ...learnJsonParsed, title: { us: courseSlug } },
|
194
|
+
// exercises,
|
195
|
+
// })
|
196
|
+
// })
|
170
197
|
app.get("/exercise/:slug/readme", async (req, res) => {
|
171
198
|
console.log("GET /exercise/:slug/readme");
|
172
199
|
const { slug } = req.params;
|
@@ -308,7 +335,6 @@ class ServeCommand extends SessionCommand_1.default {
|
|
308
335
|
const file = path.join(localAppPath, req.params.file);
|
309
336
|
res.sendFile(file);
|
310
337
|
});
|
311
|
-
// Enviar index.html para todas las rutas
|
312
338
|
app.get("/creator", (req, res) => {
|
313
339
|
res.sendFile(path.join(distPath, "index.html"));
|
314
340
|
});
|
@@ -324,6 +350,109 @@ class ServeCommand extends SessionCommand_1.default {
|
|
324
350
|
const file = path.join(distPath, "assets", req.params.file);
|
325
351
|
res.sendFile(file);
|
326
352
|
});
|
353
|
+
app.post("/actions/publish/:slug", async (req, res) => {
|
354
|
+
try {
|
355
|
+
// 1) Extraer token y body
|
356
|
+
const { slug } = req.params;
|
357
|
+
const rigoToken = req.header("x-rigo-token");
|
358
|
+
const bcToken = req.header("x-breathecode-token");
|
359
|
+
const { academyId, categoryId } = req.body;
|
360
|
+
if (!rigoToken || !bcToken || !academyId || !categoryId) {
|
361
|
+
return res
|
362
|
+
.status(400)
|
363
|
+
.json({ error: "Faltan tokens o academy/category" });
|
364
|
+
}
|
365
|
+
// 2) Leer y construir config.json vía buildConfig
|
366
|
+
const { config, exercises } = await (0, configBuilder_1.buildConfig)(bucket, slug);
|
367
|
+
// 3) Preparar dirs temporales
|
368
|
+
const prefix = `courses/${slug}/`;
|
369
|
+
const tmpRoot = path.join(os.tmpdir(), `learnpack-${slug}`);
|
370
|
+
const buildRoot = path.join(tmpRoot, "build");
|
371
|
+
rimraf.sync(tmpRoot);
|
372
|
+
mkdirp.sync(buildRoot);
|
373
|
+
// 4) Copiar UI descomprimida (_app)
|
374
|
+
const uiSrc = path.resolve(__dirname, "../ui/_app");
|
375
|
+
const copyDir = (src, dest) => {
|
376
|
+
for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
|
377
|
+
const from = path.join(src, ent.name);
|
378
|
+
const to = path.join(dest, ent.name);
|
379
|
+
if (ent.isDirectory()) {
|
380
|
+
mkdirp.sync(to);
|
381
|
+
copyDir(from, to);
|
382
|
+
}
|
383
|
+
else {
|
384
|
+
fs.copyFileSync(from, to);
|
385
|
+
}
|
386
|
+
}
|
387
|
+
};
|
388
|
+
copyDir(uiSrc, buildRoot);
|
389
|
+
// 5) Inyectar placeholders en index.html
|
390
|
+
const idxTpl = fs.readFileSync(path.join(uiSrc, "index.html"), "utf-8");
|
391
|
+
const idxHtml = idxTpl
|
392
|
+
.replace(/{{title}}/g, config.title.us)
|
393
|
+
.replace(/<title>.*<\/title>/, `<title>${config.title.us}</title>`)
|
394
|
+
.replace(/{{description}}/g, config.description.us)
|
395
|
+
.replace(/{{preview}}/g, config.preview ||
|
396
|
+
"https://raw.githubusercontent.com/learnpack/ide/master/public/learnpack.svg")
|
397
|
+
.replace(/{{slug}}/g, slug)
|
398
|
+
.replace(/{{duration}}/g, (0, misc_1.minutesToISO8601Duration)(config.duration));
|
399
|
+
fs.writeFileSync(path.join(buildRoot, "index.html"), idxHtml);
|
400
|
+
// 6) Inyectar placeholders en manifest.webmanifest
|
401
|
+
const mfTpl = fs.readFileSync(path.join(uiSrc, "manifest.webmanifest"), "utf-8");
|
402
|
+
const mf = mfTpl
|
403
|
+
.replace(/{{course_title}}/g, config.title.us)
|
404
|
+
.replace(/{{course_app_name}}/g, config.slug);
|
405
|
+
fs.writeFileSync(path.join(buildRoot, "manifest.webmanifest"), mf);
|
406
|
+
// 7) Descargar todos los archivos del bucket al buildRoot
|
407
|
+
const [files] = await bucket.getFiles({ prefix });
|
408
|
+
await Promise.all(files.map(async (file) => {
|
409
|
+
const rel = file.name.replace(prefix, "");
|
410
|
+
const out = path.join(buildRoot, rel);
|
411
|
+
mkdirp.sync(path.dirname(out));
|
412
|
+
const [buf] = await file.download();
|
413
|
+
fs.writeFileSync(out, buf);
|
414
|
+
}));
|
415
|
+
// 8) Crear el config.json en buildRoot con lo que retorna buildConfig
|
416
|
+
const fullConfig = { config, exercises };
|
417
|
+
fs.writeFileSync(path.join(buildRoot, "config.json"), JSON.stringify(fullConfig, null, 2), "utf-8");
|
418
|
+
// 9) Empaquetar en ZIP (solo contenido de buildRoot)
|
419
|
+
const zipName = `${slug}.zip`;
|
420
|
+
const zipPath = path.join(tmpRoot, zipName);
|
421
|
+
const output = fs.createWriteStream(zipPath);
|
422
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
423
|
+
output.on("close", async () => {
|
424
|
+
// 10) Subir ZIP a RigoBot
|
425
|
+
const form = new FormData();
|
426
|
+
form.append("file", fs.createReadStream(zipPath));
|
427
|
+
form.append("config", JSON.stringify(config));
|
428
|
+
const rigoRes = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/learnpack/upload`, form, {
|
429
|
+
headers: Object.assign(Object.assign({}, form.getHeaders()), { Authorization: `Token ${rigoToken}` }),
|
430
|
+
});
|
431
|
+
// 11) Crear/actualizar asset en Breathecode
|
432
|
+
// await handleAssetCreation(
|
433
|
+
// { token: bcToken, rigobot: { key: rigoToken }, id: 0 },
|
434
|
+
// { id: academyId } as any,
|
435
|
+
// config,
|
436
|
+
// rigoRes.data.url,
|
437
|
+
// categoryId
|
438
|
+
// )
|
439
|
+
rimraf.sync(tmpRoot);
|
440
|
+
console.log("RIGO RES", rigoRes.data);
|
441
|
+
return res.json({ url: rigoRes.data.url });
|
442
|
+
});
|
443
|
+
archive.on("error", err => {
|
444
|
+
console.error("ZIP Error:", err);
|
445
|
+
return res.status(500).send("Error building zip");
|
446
|
+
});
|
447
|
+
archive.pipe(output);
|
448
|
+
archive.directory(buildRoot, false);
|
449
|
+
await archive.finalize();
|
450
|
+
}
|
451
|
+
catch (error) {
|
452
|
+
console.error(error);
|
453
|
+
return res.status(500).json({ error: error.message });
|
454
|
+
}
|
455
|
+
});
|
327
456
|
app.listen(PORT, () => {
|
328
457
|
console.log(`🚀 Creator UI server running at http://localhost:${PORT}/creator`);
|
329
458
|
});
|
@@ -14555,6 +14555,9 @@ const I_ = (n) => (e) => {
|
|
14555
14555
|
undo: () => {
|
14556
14556
|
n((e) => ({ history: e.history.slice(0, -1) }))
|
14557
14557
|
},
|
14558
|
+
cleanHistory: () => {
|
14559
|
+
n(() => ({ history: [] }))
|
14560
|
+
},
|
14558
14561
|
consumables: {},
|
14559
14562
|
setConsumables: (e) =>
|
14560
14563
|
n((t) => {
|
@@ -73798,25 +73801,27 @@ function kQ() {
|
|
73798
73801
|
setFormState: t,
|
73799
73802
|
setAuth: i,
|
73800
73803
|
push: r,
|
73804
|
+
cleanHistory: a,
|
73801
73805
|
} = Wr(
|
73802
|
-
nS((
|
73803
|
-
formState:
|
73804
|
-
setFormState:
|
73805
|
-
setAuth:
|
73806
|
-
push:
|
73806
|
+
nS((h) => ({
|
73807
|
+
formState: h.formState,
|
73808
|
+
setFormState: h.setFormState,
|
73809
|
+
setAuth: h.setAuth,
|
73810
|
+
push: h.push,
|
73811
|
+
cleanHistory: h.cleanHistory,
|
73807
73812
|
}))
|
73808
73813
|
)
|
73809
73814
|
me.useEffect(() => {
|
73810
|
-
e.isCompleted &&
|
73815
|
+
e.isCompleted && l()
|
73811
73816
|
}, [e]),
|
73812
73817
|
me.useEffect(() => {
|
73813
|
-
|
73818
|
+
s()
|
73814
73819
|
}, [])
|
73815
|
-
const
|
73816
|
-
const { token:
|
73817
|
-
if (
|
73818
|
-
const
|
73819
|
-
i({ bcToken:
|
73820
|
+
const s = async () => {
|
73821
|
+
const { token: h } = bO()
|
73822
|
+
if (h) {
|
73823
|
+
const d = await vO(h)
|
73824
|
+
i({ bcToken: h, userId: d.id, rigoToken: d.rigobot.key, user: d }),
|
73820
73825
|
t({
|
73821
73826
|
variables: [
|
73822
73827
|
"description",
|
@@ -73836,13 +73841,14 @@ function kQ() {
|
|
73836
73841
|
],
|
73837
73842
|
})
|
73838
73843
|
},
|
73839
|
-
|
73840
|
-
|
73844
|
+
l = async () => {
|
73845
|
+
a()
|
73846
|
+
const h = await pO({
|
73841
73847
|
courseInfo: JSON.stringify(e),
|
73842
73848
|
prevInteractions: "",
|
73843
73849
|
}),
|
73844
|
-
|
73845
|
-
r({ lessons:
|
73850
|
+
d = h.parsed.listOfSteps.map((p) => mO(p, []))
|
73851
|
+
r({ lessons: d, courseInfo: { ...e, title: h.parsed.title } }),
|
73846
73852
|
n("/creator/syllabus"),
|
73847
73853
|
t({
|
73848
73854
|
description: "",
|
@@ -73854,7 +73860,7 @@ function kQ() {
|
|
73854
73860
|
currentStep: "description",
|
73855
73861
|
})
|
73856
73862
|
},
|
73857
|
-
|
73863
|
+
u = () =>
|
73858
73864
|
[
|
73859
73865
|
{
|
73860
73866
|
title: "Provide a description for your tutorial",
|
@@ -73865,8 +73871,8 @@ function kQ() {
|
|
73865
73871
|
className:
|
73866
73872
|
"w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white",
|
73867
73873
|
value: e.description,
|
73868
|
-
onChange: (
|
73869
|
-
t({ description:
|
73874
|
+
onChange: (d) => {
|
73875
|
+
t({ description: d.target.value })
|
73870
73876
|
},
|
73871
73877
|
}),
|
73872
73878
|
},
|
@@ -73912,8 +73918,8 @@ function kQ() {
|
|
73912
73918
|
className:
|
73913
73919
|
"w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white",
|
73914
73920
|
defaultValue: e.targetAudience,
|
73915
|
-
onBlur: (
|
73916
|
-
t({ targetAudience:
|
73921
|
+
onBlur: (d) => {
|
73922
|
+
t({ targetAudience: d.target.value })
|
73917
73923
|
},
|
73918
73924
|
}),
|
73919
73925
|
}),
|
@@ -73958,24 +73964,24 @@ function kQ() {
|
|
73958
73964
|
className:
|
73959
73965
|
"w-full h-40 border-2 border-gray-300 rounded-md p-2",
|
73960
73966
|
value: e.contentIndex,
|
73961
|
-
onChange: (
|
73962
|
-
t({ contentIndex:
|
73967
|
+
onChange: (d) => {
|
73968
|
+
t({ contentIndex: d.target.value })
|
73963
73969
|
},
|
73964
73970
|
}),
|
73965
73971
|
ve.jsx(cB, {
|
73966
|
-
onResult: (
|
73967
|
-
let
|
73968
|
-
|
73969
|
-
|
73972
|
+
onResult: (d) => {
|
73973
|
+
let p = ""
|
73974
|
+
d.forEach((g) => {
|
73975
|
+
p += `<FILE NAME="${g.name}">${g.text}</FILE>
|
73970
73976
|
`
|
73971
73977
|
}),
|
73972
|
-
t({ contentIndex:
|
73978
|
+
t({ contentIndex: p })
|
73973
73979
|
},
|
73974
73980
|
}),
|
73975
73981
|
],
|
73976
73982
|
}),
|
73977
73983
|
},
|
73978
|
-
].filter((
|
73984
|
+
].filter((d) => e.variables.includes(d.slug) || d.slug === e.currentStep)
|
73979
73985
|
return ve.jsx(ve.Fragment, {
|
73980
73986
|
children: e.isCompleted
|
73981
73987
|
? ve.jsx(tS, {
|
@@ -73988,7 +73994,7 @@ function kQ() {
|
|
73988
73994
|
})
|
73989
73995
|
: ve.jsx(gq, {
|
73990
73996
|
formState: e,
|
73991
|
-
steps:
|
73997
|
+
steps: u(),
|
73992
73998
|
setFormState: t,
|
73993
73999
|
onFinish: () => {
|
73994
74000
|
t({ isCompleted: !0 })
|
@@ -10,7 +10,7 @@
|
|
10
10
|
/>
|
11
11
|
|
12
12
|
<title>Learnpack Creator: Craft tutorials in seconds!</title>
|
13
|
-
<script type="module" crossorigin src="/creator/assets/index-
|
13
|
+
<script type="module" crossorigin src="/creator/assets/index-EA507Y8n.js"></script>
|
14
14
|
<link rel="stylesheet" crossorigin href="/creator/assets/index-ldEC0yWM.css">
|
15
15
|
</head>
|
16
16
|
<body>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { Bucket } from "@google-cloud/storage";
|
2
|
+
export type Exercise = {
|
3
|
+
title: string;
|
4
|
+
slug: string;
|
5
|
+
graded: boolean;
|
6
|
+
files: string[];
|
7
|
+
translations: Record<string, string>;
|
8
|
+
position: number;
|
9
|
+
};
|
10
|
+
export type ConfigResponse = {
|
11
|
+
config: any;
|
12
|
+
exercises: Exercise[];
|
13
|
+
};
|
14
|
+
/**
|
15
|
+
* Crea la configuración y lista de ejercicios para un curso.
|
16
|
+
*
|
17
|
+
* @param bucket - Instancia de GCS Bucket donde está el curso.
|
18
|
+
* @param courseSlug - Slug del curso a procesar.
|
19
|
+
* @returns Promise con objeto { config, exercises } listo para usar.
|
20
|
+
*/
|
21
|
+
export declare function buildConfig(bucket: Bucket, courseSlug: string): Promise<ConfigResponse>;
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.buildConfig = buildConfig;
|
4
|
+
/**
|
5
|
+
* Crea la configuración y lista de ejercicios para un curso.
|
6
|
+
*
|
7
|
+
* @param bucket - Instancia de GCS Bucket donde está el curso.
|
8
|
+
* @param courseSlug - Slug del curso a procesar.
|
9
|
+
* @returns Promise con objeto { config, exercises } listo para usar.
|
10
|
+
*/
|
11
|
+
async function buildConfig(bucket, courseSlug) {
|
12
|
+
const prefix = `courses/${courseSlug}/`;
|
13
|
+
const [files] = await bucket.getFiles({ prefix });
|
14
|
+
// 1) Leer learn.json
|
15
|
+
const learnFile = files.find(f => f.name.endsWith("learn.json"));
|
16
|
+
const [learnBuf] = await learnFile.download();
|
17
|
+
const learnJson = JSON.parse(learnBuf.toString());
|
18
|
+
// 2) Agrupar ejercicios
|
19
|
+
const map = {};
|
20
|
+
for (const file of files) {
|
21
|
+
const parts = file.name.split("/");
|
22
|
+
if (!parts.includes("exercises"))
|
23
|
+
continue;
|
24
|
+
const slug = parts[parts.indexOf("exercises") + 1];
|
25
|
+
if (!map[slug]) {
|
26
|
+
map[slug] = {
|
27
|
+
title: slug,
|
28
|
+
slug,
|
29
|
+
graded: false,
|
30
|
+
files: [],
|
31
|
+
translations: {},
|
32
|
+
};
|
33
|
+
}
|
34
|
+
const fname = parts.pop();
|
35
|
+
const m = fname.match(/^readme(?:\.([a-z]{2}))?\.md$/i);
|
36
|
+
if (m) {
|
37
|
+
const lang = m[1] || "us";
|
38
|
+
map[slug].translations[lang] = fname;
|
39
|
+
}
|
40
|
+
else {
|
41
|
+
map[slug].files.push(fname);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
const exercises = Object.values(map).map((ex, i) => (Object.assign(Object.assign({}, ex), { position: i })));
|
45
|
+
return {
|
46
|
+
config: Object.assign(Object.assign({}, learnJson), { title: { us: courseSlug } }),
|
47
|
+
exercises,
|
48
|
+
};
|
49
|
+
}
|
package/oclif.manifest.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":"5.0.
|
1
|
+
{"version":"5.0.94","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false}},"args":[]},"breakToken":{"id":"breakToken","description":"Break the token","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false},"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"serve":{"id":"serve","description":"Runs a small server to build tutorials","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
|
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.94",
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
6
6
|
"contributors": [
|
7
7
|
{
|
@@ -43,15 +43,18 @@
|
|
43
43
|
"espree": "^9.3.2",
|
44
44
|
"eta": "^1.2.0",
|
45
45
|
"express": "^4.17.1",
|
46
|
+
"form-data": "^4.0.2",
|
46
47
|
"front-matter": "^4.0.2",
|
47
48
|
"js-yaml": "^4.1.0",
|
48
49
|
"markdown-it": "^14.1.0",
|
50
|
+
"mkdirp": "^3.0.1",
|
49
51
|
"moment": "^2.27.0",
|
50
52
|
"node-emoji": "^1.10.0",
|
51
53
|
"node-fetch": "^2.7.0",
|
52
54
|
"node-persist": "^3.1.0",
|
53
55
|
"ora": "^8.2.0",
|
54
56
|
"prompts": "^2.3.2",
|
57
|
+
"rimraf": "^6.0.1",
|
55
58
|
"shelljs": "^0.8.4",
|
56
59
|
"socket.io": "^4.4.1",
|
57
60
|
"syllable": "^5.0.1",
|
package/src/commands/publish.ts
CHANGED
@@ -22,7 +22,7 @@ import { minutesToISO8601Duration } from "../utils/misc"
|
|
22
22
|
|
23
23
|
const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"
|
24
24
|
|
25
|
-
const handleAssetCreation = async (
|
25
|
+
export const handleAssetCreation = async (
|
26
26
|
sessionPayload: any,
|
27
27
|
academy: TAcademy,
|
28
28
|
learnJson: any,
|
package/src/commands/serve.ts
CHANGED
@@ -2,26 +2,27 @@ import { flags } from "@oclif/command"
|
|
2
2
|
import * as express from "express"
|
3
3
|
import * as cors from "cors"
|
4
4
|
import * as path from "path"
|
5
|
+
import * as os from "os"
|
6
|
+
import * as archiver from "archiver"
|
7
|
+
import * as mkdirp from "mkdirp"
|
8
|
+
import * as rimraf from "rimraf"
|
5
9
|
import SessionCommand from "../utils/SessionCommand"
|
6
10
|
import { Storage } from "@google-cloud/storage"
|
7
11
|
import { downloadEditor, decompress } from "../managers/file"
|
8
12
|
import * as fs from "fs"
|
9
13
|
import { translateExercise } from "../utils/rigoActions"
|
10
14
|
import * as dotenv from "dotenv"
|
15
|
+
// import { handleAssetCreation } from "./publish"
|
16
|
+
import axios from "axios"
|
17
|
+
import * as FormData from "form-data"
|
18
|
+
import { RIGOBOT_HOST } from "../utils/api"
|
19
|
+
import { minutesToISO8601Duration } from "../utils/misc"
|
20
|
+
import { buildConfig, ConfigResponse } from "../utils/configBuilder"
|
21
|
+
|
11
22
|
dotenv.config()
|
12
23
|
|
13
24
|
const frontMatter = require("front-matter")
|
14
25
|
|
15
|
-
type ExerciseMap = {
|
16
|
-
[slug: string]: {
|
17
|
-
title: string
|
18
|
-
slug: string
|
19
|
-
graded: boolean
|
20
|
-
files: string[]
|
21
|
-
translations: Record<string, string>
|
22
|
-
}
|
23
|
-
}
|
24
|
-
|
25
26
|
export default class ServeCommand extends SessionCommand {
|
26
27
|
static description = "Runs a small server to build tutorials"
|
27
28
|
|
@@ -185,59 +186,75 @@ export default class ServeCommand extends SessionCommand {
|
|
185
186
|
})
|
186
187
|
|
187
188
|
app.get("/config", async (req, res) => {
|
188
|
-
const courseSlug = req.query.slug
|
189
|
-
|
190
|
-
|
191
|
-
const learnJson = files.find(file => file.name.endsWith("learn.json"))
|
192
|
-
const learnJsonContent = await learnJson?.download()
|
193
|
-
const learnJsonParsed = JSON.parse(learnJsonContent?.toString() || "{}")
|
194
|
-
|
195
|
-
const exerciseMap: ExerciseMap = {}
|
196
|
-
|
197
|
-
// Agrupar archivos por ejercicio
|
198
|
-
for (const file of files) {
|
199
|
-
const pathParts = file.name.split("/")
|
200
|
-
const isExercise = pathParts.includes("exercises")
|
201
|
-
if (!isExercise)
|
202
|
-
continue
|
203
|
-
|
204
|
-
const slug = pathParts[pathParts.indexOf("exercises") + 1]
|
205
|
-
if (!exerciseMap[slug]) {
|
206
|
-
exerciseMap[slug] = {
|
207
|
-
title: slug,
|
208
|
-
slug: slug,
|
209
|
-
graded: false,
|
210
|
-
files: [],
|
211
|
-
translations: {},
|
212
|
-
}
|
213
|
-
}
|
214
|
-
|
215
|
-
const fileName = pathParts.at(-1)
|
216
|
-
|
217
|
-
// Traducciones
|
218
|
-
const readmeMatch = fileName?.match(/^readme(?:\.([a-z]{2}))?\.md$/i)
|
219
|
-
if (readmeMatch) {
|
220
|
-
const lang = readmeMatch[1] || "us"
|
221
|
-
exerciseMap[slug].translations[lang] = fileName || ""
|
222
|
-
} else {
|
223
|
-
exerciseMap[slug].files.push(fileName || "")
|
224
|
-
}
|
189
|
+
const courseSlug = req.query.slug as string
|
190
|
+
if (!courseSlug) {
|
191
|
+
return res.status(400).json({ error: "Course slug required" })
|
225
192
|
}
|
226
193
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
config: { ...learnJsonParsed, title: { us: courseSlug } },
|
237
|
-
exercises,
|
238
|
-
})
|
194
|
+
try {
|
195
|
+
const { config, exercises } = await buildConfig(bucket, courseSlug)
|
196
|
+
res.set("X-Creator-Web", "true")
|
197
|
+
res.set("Access-Control-Expose-Headers", "X-Creator-Web")
|
198
|
+
res.json({ config, exercises })
|
199
|
+
} catch (error) {
|
200
|
+
console.error(error)
|
201
|
+
res.status(500).json({ error: (error as Error).message })
|
202
|
+
}
|
239
203
|
})
|
240
204
|
|
205
|
+
// app.get("/config", async (req, res) => {
|
206
|
+
// const courseSlug = req.query.slug
|
207
|
+
// const files = await listFilesWithPrefix(`courses/${courseSlug}`)
|
208
|
+
|
209
|
+
// const learnJson = files.find((file) => file.name.endsWith("learn.json"))
|
210
|
+
// const learnJsonContent = await learnJson?.download()
|
211
|
+
// const learnJsonParsed = JSON.parse(learnJsonContent?.toString() || "{}")
|
212
|
+
|
213
|
+
// const exerciseMap: ExerciseMap = {}
|
214
|
+
|
215
|
+
// // Agrupar archivos por ejercicio
|
216
|
+
// for (const file of files) {
|
217
|
+
// const pathParts = file.name.split("/")
|
218
|
+
// const isExercise = pathParts.includes("exercises")
|
219
|
+
// if (!isExercise) continue
|
220
|
+
|
221
|
+
// const slug = pathParts[pathParts.indexOf("exercises") + 1]
|
222
|
+
// if (!exerciseMap[slug]) {
|
223
|
+
// exerciseMap[slug] = {
|
224
|
+
// title: slug,
|
225
|
+
// slug: slug,
|
226
|
+
// graded: false,
|
227
|
+
// files: [],
|
228
|
+
// translations: {},
|
229
|
+
// }
|
230
|
+
// }
|
231
|
+
|
232
|
+
// const fileName = pathParts.at(-1)
|
233
|
+
|
234
|
+
// // Traducciones
|
235
|
+
// const readmeMatch = fileName?.match(/^readme(?:\.([a-z]{2}))?\.md$/i)
|
236
|
+
// if (readmeMatch) {
|
237
|
+
// const lang = readmeMatch[1] || "us"
|
238
|
+
// exerciseMap[slug].translations[lang] = fileName || ""
|
239
|
+
// } else {
|
240
|
+
// exerciseMap[slug].files.push(fileName || "")
|
241
|
+
// }
|
242
|
+
// }
|
243
|
+
|
244
|
+
// const exercises = Object.values(exerciseMap).map((ex, index) => ({
|
245
|
+
// ...ex,
|
246
|
+
// position: index,
|
247
|
+
// }))
|
248
|
+
|
249
|
+
// res.set("X-Creator-Web", "true")
|
250
|
+
// res.set("Access-Control-Expose-Headers", "X-Creator-Web")
|
251
|
+
|
252
|
+
// res.send({
|
253
|
+
// config: { ...learnJsonParsed, title: { us: courseSlug } },
|
254
|
+
// exercises,
|
255
|
+
// })
|
256
|
+
// })
|
257
|
+
|
241
258
|
app.get("/exercise/:slug/readme", async (req, res) => {
|
242
259
|
console.log("GET /exercise/:slug/readme")
|
243
260
|
|
@@ -419,7 +436,6 @@ continue
|
|
419
436
|
const file = path.join(localAppPath, req.params.file)
|
420
437
|
res.sendFile(file)
|
421
438
|
})
|
422
|
-
// Enviar index.html para todas las rutas
|
423
439
|
app.get("/creator", (req, res) => {
|
424
440
|
res.sendFile(path.join(distPath, "index.html"))
|
425
441
|
})
|
@@ -438,6 +454,146 @@ continue
|
|
438
454
|
res.sendFile(file)
|
439
455
|
})
|
440
456
|
|
457
|
+
app.post("/actions/publish/:slug", async (req, res) => {
|
458
|
+
try {
|
459
|
+
// 1) Extraer token y body
|
460
|
+
const { slug } = req.params
|
461
|
+
const rigoToken = req.header("x-rigo-token")
|
462
|
+
const bcToken = req.header("x-breathecode-token")
|
463
|
+
const { academyId, categoryId } = req.body
|
464
|
+
|
465
|
+
if (!rigoToken || !bcToken || !academyId || !categoryId) {
|
466
|
+
return res
|
467
|
+
.status(400)
|
468
|
+
.json({ error: "Faltan tokens o academy/category" })
|
469
|
+
}
|
470
|
+
|
471
|
+
// 2) Leer y construir config.json vía buildConfig
|
472
|
+
const { config, exercises }: ConfigResponse = await buildConfig(
|
473
|
+
bucket,
|
474
|
+
slug
|
475
|
+
)
|
476
|
+
|
477
|
+
// 3) Preparar dirs temporales
|
478
|
+
const prefix = `courses/${slug}/`
|
479
|
+
const tmpRoot = path.join(os.tmpdir(), `learnpack-${slug}`)
|
480
|
+
const buildRoot = path.join(tmpRoot, "build")
|
481
|
+
rimraf.sync(tmpRoot)
|
482
|
+
mkdirp.sync(buildRoot)
|
483
|
+
|
484
|
+
// 4) Copiar UI descomprimida (_app)
|
485
|
+
const uiSrc = path.resolve(__dirname, "../ui/_app")
|
486
|
+
const copyDir = (src: string, dest: string) => {
|
487
|
+
for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
|
488
|
+
const from = path.join(src, ent.name)
|
489
|
+
const to = path.join(dest, ent.name)
|
490
|
+
if (ent.isDirectory()) {
|
491
|
+
mkdirp.sync(to)
|
492
|
+
copyDir(from, to)
|
493
|
+
} else {
|
494
|
+
fs.copyFileSync(from, to)
|
495
|
+
}
|
496
|
+
}
|
497
|
+
}
|
498
|
+
|
499
|
+
copyDir(uiSrc, buildRoot)
|
500
|
+
|
501
|
+
// 5) Inyectar placeholders en index.html
|
502
|
+
const idxTpl = fs.readFileSync(path.join(uiSrc, "index.html"), "utf-8")
|
503
|
+
const idxHtml = idxTpl
|
504
|
+
.replace(/{{title}}/g, config.title.us)
|
505
|
+
.replace(/<title>.*<\/title>/, `<title>${config.title.us}</title>`)
|
506
|
+
.replace(/{{description}}/g, config.description.us)
|
507
|
+
.replace(
|
508
|
+
/{{preview}}/g,
|
509
|
+
config.preview ||
|
510
|
+
"https://raw.githubusercontent.com/learnpack/ide/master/public/learnpack.svg"
|
511
|
+
)
|
512
|
+
.replace(/{{slug}}/g, slug)
|
513
|
+
.replace(/{{duration}}/g, minutesToISO8601Duration(config.duration))
|
514
|
+
fs.writeFileSync(path.join(buildRoot, "index.html"), idxHtml)
|
515
|
+
|
516
|
+
// 6) Inyectar placeholders en manifest.webmanifest
|
517
|
+
const mfTpl = fs.readFileSync(
|
518
|
+
path.join(uiSrc, "manifest.webmanifest"),
|
519
|
+
"utf-8"
|
520
|
+
)
|
521
|
+
const mf = mfTpl
|
522
|
+
.replace(/{{course_title}}/g, config.title.us)
|
523
|
+
.replace(/{{course_app_name}}/g, config.slug)
|
524
|
+
fs.writeFileSync(path.join(buildRoot, "manifest.webmanifest"), mf)
|
525
|
+
|
526
|
+
// 7) Descargar todos los archivos del bucket al buildRoot
|
527
|
+
const [files] = await bucket.getFiles({ prefix })
|
528
|
+
await Promise.all(
|
529
|
+
files.map(async file => {
|
530
|
+
const rel = file.name.replace(prefix, "")
|
531
|
+
const out = path.join(buildRoot, rel)
|
532
|
+
mkdirp.sync(path.dirname(out))
|
533
|
+
const [buf] = await file.download()
|
534
|
+
fs.writeFileSync(out, buf)
|
535
|
+
})
|
536
|
+
)
|
537
|
+
|
538
|
+
// 8) Crear el config.json en buildRoot con lo que retorna buildConfig
|
539
|
+
const fullConfig = { config, exercises }
|
540
|
+
fs.writeFileSync(
|
541
|
+
path.join(buildRoot, "config.json"),
|
542
|
+
JSON.stringify(fullConfig, null, 2),
|
543
|
+
"utf-8"
|
544
|
+
)
|
545
|
+
|
546
|
+
// 9) Empaquetar en ZIP (solo contenido de buildRoot)
|
547
|
+
const zipName = `${slug}.zip`
|
548
|
+
const zipPath = path.join(tmpRoot, zipName)
|
549
|
+
const output = fs.createWriteStream(zipPath)
|
550
|
+
const archive = archiver("zip", { zlib: { level: 9 } })
|
551
|
+
|
552
|
+
output.on("close", async () => {
|
553
|
+
// 10) Subir ZIP a RigoBot
|
554
|
+
const form = new FormData()
|
555
|
+
form.append("file", fs.createReadStream(zipPath))
|
556
|
+
form.append("config", JSON.stringify(config))
|
557
|
+
|
558
|
+
const rigoRes = await axios.post(
|
559
|
+
`${RIGOBOT_HOST}/v1/learnpack/upload`,
|
560
|
+
form,
|
561
|
+
{
|
562
|
+
headers: {
|
563
|
+
...form.getHeaders(),
|
564
|
+
Authorization: `Token ${rigoToken}`,
|
565
|
+
},
|
566
|
+
}
|
567
|
+
)
|
568
|
+
|
569
|
+
// 11) Crear/actualizar asset en Breathecode
|
570
|
+
// await handleAssetCreation(
|
571
|
+
// { token: bcToken, rigobot: { key: rigoToken }, id: 0 },
|
572
|
+
// { id: academyId } as any,
|
573
|
+
// config,
|
574
|
+
// rigoRes.data.url,
|
575
|
+
// categoryId
|
576
|
+
// )
|
577
|
+
|
578
|
+
rimraf.sync(tmpRoot)
|
579
|
+
console.log("RIGO RES", rigoRes.data)
|
580
|
+
|
581
|
+
return res.json({ url: rigoRes.data.url })
|
582
|
+
})
|
583
|
+
|
584
|
+
archive.on("error", err => {
|
585
|
+
console.error("ZIP Error:", err)
|
586
|
+
return res.status(500).send("Error building zip")
|
587
|
+
})
|
588
|
+
|
589
|
+
archive.pipe(output)
|
590
|
+
archive.directory(buildRoot, false)
|
591
|
+
await archive.finalize()
|
592
|
+
} catch (error) {
|
593
|
+
console.error(error)
|
594
|
+
return res.status(500).json({ error: (error as Error).message })
|
595
|
+
}
|
596
|
+
})
|
441
597
|
app.listen(PORT, () => {
|
442
598
|
console.log(
|
443
599
|
`🚀 Creator UI server running at http://localhost:${PORT}/creator`
|
package/src/creator/src/App.tsx
CHANGED
@@ -17,12 +17,13 @@ import FileUploader from "./components/FileUploader"
|
|
17
17
|
function App() {
|
18
18
|
const navigate = useNavigate()
|
19
19
|
|
20
|
-
const { formState, setFormState, setAuth, push } = useStore(
|
20
|
+
const { formState, setFormState, setAuth, push, cleanHistory } = useStore(
|
21
21
|
useShallow((state) => ({
|
22
22
|
formState: state.formState,
|
23
23
|
setFormState: state.setFormState,
|
24
24
|
setAuth: state.setAuth,
|
25
25
|
push: state.push,
|
26
|
+
cleanHistory: state.cleanHistory,
|
26
27
|
}))
|
27
28
|
)
|
28
29
|
|
@@ -68,6 +69,7 @@ function App() {
|
|
68
69
|
}
|
69
70
|
|
70
71
|
const handleCreateTutorial = async () => {
|
72
|
+
cleanHistory()
|
71
73
|
const res = await interactiveCreation({
|
72
74
|
courseInfo: JSON.stringify(formState),
|
73
75
|
prevInteractions: "",
|
@@ -46,13 +46,14 @@ const SyllabusEditor: React.FC = () => {
|
|
46
46
|
const [showLoginModal, setShowLoginModal] = useState(false)
|
47
47
|
const [isThinking, setIsThinking] = useState(false)
|
48
48
|
|
49
|
-
const { history, auth, setAuth, push, uploadedFiles } = useStore(
|
49
|
+
const { history, auth, setAuth, push, uploadedFiles, cleanHistory } = useStore(
|
50
50
|
useShallow((state) => ({
|
51
51
|
history: state.history,
|
52
52
|
auth: state.auth,
|
53
53
|
setAuth: state.setAuth,
|
54
54
|
push: state.push,
|
55
55
|
uploadedFiles: state.uploadedFiles,
|
56
|
+
cleanHistory: state.cleanHistory,
|
56
57
|
}))
|
57
58
|
)
|
58
59
|
|
@@ -41,6 +41,7 @@ type Store = {
|
|
41
41
|
uploadedFiles: UploadedFile[]
|
42
42
|
setUploadedFiles: (uploadedFiles: UploadedFile[]) => void
|
43
43
|
|
44
|
+
cleanHistory: () => void
|
44
45
|
history: Syllabus[]
|
45
46
|
undo: () => void
|
46
47
|
push: (syllabus: Syllabus) => void
|
@@ -96,6 +97,9 @@ const useStore = create<Store>()(
|
|
96
97
|
}
|
97
98
|
})
|
98
99
|
},
|
100
|
+
cleanHistory: () => {
|
101
|
+
set(() => ({ history: [] }))
|
102
|
+
},
|
99
103
|
consumables: {},
|
100
104
|
setConsumables: (consumables: Partial<Consumables>) =>
|
101
105
|
set((state) => {
|
@@ -14555,6 +14555,9 @@ const I_ = (n) => (e) => {
|
|
14555
14555
|
undo: () => {
|
14556
14556
|
n((e) => ({ history: e.history.slice(0, -1) }))
|
14557
14557
|
},
|
14558
|
+
cleanHistory: () => {
|
14559
|
+
n(() => ({ history: [] }))
|
14560
|
+
},
|
14558
14561
|
consumables: {},
|
14559
14562
|
setConsumables: (e) =>
|
14560
14563
|
n((t) => {
|
@@ -73798,25 +73801,27 @@ function kQ() {
|
|
73798
73801
|
setFormState: t,
|
73799
73802
|
setAuth: i,
|
73800
73803
|
push: r,
|
73804
|
+
cleanHistory: a,
|
73801
73805
|
} = Wr(
|
73802
|
-
nS((
|
73803
|
-
formState:
|
73804
|
-
setFormState:
|
73805
|
-
setAuth:
|
73806
|
-
push:
|
73806
|
+
nS((h) => ({
|
73807
|
+
formState: h.formState,
|
73808
|
+
setFormState: h.setFormState,
|
73809
|
+
setAuth: h.setAuth,
|
73810
|
+
push: h.push,
|
73811
|
+
cleanHistory: h.cleanHistory,
|
73807
73812
|
}))
|
73808
73813
|
)
|
73809
73814
|
me.useEffect(() => {
|
73810
|
-
e.isCompleted &&
|
73815
|
+
e.isCompleted && l()
|
73811
73816
|
}, [e]),
|
73812
73817
|
me.useEffect(() => {
|
73813
|
-
|
73818
|
+
s()
|
73814
73819
|
}, [])
|
73815
|
-
const
|
73816
|
-
const { token:
|
73817
|
-
if (
|
73818
|
-
const
|
73819
|
-
i({ bcToken:
|
73820
|
+
const s = async () => {
|
73821
|
+
const { token: h } = bO()
|
73822
|
+
if (h) {
|
73823
|
+
const d = await vO(h)
|
73824
|
+
i({ bcToken: h, userId: d.id, rigoToken: d.rigobot.key, user: d }),
|
73820
73825
|
t({
|
73821
73826
|
variables: [
|
73822
73827
|
"description",
|
@@ -73836,13 +73841,14 @@ function kQ() {
|
|
73836
73841
|
],
|
73837
73842
|
})
|
73838
73843
|
},
|
73839
|
-
|
73840
|
-
|
73844
|
+
l = async () => {
|
73845
|
+
a()
|
73846
|
+
const h = await pO({
|
73841
73847
|
courseInfo: JSON.stringify(e),
|
73842
73848
|
prevInteractions: "",
|
73843
73849
|
}),
|
73844
|
-
|
73845
|
-
r({ lessons:
|
73850
|
+
d = h.parsed.listOfSteps.map((p) => mO(p, []))
|
73851
|
+
r({ lessons: d, courseInfo: { ...e, title: h.parsed.title } }),
|
73846
73852
|
n("/creator/syllabus"),
|
73847
73853
|
t({
|
73848
73854
|
description: "",
|
@@ -73854,7 +73860,7 @@ function kQ() {
|
|
73854
73860
|
currentStep: "description",
|
73855
73861
|
})
|
73856
73862
|
},
|
73857
|
-
|
73863
|
+
u = () =>
|
73858
73864
|
[
|
73859
73865
|
{
|
73860
73866
|
title: "Provide a description for your tutorial",
|
@@ -73865,8 +73871,8 @@ function kQ() {
|
|
73865
73871
|
className:
|
73866
73872
|
"w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white",
|
73867
73873
|
value: e.description,
|
73868
|
-
onChange: (
|
73869
|
-
t({ description:
|
73874
|
+
onChange: (d) => {
|
73875
|
+
t({ description: d.target.value })
|
73870
73876
|
},
|
73871
73877
|
}),
|
73872
73878
|
},
|
@@ -73912,8 +73918,8 @@ function kQ() {
|
|
73912
73918
|
className:
|
73913
73919
|
"w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white",
|
73914
73920
|
defaultValue: e.targetAudience,
|
73915
|
-
onBlur: (
|
73916
|
-
t({ targetAudience:
|
73921
|
+
onBlur: (d) => {
|
73922
|
+
t({ targetAudience: d.target.value })
|
73917
73923
|
},
|
73918
73924
|
}),
|
73919
73925
|
}),
|
@@ -73958,24 +73964,24 @@ function kQ() {
|
|
73958
73964
|
className:
|
73959
73965
|
"w-full h-40 border-2 border-gray-300 rounded-md p-2",
|
73960
73966
|
value: e.contentIndex,
|
73961
|
-
onChange: (
|
73962
|
-
t({ contentIndex:
|
73967
|
+
onChange: (d) => {
|
73968
|
+
t({ contentIndex: d.target.value })
|
73963
73969
|
},
|
73964
73970
|
}),
|
73965
73971
|
ve.jsx(cB, {
|
73966
|
-
onResult: (
|
73967
|
-
let
|
73968
|
-
|
73969
|
-
|
73972
|
+
onResult: (d) => {
|
73973
|
+
let p = ""
|
73974
|
+
d.forEach((g) => {
|
73975
|
+
p += `<FILE NAME="${g.name}">${g.text}</FILE>
|
73970
73976
|
`
|
73971
73977
|
}),
|
73972
|
-
t({ contentIndex:
|
73978
|
+
t({ contentIndex: p })
|
73973
73979
|
},
|
73974
73980
|
}),
|
73975
73981
|
],
|
73976
73982
|
}),
|
73977
73983
|
},
|
73978
|
-
].filter((
|
73984
|
+
].filter((d) => e.variables.includes(d.slug) || d.slug === e.currentStep)
|
73979
73985
|
return ve.jsx(ve.Fragment, {
|
73980
73986
|
children: e.isCompleted
|
73981
73987
|
? ve.jsx(tS, {
|
@@ -73988,7 +73994,7 @@ function kQ() {
|
|
73988
73994
|
})
|
73989
73995
|
: ve.jsx(gq, {
|
73990
73996
|
formState: e,
|
73991
|
-
steps:
|
73997
|
+
steps: u(),
|
73992
73998
|
setFormState: t,
|
73993
73999
|
onFinish: () => {
|
73994
74000
|
t({ isCompleted: !0 })
|
@@ -10,7 +10,7 @@
|
|
10
10
|
/>
|
11
11
|
|
12
12
|
<title>Learnpack Creator: Craft tutorials in seconds!</title>
|
13
|
-
<script type="module" crossorigin src="/creator/assets/index-
|
13
|
+
<script type="module" crossorigin src="/creator/assets/index-EA507Y8n.js"></script>
|
14
14
|
<link rel="stylesheet" crossorigin href="/creator/assets/index-ldEC0yWM.css">
|
15
15
|
</head>
|
16
16
|
<body>
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import { Bucket, File } from "@google-cloud/storage"
|
2
|
+
|
3
|
+
export type Exercise = {
|
4
|
+
title: string
|
5
|
+
slug: string
|
6
|
+
graded: boolean
|
7
|
+
files: string[]
|
8
|
+
translations: Record<string, string>
|
9
|
+
position: number
|
10
|
+
}
|
11
|
+
|
12
|
+
export type ConfigResponse = {
|
13
|
+
config: any
|
14
|
+
exercises: Exercise[]
|
15
|
+
}
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Crea la configuración y lista de ejercicios para un curso.
|
19
|
+
*
|
20
|
+
* @param bucket - Instancia de GCS Bucket donde está el curso.
|
21
|
+
* @param courseSlug - Slug del curso a procesar.
|
22
|
+
* @returns Promise con objeto { config, exercises } listo para usar.
|
23
|
+
*/
|
24
|
+
export async function buildConfig(
|
25
|
+
bucket: Bucket,
|
26
|
+
courseSlug: string
|
27
|
+
): Promise<ConfigResponse> {
|
28
|
+
const prefix = `courses/${courseSlug}/`
|
29
|
+
const [files] = await bucket.getFiles({ prefix })
|
30
|
+
|
31
|
+
// 1) Leer learn.json
|
32
|
+
const learnFile = files.find(f => f.name.endsWith("learn.json"))!
|
33
|
+
const [learnBuf] = await learnFile.download()
|
34
|
+
const learnJson = JSON.parse(learnBuf.toString())
|
35
|
+
|
36
|
+
// 2) Agrupar ejercicios
|
37
|
+
const map: Record<string, Omit<Exercise, "position">> = {}
|
38
|
+
for (const file of files) {
|
39
|
+
const parts = file.name.split("/")
|
40
|
+
if (!parts.includes("exercises"))
|
41
|
+
continue
|
42
|
+
|
43
|
+
const slug = parts[parts.indexOf("exercises") + 1]
|
44
|
+
if (!map[slug]) {
|
45
|
+
map[slug] = {
|
46
|
+
title: slug,
|
47
|
+
slug,
|
48
|
+
graded: false,
|
49
|
+
files: [],
|
50
|
+
translations: {},
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
const fname = parts.pop()!
|
55
|
+
const m = fname.match(/^readme(?:\.([a-z]{2}))?\.md$/i)
|
56
|
+
if (m) {
|
57
|
+
const lang = m[1] || "us"
|
58
|
+
map[slug].translations[lang] = fname
|
59
|
+
} else {
|
60
|
+
map[slug].files.push(fname)
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
const exercises = Object.values(map).map((ex, i) => ({
|
65
|
+
...ex,
|
66
|
+
position: i,
|
67
|
+
}))
|
68
|
+
|
69
|
+
return {
|
70
|
+
config: { ...learnJson, title: { us: courseSlug } },
|
71
|
+
exercises,
|
72
|
+
}
|
73
|
+
}
|