@learnpack/learnpack 5.0.26 → 5.0.27

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.
package/README.md CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @learnpack/learnpack
21
21
  $ learnpack COMMAND
22
22
  running command...
23
23
  $ learnpack (-v|--version|version)
24
- @learnpack/learnpack/5.0.26 win32-x64 node-v20.16.0
24
+ @learnpack/learnpack/5.0.27 win32-x64 node-v20.16.0
25
25
  $ learnpack --help [COMMAND]
26
26
  USAGE
27
27
  $ learnpack COMMAND
@@ -75,7 +75,7 @@ DESCRIPTION
75
75
  12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
76
76
  ```
77
77
 
78
- _See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\audit.ts)_
78
+ _See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\audit.ts)_
79
79
 
80
80
  ## `learnpack clean`
81
81
 
@@ -90,7 +90,7 @@ DESCRIPTION
90
90
  Extra documentation goes here
91
91
  ```
92
92
 
93
- _See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\clean.ts)_
93
+ _See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\clean.ts)_
94
94
 
95
95
  ## `learnpack download [PACKAGE]`
96
96
 
@@ -108,7 +108,7 @@ DESCRIPTION
108
108
  Extra documentation goes here
109
109
  ```
110
110
 
111
- _See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\download.ts)_
111
+ _See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\download.ts)_
112
112
 
113
113
  ## `learnpack help [COMMAND]`
114
114
 
@@ -140,7 +140,7 @@ OPTIONS
140
140
  -y, --yes Skip all prompts and initialize an empty project
141
141
  ```
142
142
 
143
- _See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\init.ts)_
143
+ _See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\init.ts)_
144
144
 
145
145
  ## `learnpack login [PACKAGE]`
146
146
 
@@ -158,7 +158,7 @@ DESCRIPTION
158
158
  Extra documentation goes here
159
159
  ```
160
160
 
161
- _See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\login.ts)_
161
+ _See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\login.ts)_
162
162
 
163
163
  ## `learnpack logout [PACKAGE]`
164
164
 
@@ -176,7 +176,7 @@ DESCRIPTION
176
176
  Extra documentation goes here
177
177
  ```
178
178
 
179
- _See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\logout.ts)_
179
+ _See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\logout.ts)_
180
180
 
181
181
  ## `learnpack plugins`
182
182
 
@@ -307,7 +307,7 @@ OPTIONS
307
307
  -h, --help show CLI help
308
308
  ```
309
309
 
310
- _See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\publish.ts)_
310
+ _See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\publish.ts)_
311
311
 
312
312
  ## `learnpack start`
313
313
 
@@ -329,7 +329,7 @@ OPTIONS
329
329
  -y, --yes Skip all prompts and initialize an empty project
330
330
  ```
331
331
 
332
- _See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\start.ts)_
332
+ _See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\start.ts)_
333
333
 
334
334
  ## `learnpack test [EXERCISESLUG]`
335
335
 
@@ -346,7 +346,7 @@ OPTIONS
346
346
  -y, --yes Skip all prompts and initialize an empty project
347
347
  ```
348
348
 
349
- _See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\test.ts)_
349
+ _See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\test.ts)_
350
350
 
351
351
  ## `learnpack translate`
352
352
 
@@ -360,7 +360,7 @@ OPTIONS
360
360
  -y, --yes Skip all prompts and initialize an empty project
361
361
  ```
362
362
 
363
- _See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.26/src\commands\translate.ts)_
363
+ _See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.27/src\commands\translate.ts)_
364
364
  <!-- commandsstop -->
365
365
 
366
366
  > > > > > > > 0cb3e56d84c197f9d008836bb573eade212b7e57
@@ -7,92 +7,39 @@ const fs = require("fs-extra");
7
7
  const prompts = require("prompts");
8
8
  const cli_ux_1 = require("cli-ux");
9
9
  const eta = require("eta");
10
- const api_1 = require("../utils/api");
11
10
  const console_1 = require("../utils/console");
12
11
  const errors_1 = require("../utils/errors");
13
12
  const path = require("path");
14
13
  const rigoActions_1 = require("../utils/rigoActions");
