@learnpack/learnpack 2.1.43 → 2.1.45

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.
@@ -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
+ codespace_name === "$CODESPACE_NAME"
78
+ ) {
79
+ return null
80
+ }
81
+
82
+ return codespace_name
83
+ }
84
+
85
+ export default async ({
86
+ grading,
87
+ mode,
88
+ disableGrading,
89
+ version,
90
+ }: IConfigManagerAttributes): Promise<IConfigManager> => {
91
+ const confPath = getConfigPath()
92
+ Console.debug("This is the config path: ", confPath)
93
+
94
+ let configObj: IConfigObj = {}
95
+
96
+ if (confPath) {
97
+ const learnJsonContent = fs.readFileSync(confPath.config)
98
+ let hiddenBcContent = {}
99
+
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.23"
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
+ }