@learnpack/learnpack 5.0.307 → 5.0.309

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.
@@ -20,7 +20,6 @@ import * as archiver from "archiver"
20
20
  import * as mkdirp from "mkdirp"
21
21
  import { convert } from "html-to-text"
22
22
  import * as rimraf from "rimraf"
23
- import { v4 as uuidv4 } from "uuid"
24
23
  import SessionCommand from "../utils/SessionCommand"
25
24
  import { Bucket, Storage } from "@google-cloud/storage"
26
25
  import { downloadEditor, decompress } from "../managers/file"
@@ -44,7 +43,6 @@ import * as dotenv from "dotenv"
44
43
 
45
44
  // import { v4 as uuidv4 } from "uuid"
46
45
  import {
47
- extractImagesFromMarkdown,
48
46
  getFilenameFromUrl,
49
47
  getReadmeExtension,
50
48
  } from "../utils/creatorUtilities"
@@ -52,11 +50,8 @@ import {
52
50
  import axios from "axios"
53
51
  import * as FormData from "form-data"
54
52
  import api, { RIGOBOT_HOST, RIGOBOT_REALTIME_HOST } from "../utils/api"
55
- import {
56
- createUploadMiddleware,
57
- minutesToISO8601Duration,
58
- } from "../utils/misc"
59
- import { buildConfig, ConfigResponse } from "../utils/configBuilder"
53
+ import { createUploadMiddleware, minutesToISO8601Duration } from "../utils/misc"
54
+ import { buildConfig } from "../utils/configBuilder"
60
55
  import { checkReadability, slugify } from "../utils/creatorUtilities"
61
56
  import { checkAndFixSidebarPure } from "../utils/sidebarGenerator"
62
57
  import { handleAssetCreation } from "./publish"
@@ -67,20 +62,6 @@ const frontMatter = require("front-matter")
67
62
 
68
63
  dotenv.config()
69
64
 
70
- // Asegúrate de tener uuid instalado
71
- // npm install uuid
72
-
73
- function findLast<T>(
74
- array: T[],
75
- predicate: (item: T) => boolean
76
- ): T | undefined {
77
- for (let i = array.length - 1; i >= 0; i--) {
78
- if (predicate(array[i])) return array[i]
79
- }
80
-
81
- return undefined
82
- }
83
-
84
65
  export const createLearnJson = (courseInfo: FormState) => {
85
66
  // console.log("courseInfo to create learn json", courseInfo)
86
67
 
@@ -687,7 +668,6 @@ const fixPreviewUrl = (slug: string, previewUrl: string) => {
687
668
  }
688
669
 
689
670
  const expectedUrl = `https://${slug}.learn-pack.com/preview.png`
690
- console.log("Preview url fixed!", expectedUrl)
691
671
  return expectedUrl
692
672
  }
693
673
 
@@ -713,13 +693,11 @@ export default class ServeCommand extends SessionCommand {
713
693
 
714
694
  async init() {
715
695
  const { flags } = this.parse(ServeCommand)
716
- console.log("Initializing serve command")
717
696
  }
718
697
 
719
698
  async run() {
720
699
  const crendsEnv = process.env.GCP_CREDENTIALS_JSON
721
700
  if (!crendsEnv) {
722
- console.log("GCP_CREDENTIALS_JSON is not set")
723
701
  process.exit(1)
724
702
  }
725
703
 
@@ -737,10 +715,12 @@ export default class ServeCommand extends SessionCommand {
737
715
  const host = process.env.HOST
738
716
 
739
717
  if (!host) {
740
- console.log("HOST is not set")
718
+ console.log(
719
+ "ERROR: HOST is not set, please set the HOST environment variable"
720
+ )
741
721
  process.exit(1)
742
722
  } else {
743
- console.log("HOST is set to", host)
723
+ console.log("INFO: HOST is set to", host)
744
724
  }
745
725
 
746
726
  // async function listFilesWithPrefix(prefix: string) {
@@ -807,7 +787,7 @@ export default class ServeCommand extends SessionCommand {
807
787
  })
808
788
 
809
789
  stream.on("finish", () => {
810
- console.log(`✅ Uploaded to: ${file.name}`)
790
+ console.log(`INFO: Uploaded to: ${file.name}`)
811
791
  res.send("File uploaded successfully")
812
792
  })
813
793
 
@@ -817,7 +797,7 @@ export default class ServeCommand extends SessionCommand {
817
797
  const upload = createUploadMiddleware()
818
798
 
819
799
  app.post("/upload-image-file", upload.single("file"), async (req, res) => {
820
- console.log("UPLOADING IMAGE FILE")
800
+ console.log("INFO: Uploading image file")
821
801
 
822
802
  const destination = (req.body as { destination?: string }).destination
823
803
 
@@ -842,7 +822,7 @@ export default class ServeCommand extends SessionCommand {
842
822
  contentType: req.file.mimetype,
843
823
  })
844
824
 
845
- console.log(`✅ Image uploaded to ${file.name}`)
825
+ console.log(`INFO: Image uploaded to ${file.name}`)
846
826
  res.json({ message: "Image uploaded successfully", path: file.name })
847
827
  } catch (error) {
848
828
  console.error("❌ upload-image-file error:", error)
@@ -1075,9 +1055,7 @@ export default class ServeCommand extends SessionCommand {
1075
1055
  fileObj.solution,
1076
1056
  solutionFilePath
1077
1057
  )
1078
- console.log(
1079
- `✅ Saved solution file: ${solutionFilePath}`
1080
- )
1058
+ console.log(`✅ Saved solution file: ${solutionFilePath}`)
1081
1059
  } else {
1082
1060
  // If no extension, just add .solution.hide
1083
1061
  const solutionFileName = `${fileObj.name}.solution.hide`
@@ -1088,9 +1066,7 @@ export default class ServeCommand extends SessionCommand {
1088
1066
  fileObj.solution,
1089
1067
  solutionFilePath
1090
1068
  )
1091
- console.log(
1092
- `✅ Saved solution file: ${solutionFilePath}`
1093
- )
1069
+ console.log(`✅ Saved solution file: ${solutionFilePath}`)
1094
1070
  }
1095
1071
  }
1096
1072
  } catch (parseError) {
@@ -1976,10 +1952,6 @@ export default class ServeCommand extends SessionCommand {
1976
1952
 
1977
1953
  try {
1978
1954
  const { config, exercises } = await buildConfig(bucket, courseSlug)
1979
-
1980
- console.log("CONFIG", config)
1981
- console.log("EXERCISES", exercises)
1982
-
1983
1955
  res.set("X-Creator-Web", "true")
1984
1956
  res.set("Access-Control-Expose-Headers", "X-Creator-Web")
1985
1957
 
@@ -2221,11 +2193,11 @@ export default class ServeCommand extends SessionCommand {
2221
2193
  new_languages: neededLanguagesList.join(","),
2222
2194
  })
2223
2195
 
2224
- const translateTitle = JSON.parse(result.parsed.title)
2225
- const translateDescription = JSON.parse(result.parsed.description)
2196
+ const translatedTitle = JSON.parse(result.parsed.title)
2197
+ const translatedDescription = JSON.parse(result.parsed.description)
2226
2198
 
2227
- courseJson.title = translateTitle
2228
- courseJson.description = translateDescription
2199
+ courseJson.title = translatedTitle
2200
+ courseJson.description = translatedDescription
2229
2201
 
2230
2202
  await uploadFileToBucket(
2231
2203
  bucket,
@@ -2593,10 +2565,12 @@ export default class ServeCommand extends SessionCommand {
2593
2565
  .json({ error: "Authentication failed, missing tokens" })
2594
2566
  }
2595
2567
 
2596
- const { config, exercises }: ConfigResponse = await buildConfig(
2597
- bucket,
2598
- slug
2568
+ const configFile = await bucket.file(
2569
+ `courses/${slug}/.learn/config.json`
2599
2570
  )
2571
+ const [configContent] = await configFile.download()
2572
+ const configJson = JSON.parse(configContent.toString())
2573
+ const { config, exercises } = configJson
2600
2574
 
2601
2575
  const prefix = `courses/${slug}/`
2602
2576
  const tmpRoot = path.join(os.tmpdir(), `learnpack-${slug}`)
@@ -2838,9 +2812,7 @@ export default class ServeCommand extends SessionCommand {
2838
2812
  })
2839
2813
  } catch (error: any) {
2840
2814
  console.error("❌ /actions/fetch error:", error.message || error)
2841
- res
2842
- .status(500)
2843
- .json({ error: error.message || "Failed to fetch link" })
2815
+ res.status(500).json({ error: error.message || "Failed to fetch link" })
2844
2816
  }
2845
2817
  })
2846
2818
  app.delete("/packages/:slug", async (req, res) => {
@@ -2881,6 +2853,135 @@ export default class ServeCommand extends SessionCommand {
2881
2853
  }
2882
2854
  })
2883
2855
 
2856
+ app.post("/actions/change-slug", async (req, res) => {
2857
+ const { currentSlug, newSlug } = req.body
2858
+ const rigoToken = req.header("x-rigo-token")
2859
+
2860
+ if (!rigoToken) {
2861
+ return res.status(400).json({
2862
+ error: "Rigo token is required. x-rigo-token header is missing",
2863
+ })
2864
+ }
2865
+
2866
+ if (!currentSlug || !newSlug) {
2867
+ return res.status(400).json({
2868
+ error: "Both currentSlug and newSlug are required",
2869
+ })
2870
+ }
2871
+
2872
+ if (currentSlug === newSlug) {
2873
+ return res.status(200).json({
2874
+ message: "Slug unchanged",
2875
+ newSlug: currentSlug,
2876
+ })
2877
+ }
2878
+
2879
+ try {
2880
+ // Check if new slug is available via RigoBot
2881
+ const isAvailable = await isValidRigoToken(rigoToken)
2882
+ if (!isAvailable) {
2883
+ return res.status(401).json({ error: "Invalid Rigo token" })
2884
+ }
2885
+
2886
+ // Check slug availability
2887
+ const slugCheckUrl = `${RIGOBOT_HOST}/v1/learnpack/check-slug-availability?slug=${encodeURIComponent(
2888
+ newSlug
2889
+ )}`
2890
+ const slugResponse = await axios.get(slugCheckUrl)
2891
+
2892
+ if (!slugResponse.data.available) {
2893
+ return res.status(409).json({
2894
+ error: "Slug is not available",
2895
+ available: false,
2896
+ })
2897
+ }
2898
+
2899
+ // Get all files with current slug prefix
2900
+ const currentPrefix = `courses/${currentSlug}/`
2901
+ const [files] = await bucket.getFiles({ prefix: currentPrefix })
2902
+
2903
+ if (files.length === 0) {
2904
+ return res.status(404).json({
2905
+ error: "No files found for current slug",
2906
+ })
2907
+ }
2908
+
2909
+ // Copy all files to new slug location
2910
+ const newPrefix = `courses/${newSlug}/`
2911
+
2912
+ for (const file of files) {
2913
+ const newFileName = file.name.replace(currentPrefix, newPrefix)
2914
+ // eslint-disable-next-line no-await-in-loop
2915
+ await file.copy(newFileName)
2916
+ }
2917
+
2918
+ // Update learn.json with new slug
2919
+ const learnJsonFile = bucket.file(`${newPrefix}learn.json`)
2920
+ const [learnJsonContent] = await learnJsonFile.download()
2921
+ const learnJson = JSON.parse(learnJsonContent.toString())
2922
+ learnJson.slug = newSlug
2923
+ await uploadFileToBucket(
2924
+ bucket,
2925
+ JSON.stringify(learnJson),
2926
+ `${newPrefix}learn.json`
2927
+ )
2928
+
2929
+ // Update initialSyllabus.json with new slug
2930
+ const syllabusFile = bucket.file(
2931
+ `${newPrefix}.learn/initialSyllabus.json`
2932
+ )
2933
+ const [syllabusContent] = await syllabusFile.download()
2934
+ const syllabus = JSON.parse(syllabusContent.toString())
2935
+ syllabus.courseInfo.slug = newSlug
2936
+ await uploadFileToBucket(
2937
+ bucket,
2938
+ JSON.stringify(syllabus),
2939
+ `${newPrefix}.learn/initialSyllabus.json`
2940
+ )
2941
+
2942
+ // Update config.json with new slug
2943
+ const configFile = bucket.file(`${newPrefix}.learn/config.json`)
2944
+ const [configContent] = await configFile.download()
2945
+ const config = JSON.parse(configContent.toString())
2946
+ config.config.slug = newSlug
2947
+ await uploadFileToBucket(
2948
+ bucket,
2949
+ JSON.stringify(config),
2950
+ `${newPrefix}.learn/config.json`
2951
+ )
2952
+
2953
+ // Update Rigobot package slug
2954
+ const updateUrl = `${RIGOBOT_HOST}/v1/learnpack/package/${currentSlug}/`
2955
+ await axios.put(
2956
+ updateUrl,
2957
+ { new_slug: newSlug },
2958
+ {
2959
+ headers: {
2960
+ Authorization: "Token " + rigoToken.trim(),
2961
+ },
2962
+ }
2963
+ )
2964
+
2965
+ // Delete old files
2966
+ await Promise.all(files.map(file => file.delete()))
2967
+
2968
+ console.log(
2969
+ `✅ Successfully changed slug from ${currentSlug} to ${newSlug}`
2970
+ )
2971
+
2972
+ return res.json({
2973
+ message: "Slug changed successfully",
2974
+ newSlug: newSlug,
2975
+ })
2976
+ } catch (error) {
2977
+ console.error("❌ Error changing slug:", error)
2978
+ return res.status(500).json({
2979
+ error: "Failed to change slug",
2980
+ details: (error as Error).message,
2981
+ })
2982
+ }
2983
+ })
2984
+
2884
2985
  app.get("/proxy", async (req, res) => {
2885
2986
  const { url } = req.query
2886
2987
 
@@ -3020,10 +3121,7 @@ export default class ServeCommand extends SessionCommand {
3020
3121
  !metadata.rights ||
3021
3122
  !metadata.lang
3022
3123
  ) {
3023
- console.log(
3024
- "Missing required metadata for EPUB export",
3025
- metadata
3026
- )
3124
+ console.log("Missing required metadata for EPUB export", metadata)
3027
3125
  return res.status(400).json({
3028
3126
  error: "Missing required metadata for EPUB export",
3029
3127
  required: ["creator", "publisher", "title", "rights", "lang"],
@@ -3068,9 +3166,7 @@ export default class ServeCommand extends SessionCommand {
3068
3166
  })
3069
3167
  } catch (error: any) {
3070
3168
  console.error("Export error:", error)
3071
- res
3072
- .status(500)
3073
- .json({ error: "Export failed", details: error.message })
3169
+ res.status(500).json({ error: "Export failed", details: error.message })
3074
3170
  }
3075
3171
  })
3076
3172