@learnpack/learnpack 5.0.53 → 5.0.57

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 (60) hide show
  1. package/README.md +30 -12
  2. package/lib/commands/publish.js +29 -7
  3. package/lib/commands/serve.d.ts +7 -0
  4. package/lib/commands/serve.js +149 -0
  5. package/lib/managers/server/routes.js +2 -0
  6. package/lib/utils/api.d.ts +4 -0
  7. package/lib/utils/api.js +21 -4
  8. package/lib/utils/cloudStorage.d.ts +8 -0
  9. package/lib/utils/cloudStorage.js +17 -0
  10. package/oclif.manifest.json +1 -1
  11. package/package.json +3 -1
  12. package/src/commands/publish.ts +68 -12
  13. package/src/commands/serve.ts +192 -0
  14. package/src/creator/README.md +54 -0
  15. package/src/creator/eslint.config.js +28 -0
  16. package/src/creator/index.html +13 -0
  17. package/src/creator/package-lock.json +4659 -0
  18. package/src/creator/package.json +41 -0
  19. package/src/creator/public/vite.svg +1 -0
  20. package/src/creator/src/App.css +42 -0
  21. package/src/creator/src/App.tsx +221 -0
  22. package/src/creator/src/assets/react.svg +1 -0
  23. package/src/creator/src/assets/svgs.tsx +88 -0
  24. package/src/creator/src/components/Loader.tsx +28 -0
  25. package/src/creator/src/components/Login.tsx +263 -0
  26. package/src/creator/src/components/SelectableCard.tsx +30 -0
  27. package/src/creator/src/components/StepWizard.tsx +77 -0
  28. package/src/creator/src/components/SyllabusEditor.tsx +431 -0
  29. package/src/creator/src/index.css +68 -0
  30. package/src/creator/src/main.tsx +19 -0
  31. package/src/creator/src/utils/configTypes.ts +122 -0
  32. package/src/creator/src/utils/constants.ts +2 -0
  33. package/src/creator/src/utils/lib.ts +36 -0
  34. package/src/creator/src/utils/rigo.ts +391 -0
  35. package/src/creator/src/utils/store.ts +78 -0
  36. package/src/creator/src/vite-env.d.ts +1 -0
  37. package/src/creator/tsconfig.app.json +26 -0
  38. package/src/creator/tsconfig.json +7 -0
  39. package/src/creator/tsconfig.node.json +24 -0
  40. package/src/creator/vite.config.ts +13 -0
  41. package/src/creatorDist/assets/index-D92OoEoU.js +23719 -0
  42. package/src/creatorDist/assets/index-tt9JBVY0.css +987 -0
  43. package/src/creatorDist/index.html +14 -0
  44. package/src/creatorDist/vite.svg +1 -0
  45. package/src/managers/server/routes.ts +3 -0
  46. package/src/ui/_app/app.css +1 -0
  47. package/src/ui/_app/app.js +3025 -0
  48. package/src/ui/_app/favicon.ico +0 -0
  49. package/src/ui/_app/index.html +109 -0
  50. package/src/ui/_app/index.html.backup +91 -0
  51. package/src/ui/_app/learnpack.svg +7 -0
  52. package/src/ui/_app/logo-192.png +0 -0
  53. package/src/ui/_app/logo-512.png +0 -0
  54. package/src/ui/_app/logo.png +0 -0
  55. package/src/ui/_app/manifest.webmanifest +21 -0
  56. package/src/ui/_app/sw.js +30 -0
  57. package/src/ui/app.tar.gz +0 -0
  58. package/src/utils/api.ts +24 -4
  59. package/src/utils/cloudStorage.ts +24 -0
  60. package/src/utils/creds.json +13 -0
