@learnpack/learnpack 2.1.39 → 2.1.40

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 (196) hide show
  1. package/README.md +370 -35
  2. package/bin/run +17 -17
  3. package/bin/run.cmd +3 -3
  4. package/lib/commands/audit.d.ts +6 -6
  5. package/lib/commands/audit.js +342 -342
  6. package/lib/commands/clean.d.ts +8 -8
  7. package/lib/commands/clean.js +25 -25
  8. package/lib/commands/download.d.ts +13 -13
  9. package/lib/commands/download.js +55 -55
  10. package/lib/commands/init.d.ts +9 -9
  11. package/lib/commands/init.js +123 -123
  12. package/lib/commands/login.d.ts +14 -14
  13. package/lib/commands/login.js +37 -37
  14. package/lib/commands/logout.d.ts +14 -14
  15. package/lib/commands/logout.js +37 -37
  16. package/lib/commands/publish.d.ts +14 -14
  17. package/lib/commands/publish.js +82 -82
  18. package/lib/commands/start.d.ts +7 -7
  19. package/lib/commands/start.js +239 -208
  20. package/lib/commands/test.d.ts +6 -6
  21. package/lib/commands/test.js +62 -62
  22. package/lib/index.d.ts +1 -1
  23. package/lib/index.js +4 -4
  24. package/lib/managers/config/allowed_files.d.ts +5 -5
  25. package/lib/managers/config/allowed_files.js +30 -30
  26. package/lib/managers/config/defaults.d.ts +41 -41
  27. package/lib/managers/config/defaults.js +44 -44
  28. package/lib/managers/config/exercise.d.ts +36 -36
  29. package/lib/managers/config/exercise.js +236 -236
  30. package/lib/managers/config/index.d.ts +3 -3
  31. package/lib/managers/config/index.js +337 -337
  32. package/lib/managers/file.d.ts +14 -14
  33. package/lib/managers/file.js +153 -153
  34. package/lib/managers/gitpod.d.ts +3 -3
  35. package/lib/managers/gitpod.js +67 -67
  36. package/lib/managers/server/index.d.ts +6 -6
  37. package/lib/managers/server/index.js +58 -58
  38. package/lib/managers/server/routes.d.ts +4 -4
  39. package/lib/managers/server/routes.js +219 -219
  40. package/lib/managers/session.d.ts +3 -3
  41. package/lib/managers/session.js +125 -125
  42. package/lib/managers/socket.d.ts +3 -3
  43. package/lib/managers/socket.js +176 -176
  44. package/lib/managers/telemetry.d.ts +74 -74
  45. package/lib/managers/telemetry.js +206 -206
  46. package/lib/managers/test.js +84 -84
  47. package/lib/models/action.d.ts +2 -2
  48. package/lib/models/action.js +2 -2
  49. package/lib/models/audit.d.ts +15 -15
  50. package/lib/models/audit.js +2 -2
  51. package/lib/models/config-manager.d.ts +21 -21
  52. package/lib/models/config-manager.js +2 -2
  53. package/lib/models/config.d.ts +68 -67
  54. package/lib/models/config.js +2 -2
  55. package/lib/models/counter.d.ts +11 -11
  56. package/lib/models/counter.js +2 -2
  57. package/lib/models/errors.d.ts +15 -15
  58. package/lib/models/errors.js +2 -2
  59. package/lib/models/exercise-obj.d.ts +30 -30
  60. package/lib/models/exercise-obj.js +2 -2
  61. package/lib/models/file.d.ts +5 -5
  62. package/lib/models/file.js +2 -2
  63. package/lib/models/findings.d.ts +17 -17
  64. package/lib/models/findings.js +2 -2
  65. package/lib/models/flags.d.ts +10 -10
  66. package/lib/models/flags.js +2 -2
  67. package/lib/models/front-matter.d.ts +11 -11
  68. package/lib/models/front-matter.js +2 -2
  69. package/lib/models/gitpod-data.d.ts +16 -16
  70. package/lib/models/gitpod-data.js +2 -2
  71. package/lib/models/language.d.ts +4 -4
  72. package/lib/models/language.js +2 -2
  73. package/lib/models/package.d.ts +7 -7
  74. package/lib/models/package.js +2 -2
  75. package/lib/models/plugin-config.d.ts +16 -16
  76. package/lib/models/plugin-config.js +2 -2
  77. package/lib/models/session.d.ts +31 -31
  78. package/lib/models/session.js +2 -2
  79. package/lib/models/socket.d.ts +36 -32
  80. package/lib/models/socket.js +2 -2
  81. package/lib/models/status.d.ts +1 -1
  82. package/lib/models/status.js +2 -2
  83. package/lib/models/success-types.d.ts +1 -1
  84. package/lib/models/success-types.js +2 -2
  85. package/lib/plugin/command/compile.d.ts +6 -6
  86. package/lib/plugin/command/compile.js +18 -18
  87. package/lib/plugin/command/test.d.ts +6 -6
  88. package/lib/plugin/command/test.js +25 -25
  89. package/lib/plugin/index.d.ts +27 -27
  90. package/lib/plugin/index.js +7 -7
  91. package/lib/plugin/plugin.d.ts +8 -8
  92. package/lib/plugin/plugin.js +68 -68
  93. package/lib/plugin/utils.d.ts +16 -16
  94. package/lib/plugin/utils.js +58 -58
  95. package/lib/ui/download.d.ts +5 -5
  96. package/lib/ui/download.js +61 -61
  97. package/lib/utils/BaseCommand.d.ts +8 -8
  98. package/lib/utils/BaseCommand.js +41 -41
  99. package/lib/utils/SessionCommand.d.ts +10 -10
  100. package/lib/utils/SessionCommand.js +43 -43
  101. package/lib/utils/api.d.ts +14 -14
  102. package/lib/utils/api.js +255 -255
  103. package/lib/utils/audit.d.ts +16 -16
  104. package/lib/utils/audit.js +303 -303
  105. package/lib/utils/checkNotInstalled.d.ts +2 -0
  106. package/lib/utils/checkNotInstalled.js +36 -0
  107. package/lib/utils/console.d.ts +12 -12
  108. package/lib/utils/console.js +19 -19
  109. package/lib/utils/errors.d.ts +17 -17
  110. package/lib/utils/errors.js +100 -100
  111. package/lib/utils/exercisesQueue.d.ts +9 -9
  112. package/lib/utils/exercisesQueue.js +38 -38
  113. package/lib/utils/fileQueue.d.ts +40 -40
  114. package/lib/utils/fileQueue.js +168 -168
  115. package/lib/utils/misc.d.ts +1 -1
  116. package/lib/utils/misc.js +23 -23
  117. package/lib/utils/osOperations.d.ts +5 -5
  118. package/lib/utils/osOperations.js +72 -72
  119. package/lib/utils/validators.d.ts +5 -5
  120. package/lib/utils/validators.js +17 -17
  121. package/lib/utils/watcher.d.ts +2 -2
  122. package/lib/utils/watcher.js +25 -25
  123. package/oclif.manifest.json +1 -1
  124. package/package.json +139 -139
  125. package/src/commands/audit.ts +443 -443
  126. package/src/commands/clean.ts +29 -29
  127. package/src/commands/download.ts +61 -61
  128. package/src/commands/init.ts +170 -170
  129. package/src/commands/login.ts +42 -42
  130. package/src/commands/logout.ts +43 -43
  131. package/src/commands/publish.ts +107 -107
  132. package/src/commands/start.ts +53 -23
  133. package/src/commands/test.ts +85 -85
  134. package/src/index.ts +1 -1
  135. package/src/managers/config/allowed_files.ts +29 -29
  136. package/src/managers/config/defaults.ts +42 -42
  137. package/src/managers/config/exercise.ts +311 -311
  138. package/src/managers/config/index.ts +455 -455
  139. package/src/managers/file.ts +196 -196
  140. package/src/managers/gitpod.ts +84 -84
  141. package/src/managers/server/index.ts +78 -78
  142. package/src/managers/server/routes.ts +330 -330
  143. package/src/managers/session.ts +145 -145
  144. package/src/managers/socket.ts +250 -250
  145. package/src/managers/telemetry.ts +346 -346
  146. package/src/managers/test.ts +83 -83
  147. package/src/models/action.ts +10 -10
  148. package/src/models/audit.ts +16 -16
  149. package/src/models/config-manager.ts +23 -23
  150. package/src/models/config.ts +5 -3
  151. package/src/models/counter.ts +11 -11
  152. package/src/models/errors.ts +22 -22
  153. package/src/models/exercise-obj.ts +29 -29
  154. package/src/models/file.ts +5 -5
  155. package/src/models/findings.ts +18 -18
  156. package/src/models/flags.ts +10 -10
  157. package/src/models/front-matter.ts +11 -11
  158. package/src/models/gitpod-data.ts +19 -19
  159. package/src/models/language.ts +4 -4
  160. package/src/models/package.ts +7 -7
  161. package/src/models/plugin-config.ts +17 -17
  162. package/src/models/session.ts +34 -34
  163. package/src/models/socket.ts +5 -0
  164. package/src/models/status.ts +16 -16
  165. package/src/models/success-types.ts +1 -1
  166. package/src/plugin/command/compile.ts +17 -17
  167. package/src/plugin/command/test.ts +30 -30
  168. package/src/plugin/index.ts +6 -6
  169. package/src/plugin/plugin.ts +94 -94
  170. package/src/plugin/utils.ts +87 -87
  171. package/src/types/node-fetch.d.ts +1 -1
  172. package/src/ui/download.ts +71 -71
  173. package/src/utils/BaseCommand.ts +48 -48
  174. package/src/utils/SessionCommand.ts +43 -43
  175. package/src/utils/api.ts +303 -303
  176. package/src/utils/audit.ts +393 -393
  177. package/src/utils/checkNotInstalled.ts +46 -0
  178. package/src/utils/console.ts +24 -24
  179. package/src/utils/errors.ts +117 -117
  180. package/src/utils/exercisesQueue.ts +51 -51
  181. package/src/utils/fileQueue.ts +198 -198
  182. package/src/utils/misc.ts +23 -23
  183. package/src/utils/osOperations.ts +79 -79
  184. package/src/utils/templates/gitignore.txt +19 -19
  185. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +24 -24
  186. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +24 -24
  187. package/src/utils/templates/incremental/README.ejs +4 -4
  188. package/src/utils/templates/incremental/README.es.ejs +4 -4
  189. package/src/utils/templates/isolated/01-hello-world/README.es.md +26 -26
  190. package/src/utils/templates/isolated/01-hello-world/README.md +26 -26
  191. package/src/utils/templates/isolated/README.ejs +4 -4
  192. package/src/utils/templates/isolated/README.es.ejs +4 -4
  193. package/src/utils/templates/no-grading/README.ejs +4 -4
  194. package/src/utils/templates/no-grading/README.es.ejs +4 -4
  195. package/src/utils/validators.ts +18 -18
  196. package/src/utils/watcher.ts +27 -27
