@learnpack/learnpack 5.0.6 → 5.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +17 -17
  2. package/bin/run +17 -17
  3. package/bin/run.cmd +3 -3
  4. package/lib/commands/audit.js +30 -21
  5. package/lib/commands/clean.js +3 -3
  6. package/lib/commands/download.js +3 -3
  7. package/lib/commands/init.js +29 -4
  8. package/lib/commands/login.js +3 -3
  9. package/lib/commands/logout.js +3 -3
  10. package/lib/utils/rigoActions.d.ts +5 -0
  11. package/lib/utils/rigoActions.js +15 -1
  12. package/oclif.manifest.json +1 -1
  13. package/package.json +152 -152
  14. package/src/commands/audit.ts +449 -443
  15. package/src/commands/clean.ts +29 -29
  16. package/src/commands/download.ts +61 -61
  17. package/src/commands/init.ts +40 -12
  18. package/src/commands/login.ts +42 -42
  19. package/src/commands/logout.ts +43 -43
  20. package/src/commands/test.ts +85 -85
  21. package/src/index.ts +1 -1
  22. package/src/managers/config/allowed_files.ts +29 -29
  23. package/src/managers/gitpod.ts +84 -84
  24. package/src/managers/server/index.ts +78 -78
  25. package/src/managers/telemetry.ts +353 -353
  26. package/src/managers/test.ts +83 -83
  27. package/src/models/audit.ts +16 -16
  28. package/src/models/config-manager.ts +23 -23
  29. package/src/models/counter.ts +11 -11
  30. package/src/models/errors.ts +22 -22
  31. package/src/models/exercise-obj.ts +29 -29
  32. package/src/models/file.ts +5 -5
  33. package/src/models/findings.ts +18 -18
  34. package/src/models/flags.ts +10 -10
  35. package/src/models/front-matter.ts +11 -11
  36. package/src/models/gitpod-data.ts +19 -19
  37. package/src/models/language.ts +4 -4
  38. package/src/models/package.ts +7 -7
  39. package/src/models/plugin-config.ts +17 -17
  40. package/src/models/success-types.ts +1 -1
  41. package/src/plugin/command/compile.ts +17 -17
  42. package/src/plugin/command/test.ts +30 -30
  43. package/src/plugin/index.ts +6 -6
  44. package/src/plugin/plugin.ts +94 -94
  45. package/src/plugin/utils.ts +87 -87
  46. package/src/types/node-fetch.d.ts +1 -1
  47. package/src/ui/download.ts +71 -71
  48. package/src/utils/BaseCommand.ts +48 -48
  49. package/src/utils/SessionCommand.ts +43 -43
  50. package/src/utils/audit.ts +393 -393
  51. package/src/utils/errors.ts +117 -117
  52. package/src/utils/exercisesQueue.ts +51 -51
  53. package/src/utils/fileQueue.ts +199 -199
  54. package/src/utils/misc.ts +23 -23
  55. package/src/utils/osOperations.ts +79 -79
  56. package/src/utils/rigoActions.ts +27 -0
  57. package/src/utils/templates/gitignore.txt +19 -19
  58. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +24 -24
  59. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +24 -24
  60. package/src/utils/templates/incremental/.vscode/schema.json +121 -121
  61. package/src/utils/templates/incremental/.vscode/settings.json +13 -13
  62. package/src/utils/templates/incremental/README.ejs +4 -4
  63. package/src/utils/templates/incremental/README.es.ejs +4 -4
  64. package/src/utils/templates/isolated/.vscode/schema.json +121 -121
  65. package/src/utils/templates/isolated/.vscode/settings.json +13 -13
  66. package/src/utils/templates/isolated/README.ejs +4 -4
  67. package/src/utils/templates/isolated/README.es.ejs +4 -4
  68. package/src/utils/templates/no-grading/README.ejs +4 -4
  69. package/src/utils/templates/no-grading/README.es.ejs +4 -4
  70. package/src/utils/validators.ts +18 -18
  71. package/src/utils/watcher.ts +27 -27
