@learnpack/learnpack 4.0.10 → 4.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. package/README.md +16 -20
  2. package/lib/commands/audit.d.ts +6 -6
  3. package/lib/commands/audit.js +327 -327
  4. package/lib/commands/clean.d.ts +8 -8
  5. package/lib/commands/clean.js +22 -22
  6. package/lib/commands/download.d.ts +13 -13
  7. package/lib/commands/download.js +52 -52
  8. package/lib/commands/init.d.ts +9 -9
  9. package/lib/commands/init.js +127 -127
  10. package/lib/commands/login.d.ts +14 -14
  11. package/lib/commands/login.js +34 -34
  12. package/lib/commands/logout.d.ts +14 -14
  13. package/lib/commands/logout.js +34 -34
  14. package/lib/commands/publish.d.ts +11 -14
  15. package/lib/commands/publish.js +160 -82
  16. package/lib/commands/start.d.ts +7 -7
  17. package/lib/commands/start.js +252 -250
  18. package/lib/commands/test.d.ts +6 -6
  19. package/lib/commands/test.js +62 -62
  20. package/lib/index.d.ts +1 -1
  21. package/lib/index.js +4 -4
  22. package/lib/managers/config/allowed_files.d.ts +5 -5
  23. package/lib/managers/config/allowed_files.js +30 -30
  24. package/lib/managers/config/defaults.d.ts +47 -48
  25. package/lib/managers/config/defaults.js +51 -51
  26. package/lib/managers/config/exercise.d.ts +36 -36
  27. package/lib/managers/config/exercise.js +243 -236
  28. package/lib/managers/config/index.d.ts +3 -3
  29. package/lib/managers/config/index.js +464 -459
  30. package/lib/managers/file.d.ts +14 -14
  31. package/lib/managers/file.js +190 -184
  32. package/lib/managers/gitpod.d.ts +3 -3
  33. package/lib/managers/gitpod.js +67 -67
  34. package/lib/managers/server/index.d.ts +5 -6
  35. package/lib/managers/server/index.js +58 -58
  36. package/lib/managers/server/routes.d.ts +4 -4
  37. package/lib/managers/server/routes.js +228 -220
  38. package/lib/managers/session.d.ts +3 -3
  39. package/lib/managers/session.js +125 -125
  40. package/lib/managers/socket.d.ts +3 -3
  41. package/lib/managers/socket.js +188 -186
  42. package/lib/managers/telemetry.d.ts +74 -74
  43. package/lib/managers/telemetry.js +215 -214
  44. package/lib/managers/test.js +84 -84
  45. package/lib/models/action.d.ts +2 -2
  46. package/lib/models/action.js +2 -2
  47. package/lib/models/audit.d.ts +15 -15
  48. package/lib/models/audit.js +2 -2
  49. package/lib/models/config-manager.d.ts +21 -21
  50. package/lib/models/config-manager.js +2 -2
  51. package/lib/models/config.d.ts +86 -86
  52. package/lib/models/config.js +2 -2
  53. package/lib/models/counter.d.ts +11 -11
  54. package/lib/models/counter.js +2 -2
  55. package/lib/models/errors.d.ts +15 -15
  56. package/lib/models/errors.js +2 -2
  57. package/lib/models/exercise-obj.d.ts +29 -30
  58. package/lib/models/exercise-obj.js +2 -2
  59. package/lib/models/file.d.ts +5 -5
  60. package/lib/models/file.js +2 -2
  61. package/lib/models/findings.d.ts +17 -17
  62. package/lib/models/findings.js +2 -2
  63. package/lib/models/flags.d.ts +10 -10
  64. package/lib/models/flags.js +2 -2
  65. package/lib/models/front-matter.d.ts +11 -11
  66. package/lib/models/front-matter.js +2 -2
  67. package/lib/models/gitpod-data.d.ts +16 -16
  68. package/lib/models/gitpod-data.js +2 -2
  69. package/lib/models/language.d.ts +4 -4
  70. package/lib/models/language.js +2 -2
  71. package/lib/models/package.d.ts +7 -7
  72. package/lib/models/package.js +2 -2
  73. package/lib/models/plugin-config.d.ts +16 -16
  74. package/lib/models/plugin-config.js +2 -2
  75. package/lib/models/session.d.ts +31 -31
  76. package/lib/models/session.js +2 -2
  77. package/lib/models/socket.d.ts +37 -37
  78. package/lib/models/socket.js +2 -2
  79. package/lib/models/status.d.ts +1 -1
  80. package/lib/models/status.js +2 -2
  81. package/lib/models/success-types.d.ts +1 -1
  82. package/lib/models/success-types.js +2 -2
  83. package/lib/plugin/command/compile.d.ts +6 -6
  84. package/lib/plugin/command/compile.js +18 -18
  85. package/lib/plugin/command/test.d.ts +6 -6
  86. package/lib/plugin/command/test.js +25 -25
  87. package/lib/plugin/index.d.ts +27 -27
  88. package/lib/plugin/index.js +7 -7
  89. package/lib/plugin/plugin.d.ts +8 -8
  90. package/lib/plugin/plugin.js +68 -68
  91. package/lib/plugin/utils.d.ts +16 -16
  92. package/lib/plugin/utils.js +58 -58
  93. package/lib/ui/download.d.ts +5 -5
  94. package/lib/ui/download.js +62 -61
  95. package/lib/utils/BaseCommand.d.ts +8 -8
  96. package/lib/utils/BaseCommand.js +41 -41
  97. package/lib/utils/SessionCommand.d.ts +10 -10
  98. package/lib/utils/SessionCommand.js +43 -43
  99. package/lib/utils/api.d.ts +14 -14
  100. package/lib/utils/api.js +255 -255
  101. package/lib/utils/audit.d.ts +16 -16
  102. package/lib/utils/audit.js +303 -303
  103. package/lib/utils/checkNotInstalled.d.ts +8 -8
  104. package/lib/utils/checkNotInstalled.js +185 -181
  105. package/lib/utils/console.d.ts +12 -12
  106. package/lib/utils/console.js +19 -19
  107. package/lib/utils/errors.d.ts +17 -17
  108. package/lib/utils/errors.js +107 -100
  109. package/lib/utils/exercisesQueue.d.ts +9 -9
  110. package/lib/utils/exercisesQueue.js +38 -38
  111. package/lib/utils/fileQueue.d.ts +43 -43
  112. package/lib/utils/fileQueue.js +169 -169
  113. package/lib/utils/misc.d.ts +1 -1
  114. package/lib/utils/misc.js +24 -23
  115. package/lib/utils/osOperations.d.ts +5 -5
  116. package/lib/utils/osOperations.js +72 -72
  117. package/lib/utils/validators.d.ts +5 -5
  118. package/lib/utils/validators.js +16 -17
  119. package/lib/utils/watcher.d.ts +2 -2
  120. package/lib/utils/watcher.js +25 -25
  121. package/oclif.manifest.json +1 -1
  122. package/package.json +6 -4
  123. package/src/commands/publish.ts +181 -107
  124. package/src/managers/config/index.ts +5 -0
  125. package/src/managers/server/routes.ts +10 -0
  126. package/src/managers/session.ts +145 -145
