@learnpack/learnpack 5.0.54 → 5.0.58

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.
Files changed (57) hide show
  1. package/README.md +30 -12
  2. package/lib/commands/serve.d.ts +7 -0
  3. package/lib/commands/serve.js +277 -0
  4. package/lib/managers/server/routes.js +2 -0
  5. package/lib/utils/api.js +1 -1
  6. package/lib/utils/cloudStorage.d.ts +8 -0
  7. package/lib/utils/cloudStorage.js +17 -0
  8. package/oclif.manifest.json +1 -1
  9. package/package.json +4 -1
  10. package/src/commands/serve.ts +371 -0
  11. package/src/creator/README.md +54 -0
  12. package/src/creator/eslint.config.js +28 -0
  13. package/src/creator/index.html +13 -0
  14. package/src/creator/package-lock.json +4659 -0
  15. package/src/creator/package.json +41 -0
  16. package/src/creator/public/vite.svg +1 -0
  17. package/src/creator/src/App.css +42 -0
  18. package/src/creator/src/App.tsx +221 -0
  19. package/src/creator/src/assets/react.svg +1 -0
  20. package/src/creator/src/assets/svgs.tsx +88 -0
  21. package/src/creator/src/components/Loader.tsx +28 -0
  22. package/src/creator/src/components/Login.tsx +263 -0
  23. package/src/creator/src/components/SelectableCard.tsx +30 -0
  24. package/src/creator/src/components/StepWizard.tsx +77 -0
  25. package/src/creator/src/components/SyllabusEditor.tsx +431 -0
  26. package/src/creator/src/index.css +68 -0
  27. package/src/creator/src/main.tsx +19 -0
  28. package/src/creator/src/utils/configTypes.ts +122 -0
  29. package/src/creator/src/utils/constants.ts +2 -0
  30. package/src/creator/src/utils/lib.ts +36 -0
  31. package/src/creator/src/utils/rigo.ts +391 -0
  32. package/src/creator/src/utils/store.ts +78 -0
  33. package/src/creator/src/vite-env.d.ts +1 -0
  34. package/src/creator/tsconfig.app.json +26 -0
  35. package/src/creator/tsconfig.json +7 -0
  36. package/src/creator/tsconfig.node.json +24 -0
  37. package/src/creator/vite.config.ts +13 -0
  38. package/src/creatorDist/assets/index-D92OoEoU.js +23719 -0
  39. package/src/creatorDist/assets/index-tt9JBVY0.css +987 -0
  40. package/src/creatorDist/index.html +14 -0
  41. package/src/creatorDist/vite.svg +1 -0
  42. package/src/managers/server/routes.ts +3 -0
  43. package/src/ui/_app/app.css +1 -0
  44. package/src/ui/_app/app.js +3025 -0
  45. package/src/ui/_app/favicon.ico +0 -0
  46. package/src/ui/_app/index.html +109 -0
  47. package/src/ui/_app/index.html.backup +91 -0
  48. package/src/ui/_app/learnpack.svg +7 -0
  49. package/src/ui/_app/logo-192.png +0 -0
  50. package/src/ui/_app/logo-512.png +0 -0
  51. package/src/ui/_app/logo.png +0 -0
  52. package/src/ui/_app/manifest.webmanifest +21 -0
  53. package/src/ui/_app/sw.js +30 -0
  54. package/src/ui/app.tar.gz +0 -0
  55. package/src/utils/api.ts +1 -1
  56. package/src/utils/cloudStorage.ts +24 -0
  57. package/src/utils/convertCreds.js +30 -0
