@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 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.89 win32-x64 node-v22.15.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.89/src\commands\audit.ts)_
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.89/src\commands\breakToken.ts)_
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.89/src\commands\clean.ts)_
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.89/src\commands\download.ts)_
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.89/src\commands\init.ts)_
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.89/src\commands\login.ts)_
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.89/src\commands\logout.ts)_
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.89/src\commands\publish.ts)_
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.89/src\commands\serve.ts)_
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.89/src\commands\start.ts)_
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.89/src\commands\test.ts)_
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.89/src\commands\translate.ts)_
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: {
@@ -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) {
@@ -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
- const files = await listFilesWithPrefix(`courses/${courseSlug}`);
131
- const learnJson = files.find(file => file.name.endsWith("learn.json"));
132
- const learnJsonContent = await (learnJson === null || learnJson === void 0 ? void 0 : learnJson.download());
133
- const learnJsonParsed = JSON.parse((learnJsonContent === null || learnJsonContent === void 0 ? void 0 : learnJsonContent.toString()) || "{}");
134
- const exerciseMap = {};
135
- // Agrupar archivos por ejercicio
136
- for (const file of files) {
137
- const pathParts = file.name.split("/");
138
- const isExercise = pathParts.includes("exercises");
139
- if (!isExercise)
140
- continue;
141
- const slug = pathParts[pathParts.indexOf("exercises") + 1];
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((u) => ({
73803
- formState: u.formState,
73804
- setFormState: u.setFormState,
73805
- setAuth: u.setAuth,
73806
- push: u.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 && s()
73815
+ e.isCompleted && l()
73811
73816
  }, [e]),
73812
73817
  me.useEffect(() => {
73813
- a()
73818
+ s()
73814
73819
  }, [])
73815
- const a = async () => {
73816
- const { token: u } = bO()
73817
- if (u) {
73818
- const h = await vO(u)
73819
- i({ bcToken: u, userId: h.id, rigoToken: h.rigobot.key, user: h }),
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
- s = async () => {
73840
- const u = await pO({
73844
+ l = async () => {
73845
+ a()
73846
+ const h = await pO({
73841
73847
  courseInfo: JSON.stringify(e),
73842
73848
  prevInteractions: "",
73843
73849
  }),
73844
- h = u.parsed.listOfSteps.map((d) => mO(d, []))
73845
- r({ lessons: h, courseInfo: { ...e, title: u.parsed.title } }),
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
- l = () =>
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: (h) => {
73869
- t({ description: h.target.value })
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: (h) => {
73916
- t({ targetAudience: h.target.value })
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: (h) => {
73962
- t({ contentIndex: h.target.value })
73967
+ onChange: (d) => {
73968
+ t({ contentIndex: d.target.value })
73963
73969
  },
73964
73970
  }),
73965
73971
  ve.jsx(cB, {
73966
- onResult: (h) => {
73967
- let d = ""
73968
- h.forEach((p) => {
73969
- d += `<FILE NAME="${p.name}">${p.text}</FILE>
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: d })
73978
+ t({ contentIndex: p })
73973
73979
  },
73974
73980
  }),
73975
73981
  ],
73976
73982
  }),
73977
73983
  },
73978
- ].filter((h) => e.variables.includes(h.slug) || h.slug === e.currentStep)
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: l(),
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-B48vF2UK.js"></script>
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
+ }
@@ -1 +1 @@
1
- {"version":"5.0.89","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":[]}}}
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.89",
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",
@@ -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,
@@ -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
- const files = await listFilesWithPrefix(`courses/${courseSlug}`)
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
- const exercises = Object.values(exerciseMap).map((ex, index) => ({
228
- ...ex,
229
- position: index,
230
- }))
231
-
232
- res.set("X-Creator-Web", "true")
233
- res.set("Access-Control-Expose-Headers", "X-Creator-Web")
234
-
235
- res.send({
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`
@@ -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((u) => ({
73803
- formState: u.formState,
73804
- setFormState: u.setFormState,
73805
- setAuth: u.setAuth,
73806
- push: u.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 && s()
73815
+ e.isCompleted && l()
73811
73816
  }, [e]),
73812
73817
  me.useEffect(() => {
73813
- a()
73818
+ s()
73814
73819
  }, [])
73815
- const a = async () => {
73816
- const { token: u } = bO()
73817
- if (u) {
73818
- const h = await vO(u)
73819
- i({ bcToken: u, userId: h.id, rigoToken: h.rigobot.key, user: h }),
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
- s = async () => {
73840
- const u = await pO({
73844
+ l = async () => {
73845
+ a()
73846
+ const h = await pO({
73841
73847
  courseInfo: JSON.stringify(e),
73842
73848
  prevInteractions: "",
73843
73849
  }),
73844
- h = u.parsed.listOfSteps.map((d) => mO(d, []))
73845
- r({ lessons: h, courseInfo: { ...e, title: u.parsed.title } }),
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
- l = () =>
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: (h) => {
73869
- t({ description: h.target.value })
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: (h) => {
73916
- t({ targetAudience: h.target.value })
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: (h) => {
73962
- t({ contentIndex: h.target.value })
73967
+ onChange: (d) => {
73968
+ t({ contentIndex: d.target.value })
73963
73969
  },
73964
73970
  }),
73965
73971
  ve.jsx(cB, {
73966
- onResult: (h) => {
73967
- let d = ""
73968
- h.forEach((p) => {
73969
- d += `<FILE NAME="${p.name}">${p.text}</FILE>
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: d })
73978
+ t({ contentIndex: p })
73973
73979
  },
73974
73980
  }),
73975
73981
  ],
73976
73982
  }),
73977
73983
  },
73978
- ].filter((h) => e.variables.includes(h.slug) || h.slug === e.currentStep)
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: l(),
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-B48vF2UK.js"></script>
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
+ }