@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.
- package/lib/commands/serve.js +166 -18
- package/lib/creatorDist/assets/{index-XZDcEWl9.js → index-BhqDgBS9.js} +588 -585
- package/lib/creatorDist/index.html +1 -1
- package/lib/utils/rigoActions.d.ts +1 -1
- package/lib/utils/rigoActions.js +2 -2
- package/package.json +1 -1
- package/src/commands/serve.ts +3462 -3267
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +20 -9
- package/src/creatorDist/assets/{index-XZDcEWl9.js → index-BhqDgBS9.js} +588 -585
- package/src/creatorDist/index.html +1 -1
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +2271 -2276
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/rigoActions.ts +639 -638
package/lib/commands/serve.js
CHANGED
|
@@ -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;
|