@@ -1,353 +1,353 @@
1
- import { IFile } from "../models/file"
2
- import API from "../utils/api"
3
- import Console from "../utils/console"
4
-
5
- const fs = require("fs")
6
-
7
- function createUUID(): string {
8
- return (
9
- Math.random().toString(36).slice(2, 10) +
10
- Math.random().toString(36).slice(2, 10)
11
- )
12
- }
13
-
14
- function stringToBase64(input: string): string {
15
- return Buffer.from(input).toString("base64")
16
- }
17
-
18
- type TCompilationAttempt = {
19
- source_code: string;
20
- stdout: string;
21
- exit_code: number;
22
- starting_at: number;
23
- ending_at: number;
24
- };
25
-
26
- type TTestAttempt = {
27
- source_code: string;
28
- stdout: string;
29
- exit_code: number;
30
- starting_at: number;
31
- ending_at: number;
32
- };
33
-
34
- type TAIInteraction = {
35
- student_message: string;
36
- source_code: string;
37
- ai_response: string;
38
- starting_at: number;
39
- ending_at: number;
40
- };
41
-
42
- export type TStep = {
43
- slug: string;
44
- position: number;
45
- files: IFile[];
46
- is_testeable: boolean;
47
- opened_at?: number; // The time when the step was opened
48
- completed_at?: number; // If the step has tests, the time when all the tests passed, else, the time when the user opens the next step
49
- compilations: TCompilationAttempt[]; // Everytime the user tries to compile the code
50
- tests: TTestAttempt[]; // Everytime the user tries to run the tests
51
- ai_interactions: TAIInteraction[]; // Everytime the user interacts with the AI
52
- };
53
-
54
- type TWorkoutSession = {
55
- started_at: number;
56
- ended_at?: number;
57
- };
58
-
59
- type TStudent = {
60
- token: string;
61
- user_id: string;
62
- email: string;
63
- };
64
-
65
- export interface ITelemetryJSONSchema {
66
- telemetry_id?: string;
67
- user_id?: number | string;
68
- slug: string;
69
- agent?: string;
70
- tutorial_started_at?: number;
71
- last_interaction_at?: number;
72
- steps: Array<TStep>; // The steps should be the same as the exercise
73
- workout_session: TWorkoutSession[]; // It start when the user starts Learnpack, if the last_interaction_at is available, it automatically fills with that
74
- // number and start another session
75
- }
76
-
77
- type TStepEvent = "compile" | "test" | "ai_interaction" | "open_step";
78
-
79
- export type TTelemetryUrls = {
80
- streaming?: string;
81
- batch?: string;
82
- };
83
-
84
- interface ITelemetryManager {
85
- current: ITelemetryJSONSchema | null;
86
- configPath: string | null;
87
- urls: TTelemetryUrls;
88
- salute: (message: string) => void;
89
- start: (
90
- agent: string,
91
- steps: TStep[],
92
- path: string,
93
- tutorialSlug: string
94
- ) => void;
95
- prevStep?: number;
96
- registerStepEvent: (
97
- stepPosition: number,
98
- event: TStepEvent,
99
- data: any
100
- ) => void;
101
- streamEvent: (stepPosition: number, event: string, data: any) => void;
102
- submit: () => Promise<void>;
103
- finishWorkoutSession: () => void;
104
- setStudent: (student: TStudent) => void;
105
- save: () => void;
106
- retrieve: () => Promise<ITelemetryJSONSchema | null>;
107
- }
108
-
109
- const TelemetryManager: ITelemetryManager = {
110
- current: null,
111
- urls: {},
112
- configPath: "",
113
- salute: message => {
114
- Console.info(message)
115
- },
116
-
117
- start: function (agent, steps, path, tutorialSlug) {
118
- this.configPath = path
119
- if (!this.current) {
120
- this.retrieve()
121
- .then(data => {
122
- const prevTelemetry = data
123
- if (prevTelemetry) {
124
- this.current = prevTelemetry
125
- this.finishWorkoutSession()
126
- } else {
127
- this.current = {
128
- telemetry_id: createUUID(),
129
- slug: tutorialSlug,
130
- agent,
131
- tutorial_started_at: Date.now(),
132
- steps,
133
- workout_session: [
134
- {
135
- started_at: Date.now(),
136
- },
137
- ],
138
- }
139
- }
140
-
141
- this.save()
142
- this.submit()
143
- })
144
- .catch(error => {
145
- Console.debug(error)
146
- // Delete the telemetry.json if it exists
147
- fs.unlinkSync(`${this.configPath}/telemetry.json`)
148
- throw new Error(
149
- "There was a problem starting, reload LearnPack\nRun\n$ learnpack start"
150
- )
151
- })
152
- }
153
- },
154
- // verifyStudent: function () {
155
- // if (!this.current) {
156
- // return;
157
- // }
158
-
159
- // if (!this.current.user_id) {
160
-
161
- // }
162
- // },
163
-
164
- setStudent: function (student) {
165
- if (!this.current) {
166
- return
167
- }
168
-
169
- this.current.user_id = student.user_id
170
- this.save()
171
- this.submit()
172
- },
173
- finishWorkoutSession: function () {
174
- if (!this.current) {
175
- return
176
- }
177
-
178
- const lastSession =
179
- this.current?.workout_session[this.current.workout_session.length - 1]
180
- if (
181
- lastSession &&
182
- !lastSession.ended_at &&
183
- this.current?.last_interaction_at
184
- ) {
185
- lastSession.ended_at = this.current.last_interaction_at
186
- this.current.workout_session.push({
187
- started_at: Date.now(),
188
- })
189
- }
190
- },
191
-
192
- registerStepEvent: function (stepPosition, event, data) {
193
- if (!this.current) {
194
- // throw new Error("Telemetry has not been started");
195
- return
196
- }
197
-
198
- const step = this.current.steps[stepPosition]
199
- if (!step) {
200
- return
201
- }
202
-
203
- if (data.source_code) {
204
- data.source_code = stringToBase64(data.source_code)
205
- }
206
-
207
- if (data.stdout) {
208
- data.stdout = stringToBase64(data.stdout)
209
- }
210
-
211
- if (data.stderr) {
212
- data.stderr = stringToBase64(data.stderr)
213
- }
214
-
215
- if (Object.prototype.hasOwnProperty.call(data, "exitCode")) {
216
- data.exit_code = data.exitCode
217
- data.exitCode = undefined
218
- }
219
-
220
- switch (event) {
221
- case "compile":
222
- if (!step.compilations) {
223
- step.compilations = []
224
- }
225
-
226
- step.compilations.push(data)
227
- this.current.steps[stepPosition] = step
228
- break
229
- case "test":
230
- if (!step.tests) {
231
- step.tests = []
232
- }
233
-
234
- // data.stdout =
235
- step.tests.push(data)
236
- if (data.exit_code === 0) {
237
- step.completed_at = Date.now()
238
- }
239
-
240
- this.current.steps[stepPosition] = step
241
- break
242
- case "ai_interaction":
243
- if (!step.ai_interactions) {
244
- step.ai_interactions = []
245
- }
246
-
247
- step.ai_interactions.push(data)
248
- break
249
- case "open_step": {
250
- const now = Date.now()
251
-
252
- if (!step.opened_at) {
253
- step.opened_at = now
254
- this.current.steps[stepPosition] = step
255
- }
256
-
257
- if (this.prevStep || this.prevStep === 0) {
258
- const prevStep = this.current.steps[this.prevStep]
259
- if (!prevStep.is_testeable && !prevStep.completed_at) {
260
- prevStep.completed_at = now
261
- this.current.steps[this.prevStep] = prevStep
262
- }
263
- }
264
-
265
- this.prevStep = stepPosition
266
-
267
- this.submit()
268
- break
269
- }
270
-
271
- default:
272
- throw new Error(`Event type ${event} is not supported`)
273
- }
274
-
275
- this.current.last_interaction_at = Date.now()
276
- this.streamEvent(stepPosition, event, data)
277
- this.save()
278
- },
279
- retrieve: function () {
280
- return new Promise((resolve, reject) => {
281
- fs.readFile(
282
- `${this.configPath}/telemetry.json`,
283
- "utf8",
284
- (err: any, data: any) => {
285
- if (err) {
286
- if (err.code === "ENOENT") {
287
- // File does not exist, resolve with undefined
288
- resolve(null)
289
- } else {
290
- reject(err)
291
- }
292
- } else {
293
- try {
294
- resolve(JSON.parse(data))
295
- } catch (error) {
296
- reject(error)
297
- }
298
- }
299
- }
300
- )
301
- })
302
- },
303
-
304
- submit: async function () {
305
- Console.debug("Submitting telemetry...")
306
-
307
- if (!this.current)
308
- return Promise.resolve()
309
- const url = this.urls.batch
310
- if (!url) {
311
- return
312
- }
313
-
314
- const body = this.current
315
-
316
- API.sendBatchTelemetry(url, body)
317
- },
318
- save: function () {
319
- fs.writeFile(
320
- `${this.configPath}/telemetry.json`,
321
- JSON.stringify(this.current),
322
- (err: any) => {
323
- if (err)
324
- throw err
325
- }
326
- )
327
- },
328
-
329
- streamEvent: async function (stepPosition, event, data) {
330
- if (!this.current)
331
- return
332
-
333
- const url = this.urls.streaming
334
- if (!url) {
335
- return
336
- }
337
-
338
- const stepSlug = this.current.steps[stepPosition].slug
339
-
340
- const body = {
341
- slug: stepSlug,
342
- telemetry_id: this.current.telemetry_id,
343
- user_id: this.current.user_id,
344
- step_position: stepPosition,
345
- event,
346
- data,
347
- }
348
-
349
- API.sendStreamTelemetry(url, body)
350
- },
351
- }
352
-
353
- export default TelemetryManager
1
+ import { IFile } from "../models/file"
2
+ import API from "../utils/api"
3
+ import Console from "../utils/console"
4
+
5
+ const fs = require("fs")
6
+
7
+ function createUUID(): string {
8
+ return (
9
+ Math.random().toString(36).slice(2, 10) +
10
+ Math.random().toString(36).slice(2, 10)
11
+ )
12
+ }
13
+
14
+ function stringToBase64(input: string): string {
15
+ return Buffer.from(input).toString("base64")
16
+ }
17
+
18
+ type TCompilationAttempt = {
19
+ source_code: string;
20
+ stdout: string;
21
+ exit_code: number;
22
+ starting_at: number;
23
+ ending_at: number;
24
+ };
25
+
26
+ type TTestAttempt = {
27
+ source_code: string;
28
+ stdout: string;
29
+ exit_code: number;
30
+ starting_at: number;
31
+ ending_at: number;
32
+ };
33
+
34
+ type TAIInteraction = {
35
+ student_message: string;
36
+ source_code: string;
37
+ ai_response: string;
38
+ starting_at: number;
39
+ ending_at: number;
40
+ };
41
+
42
+ export type TStep = {
43
+ slug: string;
44
+ position: number;
45
+ files: IFile[];
46
+ is_testeable: boolean;
47
+ opened_at?: number; // The time when the step was opened
48
+ completed_at?: number; // If the step has tests, the time when all the tests passed, else, the time when the user opens the next step
49
+ compilations: TCompilationAttempt[]; // Everytime the user tries to compile the code
50
+ tests: TTestAttempt[]; // Everytime the user tries to run the tests
51
+ ai_interactions: TAIInteraction[]; // Everytime the user interacts with the AI
52
+ };
53
+
54
+ type TWorkoutSession = {
55
+ started_at: number;
56
+ ended_at?: number;
57
+ };
58
+
59
+ type TStudent = {
60
+ token: string;
61
+ user_id: string;
62
+ email: string;
63
+ };
64
+
65
+ export interface ITelemetryJSONSchema {
66
+ telemetry_id?: string;
67
+ user_id?: number | string;
68
+ slug: string;
69
+ agent?: string;
70
+ tutorial_started_at?: number;
71
+ last_interaction_at?: number;
72
+ steps: Array<TStep>; // The steps should be the same as the exercise
73
+ workout_session: TWorkoutSession[]; // It start when the user starts Learnpack, if the last_interaction_at is available, it automatically fills with that
74
+ // number and start another session
75
+ }
76
+
77
+ type TStepEvent = "compile" | "test" | "ai_interaction" | "open_step";
78
+
79
+ export type TTelemetryUrls = {
80
+ streaming?: string;
81
+ batch?: string;
82
+ };
83
+
84
+ interface ITelemetryManager {
85
+ current: ITelemetryJSONSchema | null;
86
+ configPath: string | null;
87
+ urls: TTelemetryUrls;
88
+ salute: (message: string) => void;
89
+ start: (
90
+ agent: string,
91
+ steps: TStep[],
92
+ path: string,
93
+ tutorialSlug: string
94
+ ) => void;
95
+ prevStep?: number;
96
+ registerStepEvent: (
97
+ stepPosition: number,
98
+ event: TStepEvent,
99
+ data: any
100
+ ) => void;
101
+ streamEvent: (stepPosition: number, event: string, data: any) => void;
102
+ submit: () => Promise<void>;
103
+ finishWorkoutSession: () => void;
104
+ setStudent: (student: TStudent) => void;
105
+ save: () => void;
106
+ retrieve: () => Promise<ITelemetryJSONSchema | null>;
107
+ }
108
+
109
+ const TelemetryManager: ITelemetryManager = {
110
+ current: null,
111
+ urls: {},
112
+ configPath: "",
113
+ salute: message => {
114
+ Console.info(message)
115
+ },
116
+
117
+ start: function (agent, steps, path, tutorialSlug) {
118
+ this.configPath = path
119
+ if (!this.current) {
120
+ this.retrieve()
121
+ .then(data => {
122
+ const prevTelemetry = data
123
+ if (prevTelemetry) {
124
+ this.current = prevTelemetry
125
+ this.finishWorkoutSession()
126
+ } else {
127
+ this.current = {
128
+ telemetry_id: createUUID(),
129
+ slug: tutorialSlug,
130
+ agent,
131
+ tutorial_started_at: Date.now(),
132
+ steps,
133
+ workout_session: [
134
+ {
135
+ started_at: Date.now(),
136
+ },
137
+ ],
138
+ }
139
+ }
140
+
141
+ this.save()
142
+ this.submit()
143
+ })
144
+ .catch(error => {
145
+ Console.debug(error)
146
+ // Delete the telemetry.json if it exists
147
+ fs.unlinkSync(`${this.configPath}/telemetry.json`)
148
+ throw new Error(
149
+ "There was a problem starting, reload LearnPack\nRun\n$ learnpack start"
150
+ )
151
+ })
152
+ }
153
+ },
154
+ // verifyStudent: function () {
155
+ // if (!this.current) {
156
+ // return;
157
+ // }
158
+
159
+ // if (!this.current.user_id) {
160
+
161
+ // }
162
+ // },
163
+
164
+ setStudent: function (student) {
165
+ if (!this.current) {
166
+ return
167
+ }
168
+
169
+ this.current.user_id = student.user_id
170
+ this.save()
171
+ this.submit()
172
+ },
173
+ finishWorkoutSession: function () {
174
+ if (!this.current) {
175
+ return
176
+ }
177
+
178
+ const lastSession =
179
+ this.current?.workout_session[this.current.workout_session.length - 1]
180
+ if (
181
+ lastSession &&
182
+ !lastSession.ended_at &&
183
+ this.current?.last_interaction_at
184
+ ) {
185
+ lastSession.ended_at = this.current.last_interaction_at
186
+ this.current.workout_session.push({
187
+ started_at: Date.now(),
188
+ })
189
+ }
190
+ },
191
+
192
+ registerStepEvent: function (stepPosition, event, data) {
193
+ if (!this.current) {
194
+ // throw new Error("Telemetry has not been started");
195
+ return
196
+ }
197
+
198
+ const step = this.current.steps[stepPosition]
199
+ if (!step) {
200
+ return
201
+ }
202
+
203
+ if (data.source_code) {
204
+ data.source_code = stringToBase64(data.source_code)
205
+ }
206
+
207
+ if (data.stdout) {
208
+ data.stdout = stringToBase64(data.stdout)
209
+ }
210
+
211
+ if (data.stderr) {
212
+ data.stderr = stringToBase64(data.stderr)
213
+ }
214
+
215
+ if (Object.prototype.hasOwnProperty.call(data, "exitCode")) {
216
+ data.exit_code = data.exitCode
217
+ data.exitCode = undefined
218
+ }
219
+
220
+ switch (event) {
221
+ case "compile":
222
+ if (!step.compilations) {
223
+ step.compilations = []
224
+ }
225
+
226
+ step.compilations.push(data)
227
+ this.current.steps[stepPosition] = step
228
+ break
229
+ case "test":
230
+ if (!step.tests) {
231
+ step.tests = []
232
+ }
233
+
234
+ // data.stdout =
235
+ step.tests.push(data)
236
+ if (data.exit_code === 0) {
237
+ step.completed_at = Date.now()
238
+ }
239
+
240
+ this.current.steps[stepPosition] = step
241
+ break
242
+ case "ai_interaction":
243
+ if (!step.ai_interactions) {
244
+ step.ai_interactions = []
245
+ }
246
+
247
+ step.ai_interactions.push(data)
248
+ break
249
+ case "open_step": {
250
+ const now = Date.now()
251
+
252
+ if (!step.opened_at) {
253
+ step.opened_at = now
254
+ this.current.steps[stepPosition] = step
255
+ }
256
+
257
+ if (this.prevStep || this.prevStep === 0) {
258
+ const prevStep = this.current.steps[this.prevStep]
259
+ if (!prevStep.is_testeable && !prevStep.completed_at) {
260
+ prevStep.completed_at = now
261
+ this.current.steps[this.prevStep] = prevStep
262
+ }
263
+ }
264
+
265
+ this.prevStep = stepPosition
266
+
267
+ this.submit()
268
+ break
269
+ }
270
+
271
+ default:
272
+ throw new Error(`Event type ${event} is not supported`)
273
+ }
274
+
275
+ this.current.last_interaction_at = Date.now()
276
+ this.streamEvent(stepPosition, event, data)
277
+ this.save()
278
+ },
279
+ retrieve: function () {
280
+ return new Promise((resolve, reject) => {
281
+ fs.readFile(
282
+ `${this.configPath}/telemetry.json`,
283
+ "utf8",
284
+ (err: any, data: any) => {
285
+ if (err) {
286
+ if (err.code === "ENOENT") {
287
+ // File does not exist, resolve with undefined
288
+ resolve(null)
289
+ } else {
290
+ reject(err)
291
+ }
292
+ } else {
293
+ try {
294
+ resolve(JSON.parse(data))
295
+ } catch (error) {
296
+ reject(error)
297
+ }
298
+ }
299
+ }
300
+ )
301
+ })
302
+ },
303
+
304
+ submit: async function () {
305
+ Console.debug("Submitting telemetry...")
306
+
307
+ if (!this.current)
308
+ return Promise.resolve()
309
+ const url = this.urls.batch
310
+ if (!url) {
311
+ return
312
+ }
313
+
314
+ const body = this.current
315
+
316
+ API.sendBatchTelemetry(url, body)
317
+ },
318
+ save: function () {
319
+ fs.writeFile(
320
+ `${this.configPath}/telemetry.json`,
321
+ JSON.stringify(this.current),
322
+ (err: any) => {
323
+ if (err)
324
+ throw err
325
+ }
326
+ )
327
+ },
328
+
329
+ streamEvent: async function (stepPosition, event, data) {
330
+ if (!this.current)
331
+ return
332
+
333
+ const url = this.urls.streaming
334
+ if (!url) {
335
+ return
336
+ }
337
+
338
+ const stepSlug = this.current.steps[stepPosition].slug
339
+
340
+ const body = {
341
+ slug: stepSlug,
342
+ telemetry_id: this.current.telemetry_id,
343
+ user_id: this.current.user_id,
344
+ step_position: stepPosition,
345
+ event,
346
+ data,
347
+ }
348
+
349
+ API.sendStreamTelemetry(url, body)
350
+ },
351
+ }
352
+
353
+ export default TelemetryManager