@learnpack/learnpack 5.0.318 → 5.0.320

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.
@@ -90,18 +90,18 @@ async function fetchComponentsYml() {
90
90
  axios_1.default.get("https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/assessment_components.yml"),
91
91
  axios_1.default.get("https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/explanatory_components.yml"),
92
92
  ]);
93
- const combinedContent = `
94
- # ASSESSMENT COMPONENTS
95
- These components are designed for evaluation and knowledge assessment:
96
-
97
- ${assessmentResponse.data}
98
-
99
- ---
100
-
101
- # EXPLANATORY COMPONENTS
102
- These components are designed for explanation and learning support:
103
-
104
- ${explanatoryResponse.data}
93
+ const combinedContent = `
94
+ # ASSESSMENT COMPONENTS
95
+ These components are designed for evaluation and knowledge assessment:
96
+
97
+ ${assessmentResponse.data}
98
+
99
+ ---
100
+
101
+ # EXPLANATORY COMPONENTS
102
+ These components are designed for explanation and learning support:
103
+
104
+ ${explanatoryResponse.data}
105
105
  `;
106
106
  return combinedContent;
107
107
  }
@@ -135,10 +135,10 @@ const createInitialSidebar = async (slugs, initialLanguage = "en") => {
135
135
  return sidebar;
136
136
  };
137
137
  const uploadInitialReadme = async (bucket, exSlug, targetDir, packageContext) => {
138
- const isGeneratingText = `
139
- \`\`\`loader slug="${exSlug}"
140
- :rigo
141
- \`\`\`
138
+ const isGeneratingText = `
139
+ \`\`\`loader slug="${exSlug}"
140
+ :rigo
141
+ \`\`\`
142
142
  `;
143
143
  const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(packageContext.language || "en")}`;
144
144
  await uploadFileToBucket(bucket, isGeneratingText, `${targetDir}/${readmeFilename}`);
@@ -148,7 +148,7 @@ const cleanFormState = (formState) => {
148
148
  return rest;
149
149
  };
150
150
  const cleanFormStateForSyllabus = (formState) => {
151
- return Object.assign(Object.assign({}, formState), { description: formState.description, technologies: formState.technologies, purposse: undefined, duration: undefined, hasContentIndex: undefined, variables: undefined, currentStep: undefined, language: undefined, isCompleted: undefined });
151
+ return Object.assign(Object.assign({}, formState), { description: formState.description, technologies: formState.technologies, contentIndex: formState.contentIndex, purposse: undefined, duration: undefined, hasContentIndex: undefined, variables: undefined, currentStep: undefined, language: undefined, isCompleted: undefined });
152
152
  };
153
153
  const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, courseJson, deployUrl) => {
154
154
  const availableLangs = Object.keys(courseJson.title);
@@ -185,6 +185,12 @@ async function startInitialContentGeneration(rigoToken, steps, packageContext, e
185
185
  const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
186
186
  console.log("Starting initial content generation for", exSlug);
187
187
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.uid}/${rigoToken}`;
188
+ // Determine if this is the first lesson
189
+ const exerciseIndex = steps.findIndex(lesson => lesson.uid === exercise.uid);
190
+ const isFirstLesson = exerciseIndex === 0;
191
+ const endpointSlug = isFirstLesson ?
192
+ "initial-step-content-generator" :
193
+ "generate-step-initial-content";
188
194
  // Emit notification that initial content generation is starting
189
195
  (0, creatorSocket_1.emitToCourse)(courseSlug, "course-creation", {
190
196
  lesson: exSlug,
@@ -203,7 +209,7 @@ async function startInitialContentGeneration(rigoToken, steps, packageContext, e
203
209
  output_language: packageContext.language || "en",
204
210
  current_syllabus: JSON.stringify(fullSyllabus),
205
211
  lesson_description: JSON.stringify(lessonCleaner(exercise)) + `-${randomCacheEvict}`,
206
- }, webhookUrl);
212
+ }, webhookUrl, endpointSlug);
207
213
  console.log("INITIAL CONTENT GENERATOR RES", res);
208
214
  return res.id;
209
215
  }
@@ -1891,6 +1897,148 @@ class ServeCommand extends SessionCommand_1.default {
1891
1897
  });
1892
1898
  }
1893
1899
  });
