create-openclass-uniminuto 1.4.0
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/LICENSE +21 -0
- package/README.md +480 -0
- package/bin/create-openclass-uniminuto.mjs +142 -0
- package/package.json +39 -0
- package/template/.github/workflows/deploy.yml +64 -0
- package/template/README.md +326 -0
- package/template/config/openclass.config.iot-desde-openclass-iot.json +77 -0
- package/template/config/openclass.config.iot-ejemplo.json +81 -0
- package/template/demo_semana1.md +11 -0
- package/template/demo_semana2.md +11 -0
- package/template/demo_semana3.md +11 -0
- package/template/demo_semana4.md +11 -0
- package/template/demo_semana5.md +11 -0
- package/template/demo_semana6.md +11 -0
- package/template/demo_semana7.md +11 -0
- package/template/demo_semana8.md +11 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/iot_semana1.md +12 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/iot_semana2.md +12 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/iot_semana3.md +12 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/iot_semana4.md +12 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/iot_semana5.md +12 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/iot_semana6.md +12 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/iot_semana7.md +12 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/iot_semana8.md +12 -0
- package/template/ejemplos/iot-semanas-1-a-3/raiz/slides.md +154 -0
- package/template/ejemplos/iot-semanas-1-a-3/semanas/iot_semana1.md +846 -0
- package/template/ejemplos/iot-semanas-1-a-3/semanas/iot_semana2.md +1040 -0
- package/template/ejemplos/iot-semanas-1-a-3/semanas/iot_semana3.md +1071 -0
- package/template/openclass.config.json +79 -0
- package/template/package-lock.json +11568 -0
- package/template/package.json +61 -0
- package/template/plantillas/launcher.md +11 -0
- package/template/plantillas/semana.md +177 -0
- package/template/public/descargas/.gitkeep +0 -0
- package/template/public/favicon.png +0 -0
- package/template/public/fondos/slide-01-portada.png +0 -0
- package/template/public/fondos/slide-02-titulo.png +0 -0
- package/template/public/fondos/slide-03-imagen-izquierda.png +0 -0
- package/template/public/fondos/slide-04-imagen-derecha.png +0 -0
- package/template/public/fondos/slide-05-template.png +0 -0
- package/template/public/fondos/slide-06-cierre.png +0 -0
- package/template/public/imagenes/.gitkeep +0 -0
- package/template/public/videos/.gitkeep +0 -0
- package/template/scripts/build-incremental.mjs +33 -0
- package/template/scripts/build-site.mjs +33 -0
- package/template/scripts/decks.mjs +29 -0
- package/template/scripts/dev-all.mjs +37 -0
- package/template/scripts/export-downloads.mjs +53 -0
- package/template/scripts/export-incremental.mjs +49 -0
- package/template/scripts/generar-desde-config.mjs +279 -0
- package/template/scripts/nuevo-curso.mjs +104 -0
- package/template/scripts/preparar-github-pages.mjs +52 -0
- package/template/scripts/publicar.mjs +36 -0
- package/template/scripts/semana.mjs +122 -0
- package/template/scripts/zip-template.mjs +70 -0
- package/template/semanas/demo_semana1.md +177 -0
- package/template/semanas/demo_semana2.md +177 -0
- package/template/semanas/demo_semana3.md +177 -0
- package/template/semanas/demo_semana4.md +177 -0
- package/template/semanas/demo_semana5.md +177 -0
- package/template/semanas/demo_semana6.md +177 -0
- package/template/semanas/demo_semana7.md +177 -0
- package/template/semanas/demo_semana8.md +177 -0
- package/template/setup/shiki.ts +10 -0
- package/template/slides.md +65 -0
- package/template/snippets/external.ts +12 -0
- package/template/theme/uniminuto/README-AutoFit.md +28 -0
- package/template/theme/uniminuto/components/AutoFitText.vue +159 -0
- package/template/theme/uniminuto/components/Counter.vue +37 -0
- package/template/theme/uniminuto/components/FontToggle.vue +42 -0
- package/template/theme/uniminuto/layouts/slide-01-portada.vue +119 -0
- package/template/theme/uniminuto/layouts/slide-02-titulo.vue +63 -0
- package/template/theme/uniminuto/layouts/slide-03-imagen-izquierda.vue +110 -0
- package/template/theme/uniminuto/layouts/slide-04-imagen-derecha.vue +110 -0
- package/template/theme/uniminuto/layouts/slide-05-titulo-superior-texto-derecha.vue +104 -0
- package/template/theme/uniminuto/layouts/slide-06-titulo-superior-texto-izquierda.vue +109 -0
- package/template/theme/uniminuto/layouts/slide-07-multimedia-con-titulo.vue +87 -0
- package/template/theme/uniminuto/layouts/slide-08-titulo-texto.vue +78 -0
- package/template/theme/uniminuto/layouts/slide-09-objetivos.vue +77 -0
- package/template/theme/uniminuto/layouts/slide-10-titulo-dos-columnas.vue +89 -0
- package/template/theme/uniminuto/layouts/slide-11-dos-titulos-dos-columnas.vue +98 -0
- package/template/theme/uniminuto/layouts/slide-12-cierre.vue +27 -0
- package/template/theme/uniminuto/layouts/slide-codigo.vue +133 -0
- package/template/theme/uniminuto/package.json +13 -0
- package/template/theme/uniminuto/styles/base.css +109 -0
- package/template/theme/uniminuto/styles/index.ts +11 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genera o actualiza una Open Class Slidev a partir de openclass.config.json.
|
|
3
|
+
*
|
|
4
|
+
* Uso principal:
|
|
5
|
+
* npm run config
|
|
6
|
+
* node scripts/generar-desde-config.mjs --config config/openclass.config.iot-ejemplo.json --force
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
function valueAfter(flag, fallback) {
|
|
14
|
+
const i = args.indexOf(flag);
|
|
15
|
+
if (i >= 0 && args[i + 1]) return args[i + 1];
|
|
16
|
+
const pair = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
17
|
+
if (pair) return pair.slice(flag.length + 1);
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const configPath = valueAfter("--config", "openclass.config.json");
|
|
22
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
23
|
+
const dryRun = args.includes("--dry-run");
|
|
24
|
+
|
|
25
|
+
function fail(message) {
|
|
26
|
+
console.error(`\n❌ ${message}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(filePath) {
|
|
31
|
+
if (!fs.existsSync(filePath)) fail(`No se encontró el archivo de configuración: ${filePath}`);
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
fail(`No se pudo leer ${filePath}. Revisa que sea JSON válido.\n${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureDir(dir) {
|
|
40
|
+
if (!dryRun) fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeFileSafe(filePath, content, { overwrite = true, label = filePath } = {}) {
|
|
44
|
+
ensureDir(path.dirname(filePath));
|
|
45
|
+
|
|
46
|
+
if (fs.existsSync(filePath) && !overwrite) {
|
|
47
|
+
console.log(`⏭ Conservado: ${label}`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!dryRun) fs.writeFileSync(filePath, content, "utf-8");
|
|
52
|
+
console.log(`${dryRun ? "🔎" : "✓"} ${label}`);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cleanShortName(value) {
|
|
57
|
+
return String(value || "")
|
|
58
|
+
.normalize("NFD")
|
|
59
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9]+/g, "")
|
|
62
|
+
.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function asArray(value) {
|
|
66
|
+
return Array.isArray(value) ? value : [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function replaceTokens(template, tokens) {
|
|
70
|
+
return Object.entries(tokens).reduce(
|
|
71
|
+
(content, [key, value]) => content.replaceAll(`{{${key}}}`, String(value ?? "")),
|
|
72
|
+
template,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readTemplate(filePath, fallback) {
|
|
77
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeBase(value) {
|
|
81
|
+
let base = value || "/";
|
|
82
|
+
if (!base.startsWith("/")) base = `/${base}`;
|
|
83
|
+
if (!base.endsWith("/")) base = `${base}/`;
|
|
84
|
+
return base;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const config = readJson(configPath);
|
|
88
|
+
const course = config.course || {};
|
|
89
|
+
const generation = config.generation || {};
|
|
90
|
+
|
|
91
|
+
const courseShort = cleanShortName(course.shortName || config.shortName || config.courseCode || "curso");
|
|
92
|
+
const courseName = String(course.fullName || course.name || config.courseName || "Nombre del curso").trim();
|
|
93
|
+
const courseYear = String(course.year || config.year || new Date().getFullYear()).trim();
|
|
94
|
+
const description = String(course.description || config.description || "Agrega aquí la descripción general del curso.").trim();
|
|
95
|
+
const openClassLabel = String(course.openClassLabel || "Open Class").trim();
|
|
96
|
+
const weeksTotal = Number(generation.weeksTotal || config.weeksTotal || config.totalWeeks || asArray(config.weeks).length || 8);
|
|
97
|
+
|
|
98
|
+
if (!courseShort) fail("El campo course.shortName es obligatorio.");
|
|
99
|
+
if (!courseName) fail("El campo course.fullName es obligatorio.");
|
|
100
|
+
if (!Number.isInteger(weeksTotal) || weeksTotal < 1) fail("generation.weeksTotal debe ser un número entero mayor o igual a 1.");
|
|
101
|
+
|
|
102
|
+
const configuredWeeks = new Map(asArray(config.weeks).map((week) => [Number(week.number), week]));
|
|
103
|
+
|
|
104
|
+
function weekInfo(number) {
|
|
105
|
+
const week = configuredWeeks.get(number) || {};
|
|
106
|
+
const title = String(week.title || `Título semana ${number}`).trim();
|
|
107
|
+
return {
|
|
108
|
+
number,
|
|
109
|
+
title,
|
|
110
|
+
date: String(week.date || courseYear).trim(),
|
|
111
|
+
centralTheme: String(week.centralTheme || week.theme || title).trim(),
|
|
112
|
+
activity: String(week.activity || "Actividad o evaluación relacionada").trim(),
|
|
113
|
+
duration: String(week.duration || "90 minutos").trim(),
|
|
114
|
+
status: String(week.status || "active").trim(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const allWeeks = Array.from({ length: weeksTotal }, (_, i) => weekInfo(i + 1));
|
|
119
|
+
const activeWeeksRaw = asArray(generation.activeWeeks || config.activeWeeks);
|
|
120
|
+
const activeWeekNumbers = activeWeeksRaw.length
|
|
121
|
+
? activeWeeksRaw.map(Number).filter((n) => Number.isInteger(n) && n >= 1 && n <= weeksTotal)
|
|
122
|
+
: allWeeks.filter((week) => week.status !== "draft" && week.status !== "inactive").map((week) => week.number);
|
|
123
|
+
|
|
124
|
+
const activeWeeks = allWeeks.filter((week) => activeWeekNumbers.includes(week.number));
|
|
125
|
+
const overwriteLaunchers = generation.overwriteLaunchers !== false || force;
|
|
126
|
+
const overwritePortal = generation.overwritePortal !== false || force;
|
|
127
|
+
const overwriteDecks = generation.overwriteDecks !== false || force;
|
|
128
|
+
const overwritePackageScripts = generation.overwritePackageScripts !== false || force;
|
|
129
|
+
const overwriteWeekContent = generation.overwriteWeekContent === true || force;
|
|
130
|
+
const exportPortal = generation.exportPortal === true;
|
|
131
|
+
|
|
132
|
+
const semanaTemplate = readTemplate(
|
|
133
|
+
"plantillas/semana.md",
|
|
134
|
+
`---\nlayout: slide-01-portada\n---\n\n::title::\n{{COURSE_NAME}}\n\n::week::\nSemana {{WEEK_NUMBER}} — {{WEEK_TITLE}}\n\n::date::\n{{WEEK_DATE}}\n\n---\nlayout: slide-12-cierre\n---\n`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const launcherTemplate = readTemplate(
|
|
138
|
+
"plantillas/launcher.md",
|
|
139
|
+
`---\ntheme: ./theme/uniminuto\ntitle: {{COURSE_NAME}} — Semana {{WEEK_NUMBER}} — {{WEEK_TITLE}}\nfavicon: /favicon.png\ncodeCopy: true\ntransition: fade\nrouterMode: hash\ndrawings:\n persist: false\nsrc: ./semanas/{{COURSE_SHORT}}_semana{{WEEK_NUMBER}}.md\n---\n`,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
function weekTokens(week) {
|
|
143
|
+
return {
|
|
144
|
+
COURSE_SHORT: courseShort,
|
|
145
|
+
COURSE_NAME: courseName,
|
|
146
|
+
COURSE_YEAR: courseYear,
|
|
147
|
+
OPEN_CLASS_LABEL: openClassLabel,
|
|
148
|
+
WEEK_NUMBER: week.number,
|
|
149
|
+
WEEK_TITLE: week.title,
|
|
150
|
+
WEEK_DATE: week.date,
|
|
151
|
+
WEEK_THEME: week.centralTheme,
|
|
152
|
+
WEEK_ACTIVITY: week.activity,
|
|
153
|
+
WEEK_DURATION: week.duration,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function portalWeekItem(week) {
|
|
158
|
+
const slug = `${courseShort}_semana${week.number}`;
|
|
159
|
+
return `### **Semana ${week.number}**\n\n<a href="./semanas/${slug}/#/1" target="_self">${week.title}</a>\n\n<a href="./descargas/${slug}.pdf" download>Descargar PDF</a> · <a href="./descargas/${slug}.pptx" download>Descargar PPTX</a>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function buildPortal() {
|
|
163
|
+
const left = activeWeeks.length
|
|
164
|
+
? activeWeeks.filter((_, index) => index < Math.ceil(activeWeeks.length / 2)).map(portalWeekItem).join("\n\n")
|
|
165
|
+
: "### Sin semanas activas\n\nEjecuta `npm run semana -- 1` para generar la primera semana.";
|
|
166
|
+
const right = activeWeeks.filter((_, index) => index >= Math.ceil(activeWeeks.length / 2)).map(portalWeekItem).join("\n\n") || "### Próximamente\n\nActiva más semanas con `npm run semana -- 2`, `npm run semana -- 3` y así sucesivamente.";
|
|
167
|
+
|
|
168
|
+
return `---\ntheme: ./theme/uniminuto\ntitle: ${courseName} — ${openClassLabel}\nfavicon: /favicon.png\ncodeCopy: true\ntransition: fade\nrouterMode: hash\ndrawings:\n persist: false\nlayout: slide-01-portada\n---\n\n::title::\n${courseName}\n\n::week::\n${openClassLabel}\n\n::date::\n${courseYear}\n\n---\nlayout: slide-08-titulo-texto\n---\n\n::title::\nDescripción general del curso\n\n::content::\n${description}\n\n---\nlayout: slide-08-titulo-texto\n---\n\n::title::\nRuta de aprendizaje\n\n::content::\nEl curso se organiza en semanas. Cada semana cuenta con un lanzador raíz y un archivo interno en la carpeta \`semanas/\`.\n\nLas presentaciones activas se controlan desde \`openclass.config.json\` mediante el campo \`generation.activeWeeks\`.\n\n---\nlayout: slide-10-titulo-dos-columnas\n---\n\n::title::\nPresentaciones disponibles\n\n::left::\n${left}\n\n::right::\n${right}\n\n---\nlayout: slide-12-cierre\n---\n`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildDecks() {
|
|
172
|
+
const entries = [
|
|
173
|
+
`function normalizeBase(value) {\n let base = value || "/";\n if (!base.startsWith("/")) base = \`/\${base}\`;\n if (!base.endsWith("/")) base = \`\${base}/\`;\n return base;\n}\n\nconst SITE_BASE = normalizeBase(process.env.SITE_BASE || "/");\n\nfunction withBase(path = "") {\n return \`\${SITE_BASE}\${path.replace(/^\\/+/, "")}\`;\n}\n\nexport const decks = [\n {\n name: "openclass-${courseShort}",\n entry: "slides.md",\n out: "dist",\n base: SITE_BASE,\n exportable: ${exportPortal ? "true" : "false"},\n },`,
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
for (const week of activeWeeks) {
|
|
177
|
+
const slug = `${courseShort}_semana${week.number}`;
|
|
178
|
+
entries.push(` {\n name: "${slug}",\n entry: "${slug}.md",\n out: "dist/semanas/${slug}",\n base: withBase("semanas/${slug}/"),\n exportable: true,\n },`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
entries.push(`];\n`);
|
|
182
|
+
return entries.join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildScripts() {
|
|
186
|
+
const scripts = {
|
|
187
|
+
dev: "slidev slides.md --open --port 3000",
|
|
188
|
+
config: "node scripts/generar-desde-config.mjs",
|
|
189
|
+
semana: "node scripts/semana.mjs",
|
|
190
|
+
"pages:check": "node scripts/preparar-github-pages.mjs",
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
for (let i = 1; i <= weeksTotal; i++) {
|
|
194
|
+
const slug = `${courseShort}_semana${i}`;
|
|
195
|
+
scripts[`dev:s${i}`] = `slidev ${slug}.md --open --port ${3000 + i}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
scripts.clean = "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"";
|
|
199
|
+
scripts["clean:downloads"] = "node -e \"require('fs').rmSync('public/descargas',{recursive:true,force:true})\"";
|
|
200
|
+
scripts["clean:cache"] = "node -e \"require('fs').rmSync('.slidev',{recursive:true,force:true}); require('fs').rmSync('node_modules/.vite',{recursive:true,force:true})\"";
|
|
201
|
+
scripts["build:index"] = "slidev build slides.md --out dist --base / --without-notes";
|
|
202
|
+
|
|
203
|
+
for (let i = 1; i <= weeksTotal; i++) {
|
|
204
|
+
const slug = `${courseShort}_semana${i}`;
|
|
205
|
+
scripts[`build:s${i}`] = `slidev build ${slug}.md --out dist/semanas/${slug} --base /semanas/${slug}/ --without-notes`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
scripts["build:all"] = "node scripts/build-site.mjs";
|
|
209
|
+
scripts["build:incremental"] = "node scripts/build-incremental.mjs";
|
|
210
|
+
scripts["export:downloads"] = "node scripts/export-downloads.mjs";
|
|
211
|
+
scripts["export:incremental"] = "node scripts/export-incremental.mjs";
|
|
212
|
+
scripts.vista = "npm run clean && npm run export:downloads && npm run build:all && http-server dist -p 4173 -c-1 -o";
|
|
213
|
+
scripts.publicar = "node scripts/publicar.mjs";
|
|
214
|
+
scripts["dev:todo"] = "node scripts/dev-all.mjs";
|
|
215
|
+
scripts.nuevo = "node scripts/nuevo-curso.mjs";
|
|
216
|
+
scripts["zip:template"] = "node scripts/zip-template.mjs";
|
|
217
|
+
scripts["preview:static"] = "npm run build:all && http-server dist -p 4173 -c-1";
|
|
218
|
+
scripts["preview:pages"] = "npm run export:downloads && npm run build:all && http-server dist -p 4173 -c-1";
|
|
219
|
+
scripts["pages:build"] = "npm run export:downloads && npm run build:all";
|
|
220
|
+
scripts["pages:preview"] = "npm run preview:pages";
|
|
221
|
+
|
|
222
|
+
return scripts;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log("\n┌──────────────────────────────────────────────┐");
|
|
226
|
+
console.log("│ Generador Open Class UNIMINUTO desde config │");
|
|
227
|
+
console.log("└──────────────────────────────────────────────┘\n");
|
|
228
|
+
console.log(`Configuración : ${configPath}`);
|
|
229
|
+
console.log(`Curso : ${courseName}`);
|
|
230
|
+
console.log(`Nombre corto : ${courseShort}`);
|
|
231
|
+
console.log(`Semanas : ${weeksTotal}`);
|
|
232
|
+
console.log(`Activas : ${activeWeeks.map((w) => w.number).join(", ") || "ninguna"}\n`);
|
|
233
|
+
|
|
234
|
+
ensureDir("semanas");
|
|
235
|
+
ensureDir("public/descargas");
|
|
236
|
+
ensureDir("public/imagenes");
|
|
237
|
+
ensureDir("public/videos");
|
|
238
|
+
|
|
239
|
+
writeFileSafe("slides.md", buildPortal(), { overwrite: overwritePortal, label: "slides.md" });
|
|
240
|
+
writeFileSafe("scripts/decks.mjs", buildDecks(), { overwrite: overwriteDecks, label: "scripts/decks.mjs" });
|
|
241
|
+
|
|
242
|
+
for (const week of allWeeks) {
|
|
243
|
+
const slug = `${courseShort}_semana${week.number}`;
|
|
244
|
+
const tokens = weekTokens(week);
|
|
245
|
+
writeFileSafe(`${slug}.md`, replaceTokens(launcherTemplate, tokens), {
|
|
246
|
+
overwrite: overwriteLaunchers,
|
|
247
|
+
label: `${slug}.md`,
|
|
248
|
+
});
|
|
249
|
+
writeFileSafe(path.join("semanas", `${slug}.md`), replaceTokens(semanaTemplate, tokens), {
|
|
250
|
+
overwrite: overwriteWeekContent,
|
|
251
|
+
label: path.join("semanas", `${slug}.md`),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (overwritePackageScripts && fs.existsSync("package.json")) {
|
|
256
|
+
const pkg = JSON.parse(fs.readFileSync("package.json", "utf-8"));
|
|
257
|
+
pkg.name = `openclass-${courseShort}`;
|
|
258
|
+
pkg.description = `Presentaciones Open Class del curso ${courseName}.`;
|
|
259
|
+
pkg.scripts = buildScripts();
|
|
260
|
+
writeFileSafe("package.json", JSON.stringify(pkg, null, 2) + "\n", { overwrite: true, label: "package.json" });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (fs.existsSync("package-lock.json")) {
|
|
264
|
+
try {
|
|
265
|
+
const lock = JSON.parse(fs.readFileSync("package-lock.json", "utf-8"));
|
|
266
|
+
lock.name = `openclass-${courseShort}`;
|
|
267
|
+
if (lock.packages && lock.packages[""]) {
|
|
268
|
+
lock.packages[""].name = `openclass-${courseShort}`;
|
|
269
|
+
}
|
|
270
|
+
writeFileSafe("package-lock.json", JSON.stringify(lock, null, 2) + "\n", { overwrite: true, label: "package-lock.json" });
|
|
271
|
+
} catch {
|
|
272
|
+
console.log("⚠️ package-lock.json no se actualizó porque no parece ser JSON válido.");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log("\n✅ Configuración generada.");
|
|
277
|
+
console.log(" Próximo paso sugerido: npm install");
|
|
278
|
+
console.log(" Vista de una semana: npm run dev:s1");
|
|
279
|
+
console.log(" Vista completa: npm run vista\n");
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asistente interactivo para crear openclass.config.json y generar el curso.
|
|
3
|
+
*
|
|
4
|
+
* Uso:
|
|
5
|
+
* npm run nuevo
|
|
6
|
+
*/
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
|
|
11
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
|
|
13
|
+
function ask(question, fallback = "") {
|
|
14
|
+
const hint = fallback ? ` [${fallback}]` : "";
|
|
15
|
+
return rl.question(` ${question}${hint}: `);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cleanShortName(value) {
|
|
19
|
+
return String(value || "")
|
|
20
|
+
.normalize("NFD")
|
|
21
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, "")
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log("\n┌──────────────────────────────────────────┐");
|
|
28
|
+
console.log("│ Nuevo curso Open Class · UNIMINUTO │");
|
|
29
|
+
console.log("└──────────────────────────────────────────┘\n");
|
|
30
|
+
|
|
31
|
+
const shortRaw = await ask("Nombre corto del curso. Ejemplo: iot, bigdata, gestionseguridad");
|
|
32
|
+
const shortName = cleanShortName(shortRaw);
|
|
33
|
+
const fullName = (await ask("Nombre completo del curso")).trim();
|
|
34
|
+
const year = (await ask("Año", String(new Date().getFullYear()))).trim() || String(new Date().getFullYear());
|
|
35
|
+
const weeksTotal = 8;
|
|
36
|
+
console.log(" Número de semanas [8]: 8");
|
|
37
|
+
const description = (await ask("Descripción breve del curso", "Agrega aquí la descripción general del curso")).trim();
|
|
38
|
+
|
|
39
|
+
if (!shortName || !fullName) {
|
|
40
|
+
console.error("\n❌ El nombre corto y el nombre completo son obligatorios.\n");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const activeRaw = (await ask("Semanas activas iniciales separadas por coma", "1")).trim() || "1";
|
|
45
|
+
const activeWeeks = activeRaw
|
|
46
|
+
.split(",")
|
|
47
|
+
.map((value) => Number(value.trim()))
|
|
48
|
+
.filter((value) => Number.isInteger(value) && value >= 1 && value <= weeksTotal);
|
|
49
|
+
|
|
50
|
+
const configureWeeks = (await ask("¿Configurar título y fecha de cada semana ahora? S/N", "S")).trim().toLowerCase();
|
|
51
|
+
const weeks = [];
|
|
52
|
+
|
|
53
|
+
for (let i = 1; i <= weeksTotal; i++) {
|
|
54
|
+
if (configureWeeks === "n" || configureWeeks === "no") {
|
|
55
|
+
weeks.push({
|
|
56
|
+
number: i,
|
|
57
|
+
title: `Título semana ${i}`,
|
|
58
|
+
date: year,
|
|
59
|
+
centralTheme: "Tema central de la semana",
|
|
60
|
+
activity: "Actividad o evaluación relacionada",
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\n Semana ${i}`);
|
|
66
|
+
const title = (await ask("Título", `Título semana ${i}`)).trim() || `Título semana ${i}`;
|
|
67
|
+
const date = (await ask("Fecha", year)).trim() || year;
|
|
68
|
+
const centralTheme = (await ask("Tema central", title)).trim() || title;
|
|
69
|
+
const activity = (await ask("Actividad/evaluación relacionada", "Actividad o evaluación relacionada")).trim() || "Actividad o evaluación relacionada";
|
|
70
|
+
weeks.push({ number: i, title, date, centralTheme, activity });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
rl.close();
|
|
74
|
+
|
|
75
|
+
const config = {
|
|
76
|
+
course: {
|
|
77
|
+
shortName,
|
|
78
|
+
fullName,
|
|
79
|
+
year,
|
|
80
|
+
description,
|
|
81
|
+
openClassLabel: "Open Class",
|
|
82
|
+
},
|
|
83
|
+
generation: {
|
|
84
|
+
weeksTotal: 8,
|
|
85
|
+
activeWeeks: activeWeeks.length ? activeWeeks : [1],
|
|
86
|
+
exportPortal: false,
|
|
87
|
+
overwriteLaunchers: true,
|
|
88
|
+
overwritePortal: true,
|
|
89
|
+
overwriteDecks: true,
|
|
90
|
+
overwritePackageScripts: true,
|
|
91
|
+
overwriteWeekContent: false,
|
|
92
|
+
},
|
|
93
|
+
weeks,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync("openclass.config.json", JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
97
|
+
console.log("\n✓ openclass.config.json creado");
|
|
98
|
+
|
|
99
|
+
const result = spawnSync(process.execPath, ["scripts/generar-desde-config.mjs", "--force"], {
|
|
100
|
+
stdio: "inherit",
|
|
101
|
+
shell: false,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
process.exit(result.status ?? 0);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifica la configuración mínima para publicar en GitHub Pages.
|
|
3
|
+
* No crea el repositorio remoto: deja los comandos listos para ejecutar.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
function exists(path) {
|
|
9
|
+
return fs.existsSync(path);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tryRead(command) {
|
|
13
|
+
try {
|
|
14
|
+
return execSync(command, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
15
|
+
} catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log("\n┌──────────────────────────────────────────┐");
|
|
21
|
+
console.log("│ Preparación para GitHub Pages │");
|
|
22
|
+
console.log("└──────────────────────────────────────────┘\n");
|
|
23
|
+
|
|
24
|
+
const workflow = ".github/workflows/deploy.yml";
|
|
25
|
+
const ok = [
|
|
26
|
+
["Workflow de GitHub Pages", exists(workflow)],
|
|
27
|
+
["package.json", exists("package.json")],
|
|
28
|
+
["scripts/decks.mjs", exists("scripts/decks.mjs")],
|
|
29
|
+
["openclass.config.json", exists("openclass.config.json")],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const [label, status] of ok) {
|
|
33
|
+
console.log(`${status ? "✓" : "✗"} ${label}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const remote = tryRead("git remote get-url origin");
|
|
37
|
+
const branch = tryRead("git branch --show-current") || "main";
|
|
38
|
+
console.log(`\nRama actual : ${branch}`);
|
|
39
|
+
console.log(`Remoto origin : ${remote || "no configurado"}`);
|
|
40
|
+
|
|
41
|
+
console.log("\nComandos sugeridos:\n");
|
|
42
|
+
if (!exists(".git")) {
|
|
43
|
+
console.log(" git init");
|
|
44
|
+
console.log(" git branch -M main");
|
|
45
|
+
}
|
|
46
|
+
if (!remote) {
|
|
47
|
+
console.log(" git remote add origin https://github.com/TU_USUARIO/TU_REPOSITORIO.git");
|
|
48
|
+
}
|
|
49
|
+
console.log(" git add -A");
|
|
50
|
+
console.log(' git commit -m "Publicación inicial Open Class"');
|
|
51
|
+
console.log(" git push -u origin main");
|
|
52
|
+
console.log("\nLuego, en GitHub: Settings → Pages → Build and deployment → Source: GitHub Actions.\n");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const fecha = new Date().toLocaleString('es-CO', {
|
|
4
|
+
timeZone: 'America/Bogota',
|
|
5
|
+
year: 'numeric',
|
|
6
|
+
month: '2-digit',
|
|
7
|
+
day: '2-digit',
|
|
8
|
+
hour: '2-digit',
|
|
9
|
+
minute: '2-digit',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const mensaje = `Actualización de diapositivas - ${fecha}`;
|
|
13
|
+
|
|
14
|
+
function run(cmd) {
|
|
15
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log('\n🔍 Verificando cambios...');
|
|
19
|
+
const status = execSync('git status --porcelain').toString().trim();
|
|
20
|
+
|
|
21
|
+
if (!status) {
|
|
22
|
+
console.log('⚠️ No hay cambios para publicar.');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log('\n📦 Preparando archivos...');
|
|
27
|
+
run('git add -A');
|
|
28
|
+
|
|
29
|
+
console.log('\n💾 Confirmando cambios...');
|
|
30
|
+
run(`git commit -m "${mensaje}"`);
|
|
31
|
+
|
|
32
|
+
console.log('\n🚀 Subiendo a GitHub...');
|
|
33
|
+
run('git push');
|
|
34
|
+
|
|
35
|
+
console.log(`\n✅ Publicado: ${mensaje}`);
|
|
36
|
+
console.log(' GitHub Actions desplegará el sitio automáticamente.\n');
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activa o genera una semana específica del curso Open Class.
|
|
3
|
+
*
|
|
4
|
+
* Uso básico:
|
|
5
|
+
* npm run semana -- 1
|
|
6
|
+
* npm run semana -- 3 --title "Sensores y actuadores" --date "Junio 15 de 2026"
|
|
7
|
+
*
|
|
8
|
+
* Este comando:
|
|
9
|
+
* - Mantiene el curso en 8 semanas.
|
|
10
|
+
* - Agrega la semana indicada a generation.activeWeeks.
|
|
11
|
+
* - Actualiza los metadatos de la semana si se pasan flags.
|
|
12
|
+
* - Regenera portal, lanzadores, scripts y decks.
|
|
13
|
+
* - No sobrescribe el contenido ya editado en semanas/*.md, salvo con --force-content.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
|
|
20
|
+
function valueAfter(flag) {
|
|
21
|
+
const i = args.indexOf(flag);
|
|
22
|
+
if (i >= 0 && args[i + 1]) return args[i + 1];
|
|
23
|
+
const pair = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
24
|
+
if (pair) return pair.slice(flag.length + 1);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fail(message) {
|
|
29
|
+
console.error(`\n❌ ${message}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJson(filePath) {
|
|
34
|
+
if (!fs.existsSync(filePath)) fail(`No se encontró ${filePath}. Ejecuta primero npm run nuevo o crea openclass.config.json.`);
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
fail(`No se pudo leer ${filePath}. Revisa que sea JSON válido.\n${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toWeekNumber() {
|
|
43
|
+
const fromFlag = valueAfter("--week") || valueAfter("-w");
|
|
44
|
+
const positional = args.find((arg) => /^\d+$/.test(arg));
|
|
45
|
+
const value = Number(fromFlag || positional);
|
|
46
|
+
if (!Number.isInteger(value) || value < 1 || value > 8) {
|
|
47
|
+
fail("Debes indicar una semana entre 1 y 8. Ejemplo: npm run semana -- 3");
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeWeek(weekNumber, existing = {}) {
|
|
53
|
+
const title = String(existing.title || `Título semana ${weekNumber}`).trim();
|
|
54
|
+
return {
|
|
55
|
+
number: weekNumber,
|
|
56
|
+
title,
|
|
57
|
+
date: String(existing.date || new Date().getFullYear()).trim(),
|
|
58
|
+
centralTheme: String(existing.centralTheme || existing.theme || title || "Tema central de la semana").trim(),
|
|
59
|
+
activity: String(existing.activity || "Actividad o evaluación relacionada").trim(),
|
|
60
|
+
duration: String(existing.duration || "90 minutos").trim(),
|
|
61
|
+
status: String(existing.status || "active").trim(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const weekNumber = toWeekNumber();
|
|
66
|
+
const configPath = valueAfter("--config") || "openclass.config.json";
|
|
67
|
+
const forceContent = args.includes("--force-content");
|
|
68
|
+
const removeOtherWeeks = args.includes("--only");
|
|
69
|
+
|
|
70
|
+
const title = valueAfter("--title");
|
|
71
|
+
const date = valueAfter("--date");
|
|
72
|
+
const centralTheme = valueAfter("--theme") || valueAfter("--central-theme");
|
|
73
|
+
const activity = valueAfter("--activity");
|
|
74
|
+
const duration = valueAfter("--duration");
|
|
75
|
+
|
|
76
|
+
const config = readJson(configPath);
|
|
77
|
+
config.course ||= {};
|
|
78
|
+
config.generation ||= {};
|
|
79
|
+
config.weeks = Array.isArray(config.weeks) ? config.weeks : [];
|
|
80
|
+
|
|
81
|
+
config.generation.weeksTotal = 8;
|
|
82
|
+
config.generation.overwriteLaunchers = true;
|
|
83
|
+
config.generation.overwritePortal = true;
|
|
84
|
+
config.generation.overwriteDecks = true;
|
|
85
|
+
config.generation.overwritePackageScripts = true;
|
|
86
|
+
config.generation.overwriteWeekContent = forceContent;
|
|
87
|
+
|
|
88
|
+
const byNumber = new Map(config.weeks.map((week) => [Number(week.number), week]));
|
|
89
|
+
for (let i = 1; i <= 8; i++) {
|
|
90
|
+
byNumber.set(i, normalizeWeek(i, byNumber.get(i)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const target = byNumber.get(weekNumber);
|
|
94
|
+
if (title) target.title = title;
|
|
95
|
+
if (date) target.date = date;
|
|
96
|
+
if (centralTheme) target.centralTheme = centralTheme;
|
|
97
|
+
if (activity) target.activity = activity;
|
|
98
|
+
if (duration) target.duration = duration;
|
|
99
|
+
target.status = "active";
|
|
100
|
+
|
|
101
|
+
const previousActive = Array.isArray(config.generation.activeWeeks) ? config.generation.activeWeeks.map(Number) : [];
|
|
102
|
+
const nextActive = removeOtherWeeks ? [weekNumber] : [...new Set([...previousActive, weekNumber])];
|
|
103
|
+
config.generation.activeWeeks = nextActive.filter((n) => Number.isInteger(n) && n >= 1 && n <= 8).sort((a, b) => a - b);
|
|
104
|
+
config.weeks = Array.from(byNumber.values()).sort((a, b) => a.number - b.number);
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
107
|
+
|
|
108
|
+
console.log("\n┌──────────────────────────────────────────┐");
|
|
109
|
+
console.log("│ Generar / activar semana Open Class │");
|
|
110
|
+
console.log("└──────────────────────────────────────────┘\n");
|
|
111
|
+
console.log(`Configuración : ${configPath}`);
|
|
112
|
+
console.log(`Semana : ${weekNumber}`);
|
|
113
|
+
console.log(`Activas : ${config.generation.activeWeeks.join(", ")}`);
|
|
114
|
+
console.log(`Contenido : ${forceContent ? "se sobrescribirá" : "se conserva si ya existe"}\n`);
|
|
115
|
+
|
|
116
|
+
const generatorArgs = ["scripts/generar-desde-config.mjs", "--config", configPath];
|
|
117
|
+
const result = spawnSync(process.execPath, generatorArgs, {
|
|
118
|
+
stdio: "inherit",
|
|
119
|
+
shell: false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
process.exit(result.status ?? 0);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genera un ZIP limpio del proyecto actual.
|
|
3
|
+
*
|
|
4
|
+
* Uso:
|
|
5
|
+
* npm run zip:template
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
const OUT = "openclass-template.zip";
|
|
12
|
+
const ROOT = process.cwd();
|
|
13
|
+
const TEMP = path.join(ROOT, "__zip_openclass_template__");
|
|
14
|
+
|
|
15
|
+
const exclude = new Set([
|
|
16
|
+
".git",
|
|
17
|
+
".slidev",
|
|
18
|
+
"node_modules",
|
|
19
|
+
"dist",
|
|
20
|
+
"__zip_openclass_template__",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const excludeFiles = new Set([OUT]);
|
|
24
|
+
|
|
25
|
+
function copyFiltered(src, dest) {
|
|
26
|
+
const name = path.basename(src);
|
|
27
|
+
if (exclude.has(name) || excludeFiles.has(name)) return;
|
|
28
|
+
|
|
29
|
+
const stat = fs.statSync(src);
|
|
30
|
+
if (stat.isDirectory()) {
|
|
31
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
32
|
+
for (const child of fs.readdirSync(src)) {
|
|
33
|
+
copyFiltered(path.join(src, child), path.join(dest, child));
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
39
|
+
fs.copyFileSync(src, dest);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fs.rmSync(TEMP, { recursive: true, force: true });
|
|
43
|
+
fs.rmSync(OUT, { force: true });
|
|
44
|
+
fs.mkdirSync(TEMP, { recursive: true });
|
|
45
|
+
|
|
46
|
+
const folderName = path.basename(ROOT);
|
|
47
|
+
const packageRoot = path.join(TEMP, folderName);
|
|
48
|
+
fs.mkdirSync(packageRoot, { recursive: true });
|
|
49
|
+
|
|
50
|
+
for (const item of fs.readdirSync(ROOT)) {
|
|
51
|
+
copyFiltered(path.join(ROOT, item), path.join(packageRoot, item));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let result;
|
|
55
|
+
if (process.platform === "win32") {
|
|
56
|
+
const ps = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory('${TEMP.replaceAll("'", "''")}', '${path.resolve(OUT).replaceAll("'", "''")}')`;
|
|
57
|
+
result = spawnSync("powershell", ["-NoProfile", "-Command", ps], { stdio: "inherit" });
|
|
58
|
+
} else {
|
|
59
|
+
result = spawnSync("zip", ["-r", path.resolve(OUT), "."], { cwd: TEMP, stdio: "inherit" });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fs.rmSync(TEMP, { recursive: true, force: true });
|
|
63
|
+
|
|
64
|
+
if (result.status !== 0) {
|
|
65
|
+
console.error("\n❌ No se pudo generar el ZIP. En Linux/macOS verifica que el comando zip esté instalado.\n");
|
|
66
|
+
process.exit(result.status ?? 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sizeKb = Math.round(fs.statSync(OUT).size / 1024);
|
|
70
|
+
console.log(`\n✅ ${OUT} generado (${sizeKb} KB).\n`);
|