@learnpack/learnpack 5.0.28 → 5.0.30

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.
@@ -469,6 +469,7 @@ fs.unlinkSync(_path)
469
469
  }
470
470
  }
471
471
  },
472
+
472
473
  buildIndex: function () {
473
474
  Console.info("Building the exercise index...")
474
475
 
@@ -511,6 +512,31 @@ fs.mkdirSync(confPath.base)
511
512
  [exercise(configObj?.config?.exercisesPath || "", 0, configObj)]
512
513
  this.save()
513
514
  },
515
+ createExercise: (slug: string, content: string, language: string) => {
516
+ try {
517
+ const dirPath = `${configObj.config?.exercisesPath}/${slug}`
518
+ if (!fs.existsSync(dirPath))
519
+ fs.mkdirSync(dirPath, { recursive: true })
520
+ const isEnglish = language === "us" || language === "en"
521
+ const fileName = isEnglish ? "README.md" : `README.${language}.md`
522
+ fs.writeFileSync(`${dirPath}/${fileName}`, content)
523
+
524
+ return true
525
+ } catch (error) {
526
+ Console.error("Error creating exercise: ", error)
527
+ return false
528
+ }
529
+ },
530
+
531
+ deleteExercise: (slug: string) => {
532
+ const dirPath = `${configObj.config?.exercisesPath}/${slug}`
533
+ if (fs.existsSync(dirPath)) {
534
+ fs.rmSync(dirPath, { recursive: true, force: true })
535
+ return true
536
+ }
537
+
538
+ return false
539
+ },
514
540
  watchIndex: function (onChange: (filename: string) => void) {
515
541
  if (configObj.config && !configObj.config.exercisesPath)
516
542
  throw ValidationError(
@@ -123,7 +123,7 @@ export const download = (url: string, dest: string) => {
123
123
  })
124
124
  file.on("error", err => {
125
125
  file.close()
126
- if (err.code === "EEXIST") {
126
+ if (err.name === "EEXIST") {
127
127
  Console.debug("File already exists")
128
128
  resolve("File already exists")
129
129
  } else {
@@ -279,9 +279,7 @@ throw new Error("File not found: " + filePath)
279
279
  TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
280
280
  }
281
281
 
282
- if (configObject.config?.editor.agent === "os") {
283
- eventManager.enqueue(dispatcher.events.START_EXERCISE, exercise)
284
- } else {
282
+ if (configObject.config?.editor.agent !== "os") {
285
283
  dispatcher.enqueue(dispatcher.events.START_EXERCISE, req.params.slug)
286
284
  }
287
285
 
@@ -383,6 +381,48 @@ throw new Error("File not found: " + filePath)
383
381
  })
384
382
  )
385
383
 
384
+ app.delete(
385
+ "/exercise/:slug/delete",
386
+ withHandler(async (req: express.Request, res: express.Response) => {
387
+ const exerciseDeleted = configManager.deleteExercise(req.params.slug)
388
+ if (exerciseDeleted) {
389
+ configManager.buildIndex()
390
+ res.json({ status: "ok" })
391
+ } else {
392
+ res.status(500).json({ error: "Failed to delete exercise" })
393
+ }
394
+ })
395
+ )
396
+
397
+ app.post(
398
+ "/exercise/:slug/create",
399
+ jsonBodyParser,
400
+ withHandler(async (req: express.Request, res: express.Response) => {
401
+ const { title, readme, language } = req.body
402
+ const { slug } = req.params
403
+
404
+ if (!title || !readme || !language) {
405
+ return res.status(400).json({ error: "Missing required fields" })
406
+ }
407
+
408
+ try {
409
+ const exerciseCreated = await configManager.createExercise(
410
+ slug,
411
+ readme,
412
+ language
413
+ )
414
+ if (exerciseCreated) {
415
+ configManager.buildIndex()
416
+ res.json({ status: "ok" })
417
+ } else {
418
+ res.status(500).json({ error: "Failed to create exercise" })
419
+ }
420
+ } catch {
421
+ res.status(500).json({ error: "Failed to create exercise" })
422
+ }
423
+ })
424
+ )
425
+
386
426
  const textBodyParser = bodyParser.text()
387
427
  app.put(
388
428
  "/exercise/:slug/file/:fileName",
@@ -163,6 +163,10 @@ const Session: ISession = {
163
163
  }
164
164
  },
165
165
  destroy: async function () {
166
+ if (!this.sessionStarted) {
167
+ await this.initialize()
168
+ }
169
+
166
170
  await storage.clear()
167
171
  this.token = null
168
172
  Console.success("You have logged out")
@@ -1,23 +1,25 @@
1
- import { IConfigObj, TGrading } from "./config"
2
- import { IExercise } from "./exercise-obj"
3
-
4
- export interface IConfigManagerAttributes {
5
- grading: TGrading;
6
- disableGrading: boolean;
7
- version: string;
8
- mode?: string;
9
- }
10
-
11
- export interface IConfigManager {
12
- validLanguages?: any;
13
- get: () => IConfigObj;
14
- clean: () => void;
15
- getExercise: (slug: string | undefined) => IExercise;
16
- startExercise: (slug: string) => IExercise;
17
- reset: (slug: string) => void;
18
- buildIndex: () => boolean | void;
19
- watchIndex: (onChange: (...args: Array<any>) => void) => void;
20
- save: () => void;
21
- noCurrentExercise: () => void;
22
- getAllExercises: () => IExercise[];
23
- }
1
+ import { IConfigObj, TGrading } from "./config"
2
+ import { IExercise } from "./exercise-obj"
3
+
4
+ export interface IConfigManagerAttributes {
5
+ grading: TGrading
6
+ disableGrading: boolean
7
+ version: string
8
+ mode?: string
9
+ }
10
+
11
+ export interface IConfigManager {
12
+ validLanguages?: any
13
+ get: () => IConfigObj
14
+ clean: () => void
15
+ getExercise: (slug: string | undefined) => IExercise
16
+ startExercise: (slug: string) => IExercise
17
+ reset: (slug: string) => void
18
+ createExercise: (slug: string, content: string, language: string) => boolean
19
+ deleteExercise: (slug: string) => boolean
20
+ buildIndex: () => boolean | void
21
+ watchIndex: (onChange: (...args: Array<any>) => void) => void
22
+ save: () => void
23
+ noCurrentExercise: () => void
24
+ getAllExercises: () => IExercise[]
25
+ }
@@ -1,147 +1,241 @@
1
- // eslint-disable-next-line
2
- const frontMatter = require("front-matter")
3
- import * as path from "path"
4
-
5
- import * as yaml from "js-yaml"
6
-
7
- type TEstimateReadingTimeReturns = {
8
- minutes: number
9
- words: number
10
- }
11
-
12
- export const estimateReadingTime = (
13
- text: string,
14
- wordsPerMinute = 150
15
- ): TEstimateReadingTimeReturns => {
16
- const words = text.trim().split(/\s+/).length
17
- const minutes = words / wordsPerMinute
18
-
19
- if (minutes < 1) {
20
- if (words === 0)
21
- return {
22
- minutes: 1,
23
- words,
24
- }
25
- } else {
26
- return {
27
- minutes,
28
- words,
29
- }
30
- }
31
-
32
- return {
33
- minutes: 1,
34
- words,
35
- }
36
- }
37
-
38
- export type PackageInfo = {
39
- grading: string
40
- difficulty: string
41
- duration: number
42
- description: {
43
- us: string
44
- }
45
- title: {
46
- us: string
47
- }
48
- }
49
-
50
- export function checkReadingTime(
51
- markdown: string,
52
- wordsPerMinute = 150
53
- ): { newMarkdown: string; exceedsThreshold: boolean } {
54
- const parsed = frontMatter(markdown)
55
- const readingTime = estimateReadingTime(parsed.body, wordsPerMinute)
56
- let attributes = parsed.attributes ? parsed.attributes : {}
57
-
58
- if (typeof parsed.attributes !== "object") {
59
- attributes = {}
60
- }
61
-
62
- const updatedAttributes = {
63
- ...attributes,
64
- readingTime,
65
- }
66
-
67
- // Convert the front matter back to a proper YAML string
68
- const yamlFrontMatter = yaml.dump(updatedAttributes).trim()
69
-
70
- // Reconstruct the markdown with the front matter
71
- const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`
72
-
73
- return {
74
- newMarkdown,
75
- exceedsThreshold: readingTime.minutes > wordsPerMinute,
76
- }
77
- }
78
-
79
- const slugify = (text: string) => {
80
- return text
81
- .toString()
82
- .normalize("NFD")
83
- .replace(/[\u0300-\u036F]/g, "")
84
- .toLowerCase()
85
- .trim()
86
- .replace(/\s+/g, "-")
87
- .replace(/[^\w-]+/g, "")
88
- }
89
-
90
- export const getExInfo = (title: string) => {
91
- // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
92
- let [exNumber, exTitle] = title.split(" - ")
93
-
94
- // Extract kind and description
95
- const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
96
- const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
97
- const description = kindMatch ? kindMatch[2].trim() : ""
98
-
99
- exNumber = exNumber.trim()
100
- // Clean title
101
- exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
102
- exTitle = slugify(exTitle)
103
-
104
- return {
105
- exNumber,
106
- kind,
107
- description,
108
- exTitle,
109
- }
110
- }
111
-
112
- export function extractImagesFromMarkdown(markdown: string) {
113
- const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
114
- const images = []
115
- let match
116
-
117
- while ((match = imageRegex.exec(markdown)) !== null) {
118
- const altText = match[1]
119
- const url = match[2]
120
- images.push({ alt: altText, url: url })
121
- }
122
-
123
- return images
124
- }
125
-
126
- export function getFilenameFromUrl(url: string) {
127
- return path.basename(url)
128
- }
129
-
130
- export const makePackageInfo = (choices: any) => {
131
- const packageInfo = {
132
- grading: choices.grading,
133
- difficulty: choices.difficulty,
134
- duration: parseInt(choices.duration),
135
- description: {
136
- us: choices.description,
137
- },
138
- title: {
139
- us: choices.title,
140
- },
141
- slug: choices.title
142
- .toLowerCase()
143
- .replace(/ /g, "-")
144
- .replace(/[^\w-]+/g, ""),
145
- }
146
- return packageInfo
147
- }
1
+ // eslint-disable-next-line
2
+ const frontMatter = require("front-matter")
3
+ import * as path from "path"
4
+ import * as fs from "fs"
5
+ import { homedir } from "os"
6
+ import { join } from "path"
7
+ import { exec } from "child_process"
8
+ import { promisify } from "util"
9
+
10
+ import * as yaml from "js-yaml"
11
+
12
+ type TEstimateReadingTimeReturns = {
13
+ minutes: number
14
+ words: number
15
+ }
16
+
17
+ export const estimateReadingTime = (
18
+ text: string,
19
+ wordsPerMinute = 150
20
+ ): TEstimateReadingTimeReturns => {
21
+ const words = text.trim().split(/\s+/).length
22
+ const minutes = words / wordsPerMinute
23
+
24
+ if (minutes < 1) {
25
+ if (words === 0)
26
+ return {
27
+ minutes: 1,
28
+ words,
29
+ }
30
+ } else {
31
+ return {
32
+ minutes,
33
+ words,
34
+ }
35
+ }
36
+
37
+ return {
38
+ minutes: 1,
39
+ words,
40
+ }
41
+ }
42
+
43
+ export type PackageInfo = {
44
+ grading: string
45
+ difficulty: string
46
+ duration: number
47
+ description: {
48
+ us: string
49
+ }
50
+ title: {
51
+ us: string
52
+ }
53
+ }
54
+
55
+ export function checkReadingTime(
56
+ markdown: string,
57
+ wordsPerMinute = 200,
58
+ maxMinutes = 1
59
+ ): {
60
+ newMarkdown: string
61
+ exceedsThreshold: boolean
62
+ minutes: number
63
+ body: string
64
+ } {
65
+ const parsed = frontMatter(markdown)
66
+
67
+ const readingTime = estimateReadingTime(parsed.body, wordsPerMinute)
68
+
69
+ let attributes = parsed.attributes ? parsed.attributes : {}
70
+
71
+ if (typeof parsed.attributes !== "object") {
72
+ attributes = {}
73
+ }
74
+
75
+ const updatedAttributes = {
76
+ ...attributes,
77
+ readingTime,
78
+ }
79
+
80
+ let yamlFrontMatter = ""
81
+ try {
82
+ yamlFrontMatter = yaml.dump(updatedAttributes).trim()
83
+ } catch {
84
+ return {
85
+ newMarkdown: "",
86
+ exceedsThreshold: false,
87
+ minutes: 0,
88
+ body: "",
89
+ }
90
+ }
91
+
92
+ // Reconstruct the markdown with the front matter
93
+ const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`
94
+
95
+ return {
96
+ newMarkdown,
97
+ exceedsThreshold: readingTime.minutes > maxMinutes,
98
+ minutes: readingTime.minutes,
99
+ body: parsed.body,
100
+ }
101
+ }
102
+
103
+ const slugify = (text: string) => {
104
+ return text
105
+ .toString()
106
+ .normalize("NFD")
107
+ .replace(/[\u0300-\u036F]/g, "")
108
+ .toLowerCase()
109
+ .trim()
110
+ .replace(/\s+/g, "-")
111
+ .replace(/[^\w-]+/g, "")
112
+ }
113
+
114
+ export const getExInfo = (title: string) => {
115
+ // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
116
+ let [exNumber, exTitle] = title.split(" - ")
117
+
118
+ // Extract kind and description
119
+ const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
120
+ const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
121
+ const description = kindMatch ? kindMatch[2].trim() : ""
122
+
123
+ exNumber = exNumber.trim()
124
+ // Clean title
125
+ exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
126
+ exTitle = slugify(exTitle)
127
+
128
+ return {
129
+ exNumber,
130
+ kind,
131
+ description,
132
+ exTitle,
133
+ }
134
+ }
135
+
136
+ export function extractImagesFromMarkdown(markdown: string) {
137
+ const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
138
+ const images = []
139
+ let match
140
+
141
+ while ((match = imageRegex.exec(markdown)) !== null) {
142
+ const altText = match[1]
143
+ const url = match[2]
144
+ images.push({ alt: altText, url: url })
145
+ }
146
+
147
+ return images
148
+ }
149
+
150
+ export function getFilenameFromUrl(url: string) {
151
+ return path.basename(url)
152
+ }
153
+
154
+ export const makePackageInfo = (choices: any) => {
155
+ const packageInfo = {
156
+ grading: choices.grading,
157
+ difficulty: "beginner",
158
+ duration: 5,
159
+ description: {
160
+ us: choices.description,
161
+ },
162
+ title: {
163
+ us: choices.title,
164
+ },
165
+ slug: choices.title
166
+ .toLowerCase()
167
+ .replace(/ /g, "-")
168
+ .replace(/[^\w-]+/g, ""),
169
+ }
170
+ return packageInfo
171
+ }
172
+
173
+ export function estimateDuration(listOfSteps: string[]): number {
174
+ let duration = 0
175
+
176
+ for (const step of listOfSteps) {
177
+ if (step.includes("[READ:")) {
178
+ duration += 1
179
+ } else if (step.includes("[QUIZ:")) {
180
+ duration += 2
181
+ } else if (step.includes("[CODE:")) {
182
+ duration += 3
183
+ }
184
+ }
185
+
186
+ return duration
187
+ }
188
+
189
+ const writeFilePromise = promisify(fs.writeFile)
190
+ const execPromise = promisify(exec)
191
+
192
+ const example_content = `
193
+ 00.1 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]
194
+ 01.1 - Introduction to Machine Learning [READ: Small introduction to important concepts such as AI, machine learning, and their applications]
195
+ 01.2 - Introduction to Deep Learning [QUIZ: Small introduction to important concepts such as AI, machine learning, and their applications]
196
+ 02.1 - Test your knowledge [CODE: Code problem to solve]
197
+ `
198
+
199
+ export async function createFileOnDesktop() {
200
+ try {
201
+ const desktopPath = join(homedir(), "Desktop")
202
+ const filePath = join(desktopPath, "content_index.txt")
203
+
204
+ const content = example_content.trim()
205
+
206
+ await writeFilePromise(filePath, content)
207
+ console.log(`File created successfully at: ${filePath}`)
208
+
209
+ await openFile(filePath)
210
+ } catch (error) {
211
+ console.error("Error:", error)
212
+ }
213
+ }
214
+
215
+ async function openFile(filePath: string) {
216
+ const platform = process.platform
217
+ let command
218
+
219
+ if (platform === "win32") {
220
+ command = `start "" "${filePath}"`
221
+ } else if (platform === "darwin") {
222
+ command = `open "${filePath}"`
223
+ } else {
224
+ command = `xdg-open "${filePath}"`
225
+ }
226
+
227
+ try {
228
+ await execPromise(command)
229
+ console.log("File opened successfully.")
230
+ } catch (error) {
231
+ console.error("Error opening the file:", error)
232
+ }
233
+ }
234
+
235
+ export function getContentIndex() {
236
+ const desktopPath = join(homedir(), "Desktop")
237
+ const filePath = join(desktopPath, "content_index.txt")
238
+
239
+ const content = fs.readFileSync(filePath, "utf8")
240
+ return content
241
+ }
@@ -293,3 +293,33 @@ export async function createPreviewReadme(
293
293
  })
294
294
  fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer)
295
295
  }