@@ -26,13 +26,15 @@ const handleAssetCreation = async (
26
26
  sessionPayload: any,
27
27
  academy: TAcademy,
28
28
  learnJson: any,
29
- learnpackDeployUrl: string
29
+ learnpackDeployUrl: string,
30
+ category: number
30
31
  ) => {
31
32
  try {
32
33
  const { exists, academyId } = await api.doesAssetExists(
33
34
  sessionPayload.token,
34
35
  learnJson.slug
35
36
  )
37
+
36
38
  if (!exists) {
37
39
  Console.info("Asset does not exist in this academy, creating it")
38
40
  const asset = await api.createAsset(sessionPayload.token, academy.id, {
@@ -43,6 +45,9 @@ const handleAssetCreation = async (
43
45
  learnpack_deploy_url: learnpackDeployUrl,
44
46
  technologies: ["node", "bash"],
45
47
  url: "https://4geeksacademy.com",
48
+ category: category,
49
+ owner: sessionPayload.id,
50
+ author: sessionPayload.id,
46
51
  })
47
52
  Console.info("Asset created with id", asset.id)
48
53
  } else {
@@ -90,13 +95,38 @@ const runAudit = (strict: boolean) => {
90
95
  )
91
96
  }
92
97
 
93
- const selectAcademy = async (academies: TAcademy[]) => {
98
+ type Academy = {
99
+ id: number
100
+ name: string
101
+ slug?: string
102
+ timezone?: string
103
+ }
104
+
105
+ type Category = {
106
+ id: number
107
+ slug: string
108
+ title: string
109
+ lang: string
110
+ academy: Academy
111
+ }
112
+
113
+ function getCategoriesByAcademy(
114
+ categories: Category[],
115
+ academy: Academy
116
+ ): Category[] {
117
+ return categories.filter((cat) => cat.academy.id === academy.id)
118
+ }
119
+
120
+ const selectAcademy = async (
121
+ academies: TAcademy[],
122
+ bcToken: string
123
+ ): Promise<{ academy: TAcademy | null; category: number }> => {
94
124
  if (academies.length === 0) {
95
- return null
125
+ return { academy: null, category: 0 }
96
126
  }
97
127
 
98
128
  if (academies.length === 1) {
99
- return academies[0]
129
+ return { academy: academies[0], category: 0 }
100
130
  }
101
131
 
102
132
  // prompts the user to select an academy to upload the assets
@@ -110,7 +140,27 @@ const selectAcademy = async (academies: TAcademy[]) => {
110
140
  value: academy,
111
141
  })),
112
142
  })
113
- return response.academy
143
+
144
+ const categories: Category[] = await api.getCategories(bcToken)
145
+ const categoriesByAcademy = getCategoriesByAcademy(
146
+ categories,
147
+ response.academy
148
+ )
149
+
150
+ const categoriesPrompt = await prompts({
151
+ type: "select",
152
+ name: "category",
153
+ message: "Select a category",
154
+ choices: categoriesByAcademy.map((category) => ({
155
+ title: category.title,
156
+ value: category.id,
157
+ })),
158
+ })
159
+
160
+ return {
161
+ academy: response.academy,
162
+ category: categoriesPrompt.category,
163
+ }
114
164
  }
115
165
 