1900
+ app.put("/courses/:courseSlug/exercises/:exerciseSlug/file/:filename/rename", express.json(), async (req, res) => {
1901
+ console.log("PUT /courses/:courseSlug/exercises/:exerciseSlug/file/:filename/rename", req.params, req.body);
1902
+ const { courseSlug, exerciseSlug, filename } = req.params;
1903
+ const { oldFilename, newFilename } = req.body;
1904
+ try {
1905
+ // Validaciones
1906
+ if (!oldFilename || !newFilename) {
1907
+ return res.status(400).json({
1908
+ error: "oldFilename and newFilename are required",
1909
+ });
1910
+ }
1911
+ // Validar que las extensiones coincidan
1912
+ const oldExt = oldFilename.slice(oldFilename.lastIndexOf("."));
1913
+ const newExt = newFilename.slice(newFilename.lastIndexOf("."));
1914
+ if (oldExt !== newExt) {
1915
+ return res.status(400).json({
1916
+ error: "File extension cannot be changed",
1917
+ });
1918
+ }
1919
+ // Validar caracteres prohibidos según GCS: < > : " / \ | ? * [ ] # y caracteres de control
1920
+ const invalidCharsList = [
1921
+ "<",
1922
+ ">",
1923
+ ":",
1924
+ '"',
1925
+ "/",
1926
+ "\\",
1927
+ "|",
1928
+ "?",
1929
+ "*",
1930
+ "[",
1931
+ "]",
1932
+ "#",
1933
+ "\r",
1934
+ "\n",
1935
+ ];
1936
+ const hasInvalidChars = invalidCharsList.some(char => newFilename.includes(char));
1937
+ if (hasInvalidChars) {
1938
+ return res.status(400).json({
1939
+ error: "Filename contains invalid characters",
1940
+ });
1941
+ }
1942
+ // Validar nombres reservados
1943
+ if (newFilename === "." ||
1944
+ newFilename === ".." ||
1945
+ newFilename.startsWith(".well-known/acme-challenge/")) {
1946
+ return res.status(400).json({
1947
+ error: "Reserved filename not allowed",
1948
+ });
1949
+ }
1950
+ // Validar longitud (máximo 1024 bytes en UTF-8)
1951
+ const newFilenameBytes = buffer_1.Buffer.from(newFilename, "utf8").length;
1952
+ if (newFilenameBytes > 1024) {
1953
+ return res.status(400).json({
1954
+ error: "Filename exceeds maximum size (1024 bytes in UTF-8)",
1955
+ });
1956
+ }
1957
+ const oldFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldFilename}`;
1958
+ const newFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newFilename}`;
1959
+ const oldFile = bucket.file(oldFilePath);
1960
+ const newFile = bucket.file(newFilePath);
1961
+ // Verificar que el archivo original existe
1962
+ const [oldExists] = await oldFile.exists();
1963
+ if (!oldExists) {
1964
+ return res.status(404).json({ error: "Source file not found" });
1965
+ }
1966
+ // Verificar que el nuevo nombre no existe
1967
+ const [newExists] = await newFile.exists();
1968
+ if (newExists) {
1969
+ return res
1970
+ .status(409)
1971
+ .json({ error: "File with new name already exists" });
1972
+ }
1973
+ // GCS no permite renombrar directamente, usar copy + delete
1974
+ await oldFile.copy(newFile);
1975
+ await oldFile.delete();
1976
+ console.log(`✅ Renamed file: ${oldFilePath} -> ${newFilePath}`);
1977
+ // Si existe un archivo de solución correspondiente, renombrarlo también
1978
+ // Patrón: nombreBase.solution.hide.extension
1979
+ let solutionRenamed = false;
1980
+ const lastDotIndex = oldFilename.lastIndexOf(".");
1981
+ if (lastDotIndex > 0 && lastDotIndex < oldFilename.length - 1) {
1982
+ // Usar la extensión que ya validamos que coincide
1983
+ const extension = oldExt; // Ya validado anteriormente
1984
+ const oldBaseName = oldFilename.slice(0, lastDotIndex);
1985
+ const newBaseName = newFilename.slice(0, newFilename.lastIndexOf("."));
1986
+ const oldSolutionFileName = `${oldBaseName}.solution.hide${extension}`;
1987
+ const newSolutionFileName = `${newBaseName}.solution.hide${extension}`;
1988
+ const oldSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldSolutionFileName}`;
1989
+ const newSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newSolutionFileName}`;
1990
+ const oldSolutionFile = bucket.file(oldSolutionFilePath);
1991
+ const newSolutionFile = bucket.file(newSolutionFilePath);
1992
+ const [solutionExists] = await oldSolutionFile.exists();
1993
+ if (solutionExists) {
1994
+ // Verificar que el nuevo nombre de solución no exista
1995
+ const [newSolutionExists] = await newSolutionFile.exists();
1996
+ if (!newSolutionExists) {
1997
+ await oldSolutionFile.copy(newSolutionFile);
1998
+ await oldSolutionFile.delete();
1999
+ solutionRenamed = true;
2000
+ console.log(`✅ Renamed solution file: ${oldSolutionFilePath} -> ${newSolutionFilePath}`);
2001
+ }
2002
+ else {
2003
+ console.log(`⚠️ Solution file ${newSolutionFileName} already exists, skipping rename`);
2004
+ }
2005
+ }
2006
+ }
2007
+ else {
2008
+ // Caso sin extensión: nombre.solution.hide
2009
+ const oldSolutionFileName = `${oldFilename}.solution.hide`;
2010
+ const newSolutionFileName = `${newFilename}.solution.hide`;
2011
+ const oldSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${oldSolutionFileName}`;
2012
+ const newSolutionFilePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${newSolutionFileName}`;
2013
+ const oldSolutionFile = bucket.file(oldSolutionFilePath);
2014
+ const newSolutionFile = bucket.file(newSolutionFilePath);
2015
+ const [solutionExists] = await oldSolutionFile.exists();
2016
+ if (solutionExists) {
2017
+ const [newSolutionExists] = await newSolutionFile.exists();
2018
+ if (!newSolutionExists) {
2019
+ await oldSolutionFile.copy(newSolutionFile);
2020
+ await oldSolutionFile.delete();
2021
+ solutionRenamed = true;
2022
+ console.log(`✅ Renamed solution file: ${oldSolutionFilePath} -> ${newSolutionFilePath}`);
2023
+ }
2024
+ else {
2025
+ console.log(`⚠️ Solution file ${newSolutionFileName} already exists, skipping rename`);
2026
+ }
2027
+ }
2028
+ }
2029
+ return res.json({
2030
+ success: true,
2031
+ message: "File renamed successfully",
2032
+ solutionRenamed,
2033
+ });
2034
+ }
2035
+ catch (error) {
2036
+ console.error("❌ Error renaming file:", error);
2037
+ return res.status(500).json({
2038
+ error: error.message || "Unable to rename file",
2039
+ });
2040
+ }
2041
+ });
1894
2042
  const YT_REGEX = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{11})/;
1895
2043
  app.get("/actions/fetch/:link", async (req, res) => {
1896
2044
  var _a, _b, _c;