15
- const api_2 = require("../utils/api");
16
- const slugify = (text) => {
17
- return text
18
- .toString()
19
- .normalize("NFD")
20
- .replace(/[\u0300-\u036F]/g, "")
21
- .toLowerCase()
22
- .trim()
23
- .replace(/\s+/g, "-")
24
- .replace(/[^\w-]+/g, "");
25
- };
26
- const getExInfo = (title) => {
27
- // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
28
- let [exNumber, exTitle] = title.split(" - ");
29
- // Extract kind and description
30
- const kindMatch = exTitle.match(/\[(.*?):(.*?)]/);
31
- const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read";
32
- const description = kindMatch ? kindMatch[2].trim() : "";
33
- exNumber = exNumber.trim();
34
- // Clean title
35
- exTitle = exTitle.replace((kindMatch === null || kindMatch === void 0 ? void 0 : kindMatch[0]) || "", "").trim();
36
- exTitle = slugify(exTitle);
37
- return {
38
- exNumber,
39
- kind,
40
- description,
41
- exTitle,
42
- };
43
- };
44
- function extractImagesFromMarkdown(markdown) {
45
- const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g;
46
- const images = [];
47
- let match;
48
- while ((match = imageRegex.exec(markdown)) !== null) {
49
- const altText = match[1];
50
- const url = match[2];
51
- images.push({ alt: altText, url: url });
52
- }
53
- return images;
54
- }
55
- function getFilenameFromUrl(url) {
56
- return path.basename(url);
57
- }
58
- const makePackageInfo = (choices) => {
59
- const packageInfo = {
60
- grading: choices.grading,
61
- difficulty: choices.difficulty,
62
- duration: parseInt(choices.duration),
63
- description: {
64
- us: choices.description,
65
- },
66
- title: {
67
- us: choices.title,
68
- },
69
- slug: choices.title
70
- .toLowerCase()
71
- .replace(/ /g, "-")
72
- .replace(/[^\w-]+/g, ""),
73
- };
74
- return packageInfo;
75
- };
76
- async function createPreviewReadme(tutorialDir, packageInfo, rigoToken, readmeContents) {
77
- const readmeFilename = `README.md`;
78
- const readmeContent = await (0, rigoActions_1.generateCourseIntroduction)(rigoToken, {
79
- course_title: packageInfo.title.us,
80
- lessons_context: readmeContents.join("\n"),
81
- });
82
- fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer);
83
- }
14
+ const api_1 = require("../utils/api");
15
+ const creatorUtilities_1 = require("../utils/creatorUtilities");
16
+ const session_1 = require("../managers/session");
84
17
  const initializeInteractiveCreation = async (rigoToken, courseInfo) => {
85
18
  let prevInteractions = "";
86
19
  let isReady = false;
87
20
  let currentSteps = [];
21
+ let currentTitle = "";
22
+ let currentDescription = "";
88
23
  while (!isReady) {
24
+ let wholeInfo = courseInfo;
25
+ wholeInfo += `
26
+ Current title: ${currentTitle}
27
+ Current description: ${currentDescription}
28
+ `;
89
29
  // eslint-disable-next-line
90
30
  const res = await (0, rigoActions_1.interactiveCreation)(rigoToken, {
91
- courseInfo: courseInfo,
31
+ courseInfo: wholeInfo,
92
32
  prevInteractions: prevInteractions,
93
33
  });
94
34
  currentSteps = res.parsed.listOfSteps;
95
35
  isReady = res.parsed.ready;
36
+ if (res.parsed.title && currentTitle !== res.parsed.title) {
37
+ currentTitle = res.parsed.title;
38
+ }
39
+ if (res.parsed.description &&
40
+ currentDescription !== res.parsed.description) {
41
+ currentDescription = res.parsed.description;
42
+ }
96
43
  if (!isReady) {
97
44
  console.log(currentSteps);
98
45
  console_1.default.info(`AI: ${res.parsed.aiMessage}`);
@@ -109,42 +56,27 @@ const initializeInteractiveCreation = async (rigoToken, courseInfo) => {
109
56
  }
110
57
  }
111
58
  return {
112
- currentSteps,
113
- prevInteractions,
59
+ steps: currentSteps,
60
+ title: currentTitle,
61
+ description: currentDescription,
62
+ interactions: prevInteractions,
114
63
  };
115
64
  };
116
65
  const handleAILogic = async (tutorialDir, packageInfo) => {
117
- console_1.default.info("Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/creators");
118
66
  fs.removeSync(path.join(tutorialDir, "exercises", "01-hello-world"));
119
- const loginPrompts = await prompts([
120
- {
121
- type: "text",
122
- name: "email",
123
- message: "What's your email?",
124
- validate: (value) => {
125
- return value.length > 0 && value.includes("@");
126
- },
127
- },
128
- {
129
- type: "password",
130
- name: "password",
131
- message: "What's your password?",
132
- validate: (value) => {
133
- return value.length > 0;
134
- },
135
- },
136
- ]);
137
- let sessionPayload;
138
- try {
139
- sessionPayload = await api_1.default.login(loginPrompts.email, loginPrompts.password);
140
- }
141
- catch (error) {
142
- console_1.default.error("Error trying to authenticate");
143
- console_1.default.error(error.message || error);
144
- return;
67
+ let sessionPayload = await session_1.default.getPayload();
68
+ if (!sessionPayload || !sessionPayload.rigobot) {
69
+ console_1.default.info("Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/creators");
70
+ try {
71
+ sessionPayload = await session_1.default.login();
72
+ }
73
+ catch (error) {
74
+ console_1.default.error("Error trying to authenticate");
75
+ console_1.default.error(error.message || error);
76
+ }
145
77
  }
146
78
  const rigoToken = sessionPayload.rigobot.key;
147
- const consumables = await (0, api_2.getConsumables)(sessionPayload.token, "ai-generation");
79
+ const consumables = await (0, api_1.getConsumables)(sessionPayload.token, "ai-generation");
148
80
  if (consumables.ai_generation === 0) {
149
81
  console_1.default.error("It seems you cannot generate tutorials with AI. Make sure you creator subscription is up to date here: https://4geeks.com/profile/subscriptions? . If you believe there is an issue you can always contact support@4geeks.com");
150
82
  process.exit(1);
@@ -155,42 +87,53 @@ const handleAILogic = async (tutorialDir, packageInfo) => {
155
87
  process.exit(1);
156
88
  }
157
89
  console_1.default.success("🎉 Let's begin this learning journey!");
158
- const packageContext = `
90
+ let packageContext = `
159
91
  \n
160
- The following information comes from user inputs
161
92
  Title: ${packageInfo.title.us}
162
93
  Description: ${packageInfo.description.us}
163
- Grading: ${packageInfo.grading}
164
94
  Difficulty: ${packageInfo.difficulty}
165
95
  Duration: ${packageInfo.duration}
166
-
167
- Use it to generate more relevant exercises
168
96
  `;
169
- const { currentSteps } = await initializeInteractiveCreation(rigoToken, packageContext);
97
+ const { steps, title, description } = await initializeInteractiveCreation(rigoToken, packageContext);
98
+ packageInfo.title.us = title;
99
+ packageInfo.description.us = description;
100
+ packageContext = `
101
+ Title: ${title}
102
+ Description: ${description}
103
+ Difficulty: ${packageInfo.difficulty}
104
+ Estimated duration: ${packageInfo.duration}
105
+ List of exercises: ${steps.join(", ")}
106
+ `;
170
107
  const exercisesDir = path.join(tutorialDir, "exercises");
171
108
  fs.ensureDirSync(exercisesDir);
172
109
  console_1.default.info("Creating lessons...");
173
- for (const [index, exercise] of currentSteps.entries()) {
174
- const { exNumber, exTitle } = getExInfo(exercise);
110
+ for (const [index, exercise] of steps.entries()) {
111
+ const { exNumber, exTitle } = (0, creatorUtilities_1.getExInfo)(exercise);
175
112
  const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`);
176
113
  fs.ensureDirSync(exerciseDir);
177
114
  }
178
- const exercisePromises = currentSteps.map(async (exercise, index) => {
179
- const { exNumber, exTitle, kind, description } = getExInfo(exercise);
115
+ const exercisePromises = steps.map(async (exercise, index) => {
116
+ const { exNumber, exTitle, kind, description } = (0, creatorUtilities_1.getExInfo)(exercise);
180
117
  const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`);
181
118
  const readme = await (0, rigoActions_1.readmeCreator)(rigoToken, {
182
119
  title: `${exNumber} - ${exTitle}`,
183
120
  output_lang: "en",
184
- list_of_exercises: currentSteps.join(","),
121
+ list_of_exercises: steps.join(","),
185
122
  tutorial_description: packageContext,
186
123
  lesson_description: description,
187
124
  kind: kind.toLowerCase(),
188
125
  });
126
+ const { exceedsThreshold, newMarkdown } = (0, creatorUtilities_1.checkReadingTime)(readme.parsed.content, 200);
127
+ if (exceedsThreshold) {
128
+ console_1.default.error("The reading time exceeds the threshold");
129
+ console_1.default.info(`Please reduce the reading time of the lesson, current reading time is ${exceedsThreshold} minutes`);
130
+ }
189
131
  const readmeFilename = "README.md";
190
- fs.writeFileSync(path.join(exerciseDir, readmeFilename), readme.parsed.content);
132
+ fs.writeFileSync(path.join(exerciseDir, readmeFilename), newMarkdown);
191
133
  if (kind.toLowerCase() === "code") {
192
134
  const codeFile = await (0, rigoActions_1.createCodeFile)(rigoToken, {
193
135
  readme: readme.parsed.content,
136
+ tutorial_info: packageContext,
194
137
  });
195
138
  fs.writeFileSync(path.join(exerciseDir, `app.${codeFile.parsed.extension.replace(".", "")}`), codeFile.parsed.content);
196
139
  }
@@ -201,11 +144,11 @@ const handleAILogic = async (tutorialDir, packageInfo) => {
201
144
  console_1.default.success("Lessons created! 🎉");
202
145
  console_1.default.info("Generating images for the lessons...");
203
146
  for (const content of readmeContents) {
204
- imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)];
147
+ imagesArray = [...imagesArray, ...(0, creatorUtilities_1.extractImagesFromMarkdown)(content)];
205
148
  }
206
149
  const imagePromises = imagesArray.map(async (image) => {
207
150
  try {
208
- const filename = getFilenameFromUrl(image.url);
151
+ const filename = (0, creatorUtilities_1.getFilenameFromUrl)(image.url);
209
152
  const imagePath = path.join(tutorialDir, ".learn", "assets", filename);
210
153
  const res = await (0, rigoActions_1.generateImage)(rigoToken, { prompt: image.alt });
211
154
  await (0, rigoActions_1.downloadImage)(res.image_url, imagePath);
@@ -219,7 +162,7 @@ const handleAILogic = async (tutorialDir, packageInfo) => {
219
162
  await Promise.all(imagePromises);
220
163
  console_1.default.info("Images generated successfully! 🎉 Your tutorial will be ready soon!");
221
164
  console_1.default.info("Creating preview readme...");
222
- await createPreviewReadme(tutorialDir, packageInfo, rigoToken, readmeContents);
165
+ await (0, rigoActions_1.createPreviewReadme)(tutorialDir, packageInfo, rigoToken, readmeContents);
223
166
  return true;
224
167
  };
225
168
  const getChoices = async (empty) => {
@@ -307,7 +250,7 @@ class InitComand extends BaseCommand_1.default {
307
250
  const { flags } = this.parse(InitComand);
308
251
  await alreadyInitialized();
309
252
  const choices = await getChoices(flags.yes);
310
- const packageInfo = makePackageInfo(choices);
253
+ const packageInfo = (0, creatorUtilities_1.makePackageInfo)(choices);
311
254
  const tutorialDir = `./${packageInfo.slug}`;
312
255
  fs.ensureDirSync(tutorialDir);
313
256
  const templatesDir = path.resolve(__dirname, "../../src/utils/templates/" + (choices.grading || "no-grading"));
@@ -0,0 +1,44 @@
1
+ type TEstimateReadingTimeReturns = {
2
+ minutes: number;
3
+ words: number;
4
+ };
5
+ export declare const estimateReadingTime: (text: string, wordsPerMinute?: number) => TEstimateReadingTimeReturns;
6
+ export type PackageInfo = {
7
+ grading: string;
8
+ difficulty: string;
9
+ duration: number;
10
+ description: {
11
+ us: string;
12
+ };
13
+ title: {
14
+ us: string;
15
+ };
16
+ };
17
+ export declare function checkReadingTime(markdown: string, wordsPerMinute?: number): {
18
+ newMarkdown: string;
19
+ exceedsThreshold: boolean;
20
+ };
21
+ export declare const getExInfo: (title: string) => {
22
+ exNumber: string;
23
+ kind: string;
24
+ description: string;
25
+ exTitle: string;
26
+ };
27
+ export declare function extractImagesFromMarkdown(markdown: string): {
28
+ alt: string;
29
+ url: string;
30
+ }[];
31
+ export declare function getFilenameFromUrl(url: string): string;
32
+ export declare const makePackageInfo: (choices: any) => {
33
+ grading: any;
34
+ difficulty: any;
35
+ duration: number;
36
+ description: {
37
+ us: any;
38
+ };
39
+ title: {
40
+ us: any;
41
+ };
42
+ slug: any;
43
+ };
44
+ export {};
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makePackageInfo = exports.getExInfo = exports.estimateReadingTime = void 0;
4
+ exports.checkReadingTime = checkReadingTime;
5
+ exports.extractImagesFromMarkdown = extractImagesFromMarkdown;
6
+ exports.getFilenameFromUrl = getFilenameFromUrl;
7
+ // eslint-disable-next-line
8
+ const frontMatter = require("front-matter");
9
+ const path = require("path");
10
+ const yaml = require("js-yaml");
11
+ const estimateReadingTime = (text, wordsPerMinute = 150) => {
12
+ const words = text.trim().split(/\s+/).length;
13
+ const minutes = words / wordsPerMinute;
14
+ if (minutes < 1) {
15
+ if (words === 0)
16
+ return {
17
+ minutes: 1,
18
+ words,
19
+ };
20
+ }
21
+ else {
22
+ return {
23
+ minutes,
24
+ words,
25
+ };
26
+ }
27
+ return {
28
+ minutes: 1,
29
+ words,
30
+ };
31
+ };
32
+ exports.estimateReadingTime = estimateReadingTime;
33
+ function checkReadingTime(markdown, wordsPerMinute = 150) {
34
+ const parsed = frontMatter(markdown);
35
+ const readingTime = (0, exports.estimateReadingTime)(parsed.body, wordsPerMinute);
36
+ let attributes = parsed.attributes ? parsed.attributes : {};
37
+ if (typeof parsed.attributes !== "object") {
38
+ attributes = {};
39
+ }
40
+ const updatedAttributes = Object.assign(Object.assign({}, attributes), { readingTime });
41
+ // Convert the front matter back to a proper YAML string
42
+ const yamlFrontMatter = yaml.dump(updatedAttributes).trim();
43
+ // Reconstruct the markdown with the front matter
44
+ const newMarkdown = `---\n${yamlFrontMatter}\n---\n\n${parsed.body}`;
45
+ return {
46
+ newMarkdown,
47
+ exceedsThreshold: readingTime.minutes > wordsPerMinute,
48
+ };
49
+ }
50
+ const slugify = (text) => {
51
+ return text
52
+ .toString()
53
+ .normalize("NFD")
54
+ .replace(/[\u0300-\u036F]/g, "")
55
+ .toLowerCase()
56
+ .trim()
57
+ .replace(/\s+/g, "-")
58
+ .replace(/[^\w-]+/g, "");
59
+ };
60
+ const getExInfo = (title) => {
61
+ // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
62
+ let [exNumber, exTitle] = title.split(" - ");
63
+ // Extract kind and description
64
+ const kindMatch = exTitle.match(/\[(.*?):(.*?)]/);
65
+ const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read";
66
+ const description = kindMatch ? kindMatch[2].trim() : "";
67
+ exNumber = exNumber.trim();
68
+ // Clean title
69
+ exTitle = exTitle.replace((kindMatch === null || kindMatch === void 0 ? void 0 : kindMatch[0]) || "", "").trim();
70
+ exTitle = slugify(exTitle);
71
+ return {
72
+ exNumber,
73
+ kind,
74
+ description,
75
+ exTitle,
76
+ };
77
+ };
78
+ exports.getExInfo = getExInfo;
79
+ function extractImagesFromMarkdown(markdown) {
80
+ const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g;
81
+ const images = [];
82
+ let match;
83
+ while ((match = imageRegex.exec(markdown)) !== null) {
84
+ const altText = match[1];
85
+ const url = match[2];
86
+ images.push({ alt: altText, url: url });
87
+ }
88
+ return images;
89
+ }
90
+ function getFilenameFromUrl(url) {
91
+ return path.basename(url);
92
+ }
93
+ const makePackageInfo = (choices) => {
94
+ const packageInfo = {
95
+ grading: choices.grading,
96
+ difficulty: choices.difficulty,
97
+ duration: parseInt(choices.duration),
98
+ description: {
99
+ us: choices.description,
100
+ },
101
+ title: {
102
+ us: choices.title,
103
+ },
104
+ slug: choices.title
105
+ .toLowerCase()
106
+ .replace(/ /g, "-")
107
+ .replace(/[^\w-]+/g, ""),
108
+ };
109
+ return packageInfo;
110
+ };
111
+ exports.makePackageInfo = makePackageInfo;
@@ -1,3 +1,4 @@
1
+ import { PackageInfo } from "./creatorUtilities";
1
2
  type TCreateReadmeInputs = {
2
3
  title: string;
3
4
  output_lang: string;
@@ -30,6 +31,7 @@ type TInteractiveCreationInputs = {
30
31
  export declare const interactiveCreation: (token: string, inputs: TInteractiveCreationInputs) => Promise<any>;
31
32
  type TCreateCodeFileInputs = {
32
33
  readme: string;
34
+ tutorial_info: string;
33
35
  };
34
36
  export declare const createCodeFile: (token: string, inputs: TCreateCodeFileInputs) => Promise<any>;
35
37
  type TCreateCodingReadmeInputs = {
@@ -49,4 +51,5 @@ type TReadmeCreatorInputs = {
49
51
  kind: string;
50
52
  };
51
53
  export declare const readmeCreator: (token: string, inputs: TReadmeCreatorInputs) => Promise<any>;
54
+ export declare function createPreviewReadme(tutorialDir: string, packageInfo: PackageInfo, rigoToken: string, readmeContents: string[]): Promise<void>;
52
55
  export {};
@@ -2,9 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
4
4
  exports.downloadImage = downloadImage;
5
+ exports.createPreviewReadme = createPreviewReadme;
5
6
  const axios_1 = require("axios");
6
7
  const fs_1 = require("fs");
7
8
  const console_1 = require("../utils/console");
9
+ const fs = require("fs");
10
+ const path = require("path");
8
11
  const RIGOBOT_HOST = "https://rigobot.herokuapp.com";
9
12
  const createReadme = async (token, inputs) => {
10
13
  try {
@@ -164,3 +167,11 @@ const readmeCreator = async (token, inputs) => {
164
167
  throw new Error("Invalid kind of lesson");
165
168
  };
166
169
  exports.readmeCreator = readmeCreator;
170
+ async function createPreviewReadme(tutorialDir, packageInfo, rigoToken, readmeContents) {
171
+ const readmeFilename = `README.md`;
172
+ const readmeContent = await (0, exports.generateCourseIntroduction)(rigoToken, {
173
+ course_title: packageInfo.title.us,
174
+ lessons_context: readmeContents.join("\n"),
175
+ });
176
+ fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer);
177
+ }
@@ -1 +1 @@
1
- {"version":"5.0.26","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
1
+ {"version":"5.0.27","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.26",
4
+ "version": "5.0.27",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -41,6 +41,7 @@
41
41
  "eta": "^1.2.0",
42
42
  "express": "^4.17.1",
43
43
  "front-matter": "^4.0.2",
44
+ "js-yaml": "^4.1.0",
44
45
  "moment": "^2.27.0",
45
46
  "node-emoji": "^1.10.0",
46
47
  "node-fetch": "^2.7.0",
@@ -60,6 +61,7 @@
60
61
  "@types/debounce": "^1.2.1",
61
62
  "@types/express": "^4.17.13",
62
63
  "@types/fs-extra": "^8.1.0",
64
+ "@types/js-yaml": "^4.0.9",
63
65
  "@types/mocha": "^5",
64
66
  "@types/mock-fs": "^4.13.1",
65
67
  "@types/node": "^10.17.60",
@@ -15,128 +15,59 @@ import {
15
15
  hasCreatorPermission,
16
16
  generateImage,
17
17
  downloadImage,
18
- generateCourseIntroduction,
19
18
  interactiveCreation,
20
19
  createCodeFile,
21
20
  readmeCreator,
21
+ createPreviewReadme,
22
22
  } from "../utils/rigoActions"
23
23
  import { getConsumables } from "../utils/api"
24
-
25
- const slugify = (text: string) => {
26
- return text
27
- .toString()
28
- .normalize("NFD")
29
- .replace(/[\u0300-\u036F]/g, "")
30
- .toLowerCase()
31
- .trim()
32
- .replace(/\s+/g, "-")
33
- .replace(/[^\w-]+/g, "")
34
- }
35
-
36
- const getExInfo = (title: string) => {
37
- // Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
38
- let [exNumber, exTitle] = title.split(" - ")
39
-
40
- // Extract kind and description
41
- const kindMatch = exTitle.match(/\[(.*?):(.*?)]/)
42
- const kind = kindMatch ? kindMatch[1].trim().toLowerCase() : "read"
43
- const description = kindMatch ? kindMatch[2].trim() : ""
44
-
45
- exNumber = exNumber.trim()
46
- // Clean title
47
- exTitle = exTitle.replace(kindMatch?.[0] || "", "").trim()
48
- exTitle = slugify(exTitle)
49
-
50
- return {
51
- exNumber,
52
- kind,
53
- description,
54
- exTitle,
55
- }
56
- }
57
-
58
- function extractImagesFromMarkdown(markdown: string) {
59
- const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
60
- const images = []
61
- let match
62
-
63
- while ((match = imageRegex.exec(markdown)) !== null) {
64
- const altText = match[1]
65
- const url = match[2]
66
- images.push({ alt: altText, url: url })
67
- }
68
-
69
- return images
70
- }
71
-
72
- function getFilenameFromUrl(url: string) {
73
- return path.basename(url)
74
- }
75
-
76
- const makePackageInfo = (choices: any) => {
77
- const packageInfo = {
78
- grading: choices.grading,
79
- difficulty: choices.difficulty,
80
- duration: parseInt(choices.duration),
81
- description: {
82
- us: choices.description,
83
- },
84
- title: {
85
- us: choices.title,
86
- },
87
- slug: choices.title
88
- .toLowerCase()
89
- .replace(/ /g, "-")
90
- .replace(/[^\w-]+/g, ""),
91
- }
92
- return packageInfo
93
- }
94
-
95
- async function createPreviewReadme(
96
- tutorialDir: string,
97
- packageInfo: PackageInfo,
98
- rigoToken: string,
99
- readmeContents: string[]
100
- ) {
101
- const readmeFilename = `README.md`
102
-
103
- const readmeContent = await generateCourseIntroduction(rigoToken, {
104
- course_title: packageInfo.title.us,
105
- lessons_context: readmeContents.join("\n"),
106
- })
107
- fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer)
108
- }
109
-
110
- type PackageInfo = {
111
- grading: string
112
- difficulty: string
113
- duration: number
114
- description: {
115
- us: string
116
- }
117
- title: {
118
- us: string
119
- }
120
- }
24
+ import {
25
+ checkReadingTime,
26
+ PackageInfo,
27
+ getExInfo,
28
+ extractImagesFromMarkdown,
29
+ getFilenameFromUrl,
30
+ makePackageInfo,
31
+ } from "../utils/creatorUtilities"
32
+ import SessionManager from "../managers/session"
121
33
 
122
34
  const initializeInteractiveCreation = async (
123
35
  rigoToken: string,
124
36
  courseInfo: string
125
37
  ): Promise<{
126
- currentSteps: string[]
127
- prevInteractions: string
38
+ steps: string[]
39
+ title: string
40
+ description: string
41
+ interactions: string
128
42
  }> => {
129
43
  let prevInteractions = ""
130
44
  let isReady = false
131
45
  let currentSteps = []
46
+ let currentTitle = ""
47
+ let currentDescription = ""
132
48
  while (!isReady) {
49
+ let wholeInfo = courseInfo
50
+ wholeInfo += `
51
+ Current title: ${currentTitle}
52
+ Current description: ${currentDescription}
53
+ `
133
54
  // eslint-disable-next-line
134
55
  const res = await interactiveCreation(rigoToken, {
135
- courseInfo: courseInfo,
56
+ courseInfo: wholeInfo,
136
57
  prevInteractions: prevInteractions,
137
58
  })
138
59
  currentSteps = res.parsed.listOfSteps
139
60
  isReady = res.parsed.ready
61
+ if (res.parsed.title && currentTitle !== res.parsed.title) {
62
+ currentTitle = res.parsed.title
63
+ }
64
+
65
+ if (
66
+ res.parsed.description &&
67
+ currentDescription !== res.parsed.description
68
+ ) {
69
+ currentDescription = res.parsed.description
70
+ }
140
71
 
141
72
  if (!isReady) {
142
73
  console.log(currentSteps)
@@ -155,44 +86,28 @@ const initializeInteractiveCreation = async (
155
86
  }
156
87
 
157
88
  return {
158
- currentSteps,
159
- prevInteractions,
89
+ steps: currentSteps,
90
+ title: currentTitle,
91
+ description: currentDescription,
92
+ interactions: prevInteractions,
160
93
  }
161
94
  }
162
95
 
163
96
  const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
164
- Console.info(
165
- "Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/creators"
166
- )
167
-
168
97
  fs.removeSync(path.join(tutorialDir, "exercises", "01-hello-world"))
169
98
 
170
- const loginPrompts = await prompts([
171
- {
172
- type: "text",
173
- name: "email",
174
- message: "What's your email?",
175
- validate: (value: string) => {
176
- return value.length > 0 && value.includes("@")
177
- },
178
- },
179
- {
180
- type: "password",
181
- name: "password",
182
- message: "What's your password?",
183
- validate: (value: string) => {
184
- return value.length > 0
185
- },
186
- },
187
- ])
99
+ let sessionPayload = await SessionManager.getPayload()
188
100
 
189
- let sessionPayload
190
- try {
191
- sessionPayload = await api.login(loginPrompts.email, loginPrompts.password)
192
- } catch (error) {
193
- Console.error("Error trying to authenticate")
194
- Console.error((error as TypeError).message || (error as string))
195
- return
101
+ if (!sessionPayload || !sessionPayload.rigobot) {
102
+ Console.info(
103
+ "Almost there! First you need to login with 4Geeks.com to use AI Generation tool for creators. You can create a new account here: https://4geeks.com/creators"
104
+ )
105
+ try {
106
+ sessionPayload = await SessionManager.login()
107
+ } catch (error) {
108
+ Console.error("Error trying to authenticate")
109
+ Console.error((error as TypeError).message || (error as string))
110
+ }
196
111
  }
197
112
 
198
113
  const rigoToken = sessionPayload.rigobot.key
@@ -219,71 +134,84 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
219
134
 
220
135
  Console.success("🎉 Let's begin this learning journey!")
221
136
 
222
- const packageContext = `
137
+ let packageContext = `
223
138
  \n
224
- The following information comes from user inputs
225
139
  Title: ${packageInfo.title.us}
226
140
  Description: ${packageInfo.description.us}
227
- Grading: ${packageInfo.grading}
228
141
  Difficulty: ${packageInfo.difficulty}
229
142
  Duration: ${packageInfo.duration}
230
-
231
- Use it to generate more relevant exercises
232
143
  `
233
144
 
234
- const { currentSteps } = await initializeInteractiveCreation(
145
+ const { steps, title, description } = await initializeInteractiveCreation(
235
146
  rigoToken,
236
147
  packageContext
237
148
  )
149
+ packageInfo.title.us = title
150
+ packageInfo.description.us = description
238
151
 
152
+ packageContext = `
153
+ Title: ${title}
154
+ Description: ${description}
155
+ Difficulty: ${packageInfo.difficulty}
156
+ Estimated duration: ${packageInfo.duration}
157
+ List of exercises: ${steps.join(", ")}
158
+ `
239
159
  const exercisesDir = path.join(tutorialDir, "exercises")
240
160
  fs.ensureDirSync(exercisesDir)
241
161
 
242
162
  Console.info("Creating lessons...")
243
- for (const [index, exercise] of currentSteps.entries()) {
163
+ for (const [index, exercise] of steps.entries()) {
244
164
  const { exNumber, exTitle } = getExInfo(exercise)
245
165
  const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`)
246
166
  fs.ensureDirSync(exerciseDir)
247
167
  }
248
168
 
249
- const exercisePromises = currentSteps.map(
250
- async (exercise: any, index: number) => {
251
- const { exNumber, exTitle, kind, description } = getExInfo(exercise)
252
- const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`)
253
-
254
- const readme = await readmeCreator(rigoToken, {
255
- title: `${exNumber} - ${exTitle}`,
256
- output_lang: "en",
257
- list_of_exercises: currentSteps.join(","),
258
- tutorial_description: packageContext,
259
- lesson_description: description,
260
- kind: kind.toLowerCase(),
261
- })
169
+ const exercisePromises = steps.map(async (exercise: any, index: number) => {
170
+ const { exNumber, exTitle, kind, description } = getExInfo(exercise)
171
+ const exerciseDir = path.join(exercisesDir, `${exNumber}-${exTitle}`)
262
172
 
263
- const readmeFilename = "README.md"
173
+ const readme = await readmeCreator(rigoToken, {
174
+ title: `${exNumber} - ${exTitle}`,
175
+ output_lang: "en",
176
+ list_of_exercises: steps.join(","),
177
+ tutorial_description: packageContext,
178
+ lesson_description: description,
179
+ kind: kind.toLowerCase(),
180
+ })
264
181
 
265
- fs.writeFileSync(
266
- path.join(exerciseDir, readmeFilename),
267
- readme.parsed.content
182
+ const { exceedsThreshold, newMarkdown } = checkReadingTime(
183
+ readme.parsed.content,
184
+ 200
185
+ )
186
+
187
+ if (exceedsThreshold) {
188
+ Console.error("The reading time exceeds the threshold")
189
+ Console.info(
190
+ `Please reduce the reading time of the lesson, current reading time is ${exceedsThreshold} minutes`
268
191
  )
192
+ }
269
193
 
270
- if (kind.toLowerCase() === "code") {
271
- const codeFile = await createCodeFile(rigoToken, {
272
- readme: readme.parsed.content,
273
- })
274
-
275
- fs.writeFileSync(
276
- path.join(
277
- exerciseDir,
278
- `app.${codeFile.parsed.extension.replace(".", "")}`
279
- ),
280
- codeFile.parsed.content
281
- )
282
- }
194
+ const readmeFilename = "README.md"
283
195
 
284
- return readme.parsed.content
196
+ fs.writeFileSync(path.join(exerciseDir, readmeFilename), newMarkdown)
197
+
198
+ if (kind.toLowerCase() === "code") {
199
+ const codeFile = await createCodeFile(rigoToken, {
200
+ readme: readme.parsed.content,
201
+ tutorial_info: packageContext,
202
+ })
203
+
204
+ fs.writeFileSync(
205
+ path.join(
206
+ exerciseDir,
207
+ `app.${codeFile.parsed.extension.replace(".", "")}`
208
+ ),
209
+ codeFile.parsed.content
210
+ )
285
211
  }
286
- )
212
+
213
+ return readme.parsed.content
214
+ })
287
215
 
288
216
  let imagesArray: any[] = []
289
217
 
@@ -0,0 +1,147 @@
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,6 +1,9 @@
1
1
  import axios from "axios"
2
2
  import { writeFile } from "fs"
3
3
  import Console from "../utils/console"
4
+ import { PackageInfo } from "./creatorUtilities"
5
+ import * as fs from "fs"
6
+ import * as path from "path"
4
7
 
5
8
  const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
6
9
 
@@ -188,6 +191,7 @@ export const interactiveCreation = async (
188
191
 
189
192
  type TCreateCodeFileInputs = {
190
193
  readme: string
194
+ tutorial_info: string
191
195
  }
192
196
 
193
197
  export const createCodeFile = async (
@@ -262,7 +266,7 @@ export const readmeCreator = async (
262
266
  return createReadme(token, createReadmeInputs)
263
267
  }
264
268
 
265
- if (inputs.kind === "code") {
269
+ if (inputs.kind === "code") {
266
270
  return createCodingReadme(token, {
267
271
  title: inputs.title,
268
272
  output_lang: inputs.output_lang,
@@ -271,7 +275,21 @@ export const readmeCreator = async (
271
275
  lesson_description: inputs.lesson_description,
272
276
  })
273
277
  }
274
-
275
- throw new Error("Invalid kind of lesson")
276
-
278
+
279
+ throw new Error("Invalid kind of lesson")
280
+ }
281
+
282
+ export async function createPreviewReadme(
283
+ tutorialDir: string,
284
+ packageInfo: PackageInfo,
285
+ rigoToken: string,
286
+ readmeContents: string[]
287
+ ) {
288
+ const readmeFilename = `README.md`
289
+
290
+ const readmeContent = await generateCourseIntroduction(rigoToken, {
291
+ course_title: packageInfo.title.us,
292
+ lessons_context: readmeContents.join("\n"),
293
+ })
294
+ fs.writeFileSync(path.join(tutorialDir, readmeFilename), readmeContent.answer)
277
295
  }