@learnpack/learnpack 2.1.53 → 2.1.56

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,320 +1,358 @@
1
- // import path from "path";
2
- import { flags } from "@oclif/command"
3
-
4
- import SessionCommand from "../utils/SessionCommand"
5
- import Console from "../utils/console"
6
- import socket from "../managers/socket"
7
- import TelemetryManager, { TStep } from "../managers/telemetry"
8
- import createServer from "../managers/server"
9
- import queue from "../utils/fileQueue"
10
- import {
11
- decompress,
12
- downloadEditor,
13
- checkIfDirectoryExists,
14
- } from "../managers/file"
15
- import { prioritizeHTMLFile } from "../utils/misc"
16
-
17
- import { IGitpodData } from "../models/gitpod-data"
18
- import { IExercise, IExerciseData } from "../models/exercise-obj"
19
- import { eventManager } from "../utils/osOperations"
20
- import { cli } from "cli-ux"
21
- import { TOpenWindowData } from "../models/socket"
22
- import {
23
- checkNotInstalledPlugins,
24
- checkNotInstalledDependencies,
25
- } from "../utils/checkNotInstalled"
26
-
27
- export default class StartCommand extends SessionCommand {
28
- static description = "Runs a small server with all the exercise instructions"
29
-
30
- static flags = {
31
- ...SessionCommand.flags,
32
- port: flags.string({ char: "p", description: "server port" }),
33
- host: flags.string({ char: "h", description: "server host" }),
34
- disableGrading: flags.boolean({
35
- char: "D",
36
- description: "disble grading functionality",
37
- default: false,
38
- }),
39
- // disableGrading: flags.boolean({char: 'dg', description: 'disble grading functionality', default: false }),
40
- watch: flags.boolean({
41
- char: "w",
42
- description: "Watch for file changes",
43
- default: false,
44
- }),
45
- editor: flags.string({
46
- char: "e",
47
- description: "[preview, extension]",
48
- options: ["extension", "preview"],
49
- }),
50
- version: flags.string({
51
- char: "v",
52
- description: "E.g: 1.0.1",
53
- default: undefined,
54
- }),
55
- grading: flags.string({
56
- char: "g",
57
- description: "[isolated, incremental]",
58
- options: ["isolated", "incremental"],
59
- }),
60
- debug: flags.boolean({
61
- char: "d",
62
- description: "debugger mode for more verbage",
63
- default: false,
64
- }),
65
- }
66
-
67
- // 🛑 IMPORTANT
68
- // Every command that will use the configManager needs this init method
69
- async init() {
70
- const { flags } = this.parse(StartCommand)
71
- await this.initSession(flags)
72
- }
73
-
74
- async run() {
75
- // get configuration object
76
- const configObject = this.configManager?.get()
77
-
78
- // TODO: console.log(this.config.platform); get the platfrom from Oclif
79
- const hasXDG = await eventManager.checkXDGInstalled()
80
-
81
- const installedPlugins = this.config.plugins.map(plugin => {
82
- return `${plugin.pjson.name}`
83
- })
84
-
85
- if (configObject) {
86
- const { config } = configObject
87
- // build exerises
88
- this.configManager?.buildIndex()
89
-
90
- Console.debug(
91
- `Grading: ${config?.grading} ${
92
- config?.disabledActions?.includes("test") ? "(disabled)" : ""
93
- }, editor: ${config?.editor.mode} ${config?.editor.version}, for ${
94
- Array.isArray(configObject?.exercises) ?
95
- configObject?.exercises.length :
96
- 0
97
- } exercises found`
98
- )
99
-
100
- const neededPlugins = await checkNotInstalledPlugins(
101
- configObject?.exercises || [],
102
- installedPlugins,
103
- this
104
- )
105
-
106
- const allDepsInstalled = await checkNotInstalledDependencies(
107
- neededPlugins.needed
108
- )
109
- if (!allDepsInstalled) {
110
- this.exit(1)
111
- }
112
-
113
- const appAlreadyExists = checkIfDirectoryExists(
114
- `${config?.dirPath}/_app`
115
- )
116
-
117
- if (!appAlreadyExists) {
118
- // download app and decompress
119
- await downloadEditor(
120
- config?.editor.version,
121
- `${config?.dirPath}/app.tar.gz`
122
- )
123
-
124
- Console.info("Decompressing LearnPack UI, this may take a minute...")
125
- await decompress(
126
- `${config?.dirPath}/app.tar.gz`,
127
- `${config?.dirPath}/_app/`
128
- )
129
- }
130
-
131
- // listen to socket commands
132
- if (config && this.configManager) {
133
- const server = await createServer(
134
- configObject,
135
- this.configManager,
136
- process.env.NODE_ENV === "test"
137
- )
138
- server.setMaxListeners(25)
139
-
140
- // I should call a method to get the EventListener
141
- const dispatcher = queue.dispatcher({
142
- create: true,
143
- path: `${config.dirPath}/vscode_queue.json`,
144
- })
145
-
146
- if (configObject.exercises) {
147
- const agent = configObject.config?.editor.agent || ""
148
- const path = configObject.config?.dirPath || ""
149
- const tutorialSlug = configObject.config?.slug || ""
150
-
151
- const steps = configObject.exercises.map(
152
- (e: IExercise, index): TStep => ({
153
- slug: e.slug,
154
- position: e.position || index,
155
- files: e.files,
156
- ai_interactions: [],
157
- compilations: [],
158
- tests: [],
159
- is_testeable: e.graded || false,
160
- })
161
- )
162
- if (path && steps.length > 0) {
163
- TelemetryManager.start(agent, steps, path, tutorialSlug)
164
- }
165
-
166
- if (config.telemetry) {
167
- TelemetryManager.urls = config.telemetry
168
- }
169
- }
170
-
171
- socket.start(config, server, false)
172
-
173
- socket.on("open", (data: IGitpodData) => {
174
- Console.debug("Opening these files: ", data)
175
-
176
- const files = prioritizeHTMLFile(data.files)
177
-
178
- if (config.editor.agent === "os") {
179
- eventManager.enqueue(dispatcher.events.OPEN_FILES, files)
180
- } else {
181
- dispatcher.enqueue(dispatcher.events.OPEN_FILES, files)
182
- }
183
-
184
- socket.ready("Ready to compile...")
185
- })
186
-
187
- socket.on("open_window", (data: TOpenWindowData) => {
188
- Console.debug("Opening window: ", data)
189
- // cli.open(data.url); This uses XDG under the ground
190
- if (config.os !== "linux" || (config.os === "linux" && hasXDG)) {
191
- eventManager.enqueue(dispatcher.events.OPEN_WINDOW, data)
192
- } else {
193
- dispatcher.enqueue(dispatcher.events.OPEN_WINDOW, data)
194
- }
195
-
196
- socket.log("open_window", "", undefined, data.url)
197
- })
198
-
199
- socket.on("open_terminal", (exercise: IExerciseData) => {
200
- Console.debug("Opening terminal: ", exercise)
201
- // eventManager.enqueue(dispatcher.events.OPEN_TERMINAL, exercise);
202
- if (config.editor.agent === "vscode") {
203
- dispatcher.enqueue(dispatcher.events.OPEN_TERMINAL, exercise)
204
- }
205
- })
206
-
207
- socket.on("reset", (exercise: IExerciseData) => {
208
- try {
209
- this.configManager?.reset(exercise.exerciseSlug)
210
- socket.ready("Ready to compile...")
211
- } catch (error) {
212
- socket.error(
213
- "compiler-error",
214
- (error as TypeError).message ||
215
- "There was an error reseting the exercise"
216
- )
217
- setTimeout(() => socket.ready("Ready to compile..."), 2000)
218
- }
219
- })
220
-
221
- socket.on("build", async (data: IExerciseData) => {
222
- const exercise = this.configManager?.getExercise(data.exerciseSlug)
223
-
224
- if (!exercise?.language) {
225
- socket.error(
226
- "compiler-error",
227
- "Impossible to detect language to build for " +
228
- data.exerciseSlug +
229
- "..."
230
- )
231
- return
232
- }
233
-
234
- socket.log(
235
- "compiling",
236
- "Building exercise " +
237
- data.exerciseSlug +
238
- " with " +
239
- exercise.language +
240
- "..."
241
- )
242
- await this.config.runHook("action", {
243
- action: "compile",
244
- socket,
245
- configuration: config,
246
- exercise,
247
- telemetry: TelemetryManager,
248
- })
249
- })
250
-
251
- socket.on("ai_interaction", (data: any) => {
252
- const { stepPosition, event, eventData } = data
253
- TelemetryManager.registerStepEvent(stepPosition, event, eventData)
254
- })
255
-
256
- socket.on("test", async (data: IExerciseData) => {
257
- const exercise = this.configManager?.getExercise(data.exerciseSlug)
258
-
259
- if (!exercise?.language) {
260
- socket.error(
261
- "compiler-error",
262
- "Impossible to detect engine language for testing for " +
263
- data.exerciseSlug +
264
- "..."
265
- )
266
- return
267
- }
268
-
269
- if (
270
- config?.disabledActions!.includes("test") ||
271
- config?.disableGrading
272
- ) {
273
- socket.ready("Grading is disabled on configuration")
274
- return true
275
- }
276
-
277
- socket.log(
278
- "testing",
279
- "Testing your exercise using the " + exercise.language + " engine."
280
- )
281
-
282
- await this.config.runHook("action", {
283
- action: "test",
284
- socket,
285
- configuration: config,
286
- exercise,
287
- telemetry: TelemetryManager,
288
- })
289
-
290
- this.configManager?.save()
291
-
292
- return true
293
- })
294
-
295
- const terminate = async () => {
296
- Console.error("Terminating Learnpack...")
297
- await TelemetryManager.submit()
298
- this.configManager?.noCurrentExercise()
299
- dispatcher.enqueue(dispatcher.events.END)
300
- process.exit()
301
- }
302
-
303
- server.on("close", terminate)
304
- process.on("SIGINT", terminate)
305
- process.on("SIGTERM", terminate)
306
- process.on("SIGHUP", terminate)
307
-
308
- // finish the server startup
309
- setTimeout(() => dispatcher.enqueue(dispatcher.events.RUNNING), 1000)
310
-
311
- // start watching for file changes
312
- if (StartCommand.flags.watch)
313
- this.configManager.watchIndex(_filename => {
314
- // Instead of reloading with socket.reload(), I just notify the frontend for the file change
315
- socket.emit("file_change", "ready", _filename)
316
- })
317
- }
318
- }
319
- }
320
- }
1
+ // import path from "path";
2
+ import { flags } from "@oclif/command"
3
+ import * as fs from "fs"
4
+ import * as path from "path"
5
+
6
+ import SessionCommand from "../utils/SessionCommand"
7
+ import Console from "../utils/console"
8
+ import socket from "../managers/socket"
9
+ import TelemetryManager, { TStep } from "../managers/telemetry"
10
+ import createServer from "../managers/server"
11
+ import queue from "../utils/fileQueue"
12
+ import {
13
+ decompress,
14
+ downloadEditor,
15
+ checkIfDirectoryExists,
16
+ } from "../managers/file"
17
+ import { prioritizeHTMLFile } from "../utils/misc"
18
+
19
+ import { IGitpodData } from "../models/gitpod-data"
20
+ import { IExercise, IExerciseData } from "../models/exercise-obj"
21
+ import { eventManager } from "../utils/osOperations"
22
+ import { cli } from "cli-ux"
23
+ import { TOpenWindowData } from "../models/socket"
24
+ import {
25
+ checkNotInstalledPlugins,
26
+ checkNotInstalledDependencies,
27
+ } from "../utils/checkNotInstalled"
28
+
29
+ export default class StartCommand extends SessionCommand {
30
+ static description = "Runs a small server with all the exercise instructions"
31
+
32
+ static flags = {
33
+ ...SessionCommand.flags,
34
+ port: flags.string({ char: "p", description: "server port" }),
35
+ host: flags.string({ char: "h", description: "server host" }),
36
+ disableGrading: flags.boolean({
37
+ char: "D",
38
+ description: "disble grading functionality",
39
+ default: false,
40
+ }),
41
+ // disableGrading: flags.boolean({char: 'dg', description: 'disble grading functionality', default: false }),
42
+ watch: flags.boolean({
43
+ char: "w",
44
+ description: "Watch for file changes",
45
+ default: false,
46
+ }),
47
+ editor: flags.string({
48
+ char: "e",
49
+ description: "[preview, extension]",
50
+ options: ["extension", "preview"],
51
+ }),
52
+ version: flags.string({
53
+ char: "v",
54
+ description: "E.g: 1.0.1",
55
+ default: undefined,
56
+ }),
57
+ grading: flags.string({
58
+ char: "g",
59
+ description: "[isolated, incremental]",
60
+ options: ["isolated", "incremental"],
61
+ }),
62
+ debug: flags.boolean({
63
+ char: "d",
64
+ description: "debugger mode for more verbage",
65
+ default: false,
66
+ }),
67
+ }
68
+
69
+ // 🛑 IMPORTANT
70
+ // Every command that will use the configManager needs this init method
71
+ async init() {
72
+ const { flags } = this.parse(StartCommand)
73
+ await this.initSession(flags)
74
+ }
75
+
76
+ async run() {
77
+ // get configuration object
78
+ const configObject = this.configManager?.get()
79
+
80
+ // TODO: console.log(this.config.platform); get the platfrom from Oclif
81
+ const hasXDG = await eventManager.checkXDGInstalled()
82
+
83
+ const installedPlugins = this.config.plugins.map(plugin => {
84
+ return `${plugin.pjson.name}`
85
+ })
86
+
87
+ if (configObject) {
88
+ const { config } = configObject
89
+ // build exerises
90
+ this.configManager?.buildIndex()
91
+
92
+ Console.debug(
93
+ `Grading: ${config?.grading} ${
94
+ config?.disabledActions?.includes("test") ? "(disabled)" : ""
95
+ }, editor: ${config?.editor.mode} ${config?.editor.version}, for ${
96
+ Array.isArray(configObject?.exercises) ?
97
+ configObject?.exercises.length :
98
+ 0
99
+ } exercises found`
100
+ )
101
+
102
+ const neededPlugins = await checkNotInstalledPlugins(
103
+ configObject?.exercises || [],
104
+ installedPlugins,
105
+ this
106
+ )
107
+
108
+ const allDepsInstalled = await checkNotInstalledDependencies(
109
+ neededPlugins.needed
110
+ )
111
+ if (!allDepsInstalled) {
112
+ this.exit(1)
113
+ }
114
+
115
+ const appAlreadyExists = checkIfDirectoryExists(
116
+ `${config?.dirPath}/_app`
117
+ )
118
+
119
+ if (!appAlreadyExists) {
120
+ // download app and decompress
121
+ await downloadEditor(
122
+ config?.editor.version,
123
+ `${config?.dirPath}/app.tar.gz`
124
+ )
125
+
126
+ Console.info("Decompressing LearnPack UI, this may take a minute...")
127
+ await decompress(
128
+ `${config?.dirPath}/app.tar.gz`,
129
+ `${config?.dirPath}/_app/`
130
+ )
131
+ }
132
+
133
+ // listen to socket commands
134
+ if (config && this.configManager) {
135
+ const server = await createServer(
136
+ configObject,
137
+ this.configManager,
138
+ process.env.NODE_ENV === "test"
139
+ )
140
+ server.setMaxListeners(25)
141
+
142
+ // I should call a method to get the EventListener
143
+ const dispatcher = queue.dispatcher({
144
+ create: true,
145
+ path: `${config.dirPath}/vscode_queue.json`,
146
+ })
147
+
148
+ if (configObject.exercises) {
149
+ const agent = configObject.config?.editor.agent || ""
150
+ const path = configObject.config?.dirPath || ""
151
+ const tutorialSlug = configObject.config?.slug || ""
152
+
153
+ const steps = configObject.exercises.map(
154
+ (e: IExercise, index): TStep => ({
155
+ slug: e.slug,
156
+ position: e.position || index,
157
+ files: e.files,
158
+ ai_interactions: [],
159
+ compilations: [],
160
+ tests: [],
161
+ is_testeable: e.graded || false,
162
+ })
163
+ )
164
+ if (path && steps.length > 0) {
165
+ TelemetryManager.start(agent, steps, path, tutorialSlug)
166
+ }
167
+
168
+ if (config.telemetry) {
169
+ TelemetryManager.urls = config.telemetry
170
+ }
171
+ }
172
+
173
+ socket.start(config, server, false)
174
+
175
+ socket.on("open", (data: IGitpodData) => {
176
+ Console.debug("Opening these files: ", data)
177
+
178
+ const files = prioritizeHTMLFile(data.files)
179
+
180
+ if (config.editor.agent === "os") {
181
+ eventManager.enqueue(dispatcher.events.OPEN_FILES, files)
182
+ } else {
183
+ dispatcher.enqueue(dispatcher.events.OPEN_FILES, files)
184
+ }
185
+
186
+ socket.ready("Ready to compile...")
187
+ })
188
+
189
+ socket.on("open_window", (data: TOpenWindowData) => {
190
+ Console.debug("Opening window: ", data)
191
+ // cli.open(data.url); This uses XDG under the ground
192
+ if (config.os !== "linux" || (config.os === "linux" && hasXDG)) {
193
+ eventManager.enqueue(dispatcher.events.OPEN_WINDOW, data)
194
+ } else {
195
+ dispatcher.enqueue(dispatcher.events.OPEN_WINDOW, data)
196
+ }
197
+
198
+ socket.log("open_window", "", undefined, data.url)
199
+ })
200
+
201
+ socket.on("open_terminal", (exercise: IExerciseData) => {
202
+ Console.debug("Opening terminal: ", exercise)
203
+ // eventManager.enqueue(dispatcher.events.OPEN_TERMINAL, exercise);
204
+ if (config.editor.agent === "vscode") {
205
+ dispatcher.enqueue(dispatcher.events.OPEN_TERMINAL, exercise)
206
+ }
207
+ })
208
+
209
+ socket.on("reset", (exercise: IExerciseData) => {
210
+ try {
211
+ this.configManager?.reset(exercise.exerciseSlug)
212
+ socket.ready("Ready to compile...")
213
+ } catch (error) {
214
+ socket.error(
215
+ "compiler-error",
216
+ (error as TypeError).message ||
217
+ "There was an error reseting the exercise"
218
+ )
219
+ setTimeout(() => socket.ready("Ready to compile..."), 2000)
220
+ }
221
+ })
222
+
223
+ socket.on("build", async (data: IExerciseData) => {
224
+ const exercise = this.configManager?.getExercise(data.exerciseSlug)
225
+
226
+ if (!exercise?.language) {
227
+ socket.error(
228
+ "compiler-error",
229
+ "Impossible to detect language to build for " +
230
+ data.exerciseSlug +
231
+ "..."
232
+ )
233
+ return
234
+ }
235
+
236
+ socket.log(
237
+ "compiling",
238
+ "Building exercise " +
239
+ data.exerciseSlug +
240
+ " with " +
241
+ exercise.language +
242
+ "..."
243
+ )
244
+ await this.config.runHook("action", {
245
+ action: "compile",
246
+ socket,
247
+ configuration: config,
248
+ exercise,
249
+ telemetry: TelemetryManager,
250
+ })
251
+ })
252
+
253
+ socket.on("ai_interaction", (data: any) => {
254
+ const { stepPosition, event, eventData } = data
255
+ TelemetryManager.registerStepEvent(stepPosition, event, eventData)
256
+ })
257
+
258
+ socket.on("test", async (data: IExerciseData) => {
259
+ const exercise = this.configManager?.getExercise(data.exerciseSlug)
260
+
261
+ if (!exercise?.language) {
262
+ socket.error(
263
+ "compiler-error",
264
+ "Impossible to detect engine language for testing for " +
265
+ data.exerciseSlug +
266
+ "..."
267
+ )
268
+ return
269
+ }
270
+
271
+ if (
272
+ config?.disabledActions!.includes("test") ||
273
+ config?.disableGrading
274
+ ) {
275
+ socket.ready("Grading is disabled on configuration")
276
+ return true
277
+ }
278
+
279
+ socket.log(
280
+ "testing",
281
+ "Testing your exercise using the " + exercise.language + " engine."
282
+ )
283
+
284
+ await this.config.runHook("action", {
285
+ action: "test",
286
+ socket,
287
+ configuration: config,
288
+ exercise,
289
+ telemetry: TelemetryManager,
290
+ })
291
+
292
+ try {
293
+ if (!configObject.config) {
294
+ return
295
+ }
296
+
297
+ const getReportPath = (ext: string) => {
298
+ if (!configObject.config?.dirPath) {
299
+ throw new Error("No directory path found in config")
300
+ }
301
+
302
+ return path.join(
303
+ configObject.config.dirPath,
304
+ "reports",
305
+ `${exercise.slug}`,
306
+ `report.${ext}`
307
+ )
308
+ }
309
+
310
+ const markdownReportPath = getReportPath("md")
311
+ const textReportPath = getReportPath("txt")
312
+
313
+ if (fs.existsSync(markdownReportPath)) {
314
+ let reportContent = ""
315
+ reportContent = fs.readFileSync(markdownReportPath, "utf8")
316
+ socket.dialog(reportContent)
317
+ }
318
+
319
+ if (fs.existsSync(textReportPath)) {
320
+ let reportContent = ""
321
+ reportContent = fs.readFileSync(textReportPath, "utf8")
322
+ socket.dialog(reportContent)
323
+ }
324
+ } catch (error) {
325
+ Console.debug("Error finding report for exercise.slug", error)
326
+ }
327
+
328
+ this.configManager?.save()
329
+
330
+ return true
331
+ })
332
+
333
+ const terminate = async () => {
334
+ Console.error("Terminating Learnpack...")
335
+ await TelemetryManager.submit()
336
+ this.configManager?.noCurrentExercise()
337
+ dispatcher.enqueue(dispatcher.events.END)
338
+ process.exit()
339
+ }
340
+
341
+ server.on("close", terminate)
342
+ process.on("SIGINT", terminate)
343
+ process.on("SIGTERM", terminate)
344
+ process.on("SIGHUP", terminate)
345
+
346
+ // finish the server startup
347
+ setTimeout(() => dispatcher.enqueue(dispatcher.events.RUNNING), 1000)
348
+
349
+ // start watching for file changes
350
+ if (StartCommand.flags.watch)
351
+ this.configManager.watchIndex(_filename => {
352
+ // Instead of reloading with socket.reload(), I just notify the frontend for the file change
353
+ socket.emit("file_change", "ready", _filename)
354
+ })
355
+ }
356
+ }
357
+ }
358
+ }