@@ -1,455 +1,455 @@
1
- import * as path from "path"
2
- import * as fs from "fs"
3
- import * as shell from "shelljs"
4
- import Console from "../../utils/console"
5
- import watch from "../../utils/watcher"
6
- import {
7
- ValidationError,
8
- NotFoundError,
9
- InternalError,
10
- } from "../../utils/errors"
11
-
12
- import defaults from "./defaults"
13
- import { exercise } from "./exercise"
14
-
15
- import { rmSync } from "../file"
16
- import { IConfigObj, TConfigObjAttributes, TMode } from "../../models/config"
17
- import {
18
- IConfigManagerAttributes,
19
- IConfigManager,
20
- } from "../../models/config-manager"
21
- import { IFile } from "../../models/file"
22
-
23
- /* exercise folder name standard */
24
-
25
- // eslint-disable-next-line
26
- const fetch = require("node-fetch");
27
- // eslint-disable-next-line
28
- const chalk = require("chalk");
29
-
30
- /* exercise folder name standard */
31
-
32
- /**
33
- * Retrieves the configuration path for the learnpack package.
34
- *
35
- * @returns An object containing the configuration file path and the base directory.
36
- * @throws NotFoundError if the learn.json file is not found in the current folder.
37
- */
38
- const getConfigPath = () => {
39
- const possibleFileNames = ["learn.json", ".learn/learn.json"]
40
- const config = possibleFileNames.find(file => fs.existsSync(file)) || null
41
- if (config && fs.existsSync(".breathecode"))
42
- return { config, base: ".breathecode" }
43
- if (config === null)
44
- throw NotFoundError(
45
- "learn.json file not found on current folder, is this a learnpack package?"
46
- )
47
- return { config, base: ".learn" }
48
- }
49
-
50
- const getExercisesPath = (base: string) => {
51
- const possibleFileNames = ["./exercises", base + "/exercises", "./"]
52
- return possibleFileNames.find(file => fs.existsSync(file)) || null
53
- }
54
-
55
- const getGitpodAddress = () => {
56
- if (shell.exec("gp -h", { silent: true }).code === 0) {
57
- return shell
58
- .exec("gp url", { silent: true })
59
- .stdout.replace(/(\r\n|\n|\r)/gm, "")
60
- }
61
-
62
- Console.debug("Gitpod command line tool not found")
63
- return "http://localhost"
64
- }
65
-
66
- const getCodespacesNamespace = () => {
67
- // Example: https://orange-rotary-phone-wxpg49q5gcg4rp-3000.app.github.dev
68
-
69
- const codespace_name = shell
70
- .exec("echo $CODESPACE_NAME", { silent: true })
71
- .stdout.replace(/(\r\n|\n|\r)/gm, "")
72
-
73
- if (
74
- !codespace_name ||
75
- codespace_name === "" ||
76
- codespace_name === undefined ||
77
- // ! I added this line
78
- codespace_name === "$CODESPACE_NAME"
79
- ) {
80
- return null
81
- }
82
-
83
- return codespace_name
84
- }
85
-
86
- export default async ({
87
- grading,
88
- mode,
89
- disableGrading,
90
- version,
91
- }: IConfigManagerAttributes): Promise<IConfigManager> => {
92
- const confPath = getConfigPath()
93
- Console.debug("This is the config path: ", confPath)
94
-
95
- let configObj: IConfigObj = {}
96
-
97
- if (confPath) {
98
- const learnJsonContent = fs.readFileSync(confPath.config)
99
- let hiddenBcContent = {}
100
- if (fs.existsSync(confPath.base + "/config.json")) {
101
- hiddenBcContent = fs.readFileSync(confPath.base + "/config.json")
102
- hiddenBcContent = JSON.parse(hiddenBcContent as string)
103
- if (!hiddenBcContent)
104
- throw new Error(
105
- `Invalid ${confPath.base}/config.json syntax: Unable to parse.`
106
- )
107
- }
108
-
109
- const jsonConfig = JSON.parse(`${learnJsonContent}`)
110
-
111
- if (!jsonConfig)
112
- throw new Error(`Invalid ${confPath.config} syntax: Unable to parse.`)
113
-
114
- let session: number
115
-
116
- // add using id to the installation
117
- if (!jsonConfig.session) {
118
- session = Math.floor(Math.random() * 10_000_000_000_000_000_000)
119
- } else {
120
- session = jsonConfig.session
121
- delete jsonConfig.session
122
- }
123
-
124
- configObj = deepMerge(hiddenBcContent, {
125
- config: jsonConfig,
126
- session: session,
127
- })
128
- } else {
129
- throw ValidationError(
130
- "No learn.json file has been found, make sure you are in the correct directory. To start a new LearnPack package run: $ learnpack init my-course-name"
131
- )
132
- }
133
-
134
- configObj = deepMerge(defaults || {}, configObj, {
135
- config: {
136
- grading: grading || configObj.config?.grading,
137
- configPath: confPath.config,
138
- },
139
- })
140
- if (!configObj.config)
141
- throw InternalError("Config object not found")
142
-
143
- configObj.config.outputPath = confPath.base + "/dist"
144
-
145
- Console.debug("This is your configuration object: ", {
146
- ...configObj,
147
- exercises: configObj.exercises ?
148
- configObj.exercises.map(e => e.slug) :
149
- [],
150
- })
151
-
152
- // auto detect agent (if possible)
153
- const codespaces_workspace = getCodespacesNamespace()
154
- Console.debug("This is the codespace namespace: ", codespaces_workspace)
155
-
156
- Console.debug(
157
- "This is the agent, and this should be null an the beginning: ",
158
- configObj.config?.editor?.agent
159
- )
160
-
161
- if (shell.which("gp") && configObj && configObj.config) {
162
- Console.debug("Gitpod detected")
163
- configObj.address = getGitpodAddress()
164
- configObj.config.publicUrl = `https://${
165
- configObj.config.port
166
- }-${configObj.address?.slice(8)}`
167
- configObj.config.editor.agent = "vscode"
168
- } else if (configObj.config && Boolean(codespaces_workspace)) {
169
- Console.debug("Codespaces detected: ", codespaces_workspace)
170
- configObj.address = `https://${codespaces_workspace}.github.dev`
171
- configObj.config.publicUrl = `https://${codespaces_workspace}-${configObj.config.port}.app.github.dev`
172
- configObj.config.editor.agent = "vscode"
173
- } else {
174
- Console.debug("Neither Gitpod nor Codespaces detected, using localhost.")
175
- configObj.address = `http://localhost:${configObj.config.port}`
176
- configObj.config.publicUrl = `http://localhost:${configObj.config.port}`
177
- }
178
-
179
- if (!configObj.config.editor.agent) {
180
- configObj.config.editor.agent =
181
- process.env.TERM_PROGRAM === "vscode" ? "vscode" : "os"
182
- }
183
-
184
- if (configObj.config && !configObj.config.publicUrl)
185
- configObj.config.publicUrl = `${configObj.address}:${configObj.config.port}`
186
-
187
- if (configObj.config && !configObj.config.editor.mode && mode) {
188
- configObj.config.editor.mode = mode as TMode
189
- }
190
-
191
- if (!configObj.config.editor.mode) {
192
- configObj.config.editor.mode =
193
- configObj.config.editor.agent === "vscode" ? "extension" : "preview"
194
- }
195
-
196
- if (version)
197
- configObj.config.editor.version = version
198
- else if (configObj.config.editor.version === null) {
199
- Console.debug("Config version not found, downloading default.")
200
- const resp = await fetch(
201
- "https://raw.githubusercontent.com/learnpack/ide/master/package.json"
202
- )
203
- const packageJSON = await resp.json()
204
- configObj.config.editor.version = packageJSON.version || "3.1.22"
205
- }
206
-
207
- configObj.config.dirPath = "./" + confPath.base
208
- configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./"
209
-
210
- return {
211
- validLanguages: {},
212
- get: () => configObj,
213
- validateEngine: function (language: string, server: any, socket: any) {
214
- // eslint-disable-next-line
215
- const alias = (_l: string) => {
216
- const map: any = {
217
- python3: "python",
218
- }
219
- if (map[_l])
220
- return map[_l]
221
- return _l
222
- }
223
-
224
- // decode aliases
225
- language = alias(language)
226
-
227
- if (this.validLanguages[language])
228
- return true
229
-
230
- Console.debug(`Validating engine for ${language} compilation`)
231
- let result = shell.exec("learnpack plugins", { silent: true })
232
-
233
- if (
234
- result.code === 0 &&
235
- result.stdout.includes(`learnpack-${language}`)
236
- ) {
237
- this.validLanguages[language] = true
238
- return true
239
- }
240
-
241
- Console.info(`Language engine for ${language} not found, installing...`)
242
- // Install the compiler in their new versions
243
- result = shell.exec(`learnpack plugins:install learnpack-${language}`, {
244
- silent: true,
245
- })
246
- if (result.code === 0) {
247
- socket.log(
248
- "compiling",
249
- "Installing the python compiler, you will have to reset the exercises after installation by writing on your terminal: $ learnpack run"
250
- )
251
- Console.info(
252
- `Successfully installed the ${language} exercise engine, \n please start learnpack again by running the following command: \n ${chalk.white(
253
- "$ learnpack start"
254
- )}\n\n `
255
- )
256
- server.terminate()
257
- return false
258
- }
259
-
260
- this.validLanguages[language] = false
261
- socket.error(`Error installing ${language} exercise engine`)
262
- Console.error(`Error installing ${language} exercise engine`)
263
- Console.log(result.stdout)
264
- throw InternalError(`Error installing ${language} exercise engine`)
265
- },
266
- clean: () => {
267
- if (configObj.config) {
268
- if (configObj.config.outputPath) {
269
- rmSync(configObj.config.outputPath)
270
- }
271
-
272
- rmSync(configObj.config.dirPath + "/_app")
273
- rmSync(configObj.config.dirPath + "/reports")
274
- rmSync(configObj.config.dirPath + "/.session")
275
- rmSync(configObj.config.dirPath + "/resets")
276
-
277
- // clean tag gz
278
- if (fs.existsSync(configObj.config.dirPath + "/app.tar.gz"))
279
- fs.unlinkSync(configObj.config.dirPath + "/app.tar.gz")
280
-
281
- if (fs.existsSync(configObj.config.dirPath + "/config.json"))
282
- fs.unlinkSync(configObj.config.dirPath + "/config.json")
283
-
284
- if (fs.existsSync(configObj.config.dirPath + "/telemetry.json"))
285
- fs.unlinkSync(configObj.config.dirPath + "/telemetry.json")
286
-
287
- if (fs.existsSync(configObj.config.dirPath + "/vscode_queue.json"))
288
- fs.unlinkSync(configObj.config.dirPath + "/vscode_queue.json")
289
- }
290
- },
291
- getExercise: slug => {
292
- Console.debug("ExercisePath Slug", slug)
293
- const exercise = (configObj.exercises || []).find(
294
- ex => ex.slug === slug
295
- )
296
- if (!exercise)
297
- throw ValidationError(`Exercise ${slug} not found`)
298
-
299
- return exercise
300
- },
301
- getAllExercises: () => {
302
- return configObj.exercises
303
- },
304
- startExercise: function (slug: string) {
305
- const exercise = this.getExercise(slug)
306
-
307
- // set config.json with current exercise
308
- configObj.currentExercise = exercise.slug
309
-
310
- this.save()
311
-
312
- // eslint-disable-next-line
313
- exercise.files.forEach((f: IFile) => {
314
- if (configObj.config) {
315
- const _path = configObj.config.outputPath + "/" + f.name
316
- if (f.hidden === false && fs.existsSync(_path))
317
- fs.unlinkSync(_path)
318
- }
319
- })
320
-
321
- return exercise
322
- },
323
- noCurrentExercise: function () {
324
- configObj.currentExercise = null
325
- this.save()
326
- },
327
- reset: slug => {
328
- if (
329
- configObj.config &&
330
- !fs.existsSync(`${configObj.config.dirPath}/resets/` + slug)
331
- )
332
- throw ValidationError("Could not find the original files for " + slug)
333
-
334
- const exercise = (configObj.exercises || []).find(
335
- ex => ex.slug === slug
336
- )
337
- if (!exercise)
338
- throw ValidationError(
339
- `Exercise ${slug} not found on the configuration`
340
- )
341
-
342
- if (configObj.config) {
343
- for (const fileName of fs.readdirSync(
344
- `${configObj.config.dirPath}/resets/${slug}/`
345
- )) {
346
- const content = fs.readFileSync(
347
- `${configObj.config?.dirPath}/resets/${slug}/${fileName}`
348
- )
349
- fs.writeFileSync(`${exercise.path}/${fileName}`, content)
350
- }
351
- }
352
- },
353
- buildIndex: function () {
354
- Console.info("Building the exercise index...")
355
-
356
- const isDirectory = (source: string) => {
357
- const name = path.basename(source)
358
- if (name === path.basename(configObj?.config?.dirPath || ""))
359
- return false
360
- // ignore folders that start with a dot
361
- if (name.charAt(0) === "." || name.charAt(0) === "_")
362
- return false
363
-
364
- return fs.lstatSync(source).isDirectory()
365
- }
366
-
367
- const getDirectories = (source: string) =>
368
- fs
369
- .readdirSync(source)
370
- .map(name => path.join(source, name))
371
- .filter(isDirectory)
372
- // add the .learn folder
373
- if (!fs.existsSync(confPath.base))
374
- fs.mkdirSync(confPath.base)
375
- // add the outout folder where webpack will publish the the html/css/js files
376
- if (
377
- configObj.config &&
378
- configObj.config.outputPath &&
379
- !fs.existsSync(configObj.config.outputPath)
380
- )
381
- fs.mkdirSync(configObj.config.outputPath)
382
-
383
- // TODO: we could use npm library front-mater to read the title of the exercises from the README.md
384
- const grupedByDirectory = getDirectories(
385
- configObj?.config?.exercisesPath || ""
386
- )
387
- configObj.exercises =
388
- grupedByDirectory.length > 0 ?
389
- grupedByDirectory.map((path, position) =>
390
- exercise(path, position, configObj)
391
- ) :
392
- [exercise(configObj?.config?.exercisesPath || "", 0, configObj)]
393
- this.save()
394
- },
395
- watchIndex: function (onChange: (filename: string) => void) {
396
- if (configObj.config && !configObj.config.exercisesPath)
397
- throw ValidationError(
398
- "No exercises directory to watch: " + configObj.config.exercisesPath
399
- )
400
-
401
- this.buildIndex()
402
-
403
- watch(configObj?.config?.exercisesPath || "", onChange)
404
- .then(() => {
405
- Console.debug("Changes detected on your exercises")
406
- this.buildIndex()
407
- // if (onChange) onChange(filename);
408
- })
409
- .catch(error => {
410
- throw error
411
- })
412
- },
413
-
414
- save: () => {
415
- // Console.debug("Saving configuration with: ", configObj)
416
-
417
- // remove the duplicates form the actions array
418
- // configObj.config.actions = [...new Set(configObj.config.actions)];
419
- if (configObj.config) {
420
- configObj.config.translations = [
421
- ...new Set(configObj.config.translations),
422
- ]
423
- fs.writeFileSync(
424
- configObj.config.dirPath + "/config.json",
425
- JSON.stringify(configObj, null, 4)
426
- )
427
- }
428
- },
429
- } as IConfigManager
430
- }
431
-
432
- function deepMerge(...sources: any): any {
433
- let acc: any = {}
434
- for (const source of sources) {
435
- if (Array.isArray(source)) {
436
- if (!Array.isArray(acc)) {
437
- acc = []
438
- }
439
-
440
- acc = [...source]
441
- } else if (source instanceof Object) {
442
- // eslint-disable-next-line
443
- for (let [key, value] of Object.entries(source)) {
444
- if (value instanceof Object && key in acc) {
445
- value = deepMerge(acc[key], value)
446
- }
447
-
448
- if (value !== undefined)
449
- acc = { ...acc, [key]: value }
450
- }
451
- }
452
- }
453
-
454
- return acc
455
- }
1
+ import * as path from "path"
2
+ import * as fs from "fs"
3
+ import * as shell from "shelljs"
4
+ import Console from "../../utils/console"
5
+ import watch from "../../utils/watcher"
6
+ import {
7
+ ValidationError,
8
+ NotFoundError,
9
+ InternalError,
10
+ } from "../../utils/errors"
11
+
12
+ import defaults from "./defaults"
13
+ import { exercise } from "./exercise"
14
+
15
+ import { rmSync } from "../file"
16
+ import { IConfigObj, TConfigObjAttributes, TMode } from "../../models/config"
17
+ import {
18
+ IConfigManagerAttributes,
19
+ IConfigManager,
20
+ } from "../../models/config-manager"
21
+ import { IFile } from "../../models/file"
22
+
23
+ /* exercise folder name standard */
24
+
25
+ // eslint-disable-next-line
26
+ const fetch = require("node-fetch");
27
+ // eslint-disable-next-line
28
+ const chalk = require("chalk");
29
+
30
+ /* exercise folder name standard */
31
+
32
+ /**
33
+ * Retrieves the configuration path for the learnpack package.
34
+ *
35
+ * @returns An object containing the configuration file path and the base directory.
36
+ * @throws NotFoundError if the learn.json file is not found in the current folder.
37
+ */
38
+ const getConfigPath = () => {
39
+ const possibleFileNames = ["learn.json", ".learn/learn.json"]
40
+ const config = possibleFileNames.find(file => fs.existsSync(file)) || null
41
+ if (config && fs.existsSync(".breathecode"))
42
+ return { config, base: ".breathecode" }
43
+ if (config === null)
44
+ throw NotFoundError(
45
+ "learn.json file not found on current folder, is this a learnpack package?"
46
+ )
47
+ return { config, base: ".learn" }
48
+ }
49
+
50
+ const getExercisesPath = (base: string) => {
51
+ const possibleFileNames = ["./exercises", base + "/exercises", "./"]
52
+ return possibleFileNames.find(file => fs.existsSync(file)) || null
53
+ }
54
+
55
+ const getGitpodAddress = () => {
56
+ if (shell.exec("gp -h", { silent: true }).code === 0) {
57
+ return shell
58
+ .exec("gp url", { silent: true })
59
+ .stdout.replace(/(\r\n|\n|\r)/gm, "")
60
+ }
61
+
62
+ Console.debug("Gitpod command line tool not found")
63
+ return "http://localhost"
64
+ }
65
+
66
+ const getCodespacesNamespace = () => {
67
+ // Example: https://orange-rotary-phone-wxpg49q5gcg4rp-3000.app.github.dev
68
+
69
+ const codespace_name = shell
70
+ .exec("echo $CODESPACE_NAME", { silent: true })
71
+ .stdout.replace(/(\r\n|\n|\r)/gm, "")
72
+
73
+ if (
74
+ !codespace_name ||
75
+ codespace_name === "" ||
76
+ codespace_name === undefined ||
77
+ // ! I added this line
78
+ codespace_name === "$CODESPACE_NAME"
79
+ ) {
80
+ return null
81
+ }
82
+
83
+ return codespace_name
84
+ }
85
+
86
+ export default async ({
87
+ grading,
88
+ mode,
89
+ disableGrading,
90
+ version,
91
+ }: IConfigManagerAttributes): Promise<IConfigManager> => {
92
+ const confPath = getConfigPath()
93
+ Console.debug("This is the config path: ", confPath)
94
+
95
+ let configObj: IConfigObj = {}
96
+
97
+ if (confPath) {
98
+ const learnJsonContent = fs.readFileSync(confPath.config)
99
+ let hiddenBcContent = {}
100
+ if (fs.existsSync(confPath.base + "/config.json")) {
101
+ hiddenBcContent = fs.readFileSync(confPath.base + "/config.json")
102
+ hiddenBcContent = JSON.parse(hiddenBcContent as string)
103
+ if (!hiddenBcContent)
104
+ throw new Error(
105
+ `Invalid ${confPath.base}/config.json syntax: Unable to parse.`
106
+ )
107
+ }
108
+
109
+ const jsonConfig = JSON.parse(`${learnJsonContent}`)
110
+
111
+ if (!jsonConfig)
112
+ throw new Error(`Invalid ${confPath.config} syntax: Unable to parse.`)
113
+
114
+ let session: number
115
+
116
+ // add using id to the installation
117
+ if (!jsonConfig.session) {
118
+ session = Math.floor(Math.random() * 10_000_000_000_000_000_000)
119
+ } else {
120
+ session = jsonConfig.session
121
+ delete jsonConfig.session
122
+ }
123
+
124
+ configObj = deepMerge(hiddenBcContent, {
125
+ config: jsonConfig,
126
+ session: session,
127
+ })
128
+ } else {
129
+ throw ValidationError(
130
+ "No learn.json file has been found, make sure you are in the correct directory. To start a new LearnPack package run: $ learnpack init my-course-name"
131
+ )
132
+ }
133
+
134
+ configObj = deepMerge(defaults || {}, configObj, {
135
+ config: {
136
+ grading: grading || configObj.config?.grading,
137
+ configPath: confPath.config,
138
+ },
139
+ })
140
+ if (!configObj.config)
141
+ throw InternalError("Config object not found")
142
+
143
+ configObj.config.outputPath = confPath.base + "/dist"
144
+
145
+ Console.debug("This is your configuration object: ", {
146
+ ...configObj,
147
+ exercises: configObj.exercises ?
148
+ configObj.exercises.map(e => e.slug) :
149
+ [],
150
+ })
151
+
152
+ // auto detect agent (if possible)
153
+ const codespaces_workspace = getCodespacesNamespace()
154
+ Console.debug("This is the codespace namespace: ", codespaces_workspace)
155
+
156
+ Console.debug(
157
+ "This is the agent, and this should be null an the beginning: ",
158
+ configObj.config?.editor?.agent
159
+ )
160
+
161
+ if (shell.which("gp") && configObj && configObj.config) {
162
+ Console.debug("Gitpod detected")
163
+ configObj.address = getGitpodAddress()
164
+ configObj.config.publicUrl = `https://${
165
+ configObj.config.port
166
+ }-${configObj.address?.slice(8)}`
167
+ configObj.config.editor.agent = "vscode"
168
+ } else if (configObj.config && Boolean(codespaces_workspace)) {
169
+ Console.debug("Codespaces detected: ", codespaces_workspace)
170
+ configObj.address = `https://${codespaces_workspace}.github.dev`
171
+ configObj.config.publicUrl = `https://${codespaces_workspace}-${configObj.config.port}.app.github.dev`
172
+ configObj.config.editor.agent = "vscode"
173
+ } else {
174
+ Console.debug("Neither Gitpod nor Codespaces detected, using localhost.")
175
+ configObj.address = `http://localhost:${configObj.config.port}`
176
+ configObj.config.publicUrl = `http://localhost:${configObj.config.port}`
177
+ }
178
+
179
+ if (!configObj.config.editor.agent) {
180
+ configObj.config.editor.agent =
181
+ process.env.TERM_PROGRAM === "vscode" ? "vscode" : "os"
182
+ }
183
+
184
+ if (configObj.config && !configObj.config.publicUrl)
185
+ configObj.config.publicUrl = `${configObj.address}:${configObj.config.port}`
186
+
187
+ if (configObj.config && !configObj.config.editor.mode && mode) {
188
+ configObj.config.editor.mode = mode as TMode
189
+ }
190
+
191
+ if (!configObj.config.editor.mode) {
192
+ configObj.config.editor.mode =
193
+ configObj.config.editor.agent === "vscode" ? "extension" : "preview"
194
+ }
195
+
196
+ if (version)
197
+ configObj.config.editor.version = version
198
+ else if (configObj.config.editor.version === null) {
199
+ Console.debug("Config version not found, downloading default.")
200
+ const resp = await fetch(
201
+ "https://raw.githubusercontent.com/learnpack/ide/master/package.json"
202
+ )
203
+ const packageJSON = await resp.json()
204
+ configObj.config.editor.version = packageJSON.version || "3.1.22"
205
+ }
206
+
207
+ configObj.config.dirPath = "./" + confPath.base
208
+ configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./"
209
+
210
+ return {
211
+ validLanguages: {},
212
+ get: () => configObj,
213
+ validateEngine: function (language: string, server: any, socket: any) {
214
+ // eslint-disable-next-line
215
+ const alias = (_l: string) => {
216
+ const map: any = {
217
+ python3: "python",
218
+ }
219
+ if (map[_l])
220
+ return map[_l]
221
+ return _l
222
+ }
223
+
224
+ // decode aliases
225
+ language = alias(language)
226
+
227
+ if (this.validLanguages[language])
228
+ return true
229
+
230
+ Console.debug(`Validating engine for ${language} compilation`)
231
+ let result = shell.exec("learnpack plugins", { silent: true })
232
+
233
+ if (
234
+ result.code === 0 &&
235
+ result.stdout.includes(`learnpack-${language}`)
236
+ ) {
237
+ this.validLanguages[language] = true
238
+ return true
239
+ }
240
+
241
+ Console.info(`Language engine for ${language} not found, installing...`)
242
+ // Install the compiler in their new versions
243
+ result = shell.exec(`learnpack plugins:install learnpack-${language}`, {
244
+ silent: true,
245
+ })
246
+ if (result.code === 0) {
247
+ socket.log(
248
+ "compiling",
249
+ "Installing the python compiler, you will have to reset the exercises after installation by writing on your terminal: $ learnpack run"
250
+ )
251
+ Console.info(
252
+ `Successfully installed the ${language} exercise engine, \n please start learnpack again by running the following command: \n ${chalk.white(
253
+ "$ learnpack start"
254
+ )}\n\n `
255
+ )
256
+ server.terminate()
257
+ return false
258
+ }
259
+
260
+ this.validLanguages[language] = false
261
+ socket.error(`Error installing ${language} exercise engine`)
262
+ Console.error(`Error installing ${language} exercise engine`)
263
+ Console.log(result.stdout)
264
+ throw InternalError(`Error installing ${language} exercise engine`)
265
+ },
266
+ clean: () => {
267
+ if (configObj.config) {
268
+ if (configObj.config.outputPath) {
269
+ rmSync(configObj.config.outputPath)
270
+ }
271
+
272
+ rmSync(configObj.config.dirPath + "/_app")
273
+ rmSync(configObj.config.dirPath + "/reports")
274
+ rmSync(configObj.config.dirPath + "/.session")
275
+ rmSync(configObj.config.dirPath + "/resets")
276
+
277
+ // clean tag gz
278
+ if (fs.existsSync(configObj.config.dirPath + "/app.tar.gz"))
279
+ fs.unlinkSync(configObj.config.dirPath + "/app.tar.gz")
280
+
281
+ if (fs.existsSync(configObj.config.dirPath + "/config.json"))
282
+ fs.unlinkSync(configObj.config.dirPath + "/config.json")
283
+
284
+ if (fs.existsSync(configObj.config.dirPath + "/telemetry.json"))
285
+ fs.unlinkSync(configObj.config.dirPath + "/telemetry.json")
286
+
287
+ if (fs.existsSync(configObj.config.dirPath + "/vscode_queue.json"))
288
+ fs.unlinkSync(configObj.config.dirPath + "/vscode_queue.json")
289
+ }
290
+ },
291
+ getExercise: slug => {
292
+ Console.debug("ExercisePath Slug", slug)
293
+ const exercise = (configObj.exercises || []).find(
294
+ ex => ex.slug === slug
295
+ )
296
+ if (!exercise)
297
+ throw ValidationError(`Exercise ${slug} not found`)
298
+
299
+ return exercise
300
+ },
301
+ getAllExercises: () => {
302
+ return configObj.exercises
303
+ },
304
+ startExercise: function (slug: string) {
305
+ const exercise = this.getExercise(slug)
306
+
307
+ // set config.json with current exercise
308
+ configObj.currentExercise = exercise.slug
309
+
310
+ this.save()
311
+
312
+ // eslint-disable-next-line
313
+ exercise.files.forEach((f: IFile) => {
314
+ if (configObj.config) {
315
+ const _path = configObj.config.outputPath + "/" + f.name
316
+ if (f.hidden === false && fs.existsSync(_path))
317
+ fs.unlinkSync(_path)
318
+ }
319
+ })
320
+
321
+ return exercise
322
+ },
323
+ noCurrentExercise: function () {
324
+ configObj.currentExercise = null
325
+ this.save()
326
+ },
327
+ reset: slug => {
328
+ if (
329
+ configObj.config &&
330
+ !fs.existsSync(`${configObj.config.dirPath}/resets/` + slug)
331
+ )
332
+ throw ValidationError("Could not find the original files for " + slug)
333
+
334
+ const exercise = (configObj.exercises || []).find(
335
+ ex => ex.slug === slug
336
+ )
337
+ if (!exercise)
338
+ throw ValidationError(
339
+ `Exercise ${slug} not found on the configuration`
340
+ )
341
+
342
+ if (configObj.config) {
343
+ for (const fileName of fs.readdirSync(
344
+ `${configObj.config.dirPath}/resets/${slug}/`
345
+ )) {
346
+ const content = fs.readFileSync(
347
+ `${configObj.config?.dirPath}/resets/${slug}/${fileName}`
348
+ )
349
+ fs.writeFileSync(`${exercise.path}/${fileName}`, content)
350
+ }
351
+ }
352
+ },
353
+ buildIndex: function () {
354
+ Console.info("Building the exercise index...")
355
+
356
+ const isDirectory = (source: string) => {
357
+ const name = path.basename(source)
358
+ if (name === path.basename(configObj?.config?.dirPath || ""))
359
+ return false
360
+ // ignore folders that start with a dot
361
+ if (name.charAt(0) === "." || name.charAt(0) === "_")
362
+ return false
363
+
364
+ return fs.lstatSync(source).isDirectory()
365
+ }
366
+
367
+ const getDirectories = (source: string) =>
368
+ fs
369
+ .readdirSync(source)
370
+ .map(name => path.join(source, name))
371
+ .filter(isDirectory)
372
+ // add the .learn folder
373
+ if (!fs.existsSync(confPath.base))
374
+ fs.mkdirSync(confPath.base)
375
+ // add the outout folder where webpack will publish the the html/css/js files
376
+ if (
377
+ configObj.config &&
378
+ configObj.config.outputPath &&
379
+ !fs.existsSync(configObj.config.outputPath)
380
+ )
381
+ fs.mkdirSync(configObj.config.outputPath)
382
+
383
+ // TODO: we could use npm library front-mater to read the title of the exercises from the README.md
384
+ const grupedByDirectory = getDirectories(
385
+ configObj?.config?.exercisesPath || ""
386
+ )
387
+ configObj.exercises =
388
+ grupedByDirectory.length > 0 ?
389
+ grupedByDirectory.map((path, position) =>
390
+ exercise(path, position, configObj)
391
+ ) :
392
+ [exercise(configObj?.config?.exercisesPath || "", 0, configObj)]
393
+ this.save()
394
+ },
395
+ watchIndex: function (onChange: (filename: string) => void) {
396
+ if (configObj.config && !configObj.config.exercisesPath)
397
+ throw ValidationError(
398
+ "No exercises directory to watch: " + configObj.config.exercisesPath
399
+ )
400
+
401
+ this.buildIndex()
402
+
403
+ watch(configObj?.config?.exercisesPath || "", onChange)
404
+ .then(() => {
405
+ Console.debug("Changes detected on your exercises")
406
+ this.buildIndex()
407
+ // if (onChange) onChange(filename);
408
+ })
409
+ .catch(error => {
410
+ throw error
411
+ })
412
+ },
413
+
414
+ save: () => {
415
+ // Console.debug("Saving configuration with: ", configObj)
416
+
417
+ // remove the duplicates form the actions array
418
+ // configObj.config.actions = [...new Set(configObj.config.actions)];
419
+ if (configObj.config) {
420
+ configObj.config.translations = [
421
+ ...new Set(configObj.config.translations),
422
+ ]
423
+ fs.writeFileSync(
424
+ configObj.config.dirPath + "/config.json",
425
+ JSON.stringify(configObj, null, 4)
426
+ )
427
+ }
428
+ },
429
+ } as IConfigManager
430
+ }
431
+
432
+ function deepMerge(...sources: any): any {
433
+ let acc: any = {}
434
+ for (const source of sources) {
435
+ if (Array.isArray(source)) {
436
+ if (!Array.isArray(acc)) {
437
+ acc = []
438
+ }
439
+
440
+ acc = [...source]
441
+ } else if (source instanceof Object) {
442
+ // eslint-disable-next-line
443
+ for (let [key, value] of Object.entries(source)) {
444
+ if (value instanceof Object && key in acc) {
445
+ value = deepMerge(acc[key], value)
446
+ }
447
+
448
+ if (value !== undefined)
449
+ acc = { ...acc, [key]: value }
450
+ }
451
+ }
452
+ }
453
+
454
+ return acc
455
+ }