116
166
  class BuildCommand extends SessionCommand {
@@ -193,7 +243,10 @@ class BuildCommand extends SessionCommand {
193
243
 
194
244
  const academies = await api.listUserAcademies(sessionPayload.token)
195
245
  // // console.log(academies, "academies")
196
- const academy = await selectAcademy(academies)
246
+ const { academy, category } = await selectAcademy(
247
+ academies,
248
+ sessionPayload.token
249
+ )
197
250
 
198
251
  // Read learn.json to get the slug
199
252
  const learnJsonPath = path.join(process.cwd(), "learn.json")
@@ -353,12 +406,15 @@ class BuildCommand extends SessionCommand {
353
406
  fs.unlinkSync(zipFilePath)
354
407
  this.removeDirectory(buildDir)
355
408
 
356
- await handleAssetCreation(
357
- sessionPayload,
358
- academy,
359
- learnJson,
360
- res.data.url
361
- )
409
+ if (academy) {
410
+ await handleAssetCreation(
411
+ sessionPayload,
412
+ academy,
413
+ learnJson,
414
+ res.data.url,
415
+ category
416
+ )
417
+ }
362
418
  } catch (error) {
363
419
  if (axios.isAxiosError(error)) {
364
420
  if (error.response && error.response.status === 403) {
@@ -0,0 +1,192 @@
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
+ const frontMatter = require("front-matter")
10
+
11
+ function getSlugFromPath(path: string) {
12
+ const parts = path.split("/").filter(Boolean)
13
+ if (parts.length < 2)
14
+ return null
15
+ return parts[parts.length - 2]
16
+ }
17
+
18
+ const bucketStorage = new Storage({
19
+ keyFilename: path.resolve(__dirname, "../utils/creds.json"),
20
+ })
21
+ const bucket = bucketStorage.bucket("learnpack")
22
+
23
+ async function listFilesWithPrefix(prefix: string) {
24
+ const [files] = await bucket.getFiles({ prefix })
25
+ return files
26
+ }
27
+
28
+ export default class ServeCommand extends SessionCommand {
29
+ static description = "Runs a small server to build tutorials"
30
+
31
+ static flags = {
32
+ ...SessionCommand.flags,
33
+ port: flags.string({ char: "p", description: "server port" }),
34
+ host: flags.string({ char: "h", description: "server host" }),
35
+ debug: flags.boolean({
36
+ char: "d",
37
+ description: "debugger mode for more verbage",
38
+ default: false,
39
+ }),
40
+ }
41
+
42
+ async init() {
43
+ const { flags } = this.parse(ServeCommand)
44
+ console.log("Initializing serve command")
45
+ }
46
+
47
+ async run() {
48
+ const app = express()
49
+ const PORT = 3000
50
+
51
+ const distPath = path.resolve(__dirname, "../creatorDist")
52
+
53
+ // Servir archivos estáticos
54
+ // app.use(express.static(distPath))
55
+ app.use(express.json())
56
+ app.use(cors())
57
+
58
+ const appPath = path.resolve(__dirname, "../ui/_app")
59
+ const tarPath = path.resolve(__dirname, "../ui/app.tar.gz")
60
+ if (fs.existsSync(appPath)) {
61
+ fs.rmSync(appPath, { recursive: true })
62
+ }
63
+
64
+ if (fs.existsSync(tarPath)) {
65
+ fs.rmSync(tarPath)
66
+ }
67
+
68
+ await downloadEditor("5.0.0", `${__dirname}/../ui/app.tar.gz`)
69
+
70
+ await decompress(
71
+ `${__dirname}/../ui/app.tar.gz`,
72
+ `${__dirname}/../ui/_app/`
73
+ )
74
+
75
+ const localAppPath = path.resolve(__dirname, "../ui/_app")
76
+ // app.use(express.static(localAppPath))
77
+
78
+ app.post("/upload", async (req, res) => {
79
+ const { content, destination } = req.body
80
+ // console.log("UPLOAD", content, destination)
81
+
82
+ if (!content || !destination) {
83
+ return res.status(400).send("Missing content or destination")
84
+ }
85
+
86
+ if (!bucket) {
87
+ return res.status(500).send("Upload failed")
88
+ }
89
+
90
+ const buffer = Buffer.from(content, "utf-8")
91
+ const file = bucket.file(destination)
92
+
93
+ const stream = file.createWriteStream({
94
+ resumable: false,
95
+ contentType: "text/plain",
96
+ })
97
+
98
+ stream.on("error", err => {
99
+ console.error("❌ Error uploading:", err)
100
+ res.status(500).send("Upload failed")
101
+ })
102
+
103
+ stream.on("finish", () => {
104
+ console.log(`✅ Uploaded to: ${file.name}`)
105
+ res.send("File uploaded successfully")
106
+ })
107
+
108
+ stream.end(buffer)
109
+ })
110
+
111
+ app.get("/", async (req, res) => {
112
+ // The the ui/_app/index.html
113
+ console.log("GET /")
114
+
115
+ const file = path.resolve(__dirname, "../ui/_app/index.html")
116
+ res.sendFile(file)
117
+ })
118
+
119
+ app.get("/config", async (req, res) => {
120
+ console.log("GET /config")
121
+ console.log(req.query.slug, "QUERY")
122
+ const files = await listFilesWithPrefix("courses/" + req.query.slug)
123
+
124
+ const learnJson = files.find(file => file.name.endsWith("learn.json"))
125
+
126
+ const learnJsonContent = await learnJson?.download()
127
+ const learnJsonParsed = JSON.parse(learnJsonContent?.toString() || "{}")
128
+ const allExercises = files.filter(file =>
129
+ file.name.includes("exercises/")
130
+ )
131
+ const exercises = allExercises.map(exercise => {
132
+ const slug = getSlugFromPath(exercise.name)
133
+ return {
134
+ title: slug,
135
+ slug,
136
+ graded: false,
137
+ }
138
+ })
139
+ // console.log(learnJsonParsed, "LEARN JSON PARSED")
140
+
141
+ res.send({
142
+ config: { ...learnJsonParsed },
143
+ exercises,
144
+ })
145
+ })
146
+
147
+ app.get("/exercise/:slug/readme", async (req, res) => {
148
+ console.log("GET /exercise/:slug/readme")
149
+ const { slug } = req.params
150
+ const query = req.query
151
+ console.log(query, "QUERY")
152
+ const courseSlug = query.slug
153
+
154
+ const file = await bucket.file(
155
+ "courses/" + courseSlug + "/exercises/" + slug + "/README.md"
156
+ )
157
+ const content = await file.download()
158
+ const { attributes, body } = frontMatter(content[0].toString())
159
+ res.send({
160
+ attributes,
161
+ body,
162
+ })
163
+ })
164
+
165
+ app.get("/assets/:file", (req, res) => {
166
+ const file = path.join(localAppPath, req.params.file)
167
+ res.sendFile(file)
168
+ })
169
+ // Enviar index.html para todas las rutas
170
+ app.get("/creator", (req, res) => {
171
+ res.sendFile(path.join(distPath, "index.html"))
172
+ })
173
+ app.get("/creator/syllabus", (req, res) => {
174
+ res.sendFile(path.join(distPath, "index.html"))
175
+ })
176
+
177
+ app.get("/creator/:file", (req, res) => {
178
+ console.log("GET /creator/:file", req.params.file)
179
+ const file = path.join(distPath, req.params.file)
180
+ res.sendFile(file)
181
+ })
182
+
183
+ app.get("/creator/assets/:file", (req, res) => {
184
+ const file = path.join(distPath, "assets", req.params.file)
185
+ res.sendFile(file)
186
+ })
187
+
188
+ app.listen(PORT, () => {
189
+ console.log(`🚀 Creator UI server running at http://localhost:${PORT}`)
190
+ })
191
+ }
192
+ }
@@ -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>