@@ -0,0 +1,371 @@
1
+ import { flags } from "@oclif/command"
2
+ import * as express from "express"
3
+ import * as cors from "cors"
4
+ import * as path from "path"
5
+ import SessionCommand from "../utils/SessionCommand"
6
+ import { Storage } from "@google-cloud/storage"
7
+ import { downloadEditor, decompress } from "../managers/file"
8
+ import * as fs from "fs"
9
+ import { translateExercise } from "../utils/rigoActions"
10
+ import * as dotenv from "dotenv"
11
+ dotenv.config()
12
+
13
+ const frontMatter = require("front-matter")
14
+
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
+ const crendsEnv = process.env.GCP_CREDENTIALS_JSON
26
+ if (!crendsEnv) {
27
+ console.log("GCP_CREDENTIALS_JSON is not set")
28
+ process.exit(1)
29
+ }
30
+
31
+ const credentials = JSON.parse(crendsEnv)
32
+
33
+ const bucketStorage = new Storage({
34
+ credentials,
35
+ })
36
+
37
+ const bucket = bucketStorage.bucket("learnpack")
38
+
39
+ async function listFilesWithPrefix(prefix: string) {
40
+ const [files] = await bucket.getFiles({ prefix })
41
+ return files
42
+ }
43
+
44
+ export default class ServeCommand extends SessionCommand {
45
+ static description = "Runs a small server to build tutorials"
46
+
47
+ static flags = {
48
+ ...SessionCommand.flags,
49
+ port: flags.string({ char: "p", description: "server port" }),
50
+ host: flags.string({ char: "h", description: "server host" }),
51
+ debug: flags.boolean({
52
+ char: "d",
53
+ description: "debugger mode for more verbage",
54
+ default: false,
55
+ }),
56
+ }
57
+
58
+ async init() {
59
+ const { flags } = this.parse(ServeCommand)
60
+ console.log("Initializing serve command")
61
+ }
62
+
63
+ async run() {
64
+ const app = express()
65
+ const PORT = 3000
66
+
67
+ const distPath = path.resolve(__dirname, "../creatorDist")
68
+
69
+ // Servir archivos estáticos
70
+ // app.use(express.static(distPath))
71
+ app.use(express.json())
72
+ app.use(cors())
73
+
74
+ const appPath = path.resolve(__dirname, "../ui/_app")
75
+ const tarPath = path.resolve(__dirname, "../ui/app.tar.gz")
76
+ if (fs.existsSync(appPath)) {
77
+ fs.rmSync(appPath, { recursive: true })
78
+ }
79
+
80
+ if (fs.existsSync(tarPath)) {
81
+ fs.rmSync(tarPath)
82
+ }
83
+
84
+ await downloadEditor("5.0.0", `${__dirname}/../ui/app.tar.gz`)
85
+
86
+ await decompress(
87
+ `${__dirname}/../ui/app.tar.gz`,
88
+ `${__dirname}/../ui/_app/`
89
+ )
90
+
91
+ const localAppPath = path.resolve(__dirname, "../ui/_app")
92
+ // app.use(express.static(localAppPath))
93
+
94
+ app.post("/upload", async (req, res) => {
95
+ const { content, destination } = req.body
96
+ // console.log("UPLOAD", content, destination)
97
+
98
+ if (!content || !destination) {
99
+ return res.status(400).send("Missing content or destination")
100
+ }
101
+
102
+ if (!bucket) {
103
+ return res.status(500).send("Upload failed")
104
+ }
105
+
106
+ const buffer = Buffer.from(content, "utf-8")
107
+ const file = bucket.file(destination)
108
+
109
+ const stream = file.createWriteStream({
110
+ resumable: false,
111
+ contentType: "text/plain",
112
+ })
113
+
114
+ stream.on("error", err => {
115
+ console.error("❌ Error uploading:", err)
116
+ res.status(500).send("Upload failed")
117
+ })
118
+
119
+ stream.on("finish", () => {
120
+ console.log(`✅ Uploaded to: ${file.name}`)
121
+ res.send("File uploaded successfully")
122
+ })
123
+
124
+ stream.end(buffer)
125
+ })
126
+
127
+ app.get("/", async (req, res) => {
128
+ // The the ui/_app/index.html
129
+ console.log("GET /")
130
+
131
+ const file = path.resolve(__dirname, "../ui/_app/index.html")
132
+ res.sendFile(file)
133
+ })
134
+
135
+ app.get("/config", async (req, res) => {
136
+ const courseSlug = req.query.slug
137
+ const files = await listFilesWithPrefix(`courses/${courseSlug}`)
138
+
139
+ const learnJson = files.find(file => file.name.endsWith("learn.json"))
140
+ const learnJsonContent = await learnJson?.download()
141
+ const learnJsonParsed = JSON.parse(learnJsonContent?.toString() || "{}")
142
+
143
+ const exerciseMap: ExerciseMap = {}
144
+
145
+ // Agrupar archivos por ejercicio
146
+ for (const file of files) {
147
+ const pathParts = file.name.split("/")
148
+ const isExercise = pathParts.includes("exercises")
149
+ if (!isExercise)
150
+ continue
151
+
152
+ const slug = pathParts[pathParts.indexOf("exercises") + 1]
153
+ if (!exerciseMap[slug]) {
154
+ exerciseMap[slug] = {
155
+ title: slug,
156
+ slug: slug,
157
+ graded: false,
158
+ files: [],
159
+ translations: {},
160
+ }
161
+ }
162
+
163
+ const fileName = pathParts.at(-1)
164
+
165
+ // Traducciones
166
+ const readmeMatch = fileName?.match(/^readme(?:\.([a-z]{2}))?\.md$/i)
167
+ if (readmeMatch) {
168
+ const lang = readmeMatch[1] || "us"
169
+ exerciseMap[slug].translations[lang] = fileName || ""
170
+ } else {
171
+ exerciseMap[slug].files.push(fileName || "")
172
+ }
173
+ }
174
+
175
+ const exercises = Object.values(exerciseMap).map((ex, index) => ({
176
+ ...ex,
177
+ position: index,
178
+ }))
179
+
180
+ res.set("X-Creator-Web", "true")
181
+ res.set("Access-Control-Expose-Headers", "X-Creator-Web")
182
+
183
+ res.send({
184
+ config: { ...learnJsonParsed, title: { us: courseSlug } },
185
+ exercises,
186
+ })
187
+ })
188
+
189
+ app.get("/exercise/:slug/readme", async (req, res) => {
190
+ console.log("GET /exercise/:slug/readme")
191
+
192
+ const { slug } = req.params
193
+ const courseSlug = req.query.slug
194
+ const lang = req.query.lang || "us"
195
+
196
+ const basePath = `courses/${courseSlug}/exercises/${slug}/`
197
+ const filename = lang === "us" ? "README.md" : `README.${lang}.md`
198
+
199
+ const file = bucket.file(basePath + filename)
200
+
201
+ let contentBuffer
202
+
203
+ try {
204
+ contentBuffer = await file.download()
205
+ } catch {
206
+ if (lang !== "us") {
207
+ console.warn(`No README for lang '${lang}', falling back to 'us'`)
208
+ const fallbackFile = bucket.file(basePath + "README.md")
209
+ contentBuffer = await fallbackFile.download()
210
+ } else {
211
+ return res.status(404).json({ error: "README not found" })
212
+ }
213
+ }
214
+
215
+ const { attributes, body } = frontMatter(contentBuffer[0].toString())
216
+
217
+ res.send({ attributes, body })
218
+ })
219
+
220
+ app.put(
221
+ "/exercise/:slug/file/:fileName",
222
+ express.text(),
223
+ async (req, res) => {
224
+ const { slug, fileName } = req.params
225
+ const query = req.query
226
+ console.log(`PUT /exercise/${slug}/file/${fileName}`)
227
+
228
+ const courseSlug = query.slug
229
+ console.log("COURSE SLUG", courseSlug)
230
+
231
+ // Update the file in the bucket
232
+ const file = await bucket.file(
233
+ "courses/" + courseSlug + "/exercises/" + slug + "/" + fileName
234
+ )
235
+ await file.save(req.body)
236
+ const created = await file.exists()
237
+ console.log("File updated", created)
238
+ res.send({
239
+ message: "File updated",
240
+ created,
241
+ })
242
+ }
243
+ )
244
+ app.post("/exercise/:slug/create", async (req, res) => {
245
+ console.log("POST /exercise/:slug/create")
246
+ const query = req.query
247
+ const { title, readme, language } = req.body
248
+
249
+ if (!title || !readme) {
250
+ return res
251
+ .status(400)
252
+ .json({ error: "Missing title or readme content" })
253
+ }
254
+
255
+ const courseSlug = query.slug
256
+
257
+ const fileName = `courses/${courseSlug}/exercises/${title}/README${
258
+ language === "us" || language === "en" ? "" : `.${language}`
259
+ }.md`
260
+ const file = bucket.file(fileName)
261
+ await file.save(readme)
262
+ const created = await file.exists()
263
+ res.send({
264
+ message: "File updated",
265
+ created,
266
+ })
267
+ })
268
+
269
+ app.put("/actions/rename", async (req, res) => {
270
+ console.log("PUT /actions/rename")
271
+ const { slug, newSlug } = req.body
272
+ const query = req.query
273
+ const courseSlug = query.slug
274
+ const filePrefix = `courses/${courseSlug}/exercises/${slug}/`
275
+ const [files] = await bucket.getFiles({ prefix: filePrefix })
276
+ for (const file of files) {
277
+ const newFileName = file.name.replace(slug, newSlug)
278
+ // eslint-disable-next-line no-await-in-loop
279
+ await file.rename(newFileName)
280
+ }
281
+
282
+ res.send({ message: "Files renamed" })
283
+ })
284
+
285
+ app.post("/actions/translate", express.json(), async (req, res) => {
286
+ console.log("POST /actions/translate")
287
+ const { exerciseSlugs, languages, rigoToken } = req.body
288
+ const query = req.query
289
+ const courseSlug = query.slug
290
+
291
+ console.log("EXERCISE SLUGS", exerciseSlugs)
292
+ console.log("LANGUAGES", languages)
293
+ console.log("RIGO TOKEN", rigoToken)
294
+
295
+ if (!rigoToken) {
296
+ return res.status(400).json({ error: "RigoToken not found" })
297
+ }
298
+
299
+ const languagesToTranslate: string[] = languages.split(",")
300
+
301
+ try {
302
+ await Promise.all(
303
+ exerciseSlugs.map(async (slug: string) => {
304
+ const readmePath = `courses/${courseSlug}/exercises/${slug}/README.md`
305
+ const readme = await bucket.file(readmePath).download()
306
+ await Promise.all(
307
+ languagesToTranslate.map(async (language: string) => {
308
+ const response = await translateExercise(rigoToken, {
309
+ text_to_translate: readme.toString(),
310
+ output_language: language,
311
+ exercise_slug: slug,
312
+ })
313
+
314
+ const translatedReadme = await bucket.file(
315
+ `courses/${courseSlug}/exercises/${slug}/README.${response.parsed.output_language_code}.md`
316
+ )
317
+ await translatedReadme.save(response.parsed.translation)
318
+ })
319
+ )
320
+ })
321
+ )
322
+
323
+ return res.status(200).json({ message: "Translated exercises" })
324
+ } catch (error) {
325
+ console.log(error, "ERROR")
326
+ return res.status(400).json({ error: (error as Error).message })
327
+ }
328
+ })
329
+
330
+ app.delete("/exercise/:slug/delete", async (req, res) => {
331
+ const { slug } = req.params
332
+ const query = req.query
333
+ const courseSlug = query.slug
334
+ const filePrefix = `courses/${courseSlug}/exercises/${slug}/`
335
+ const [files] = await bucket.getFiles({ prefix: filePrefix })
336
+ for (const file of files) {
337
+ // eslint-disable-next-line no-await-in-loop
338
+ await file.delete()
339
+ }
340
+
341
+ res.send({ message: "Files deleted" })
342
+ })
343
+
344
+ app.get("/assets/:file", (req, res) => {
345
+ const file = path.join(localAppPath, req.params.file)
346
+ res.sendFile(file)
347
+ })
348
+ // Enviar index.html para todas las rutas
349
+ app.get("/creator", (req, res) => {
350
+ res.sendFile(path.join(distPath, "index.html"))
351
+ })
352
+ app.get("/creator/syllabus", (req, res) => {
353
+ res.sendFile(path.join(distPath, "index.html"))
354
+ })
355
+
356
+ app.get("/creator/:file", (req, res) => {
357
+ console.log("GET /creator/:file", req.params.file)
358
+ const file = path.join(distPath, req.params.file)
359
+ res.sendFile(file)
360
+ })
361
+
362
+ app.get("/creator/assets/:file", (req, res) => {
363
+ const file = path.join(distPath, "assets", req.params.file)
364
+ res.sendFile(file)
365
+ })
366
+
367
+ app.listen(PORT, () => {
368
+ console.log(`🚀 Creator UI server running at http://localhost:${PORT}`)
369
+ })
370
+ }
371
+ }
@@ -0,0 +1,54 @@
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## Expanding the ESLint configuration
11
+
12
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
13
+
14
+ ```js
15
+ export default tseslint.config({
16
+ extends: [
17
+ // Remove ...tseslint.configs.recommended and replace with this
18
+ ...tseslint.configs.recommendedTypeChecked,
19
+ // Alternatively, use this for stricter rules
20
+ ...tseslint.configs.strictTypeChecked,
21
+ // Optionally, add this for stylistic rules
22
+ ...tseslint.configs.stylisticTypeChecked,
23
+ ],
24
+ languageOptions: {
25
+ // other options...
26
+ parserOptions: {
27
+ project: ["./tsconfig.node.json", "./tsconfig.app.json"],
28
+ tsconfigRootDir: import.meta.dirname,
29
+ },
30
+ },
31
+ })
32
+ ```
33
+
34
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
35
+
36
+ ```js
37
+ // eslint.config.js
38
+ import reactX from "eslint-plugin-react-x"
39
+ import reactDom from "eslint-plugin-react-dom"
40
+
41
+ export default tseslint.config({
42
+ plugins: {
43
+ // Add the react-x and react-dom plugins
44
+ "react-x": reactX,
45
+ "react-dom": reactDom,
46
+ },
47
+ rules: {
48
+ // other rules...
49
+ // Enable its recommended typescript rules
50
+ ...reactX.configs["recommended-typescript"].rules,
51
+ ...reactDom.configs.recommended.rules,
52
+ },
53
+ })
54
+ ```
@@ -0,0 +1,28 @@
1
+ import js from "@eslint/js"
2
+ import globals from "globals"
3
+ import reactHooks from "eslint-plugin-react-hooks"
4
+ import reactRefresh from "eslint-plugin-react-refresh"
5
+ import tseslint from "typescript-eslint"
6
+
7
+ export default tseslint.config(
8
+ { ignores: ["dist"] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ["**/*.{ts,tsx}"],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ "react-hooks": reactHooks,
18
+ "react-refresh": reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ "react-refresh/only-export-components": [
23
+ "warn",
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ }
28
+ )
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + React + TS</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>