296
+
297
+ // {"lesson": "The text of the lesson", "number_of_words": "Words lenght of the lesson", "expected_number_words": "The expected number of words"}
298
+
299
+ type TReduceReadmeInputs = {
300
+ lesson: string
301
+ number_of_words: string
302
+ expected_number_words: string
303
+ }
304
+ export async function reduceReadme(
305
+ rigoToken: string,
306
+ inputs: TReduceReadmeInputs
307
+ ) {
308
+ try {
309
+ const response = await axios.post(
310
+ `${RIGOBOT_HOST}/v1/prompting/completion/588/`,
311
+ { inputs, include_purpose_objective: false, execute_async: false },
312
+ {
313
+ headers: {
314
+ "Content-Type": "application/json",
315
+ Authorization: "Token " + rigoToken,
316
+ },
317
+ }
318
+ )
319
+
320
+ return response.data
321
+ } catch (error) {
322
+ Console.debug(error)
323
+ return null
324
+ }
325
+ }
@@ -0,0 +1,29 @@
1
+ # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
+
4
+ name: Learnpack audit
5
+
6
+ on:
7
+ push:
8
+ branches: [ main ]
9
+ pull_request:
10
+ branches: [ main ]
11
+
12
+ jobs:
13
+ build:
14
+
15
+ runs-on: ubuntu-latest
16
+
17
+ strategy:
18
+ matrix:
19
+ node-version: [20.x]
20
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21
+
22
+ steps:
23
+ - uses: actions/checkout@v2
24
+ - name: Use Node.js ${{ matrix.node-version }}
25
+ uses: actions/setup-node@v2
26
+ with:
27
+ node-version: ${{ matrix.node-version }}
28
+ - run: npm install @learnpack/learnpack@latest -g
29
+ - run: learnpack audit
@@ -0,0 +1,29 @@
1
+ # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
+
4
+ name: Learnpack audit
5
+
6
+ on:
7
+ push:
8
+ branches: [ main ]
9
+ pull_request:
10
+ branches: [ main ]
11
+
12
+ jobs:
13
+ build:
14
+
15
+ runs-on: ubuntu-latest
16
+
17
+ strategy:
18
+ matrix:
19
+ node-version: [20.x]
20
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21
+
22
+ steps:
23
+ - uses: actions/checkout@v2
24
+ - name: Use Node.js ${{ matrix.node-version }}
25
+ uses: actions/setup-node@v2
26
+ with:
27
+ node-version: ${{ matrix.node-version }}
28
+ - run: npm install @learnpack/learnpack@latest -g
29
+ - run: learnpack audit