@learnpack/learnpack 2.1.39 → 2.1.41

Sign up to get free protection for your applications and to get access to all the features.
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
+ }