@@ -1,413 +1,418 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const path = require("path");
4
- const fs = require("fs");
5
- const shell = require("shelljs");
6
- const child_process_1 = require("child_process");
7
- const util_1 = require("util");
8
- const console_1 = require("../../utils/console");
9
- const watcher_1 = require("../../utils/watcher");
10
- const errors_1 = require("../../utils/errors");
11
- const defaults_1 = require("./defaults");
12
- const exercise_1 = require("./exercise");
13
- const file_1 = require("../file");
14
- /* exercise folder name standard */
15
- const execAsync = util_1.promisify(child_process_1.exec);
16
- // eslint-disable-next-line
17
- const fetch = require("node-fetch");
18
- // eslint-disable-next-line
19
- const chalk = require("chalk");
20
- /* exercise folder name standard */
21
- /**
22
- * Retrieves the configuration path for the learnpack package.
23
- *
24
- * @returns An object containing the configuration file path and the base directory.
25
- * @throws NotFoundError if the learn.json file is not found in the current folder.
26
- */
27
- const getConfigPath = () => {
28
- const possibleFileNames = ["learn.json", ".learn/learn.json"];
29
- const config = possibleFileNames.find(file => fs.existsSync(file)) || null;
30
- if (config && fs.existsSync(".breathecode"))
31
- return { config, base: ".breathecode" };
32
- if (config === null)
33
- throw errors_1.NotFoundError("learn.json file not found on current folder, is this a learnpack package?");
34
- return { config, base: ".learn" };
35
- };
36
- const getExercisesPath = (base) => {
37
- const possibleFileNames = ["./exercises", base + "/exercises", "./"];
38
- return possibleFileNames.find(file => fs.existsSync(file)) || null;
39
- };
40
- const getGitpodAddress = () => {
41
- if (shell.exec("gp -h", { silent: true }).code === 0) {
42
- return shell
43
- .exec("gp url", { silent: true })
44
- .stdout.replace(/(\r\n|\n|\r)/gm, "");
45
- }
46
- console_1.default.debug("Gitpod command line tool not found");
47
- return "http://localhost";
48
- };
49
- const getCodespacesNamespace = () => {
50
- // Example: https://orange-rotary-phone-wxpg49q5gcg4rp-3000.app.github.dev
51
- const codespace_name = shell
52
- .exec("echo $CODESPACE_NAME", { silent: true })
53
- .stdout.replace(/(\r\n|\n|\r)/gm, "");
54
- if (!codespace_name ||
55
- codespace_name === "" ||
56
- codespace_name === undefined ||
57
- codespace_name === "$CODESPACE_NAME") {
58
- return null;
59
- }
60
- return codespace_name;
61
- };
62
- exports.default = async ({ grading, mode, disableGrading, version, }) => {
63
- var _a, _b, _c, _d, _e, _f;
64
- const confPath = getConfigPath();
65
- console_1.default.debug("This is the config path: ", confPath);
66
- let configObj = {};
67
- if (confPath) {
68
- const learnJsonContent = fs.readFileSync(confPath.config);
69
- let hiddenBcContent = {};
70
- if (fs.existsSync(confPath.base + "/config.json")) {
71
- hiddenBcContent = fs.readFileSync(confPath.base + "/config.json");
72
- hiddenBcContent = JSON.parse(hiddenBcContent);
73
- if (!hiddenBcContent)
74
- throw new Error(`Invalid ${confPath.base}/config.json syntax: Unable to parse.`);
75
- }
76
- const jsonConfig = JSON.parse(`${learnJsonContent}`);
77
- if (!jsonConfig)
78
- throw new Error(`Invalid ${confPath.config} syntax: Unable to parse.`);
79
- let session;
80
- // add using id to the installation
81
- if (!jsonConfig.session) {
82
- session = Math.floor(Math.random() * 10000000000000000000);
83
- }
84
- else {
85
- session = jsonConfig.session;
86
- delete jsonConfig.session;
87
- }
88
- configObj = deepMerge(hiddenBcContent, {
89
- config: jsonConfig,
90
- session: session,
91
- });
92
- }
93
- else {
94
- throw errors_1.ValidationError("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");
95
- }
96
- configObj = deepMerge(defaults_1.default || {}, configObj, {
97
- config: {
98
- grading: grading || ((_a = configObj.config) === null || _a === void 0 ? void 0 : _a.grading),
99
- configPath: confPath.config,
100
- },
101
- });
102
- if (!configObj.config)
103
- throw errors_1.InternalError("Config object not found");
104
- configObj.config.outputPath = confPath.base + "/dist";
105
- console_1.default.debug("This is your configuration object: ", Object.assign(Object.assign({}, configObj), { exercises: configObj.exercises ?
106
- configObj.exercises.map(e => e.slug) :
107
- [] }));
108
- // auto detect agent (if possible)
109
- const codespaces_workspace = getCodespacesNamespace();
110
- console_1.default.debug("This is the codespace namespace: ", codespaces_workspace);
111
- if (!configObj.config.editor) {
112
- configObj.config.editor = {};
113
- }
114
- console_1.default.debug("This is the agent, and this should be null an the beginning: ", (_c = (_b = configObj.config) === null || _b === void 0 ? void 0 : _b.editor) === null || _c === void 0 ? void 0 : _c.agent);
115
- if ((_e = (_d = configObj.config) === null || _d === void 0 ? void 0 : _d.editor) === null || _e === void 0 ? void 0 : _e.agent) {
116
- if (configObj.config.suggestions) {
117
- configObj.config.suggestions.agent = configObj.config.editor.agent;
118
- }
119
- else {
120
- configObj.config.suggestions = { agent: configObj.config.editor.agent };
121
- }
122
- }
123
- if (shell.which("gp") && configObj && configObj.config) {
124
- console_1.default.debug("Gitpod detected");
125
- configObj.address = getGitpodAddress();
126
- configObj.config.publicUrl = `https://${configObj.config.port}-${(_f = configObj.address) === null || _f === void 0 ? void 0 : _f.slice(8)}`;
127
- configObj.config.editor.agent = "vscode";
128
- }
129
- else if (configObj.config && Boolean(codespaces_workspace)) {
130
- console_1.default.debug("Codespaces detected: ", codespaces_workspace);
131
- configObj.address = `https://${codespaces_workspace}.github.dev`;
132
- configObj.config.publicUrl = `https://${codespaces_workspace}-${configObj.config.port}.app.github.dev`;
133
- configObj.config.editor.agent = "vscode";
134
- }
135
- else {
136
- console_1.default.debug("Neither Gitpod nor Codespaces detected, using localhost.");
137
- configObj.address = `http://localhost:${configObj.config.port}`;
138
- configObj.config.publicUrl = `http://localhost:${configObj.config.port}`;
139
- configObj.config.editor.agent =
140
- process.env.TERM_PROGRAM === "vscode" ? "vscode" : "os";
141
- }
142
- if (configObj.config.editor.agent === "vscode" && isCodeCommandAvailable()) {
143
- const extensionName = "learn-pack.learnpack-vscode";
144
- if (!isExtensionInstalled(extensionName)) {
145
- console_1.default.info("The LearnPack extension is not installed, trying to install it automatically.");
146
- if (!installExtension(extensionName)) {
147
- configObj.config.warnings = {
148
- extension: EXTENSION_INSTALLATION_STEPS,
149
- };
150
- console_1.default.warning("The LearnPack extension is not installed and this tutorial is meant to be run inside VSCode. Please install the LearnPack extension.");
151
- }
152
- }
153
- }
154
- if (configObj.config.suggestions &&
155
- configObj.config.editor &&
156
- configObj.config.suggestions.agent !== null &&
157
- configObj.config.suggestions.agent !== configObj.config.editor.agent) {
158
- console_1.default.warning(`The suggested agent "${configObj.config.suggestions.agent}" is different from the detected agent "${configObj.config.editor.agent}"`);
159
- try {
160
- configObj.config.warnings = Object.assign(Object.assign({}, configObj.config.warnings), { agent: buildAgentWarning(configObj.config.editor.agent, configObj.config.suggestions.agent) });
161
- }
162
- catch (_g) {
163
- console_1.default.error("IMPOSSIBLE TO SET WARNING");
164
- }
165
- }
166
- if (configObj.config && !configObj.config.publicUrl)
167
- configObj.config.publicUrl = `${configObj.address}:${configObj.config.port}`;
168
- if (configObj.config && !configObj.config.editor.mode && mode) {
169
- configObj.config.editor.mode = mode;
170
- }
171
- configObj.config.editor.mode =
172
- process.env.TERM_PROGRAM === "vscode" ? "extension" : "preview";
173
- if (version)
174
- configObj.config.editor.version = version;
175
- else if (configObj.config.editor.version === null) {
176
- console_1.default.debug("Config version not found, downloading default.");
177
- const resp = await fetch("https://raw.githubusercontent.com/learnpack/ide/master/package.json");
178
- const packageJSON = await resp.json();
179
- configObj.config.editor.version = packageJSON.version || "4.0.2";
180
- }
181
- configObj.config.dirPath = "./" + confPath.base;
182
- configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./";
183
- if (configObj.config.variables) {
184
- const allowedCommands = new Set(["ls", "pwd", "echo", "node", "python"]);
185
- const variableKeys = Object.keys(configObj.config.variables);
186
- const promises = [];
187
- for (const v of variableKeys) {
188
- if (Object.prototype.hasOwnProperty.call(configObj.config.variables, v)) {
189
- const variable = configObj.config.variables[v];
190
- if (typeof variable === "string") {
191
- continue;
192
- }
193
- const type = variable.type;
194
- if (type === "command") {
195
- const promise = (async () => {
196
- try {
197
- // Validate the command
198
- if (!allowedCommands.has(variable.source.split(" ")[0])) {
199
- throw new Error("Command not allowed");
200
- }
201
- // Execute the code and replace the value
202
- const { stdout } = await execAsync(variable.source);
203
- if (!configObj.config || !configObj.config.variables)
204
- return;
205
- configObj.config.variables[v] = stdout.trim();
206
- }
207
- catch (error) {
208
- console.error(`Error executing code: ${error}`);
209
- }
210
- })();
211
- promises.push(promise);
212
- }
213
- else if (type === "string") {
214
- configObj.config.variables[v] = variable.source;
215
- }
216
- }
217
- }
218
- await Promise.all(promises);
219
- }
220
- return {
221
- validLanguages: {},
222
- get: () => {
223
- return configObj;
224
- },
225
- validateEngine: function (language, server, socket) {
226
- // eslint-disable-next-line
227
- const alias = (_l) => {
228
- const map = {
229
- python3: "python",
230
- };
231
- if (map[_l])
232
- return map[_l];
233
- return _l;
234
- };
235
- // decode aliases
236
- language = alias(language);
237
- if (this.validLanguages[language])
238
- return true;
239
- console_1.default.debug(`Validating engine for ${language} compilation`);
240
- let result = shell.exec("learnpack plugins", { silent: true });
241
- if (result.code === 0 &&
242
- result.stdout.includes(`@learnpack/${language}`)) {
243
- this.validLanguages[language] = true;
244
- return true;
245
- }
246
- console_1.default.info(`Language engine for ${language} not found, installing...`);
247
- // Install the compiler in their new versions
248
- result = shell.exec(`learnpack plugins:install @learnpack/${language}`, {
249
- silent: true,
250
- });
251
- if (result.code === 0) {
252
- socket.log("compiling", "Installing the python compiler, you will have to reset the exercises after installation by writing on your terminal: $ learnpack run");
253
- console_1.default.info(`Successfully installed the ${language} exercise engine, \n please start learnpack again by running the following command: \n ${chalk.white("$ learnpack start")}\n\n `);
254
- server.terminate();
255
- return false;
256
- }
257
- this.validLanguages[language] = false;
258
- socket.error(`Error installing ${language} exercise engine`);
259
- console_1.default.error(`Error installing ${language} exercise engine`);
260
- console_1.default.log(result.stdout);
261
- throw errors_1.InternalError(`Error installing ${language} exercise engine`);
262
- },
263
- clean: () => {
264
- if (configObj.config) {
265
- if (configObj.config.outputPath) {
266
- file_1.rmSync(configObj.config.outputPath);
267
- }
268
- file_1.rmSync(configObj.config.dirPath + "/_app");
269
- file_1.rmSync(configObj.config.dirPath + "/reports");
270
- file_1.rmSync(configObj.config.dirPath + "/.session");
271
- file_1.rmSync(configObj.config.dirPath + "/resets");
272
- // clean tag gz
273
- if (fs.existsSync(configObj.config.dirPath + "/app.tar.gz"))
274
- fs.unlinkSync(configObj.config.dirPath + "/app.tar.gz");
275
- if (fs.existsSync(configObj.config.dirPath + "/config.json"))
276
- fs.unlinkSync(configObj.config.dirPath + "/config.json");
277
- if (fs.existsSync(configObj.config.dirPath + "/telemetry.json"))
278
- fs.unlinkSync(configObj.config.dirPath + "/telemetry.json");
279
- if (fs.existsSync(configObj.config.dirPath + "/vscode_queue.json"))
280
- fs.unlinkSync(configObj.config.dirPath + "/vscode_queue.json");
281
- }
282
- },
283
- getExercise: slug => {
284
- console_1.default.debug("ExercisePath Slug", slug);
285
- const exercise = (configObj.exercises || []).find(ex => ex.slug === slug);
286
- if (!exercise)
287
- throw errors_1.ValidationError(`Exercise ${slug} not found`);
288
- return exercise;
289
- },
290
- getAllExercises: () => {
291
- return configObj.exercises;
292
- },
293
- startExercise: function (slug) {
294
- const exercise = this.getExercise(slug);
295
- // set config.json with current exercise
296
- configObj.currentExercise = exercise.slug;
297
- this.save();
298
- // eslint-disable-next-line
299
- exercise.files.forEach((f) => {
300
- if (configObj.config) {
301
- const _path = configObj.config.outputPath + "/" + f.name;
302
- if (f.hidden === false && fs.existsSync(_path))
303
- fs.unlinkSync(_path);
304
- }
305
- });
306
- return exercise;
307
- },
308
- noCurrentExercise: function () {
309
- configObj.currentExercise = null;
310
- this.save();
311
- },
312
- reset: slug => {
313
- var _a;
314
- if (configObj.config &&
315
- !fs.existsSync(`${configObj.config.dirPath}/resets/` + slug))
316
- throw errors_1.ValidationError("Could not find the original files for " + slug);
317
- const exercise = (configObj.exercises || []).find(ex => ex.slug === slug);
318
- if (!exercise)
319
- throw errors_1.ValidationError(`Exercise ${slug} not found on the configuration`);
320
- if (configObj.config) {
321
- for (const fileName of fs.readdirSync(`${configObj.config.dirPath}/resets/${slug}/`)) {
322
- const content = fs.readFileSync(`${(_a = configObj.config) === null || _a === void 0 ? void 0 : _a.dirPath}/resets/${slug}/${fileName}`);
323
- fs.writeFileSync(`${exercise.path}/${fileName}`, content);
324
- }
325
- }
326
- },
327
- buildIndex: function () {
328
- var _a, _b;
329
- console_1.default.info("Building the exercise index...");
330
- const isDirectory = (source) => {
331
- var _a;
332
- const name = path.basename(source);
333
- if (name === path.basename(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.dirPath) || ""))
334
- return false;
335
- // ignore folders that start with a dot
336
- if (name.charAt(0) === "." || name.charAt(0) === "_")
337
- return false;
338
- return fs.lstatSync(source).isDirectory();
339
- };
340
- const getDirectories = (source) => fs
341
- .readdirSync(source)
342
- .map(name => path.join(source, name))
343
- .filter(isDirectory);
344
- // add the .learn folder
345
- if (!fs.existsSync(confPath.base))
346
- fs.mkdirSync(confPath.base);
347
- // add the outout folder where webpack will publish the the html/css/js files
348
- if (configObj.config &&
349
- configObj.config.outputPath &&
350
- !fs.existsSync(configObj.config.outputPath))
351
- fs.mkdirSync(configObj.config.outputPath);
352
- // TODO: we could use npm library front-mater to read the title of the exercises from the README.md
353
- const grupedByDirectory = getDirectories(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.exercisesPath) || "");
354
- configObj.exercises =
355
- grupedByDirectory.length > 0 ?
356
- grupedByDirectory.map((path, position) => exercise_1.exercise(path, position, configObj)) :
357
- [exercise_1.exercise(((_b = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _b === void 0 ? void 0 : _b.exercisesPath) || "", 0, configObj)];
358
- this.save();
359
- },
360
- watchIndex: function (onChange) {
361
- var _a;
362
- if (configObj.config && !configObj.config.exercisesPath)
363
- throw errors_1.ValidationError("No exercises directory to watch: " + configObj.config.exercisesPath);
364
- this.buildIndex();
365
- watcher_1.default(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.exercisesPath) || "", onChange)
366
- .then(() => {
367
- console_1.default.debug("Changes detected on your exercises");
368
- this.buildIndex();
369
- // if (onChange) onChange(filename);
370
- })
371
- .catch(error => {
372
- throw error;
373
- });
374
- },
375
- save: () => {
376
- console_1.default.debug("Saving configuration with: ", configObj);
377
- // remove the duplicates form the actions array
378
- // configObj.config.actions = [...new Set(configObj.config.actions)];
379
- if (configObj.config) {
380
- configObj.config.translations = [
381
- ...new Set(configObj.config.translations),
382
- ];
383
- fs.writeFileSync(configObj.config.dirPath + "/config.json", JSON.stringify(configObj, null, 4));
384
- }
385
- },
386
- };
387
- };
388
- function deepMerge(...sources) {
389
- let acc = {};
390
- for (const source of sources) {
391
- if (Array.isArray(source)) {
392
- if (!Array.isArray(acc)) {
393
- acc = [];
394
- }
395
- acc = [...source];
396
- }
397
- else if (source instanceof Object) {
398
- // eslint-disable-next-line
399
- for (let [key, value] of Object.entries(source)) {
400
- if (value instanceof Object && key in acc) {
401
- value = deepMerge(acc[key], value);
402
- }
403
- if (value !== undefined)
404
- acc = Object.assign(Object.assign({}, acc), { [key]: value });
405
- }
406
- }
407
- }
408
- return acc;
409
- }
410
- const buildAgentWarning = (current, suggested) => {
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+ const shell = require("shelljs");
6
+ const child_process_1 = require("child_process");
7
+ const util_1 = require("util");
8
+ const console_1 = require("../../utils/console");
9
+ const watcher_1 = require("../../utils/watcher");
10
+ const errors_1 = require("../../utils/errors");
11
+ const defaults_1 = require("./defaults");
12
+ const exercise_1 = require("./exercise");
13
+ const file_1 = require("../file");
14
+ /* exercise folder name standard */
15
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
16
+ // eslint-disable-next-line
17
+ const fetch = require("node-fetch");
18
+ // eslint-disable-next-line
19
+ const chalk = require("chalk");
20
+ /* exercise folder name standard */
21
+ /**
22
+ * Retrieves the configuration path for the learnpack package.
23
+ *
24
+ * @returns An object containing the configuration file path and the base directory.
25
+ * @throws NotFoundError if the learn.json file is not found in the current folder.
26
+ */
27
+ const getConfigPath = () => {
28
+ const possibleFileNames = ["learn.json", ".learn/learn.json"];
29
+ const config = possibleFileNames.find(file => fs.existsSync(file)) || null;
30
+ if (config && fs.existsSync(".breathecode"))
31
+ return { config, base: ".breathecode" };
32
+ if (config === null)
33
+ throw (0, errors_1.NotFoundError)("learn.json file not found on current folder, is this a learnpack package?");
34
+ return { config, base: ".learn" };
35
+ };
36
+ const getExercisesPath = (base) => {
37
+ const possibleFileNames = ["./exercises", base + "/exercises", "./"];
38
+ return possibleFileNames.find(file => fs.existsSync(file)) || null;
39
+ };
40
+ const getGitpodAddress = () => {
41
+ if (shell.exec("gp -h", { silent: true }).code === 0) {
42
+ return shell
43
+ .exec("gp url", { silent: true })
44
+ .stdout.replace(/(\r\n|\n|\r)/gm, "");
45
+ }
46
+ console_1.default.debug("Gitpod command line tool not found");
47
+ return "http://localhost";
48
+ };
49
+ const getCodespacesNamespace = () => {
50
+ // Example: https://orange-rotary-phone-wxpg49q5gcg4rp-3000.app.github.dev
51
+ const codespace_name = shell
52
+ .exec("echo $CODESPACE_NAME", { silent: true })
53
+ .stdout.replace(/(\r\n|\n|\r)/gm, "");
54
+ if (!codespace_name ||
55
+ codespace_name === "" ||
56
+ codespace_name === undefined ||
57
+ codespace_name === "$CODESPACE_NAME") {
58
+ return null;
59
+ }
60
+ return codespace_name;
61
+ };
62
+ exports.default = async ({ grading, mode, disableGrading, version, }) => {
63
+ var _a, _b, _c, _d, _e, _f;
64
+ const confPath = getConfigPath();
65
+ console_1.default.debug("This is the config path: ", confPath);
66
+ let configObj = {};
67
+ if (confPath) {
68
+ const learnJsonContent = fs.readFileSync(confPath.config);
69
+ let hiddenBcContent = {};
70
+ if (fs.existsSync(confPath.base + "/config.json")) {
71
+ hiddenBcContent = fs.readFileSync(confPath.base + "/config.json");
72
+ hiddenBcContent = JSON.parse(hiddenBcContent);
73
+ if (!hiddenBcContent)
74
+ throw new Error(`Invalid ${confPath.base}/config.json syntax: Unable to parse.`);
75
+ }
76
+ const jsonConfig = JSON.parse(`${learnJsonContent}`);
77
+ if (!jsonConfig)
78
+ throw new Error(`Invalid ${confPath.config} syntax: Unable to parse.`);
79
+ let session;
80
+ // add using id to the installation
81
+ if (!jsonConfig.session) {
82
+ session = Math.floor(Math.random() * 10000000000000000000);
83
+ }
84
+ else {
85
+ session = jsonConfig.session;
86
+ delete jsonConfig.session;
87
+ }
88
+ configObj = deepMerge(hiddenBcContent, {
89
+ config: jsonConfig,
90
+ session: session,
91
+ });
92
+ }
93
+ else {
94
+ throw (0, errors_1.ValidationError)("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");
95
+ }
96
+ configObj = deepMerge(defaults_1.default || {}, configObj, {
97
+ config: {
98
+ grading: grading || ((_a = configObj.config) === null || _a === void 0 ? void 0 : _a.grading),
99
+ configPath: confPath.config,
100
+ },
101
+ });
102
+ if (!configObj.config)
103
+ throw (0, errors_1.InternalError)("Config object not found");
104
+ configObj.config.outputPath = confPath.base + "/dist";
105
+ console_1.default.debug("This is your configuration object: ", Object.assign(Object.assign({}, configObj), { exercises: configObj.exercises ?
106
+ configObj.exercises.map(e => e.slug) :
107
+ [] }));
108
+ // auto detect agent (if possible)
109
+ const codespaces_workspace = getCodespacesNamespace();
110
+ console_1.default.debug("This is the codespace namespace: ", codespaces_workspace);
111
+ if (!configObj.config.editor) {
112
+ configObj.config.editor = {};
113
+ }
114
+ console_1.default.debug("This is the agent, and this should be null an the beginning: ", (_c = (_b = configObj.config) === null || _b === void 0 ? void 0 : _b.editor) === null || _c === void 0 ? void 0 : _c.agent);
115
+ if ((_e = (_d = configObj.config) === null || _d === void 0 ? void 0 : _d.editor) === null || _e === void 0 ? void 0 : _e.agent) {
116
+ if (configObj.config.suggestions) {
117
+ configObj.config.suggestions.agent = configObj.config.editor.agent;
118
+ }
119
+ else {
120
+ configObj.config.suggestions = { agent: configObj.config.editor.agent };
121
+ }
122
+ }
123
+ if (shell.which("gp") && configObj && configObj.config) {
124
+ console_1.default.debug("Gitpod detected");
125
+ configObj.address = getGitpodAddress();
126
+ configObj.config.publicUrl = `https://${configObj.config.port}-${(_f = configObj.address) === null || _f === void 0 ? void 0 : _f.slice(8)}`;
127
+ configObj.config.editor.agent = "vscode";
128
+ }
129
+ else if (configObj.config && Boolean(codespaces_workspace)) {
130
+ console_1.default.debug("Codespaces detected: ", codespaces_workspace);
131
+ configObj.address = `https://${codespaces_workspace}.github.dev`;
132
+ configObj.config.publicUrl = `https://${codespaces_workspace}-${configObj.config.port}.app.github.dev`;
133
+ configObj.config.editor.agent = "vscode";
134
+ }
135
+ else {
136
+ console_1.default.debug("Neither Gitpod nor Codespaces detected, using localhost.");
137
+ configObj.address = `http://localhost:${configObj.config.port}`;
138
+ configObj.config.publicUrl = `http://localhost:${configObj.config.port}`;
139
+ configObj.config.editor.agent =
140
+ process.env.TERM_PROGRAM === "vscode" ? "vscode" : "os";
141
+ }
142
+ if (configObj.config.editor.agent === "vscode" && isCodeCommandAvailable()) {
143
+ const extensionName = "learn-pack.learnpack-vscode";
144
+ if (!isExtensionInstalled(extensionName)) {
145
+ console_1.default.info("The LearnPack extension is not installed, trying to install it automatically.");
146
+ if (!installExtension(extensionName)) {
147
+ configObj.config.warnings = {
148
+ extension: EXTENSION_INSTALLATION_STEPS,
149
+ };
150
+ console_1.default.warning("The LearnPack extension is not installed and this tutorial is meant to be run inside VSCode. Please install the LearnPack extension.");
151
+ }
152
+ }
153
+ }
154
+ if (configObj.config.suggestions &&
155
+ configObj.config.editor &&
156
+ configObj.config.suggestions.agent !== null &&
157
+ configObj.config.suggestions.agent !== configObj.config.editor.agent) {
158
+ console_1.default.warning(`The suggested agent "${configObj.config.suggestions.agent}" is different from the detected agent "${configObj.config.editor.agent}"`);
159
+ try {
160
+ configObj.config.warnings = Object.assign(Object.assign({}, configObj.config.warnings), { agent: buildAgentWarning(configObj.config.editor.agent, configObj.config.suggestions.agent) });
161
+ }
162
+ catch (_g) {
163
+ console_1.default.error("IMPOSSIBLE TO SET WARNING");
164
+ }
165
+ }
166
+ if (configObj.config && !configObj.config.publicUrl)
167
+ configObj.config.publicUrl = `${configObj.address}:${configObj.config.port}`;
168
+ if (configObj.config && !configObj.config.editor.mode && mode) {
169
+ configObj.config.editor.mode = mode;
170
+ }
171
+ configObj.config.editor.mode =
172
+ process.env.TERM_PROGRAM === "vscode" ? "extension" : "preview";
173
+ if (version)
174
+ configObj.config.editor.version = version;
175
+ else if (configObj.config.editor.version === null) {
176
+ console_1.default.debug("Config version not found, downloading default.");
177
+ const resp = await fetch("https://raw.githubusercontent.com/learnpack/ide/master/package.json");
178
+ const packageJSON = await resp.json();
179
+ configObj.config.editor.version = packageJSON.version || "4.0.2";
180
+ }
181
+ configObj.config.dirPath = "./" + confPath.base;
182
+ configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./";
183
+ if (configObj.config.variables) {
184
+ const allowedCommands = new Set(["ls", "pwd", "echo", "node", "python"]);
185
+ const variableKeys = Object.keys(configObj.config.variables);
186
+ const promises = [];
187
+ for (const v of variableKeys) {
188
+ if (Object.prototype.hasOwnProperty.call(configObj.config.variables, v)) {
189
+ const variable = configObj.config.variables[v];
190
+ if (typeof variable === "string") {
191
+ continue;
192
+ }
193
+ const type = variable.type;
194
+ if (type === "command") {
195
+ const promise = (async () => {
196
+ try {
197
+ // Validate the command
198
+ if (!allowedCommands.has(variable.source.split(" ")[0])) {
199
+ throw new Error("Command not allowed");
200
+ }
201
+ // Execute the code and replace the value
202
+ const { stdout } = await execAsync(variable.source);
203
+ if (!configObj.config || !configObj.config.variables)
204
+ return;
205
+ configObj.config.variables[v] = stdout.trim();
206
+ }
207
+ catch (error) {
208
+ console.error(`Error executing code: ${error}`);
209
+ }
210
+ })();
211
+ promises.push(promise);
212
+ }
213
+ else if (type === "string") {
214
+ configObj.config.variables[v] = variable.source;
215
+ }
216
+ }
217
+ }
218
+ await Promise.all(promises);
219
+ }
220
+ return {
221
+ validLanguages: {},
222
+ get: () => {
223
+ return configObj;
224
+ },
225
+ validateEngine: function (language, server, socket) {
226
+ // eslint-disable-next-line
227
+ const alias = (_l) => {
228
+ const map = {
229
+ python3: "python",
230
+ };
231
+ if (map[_l])
232
+ return map[_l];
233
+ return _l;
234
+ };
235
+ // decode aliases
236
+ language = alias(language);
237
+ if (this.validLanguages[language])
238
+ return true;
239
+ console_1.default.debug(`Validating engine for ${language} compilation`);
240
+ let result = shell.exec("learnpack plugins", { silent: true });
241
+ if (result.code === 0 &&
242
+ result.stdout.includes(`@learnpack/${language}`)) {
243
+ this.validLanguages[language] = true;
244
+ return true;
245
+ }
246
+ console_1.default.info(`Language engine for ${language} not found, installing...`);
247
+ // Install the compiler in their new versions
248
+ result = shell.exec(`learnpack plugins:install @learnpack/${language}`, {
249
+ silent: true,
250
+ });
251
+ if (result.code === 0) {
252
+ socket.log("compiling", "Installing the python compiler, you will have to reset the exercises after installation by writing on your terminal: $ learnpack run");
253
+ console_1.default.info(`Successfully installed the ${language} exercise engine, \n please start learnpack again by running the following command: \n ${chalk.white("$ learnpack start")}\n\n `);
254
+ server.terminate();
255
+ return false;
256
+ }
257
+ this.validLanguages[language] = false;
258
+ socket.error(`Error installing ${language} exercise engine`);
259
+ console_1.default.error(`Error installing ${language} exercise engine`);
260
+ console_1.default.log(result.stdout);
261
+ throw (0, errors_1.InternalError)(`Error installing ${language} exercise engine`);
262
+ },
263
+ logout: () => {
264
+ if (configObj.config) {
265
+ (0, file_1.rmSync)(configObj.config.dirPath + "/.session");
266
+ }
267
+ },
268
+ clean: () => {
269
+ if (configObj.config) {
270
+ if (configObj.config.outputPath) {
271
+ (0, file_1.rmSync)(configObj.config.outputPath);
272
+ }
273
+ (0, file_1.rmSync)(configObj.config.dirPath + "/_app");
274
+ (0, file_1.rmSync)(configObj.config.dirPath + "/reports");
275
+ (0, file_1.rmSync)(configObj.config.dirPath + "/.session");
276
+ (0, file_1.rmSync)(configObj.config.dirPath + "/resets");
277
+ // clean tag gz
278
+ if (fs.existsSync(configObj.config.dirPath + "/app.tar.gz"))
279
+ fs.unlinkSync(configObj.config.dirPath + "/app.tar.gz");
280
+ if (fs.existsSync(configObj.config.dirPath + "/config.json"))
281
+ fs.unlinkSync(configObj.config.dirPath + "/config.json");
282
+ if (fs.existsSync(configObj.config.dirPath + "/telemetry.json"))
283
+ fs.unlinkSync(configObj.config.dirPath + "/telemetry.json");
284
+ if (fs.existsSync(configObj.config.dirPath + "/vscode_queue.json"))
285
+ fs.unlinkSync(configObj.config.dirPath + "/vscode_queue.json");
286
+ }
287
+ },
288
+ getExercise: slug => {
289
+ console_1.default.debug("ExercisePath Slug", slug);
290
+ const exercise = (configObj.exercises || []).find(ex => ex.slug === slug);
291
+ if (!exercise)
292
+ throw (0, errors_1.ValidationError)(`Exercise ${slug} not found`);
293
+ return exercise;
294
+ },
295
+ getAllExercises: () => {
296
+ return configObj.exercises;
297
+ },
298
+ startExercise: function (slug) {
299
+ const exercise = this.getExercise(slug);
300
+ // set config.json with current exercise
301
+ configObj.currentExercise = exercise.slug;
302
+ this.save();
303
+ // eslint-disable-next-line
304
+ exercise.files.forEach((f) => {
305
+ if (configObj.config) {
306
+ const _path = configObj.config.outputPath + "/" + f.name;
307
+ if (f.hidden === false && fs.existsSync(_path))
308
+ fs.unlinkSync(_path);
309
+ }
310
+ });
311
+ return exercise;
312
+ },
313
+ noCurrentExercise: function () {
314
+ configObj.currentExercise = null;
315
+ this.save();
316
+ },
317
+ reset: slug => {
318
+ var _a;
319
+ if (configObj.config &&
320
+ !fs.existsSync(`${configObj.config.dirPath}/resets/` + slug))
321
+ throw (0, errors_1.ValidationError)("Could not find the original files for " + slug);
322
+ const exercise = (configObj.exercises || []).find(ex => ex.slug === slug);
323
+ if (!exercise)
324
+ throw (0, errors_1.ValidationError)(`Exercise ${slug} not found on the configuration`);
325
+ if (configObj.config) {
326
+ for (const fileName of fs.readdirSync(`${configObj.config.dirPath}/resets/${slug}/`)) {
327
+ const content = fs.readFileSync(`${(_a = configObj.config) === null || _a === void 0 ? void 0 : _a.dirPath}/resets/${slug}/${fileName}`);
328
+ fs.writeFileSync(`${exercise.path}/${fileName}`, content);
329
+ }
330
+ }
331
+ },
332
+ buildIndex: function () {
333
+ var _a, _b;
334
+ console_1.default.info("Building the exercise index...");
335
+ const isDirectory = (source) => {
336
+ var _a;
337
+ const name = path.basename(source);
338
+ if (name === path.basename(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.dirPath) || ""))
339
+ return false;
340
+ // ignore folders that start with a dot
341
+ if (name.charAt(0) === "." || name.charAt(0) === "_")
342
+ return false;
343
+ return fs.lstatSync(source).isDirectory();
344
+ };
345
+ const getDirectories = (source) => fs
346
+ .readdirSync(source)
347
+ .map(name => path.join(source, name))
348
+ .filter(isDirectory);
349
+ // add the .learn folder
350
+ if (!fs.existsSync(confPath.base))
351
+ fs.mkdirSync(confPath.base);
352
+ // add the outout folder where webpack will publish the the html/css/js files
353
+ if (configObj.config &&
354
+ configObj.config.outputPath &&
355
+ !fs.existsSync(configObj.config.outputPath))
356
+ fs.mkdirSync(configObj.config.outputPath);
357
+ // TODO: we could use npm library front-mater to read the title of the exercises from the README.md
358
+ const grupedByDirectory = getDirectories(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.exercisesPath) || "");
359
+ configObj.exercises =
360
+ grupedByDirectory.length > 0 ?
361
+ grupedByDirectory.map((path, position) => (0, exercise_1.exercise)(path, position, configObj)) :
362
+ [(0, exercise_1.exercise)(((_b = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _b === void 0 ? void 0 : _b.exercisesPath) || "", 0, configObj)];
363
+ this.save();
364
+ },
365
+ watchIndex: function (onChange) {
366
+ var _a;
367
+ if (configObj.config && !configObj.config.exercisesPath)
368
+ throw (0, errors_1.ValidationError)("No exercises directory to watch: " + configObj.config.exercisesPath);
369
+ this.buildIndex();
370
+ (0, watcher_1.default)(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.exercisesPath) || "", onChange)
371
+ .then(() => {
372
+ console_1.default.debug("Changes detected on your exercises");
373
+ this.buildIndex();
374
+ // if (onChange) onChange(filename);
375
+ })
376
+ .catch(error => {
377
+ throw error;
378
+ });
379
+ },
380
+ save: () => {
381
+ console_1.default.debug("Saving configuration with: ", configObj);
382
+ // remove the duplicates form the actions array
383
+ // configObj.config.actions = [...new Set(configObj.config.actions)];
384
+ if (configObj.config) {
385
+ configObj.config.translations = [
386
+ ...new Set(configObj.config.translations),
387
+ ];
388
+ fs.writeFileSync(configObj.config.dirPath + "/config.json", JSON.stringify(configObj, null, 4));
389
+ }
390
+ },
391
+ };
392
+ };
393
+ function deepMerge(...sources) {
394
+ let acc = {};
395
+ for (const source of sources) {
396
+ if (Array.isArray(source)) {
397
+ if (!Array.isArray(acc)) {
398
+ acc = [];
399
+ }
400
+ acc = [...source];
401
+ }
402
+ else if (source instanceof Object) {
403
+ // eslint-disable-next-line
404
+ for (let [key, value] of Object.entries(source)) {
405
+ if (value instanceof Object && key in acc) {
406
+ value = deepMerge(acc[key], value);
407
+ }
408
+ if (value !== undefined)
409
+ acc = Object.assign(Object.assign({}, acc), { [key]: value });
410
+ }
411
+ }
412
+ }
413
+ return acc;
414
+ }
415
+ const buildAgentWarning = (current, suggested) => {
411
416
  const message = `# Agent mismatch!\n
412
417
 
413
418
  In LearnPack, the agent is in charge of running the LearnPack interface.
@@ -417,11 +422,11 @@ You're currently using LearnPack through \`${current}\` but the suggested agent
417
422
  We recommend strongly recommend changing your agent.
418
423
 
419
424
  ${stepsToChangeAgent(suggested)}
420
- `;
421
- return message;
422
- };
423
- const stepsToChangeAgent = (agent) => {
424
- if (agent === "vscode") {
425
+ `;
426
+ return message;
427
+ };
428
+ const stepsToChangeAgent = (agent) => {
429
+ if (agent === "vscode") {
425
430
  return `
426
431
  # Steps to Change Agent to VSCode
427
432
 
@@ -444,9 +449,9 @@ const stepsToChangeAgent = (agent) => {
444
449
  \`\`\`
445
450
 
446
451
  We strongly recommend using VSCode for a better learning experience.
447
- `;
448
- }
449
- if (agent === "os") {
452
+ `;
453
+ }
454
+ if (agent === "os") {
450
455
  return `
451
456
  # Steps to Change Agent to OS
452
457
 
@@ -468,31 +473,31 @@ This learning package was designed to run outside of VSCode. We strongly recomme
468
473
  \`\`\`
469
474
 
470
475
  We strongly recommend running the package independently for a better learning experience.
471
- `;
472
- }
473
- return "";
474
- };
475
- /**
476
- * Checks if the 'code' command is available in the terminal.
477
- *
478
- * @returns {boolean} True if the 'code' command is available, false otherwise.
479
- */
480
- const isCodeCommandAvailable = () => {
481
- return shell.which("code") !== null;
482
- };
483
- /**
484
- * Checks if a specific VSCode extension is installed.
485
- *
486
- * @param {string} extensionName - The name of the extension to check.
487
- * @returns {boolean} True if the extension is installed, false otherwise.
488
- */
489
- const isExtensionInstalled = (extensionName) => {
490
- if (!isCodeCommandAvailable()) {
491
- throw new Error("The 'code' command is not available in the terminal. Please ensure that VSCode is installed and the 'code' command is added to your PATH.");
492
- }
493
- const result = shell.exec("code --list-extensions", { silent: true });
494
- return result.stdout.split("\n").includes(extensionName);
495
- };
476
+ `;
477
+ }
478
+ return "";
479
+ };
480
+ /**
481
+ * Checks if the 'code' command is available in the terminal.
482
+ *
483
+ * @returns {boolean} True if the 'code' command is available, false otherwise.
484
+ */
485
+ const isCodeCommandAvailable = () => {
486
+ return shell.which("code") !== null;
487
+ };
488
+ /**
489
+ * Checks if a specific VSCode extension is installed.
490
+ *
491
+ * @param {string} extensionName - The name of the extension to check.
492
+ * @returns {boolean} True if the extension is installed, false otherwise.
493
+ */
494
+ const isExtensionInstalled = (extensionName) => {
495
+ if (!isCodeCommandAvailable()) {
496
+ throw new Error("The 'code' command is not available in the terminal. Please ensure that VSCode is installed and the 'code' command is added to your PATH.");
497
+ }
498
+ const result = shell.exec("code --list-extensions", { silent: true });
499
+ return result.stdout.split("\n").includes(extensionName);
500
+ };
496
501
  const EXTENSION_INSTALLATION_STEPS = `
497
502
  # Steps to Install LearnPack VSCode Extension
498
503
 
@@ -518,19 +523,19 @@ const EXTENSION_INSTALLATION_STEPS = `
518
523
  - If the extension is listed, it means the installation was successful.
519
524
 
520
525
  We strongly recommend using the LearnPack extension in VSCode for a better learning experience.
521
- `;
522
- /**
523
- * Installs the LearnPack VSCode extension if the 'code' command is available.
524
- *
525
- * @param {string} extensionName - The name of the extension to install.
526
- * @returns {boolean} True if the installation was successful, false otherwise.
527
- */
528
- const installExtension = (extensionName) => {
529
- if (!isCodeCommandAvailable()) {
530
- throw new Error("The 'code' command is not available in the terminal. Please ensure that VSCode is installed and the 'code' command is added to your PATH.");
531
- }
532
- const result = shell.exec(`code --install-extension ${extensionName}`, {
533
- silent: true,
534
- });
535
- return result.code === 0;
536
- };
526
+ `;
527
+ /**
528
+ * Installs the LearnPack VSCode extension if the 'code' command is available.
529
+ *
530
+ * @param {string} extensionName - The name of the extension to install.
531
+ * @returns {boolean} True if the installation was successful, false otherwise.
532
+ */
533
+ const installExtension = (extensionName) => {
534
+ if (!isCodeCommandAvailable()) {
535
+ throw new Error("The 'code' command is not available in the terminal. Please ensure that VSCode is installed and the 'code' command is added to your PATH.");
536
+ }
537
+ const result = shell.exec(`code --install-extension ${extensionName}`, {
538
+ silent: true,
539
+ });
540
+ return result.code === 0;
541
+ };