@learnpack/learnpack 4.0.18 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +10 -10
- package/lib/commands/init.js +155 -6
- package/lib/utils/rigoActions.d.ts +15 -0
- package/lib/utils/rigoActions.js +87 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/src/commands/init.ts +209 -9
- package/src/utils/rigoActions.ts +111 -0
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/
|
24
|
+
@learnpack/learnpack/5.0.0 win32-x64 node-v20.16.0
|
25
25
|
$ learnpack --help [COMMAND]
|
26
26
|
USAGE
|
27
27
|
$ learnpack COMMAND
|
@@ -74,7 +74,7 @@ DESCRIPTION
|
|
74
74
|
12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
|
75
75
|
```
|
76
76
|
|
77
|
-
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/
|
77
|
+
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\audit.ts)_
|
78
78
|
|
79
79
|
## `learnpack clean`
|
80
80
|
|
@@ -89,7 +89,7 @@ DESCRIPTION
|
|
89
89
|
Extra documentation goes here
|
90
90
|
```
|
91
91
|
|
92
|
-
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/
|
92
|
+
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\clean.ts)_
|
93
93
|
|
94
94
|
## `learnpack download [PACKAGE]`
|
95
95
|
|
@@ -107,7 +107,7 @@ DESCRIPTION
|
|
107
107
|
Extra documentation goes here
|
108
108
|
```
|
109
109
|
|
110
|
-
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/
|
110
|
+
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\download.ts)_
|
111
111
|
|
112
112
|
## `learnpack help [COMMAND]`
|
113
113
|
|
@@ -138,7 +138,7 @@ OPTIONS
|
|
138
138
|
-h, --grading show CLI help
|
139
139
|
```
|
140
140
|
|
141
|
-
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/
|
141
|
+
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\init.ts)_
|
142
142
|
|
143
143
|
## `learnpack login [PACKAGE]`
|
144
144
|
|
@@ -156,7 +156,7 @@ DESCRIPTION
|
|
156
156
|
Extra documentation goes here
|
157
157
|
```
|
158
158
|
|
159
|
-
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/
|
159
|
+
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\login.ts)_
|
160
160
|
|
161
161
|
## `learnpack logout [PACKAGE]`
|
162
162
|
|
@@ -174,7 +174,7 @@ DESCRIPTION
|
|
174
174
|
Extra documentation goes here
|
175
175
|
```
|
176
176
|
|
177
|
-
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/
|
177
|
+
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\logout.ts)_
|
178
178
|
|
179
179
|
## `learnpack plugins`
|
180
180
|
|
@@ -305,7 +305,7 @@ OPTIONS
|
|
305
305
|
-h, --help show CLI help
|
306
306
|
```
|
307
307
|
|
308
|
-
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/
|
308
|
+
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\publish.ts)_
|
309
309
|
|
310
310
|
## `learnpack start`
|
311
311
|
|
@@ -326,7 +326,7 @@ OPTIONS
|
|
326
326
|
-w, --watch Watch for file changes
|
327
327
|
```
|
328
328
|
|
329
|
-
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/
|
329
|
+
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\start.ts)_
|
330
330
|
|
331
331
|
## `learnpack test [EXERCISESLUG]`
|
332
332
|
|
@@ -340,7 +340,7 @@ ARGUMENTS
|
|
340
340
|
EXERCISESLUG The name of the exercise to test
|
341
341
|
```
|
342
342
|
|
343
|
-
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/
|
343
|
+
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.0/src\commands\test.ts)_
|
344
344
|
<!-- commandsstop -->
|
345
345
|
|
346
346
|
> > > > > > > 0cb3e56d84c197f9d008836bb573eade212b7e57
|
package/lib/commands/init.js
CHANGED
@@ -7,13 +7,143 @@ 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");
|
10
11
|
const console_1 = require("../utils/console");
|
11
12
|
const errors_1 = require("../utils/errors");
|
12
13
|
const path = require("path");
|
14
|
+
const rigoActions_1 = require("../utils/rigoActions");
|
15
|
+
const slugify = (text) => {
|
16
|
+
return text
|
17
|
+
.toString()
|
18
|
+
.normalize("NFD")
|
19
|
+
.replace(/[\u0300-\u036F]/g, "")
|
20
|
+
.toLowerCase()
|
21
|
+
.trim()
|
22
|
+
.replace(/\s+/g, "-")
|
23
|
+
.replace(/[^\w-]+/g, "");
|
24
|
+
};
|
25
|
+
const getExNumber = (index) => {
|
26
|
+
return index < 10 ? `0${index}` : `${index}`;
|
27
|
+
};
|
28
|
+
function extractImagesFromMarkdown(markdown) {
|
29
|
+
const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g;
|
30
|
+
const images = [];
|
31
|
+
let match;
|
32
|
+
while ((match = imageRegex.exec(markdown)) !== null) {
|
33
|
+
const altText = match[1];
|
34
|
+
const url = match[2];
|
35
|
+
images.push({ alt: altText, url: url });
|
36
|
+
}
|
37
|
+
return images;
|
38
|
+
}
|
39
|
+
function getFilenameFromUrl(url) {
|
40
|
+
return path.basename(url);
|
41
|
+
}
|
42
|
+
const handleAILogic = async (tutorialDir) => {
|
43
|
+
console_1.default.info("Almost there! First you need to login to use the AI creator");
|
44
|
+
fs.removeSync(path.join(tutorialDir, "exercises", "01-hello-world"));
|
45
|
+
const loginPrompts = await prompts([
|
46
|
+
{
|
47
|
+
type: "text",
|
48
|
+
name: "email",
|
49
|
+
message: "What's your email?",
|
50
|
+
validate: (value) => {
|
51
|
+
return value.length > 0 && value.includes("@");
|
52
|
+
},
|
53
|
+
},
|
54
|
+
{
|
55
|
+
type: "password",
|
56
|
+
name: "password",
|
57
|
+
message: "What's your password?",
|
58
|
+
validate: (value) => {
|
59
|
+
return value.length > 0;
|
60
|
+
},
|
61
|
+
},
|
62
|
+
]);
|
63
|
+
let sessionPayload;
|
64
|
+
try {
|
65
|
+
sessionPayload = await api_1.default.login(loginPrompts.email, loginPrompts.password);
|
66
|
+
}
|
67
|
+
catch (error) {
|
68
|
+
console_1.default.error("Error trying to authenticate");
|
69
|
+
console_1.default.error(error.message || error);
|
70
|
+
return;
|
71
|
+
}
|
72
|
+
const rigoToken = sessionPayload.rigobot.key;
|
73
|
+
const isCreator = await (0, rigoActions_1.hasCreatorPermission)(rigoToken);
|
74
|
+
if (!isCreator) {
|
75
|
+
console_1.default.error("👀 Oops! You need to be a creator to use the AI creator. Please contact support");
|
76
|
+
return;
|
77
|
+
}
|
78
|
+
console_1.default.success("🎉 Let's begin this learning journey!");
|
79
|
+
const aiChoices = await prompts([
|
80
|
+
{
|
81
|
+
type: "text",
|
82
|
+
name: "tutorialAbout",
|
83
|
+
message: "What kind of tutorial do you want to create? Please expand a little on the content, the outcome and the goal of the tutorial",
|
84
|
+
initial: "",
|
85
|
+
},
|
86
|
+
{
|
87
|
+
type: "text",
|
88
|
+
name: "exercisesNumber",
|
89
|
+
message: "How many steps or exercises do you want? Please provide a number",
|
90
|
+
validate: (value) => {
|
91
|
+
const n = Math.floor(Number(value));
|
92
|
+
return n !== Number.POSITIVE_INFINITY && String(n) === value && n > 0;
|
93
|
+
},
|
94
|
+
},
|
95
|
+
]);
|
96
|
+
const inputs = {
|
97
|
+
tutorial_about: aiChoices.tutorialAbout,
|
98
|
+
number_of_exercises: aiChoices.exercisesNumber,
|
99
|
+
};
|
100
|
+
console_1.default.info("Creating lessons...");
|
101
|
+
const res = await (0, rigoActions_1.getExercisesNames)(rigoToken, inputs);
|
102
|
+
const exercisesDir = path.join(tutorialDir, "exercises");
|
103
|
+
fs.ensureDirSync(exercisesDir);
|
104
|
+
for (const [index, exercise] of res.parsed.exercises.entries()) {
|
105
|
+
const exerciseDir = path.join(exercisesDir, `${getExNumber(index)}-${slugify(exercise)}`);
|
106
|
+
fs.ensureDirSync(exerciseDir);
|
107
|
+
}
|
108
|
+
const exercisePromises = res.parsed.exercises.map(async (exercise, index) => {
|
109
|
+
const exerciseDir = path.join(exercisesDir, `${getExNumber(index)}-${slugify(exercise)}`);
|
110
|
+
const readme = await (0, rigoActions_1.createReadme)(rigoToken, {
|
111
|
+
title: `\`${getExNumber(index)}\` ${exercise}`,
|
112
|
+
output_lang: "en",
|
113
|
+
list_of_exercises: res.parsed.exercises.join(","),
|
114
|
+
tutorial_description: aiChoices.tutorialAbout,
|
115
|
+
});
|
116
|
+
const readmeFilename = "README.md";
|
117
|
+
fs.writeFileSync(path.join(exerciseDir, readmeFilename), readme.parsed.content);
|
118
|
+
return readme.parsed.content;
|
119
|
+
});
|
120
|
+
let imagesArray = [];
|
121
|
+
const readmeContents = await Promise.all(exercisePromises);
|
122
|
+
console_1.default.success("Lessons created! 🎉");
|
123
|
+
console_1.default.info("Generating images for the lessons...");
|
124
|
+
for (const content of readmeContents) {
|
125
|
+
imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)];
|
126
|
+
}
|
127
|
+
const imagePromises = imagesArray.map(async (image) => {
|
128
|
+
try {
|
129
|
+
const filename = getFilenameFromUrl(image.url);
|
130
|
+
const imagePath = path.join(tutorialDir, ".learn", "assets", filename);
|
131
|
+
const res = await (0, rigoActions_1.generateImage)(rigoToken, { prompt: image.alt });
|
132
|
+
await (0, rigoActions_1.downloadImage)(res.image_url, imagePath);
|
133
|
+
return true;
|
134
|
+
}
|
135
|
+
catch (_a) {
|
136
|
+
console_1.default.error(`Error downloading image ${image.url}`);
|
137
|
+
return false;
|
138
|
+
}
|
139
|
+
});
|
140
|
+
await Promise.all(imagePromises);
|
141
|
+
console_1.default.info("Images generated successfully! 🎉 Your tutorial will be ready soon!");
|
142
|
+
return true;
|
143
|
+
};
|
13
144
|
class InitComand extends BaseCommand_1.default {
|
14
145
|
async run() {
|
15
146
|
const { flags } = this.parse(InitComand);
|
16
|
-
// if the folder/file .learn or .breathecode aleady exists
|
17
147
|
await alreadyInitialized();
|
18
148
|
const choices = await prompts([
|
19
149
|
{
|
@@ -68,26 +198,44 @@ class InitComand extends BaseCommand_1.default {
|
|
68
198
|
return n !== Number.POSITIVE_INFINITY && String(n) === value && n >= 0;
|
69
199
|
},
|
70
200
|
},
|
201
|
+
{
|
202
|
+
type: "select",
|
203
|
+
name: "useAI",
|
204
|
+
message: "Want a little bit of AI magic to help you? Our AI can craft the tutorial for you",
|
205
|
+
choices: [
|
206
|
+
{
|
207
|
+
title: "Yes, please help me",
|
208
|
+
value: "yes",
|
209
|
+
},
|
210
|
+
{ title: "No, thanks, I prefer to do it manually", value: "no" },
|
211
|
+
],
|
212
|
+
},
|
71
213
|
]);
|
72
214
|
const packageInfo = {
|
73
215
|
grading: choices.grading,
|
74
216
|
difficulty: choices.difficulty,
|
75
217
|
duration: parseInt(choices.duration),
|
76
|
-
description:
|
77
|
-
|
218
|
+
description: {
|
219
|
+
us: choices.description,
|
220
|
+
},
|
221
|
+
title: {
|
222
|
+
us: choices.title,
|
223
|
+
},
|
78
224
|
slug: choices.title
|
79
225
|
.toLowerCase()
|
80
226
|
.replace(/ /g, "-")
|
81
227
|
.replace(/[^\w-]+/g, ""),
|
82
228
|
};
|
83
229
|
const tutorialDir = `./${packageInfo.slug}`;
|
84
|
-
fs.ensureDirSync(tutorialDir);
|
85
|
-
cli_ux_1.default.action.start("Initializing package");
|
86
|
-
const languages = ["en", "es"];
|
230
|
+
fs.ensureDirSync(tutorialDir);
|
87
231
|
const templatesDir = path.resolve(__dirname, "../../src/utils/templates/" + (choices.grading || "no-grading"));
|
88
232
|
if (!fs.existsSync(templatesDir))
|
89
233
|
throw (0, errors_1.ValidationError)(`Template ${templatesDir} does not exists`);
|
90
234
|
await fs.copySync(templatesDir, tutorialDir);
|
235
|
+
if (choices.useAI === "yes") {
|
236
|
+
await handleAILogic(tutorialDir);
|
237
|
+
}
|
238
|
+
const languages = ["en", "es"];
|
91
239
|
// Creating README files
|
92
240
|
for (const language of languages) {
|
93
241
|
const readmeFilename = `README${language !== "en" ? `.${language}` : ""}`;
|
@@ -97,6 +245,7 @@ class InitComand extends BaseCommand_1.default {
|
|
97
245
|
if (fs.existsSync(path.join(tutorialDir, `${readmeFilename}.ejs`)))
|
98
246
|
fs.removeSync(path.join(tutorialDir, `${readmeFilename}.ejs`));
|
99
247
|
}
|
248
|
+
cli_ux_1.default.action.start("Initializing package");
|
100
249
|
if (!fs.existsSync(path.join(tutorialDir, ".gitignore")))
|
101
250
|
fs.copyFile(path.resolve(__dirname, "../../src/utils/templates/gitignore.txt"), path.join(tutorialDir, ".gitignore"));
|
102
251
|
fs.writeFileSync(path.join(tutorialDir, "learn.json"), JSON.stringify(packageInfo, null, 2));
|
@@ -0,0 +1,15 @@
|
|
1
|
+
export declare const getExercisesNames: (token: string, inputs: object) => Promise<any>;
|
2
|
+
type TCreateReadmeInputs = {
|
3
|
+
title: string;
|
4
|
+
output_lang: string;
|
5
|
+
list_of_exercises: string;
|
6
|
+
tutorial_description: string;
|
7
|
+
};
|
8
|
+
export declare const createReadme: (token: string, inputs: TCreateReadmeInputs) => Promise<any>;
|
9
|
+
export declare const hasCreatorPermission: (token: string) => Promise<boolean>;
|
10
|
+
type TGenerateImageParams = {
|
11
|
+
prompt: string;
|
12
|
+
};
|
13
|
+
export declare const generateImage: (token: string, { prompt }: TGenerateImageParams) => Promise<any>;
|
14
|
+
export declare function downloadImage(imageUrl: string, savePath: string): Promise<void>;
|
15
|
+
export {};
|
@@ -0,0 +1,87 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.generateImage = exports.hasCreatorPermission = exports.createReadme = exports.getExercisesNames = void 0;
|
4
|
+
exports.downloadImage = downloadImage;
|
5
|
+
const axios_1 = require("axios");
|
6
|
+
const fs_1 = require("fs");
|
7
|
+
const RIGOBOT_HOST = "https://rigobot.herokuapp.com";
|
8
|
+
const getExercisesNames = async (token, inputs) => {
|
9
|
+
const result = await fetch(`${RIGOBOT_HOST}/v1/prompting/completion/60/`, {
|
10
|
+
method: "POST",
|
11
|
+
headers: {
|
12
|
+
"Content-Type": "application/json",
|
13
|
+
Authorization: "Token " + token,
|
14
|
+
},
|
15
|
+
body: JSON.stringify({
|
16
|
+
inputs: inputs,
|
17
|
+
include_purpose_objective: false,
|
18
|
+
execute_async: false,
|
19
|
+
}),
|
20
|
+
});
|
21
|
+
const json = await result.json();
|
22
|
+
return json;
|
23
|
+
};
|
24
|
+
exports.getExercisesNames = getExercisesNames;
|
25
|
+
const createReadme = async (token, inputs) => {
|
26
|
+
const result = await fetch(`${RIGOBOT_HOST}/v1/prompting/completion/61/`, {
|
27
|
+
method: "POST",
|
28
|
+
headers: {
|
29
|
+
"Content-Type": "application/json",
|
30
|
+
Authorization: "Token " + token,
|
31
|
+
},
|
32
|
+
body: JSON.stringify({
|
33
|
+
inputs: inputs,
|
34
|
+
include_purpose_objective: false,
|
35
|
+
execute_async: false,
|
36
|
+
}),
|
37
|
+
});
|
38
|
+
const json = await result.json();
|
39
|
+
return json;
|
40
|
+
};
|
41
|
+
exports.createReadme = createReadme;
|
42
|
+
const hasCreatorPermission = async (token) => {
|
43
|
+
try {
|
44
|
+
const result = await fetch(`${RIGOBOT_HOST}/v1/learnpack/permissions/creator`, {
|
45
|
+
method: "GET",
|
46
|
+
headers: {
|
47
|
+
"Content-Type": "application/json",
|
48
|
+
Authorization: "Token " + token,
|
49
|
+
},
|
50
|
+
});
|
51
|
+
if (result.status === 403)
|
52
|
+
return false;
|
53
|
+
return true;
|
54
|
+
}
|
55
|
+
catch (_a) {
|
56
|
+
return false;
|
57
|
+
}
|
58
|
+
};
|
59
|
+
exports.hasCreatorPermission = hasCreatorPermission;
|
60
|
+
const generateImage = async (token, { prompt }) => {
|
61
|
+
try {
|
62
|
+
const result = await fetch(`${RIGOBOT_HOST}/v1/learnpack/tools/images`, {
|
63
|
+
method: "POST",
|
64
|
+
headers: {
|
65
|
+
"Content-Type": "application/json",
|
66
|
+
Authorization: "Token " + token,
|
67
|
+
},
|
68
|
+
body: JSON.stringify({ prompt }),
|
69
|
+
});
|
70
|
+
const json = await result.json();
|
71
|
+
return json;
|
72
|
+
}
|
73
|
+
catch (error) {
|
74
|
+
console.log(error);
|
75
|
+
return null;
|
76
|
+
}
|
77
|
+
};
|
78
|
+
exports.generateImage = generateImage;
|
79
|
+
async function downloadImage(imageUrl, savePath) {
|
80
|
+
const response = await axios_1.default.get(imageUrl, { responseType: "arraybuffer" });
|
81
|
+
const buffer = Buffer.from(response.data, "binary");
|
82
|
+
(0, fs_1.writeFile)(savePath, buffer, err => {
|
83
|
+
if (err) {
|
84
|
+
console.error("Error saving the image:", err);
|
85
|
+
}
|
86
|
+
});
|
87
|
+
}
|
package/oclif.manifest.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":"
|
1
|
+
{"version":"5.0.0","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":{"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":{"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":{},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]}}}
|
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
@@ -1,16 +1,196 @@
|
|
1
1
|
import { flags } from "@oclif/command"
|
2
2
|
import BaseCommand from "../utils/BaseCommand"
|
3
|
-
|
4
3
|
// eslint-disable-next-line
|
5
4
|
import * as fs from "fs-extra"
|
6
5
|
import * as prompts from "prompts"
|
7
6
|
import cli from "cli-ux"
|
8
7
|
import * as eta from "eta"
|
9
8
|
|
9
|
+
import api from "../utils/api"
|
10
10
|
import Console from "../utils/console"
|
11
11
|
import { ValidationError } from "../utils/errors"
|
12
12
|
|
13
13
|
import * as path from "path"
|
14
|
+
import {
|
15
|
+
hasCreatorPermission,
|
16
|
+
createReadme,
|
17
|
+
getExercisesNames,
|
18
|
+
generateImage,
|
19
|
+
downloadImage,
|
20
|
+
} from "../utils/rigoActions"
|
21
|
+
|
22
|
+
const slugify = (text: string) => {
|
23
|
+
return text
|
24
|
+
.toString()
|
25
|
+
.normalize("NFD")
|
26
|
+
.replace(/[\u0300-\u036F]/g, "")
|
27
|
+
.toLowerCase()
|
28
|
+
.trim()
|
29
|
+
.replace(/\s+/g, "-")
|
30
|
+
.replace(/[^\w-]+/g, "")
|
31
|
+
}
|
32
|
+
|
33
|
+
const getExNumber = (index: number) => {
|
34
|
+
return index < 10 ? `0${index}` : `${index}`
|
35
|
+
}
|
36
|
+
|
37
|
+
function extractImagesFromMarkdown(markdown: string) {
|
38
|
+
const imageRegex = /!\[([^\]]*)]\(([^)]+)\)/g
|
39
|
+
const images = []
|
40
|
+
let match
|
41
|
+
|
42
|
+
while ((match = imageRegex.exec(markdown)) !== null) {
|
43
|
+
const altText = match[1]
|
44
|
+
const url = match[2]
|
45
|
+
images.push({ alt: altText, url: url })
|
46
|
+
}
|
47
|
+
|
48
|
+
return images
|
49
|
+
}
|
50
|
+
|
51
|
+
function getFilenameFromUrl(url: string) {
|
52
|
+
return path.basename(url)
|
53
|
+
}
|
54
|
+
|
55
|
+
const handleAILogic = async (tutorialDir: string) => {
|
56
|
+
Console.info("Almost there! First you need to login to use the AI creator")
|
57
|
+
|
58
|
+
fs.removeSync(path.join(tutorialDir, "exercises", "01-hello-world"))
|
59
|
+
|
60
|
+
const loginPrompts = await prompts([
|
61
|
+
{
|
62
|
+
type: "text",
|
63
|
+
name: "email",
|
64
|
+
message: "What's your email?",
|
65
|
+
validate: (value: string) => {
|
66
|
+
return value.length > 0 && value.includes("@")
|
67
|
+
},
|
68
|
+
},
|
69
|
+
{
|
70
|
+
type: "password",
|
71
|
+
name: "password",
|
72
|
+
message: "What's your password?",
|
73
|
+
validate: (value: string) => {
|
74
|
+
return value.length > 0
|
75
|
+
},
|
76
|
+
},
|
77
|
+
])
|
78
|
+
|
79
|
+
let sessionPayload
|
80
|
+
try {
|
81
|
+
sessionPayload = await api.login(loginPrompts.email, loginPrompts.password)
|
82
|
+
} catch (error) {
|
83
|
+
Console.error("Error trying to authenticate")
|
84
|
+
Console.error((error as TypeError).message || (error as string))
|
85
|
+
return
|
86
|
+
}
|
87
|
+
|
88
|
+
const rigoToken = sessionPayload.rigobot.key
|
89
|
+
|
90
|
+
const isCreator = await hasCreatorPermission(rigoToken)
|
91
|
+
if (!isCreator) {
|
92
|
+
Console.error(
|
93
|
+
"👀 Oops! You need to be a creator to use the AI creator. Please contact support"
|
94
|
+
)
|
95
|
+
return
|
96
|
+
}
|
97
|
+
|
98
|
+
Console.success("🎉 Let's begin this learning journey!")
|
99
|
+
|
100
|
+
const aiChoices = await prompts([
|
101
|
+
{
|
102
|
+
type: "text",
|
103
|
+
name: "tutorialAbout",
|
104
|
+
message:
|
105
|
+
"What kind of tutorial do you want to create? Please expand a little on the content, the outcome and the goal of the tutorial",
|
106
|
+
initial: "",
|
107
|
+
},
|
108
|
+
{
|
109
|
+
type: "text",
|
110
|
+
name: "exercisesNumber",
|
111
|
+
message:
|
112
|
+
"How many steps or exercises do you want? Please provide a number",
|
113
|
+
validate: (value: string) => {
|
114
|
+
const n = Math.floor(Number(value))
|
115
|
+
return n !== Number.POSITIVE_INFINITY && String(n) === value && n > 0
|
116
|
+
},
|
117
|
+
},
|
118
|
+
])
|
119
|
+
|
120
|
+
const inputs = {
|
121
|
+
tutorial_about: aiChoices.tutorialAbout,
|
122
|
+
number_of_exercises: aiChoices.exercisesNumber,
|
123
|
+
}
|
124
|
+
Console.info("Creating lessons...")
|
125
|
+
const res = await getExercisesNames(rigoToken, inputs)
|
126
|
+
|
127
|
+
const exercisesDir = path.join(tutorialDir, "exercises")
|
128
|
+
fs.ensureDirSync(exercisesDir)
|
129
|
+
|
130
|
+
for (const [index, exercise] of res.parsed.exercises.entries()) {
|
131
|
+
const exerciseDir = path.join(
|
132
|
+
exercisesDir,
|
133
|
+
`${getExNumber(index)}-${slugify(exercise)}`
|
134
|
+
)
|
135
|
+
fs.ensureDirSync(exerciseDir)
|
136
|
+
}
|
137
|
+
|
138
|
+
const exercisePromises = res.parsed.exercises.map(
|
139
|
+
async (exercise: any, index: number) => {
|
140
|
+
const exerciseDir = path.join(
|
141
|
+
exercisesDir,
|
142
|
+
`${getExNumber(index)}-${slugify(exercise)}`
|
143
|
+
)
|
144
|
+
|
145
|
+
const readme = await createReadme(rigoToken, {
|
146
|
+
title: `\`${getExNumber(index)}\` ${exercise}`,
|
147
|
+
output_lang: "en",
|
148
|
+
list_of_exercises: res.parsed.exercises.join(","),
|
149
|
+
tutorial_description: aiChoices.tutorialAbout,
|
150
|
+
})
|
151
|
+
|
152
|
+
const readmeFilename = "README.md"
|
153
|
+
|
154
|
+
fs.writeFileSync(
|
155
|
+
path.join(exerciseDir, readmeFilename),
|
156
|
+
readme.parsed.content
|
157
|
+
)
|
158
|
+
return readme.parsed.content
|
159
|
+
}
|
160
|
+
)
|
161
|
+
|
162
|
+
let imagesArray: any[] = []
|
163
|
+
|
164
|
+
const readmeContents = await Promise.all(exercisePromises)
|
165
|
+
|
166
|
+
Console.success("Lessons created! 🎉")
|
167
|
+
Console.info("Generating images for the lessons...")
|
168
|
+
|
169
|
+
for (const content of readmeContents) {
|
170
|
+
imagesArray = [...imagesArray, ...extractImagesFromMarkdown(content)]
|
171
|
+
}
|
172
|
+
|
173
|
+
const imagePromises = imagesArray.map(async (image: any) => {
|
174
|
+
try {
|
175
|
+
const filename = getFilenameFromUrl(image.url)
|
176
|
+
const imagePath = path.join(tutorialDir, ".learn", "assets", filename)
|
177
|
+
|
178
|
+
const res = await generateImage(rigoToken, { prompt: image.alt })
|
179
|
+
await downloadImage(res.image_url, imagePath)
|
180
|
+
return true
|
181
|
+
} catch {
|
182
|
+
Console.error(`Error downloading image ${image.url}`)
|
183
|
+
|
184
|
+
return false
|
185
|
+
}
|
186
|
+
})
|
187
|
+
await Promise.all(imagePromises)
|
188
|
+
Console.info(
|
189
|
+
"Images generated successfully! 🎉 Your tutorial will be ready soon!"
|
190
|
+
)
|
191
|
+
|
192
|
+
return true
|
193
|
+
}
|
14
194
|
|
15
195
|
class InitComand extends BaseCommand {
|
16
196
|
static description =
|
@@ -24,7 +204,6 @@ class InitComand extends BaseCommand {
|
|
24
204
|
async run() {
|
25
205
|
const { flags } = this.parse(InitComand)
|
26
206
|
|
27
|
-
// if the folder/file .learn or .breathecode aleady exists
|
28
207
|
await alreadyInitialized()
|
29
208
|
|
30
209
|
const choices = await prompts([
|
@@ -80,14 +259,31 @@ class InitComand extends BaseCommand {
|
|
80
259
|
return n !== Number.POSITIVE_INFINITY && String(n) === value && n >= 0
|
81
260
|
},
|
82
261
|
},
|
262
|
+
{
|
263
|
+
type: "select",
|
264
|
+
name: "useAI",
|
265
|
+
message:
|
266
|
+
"Want a little bit of AI magic to help you? Our AI can craft the tutorial for you",
|
267
|
+
choices: [
|
268
|
+
{
|
269
|
+
title: "Yes, please help me",
|
270
|
+
value: "yes",
|
271
|
+
},
|
272
|
+
{ title: "No, thanks, I prefer to do it manually", value: "no" },
|
273
|
+
],
|
274
|
+
},
|
83
275
|
])
|
84
276
|
|
85
277
|
const packageInfo = {
|
86
278
|
grading: choices.grading,
|
87
279
|
difficulty: choices.difficulty,
|
88
280
|
duration: parseInt(choices.duration),
|
89
|
-
description:
|
90
|
-
|
281
|
+
description: {
|
282
|
+
us: choices.description,
|
283
|
+
},
|
284
|
+
title: {
|
285
|
+
us: choices.title,
|
286
|
+
},
|
91
287
|
slug: choices.title
|
92
288
|
.toLowerCase()
|
93
289
|
.replace(/ /g, "-")
|
@@ -95,11 +291,7 @@ class InitComand extends BaseCommand {
|
|
95
291
|
}
|
96
292
|
|
97
293
|
const tutorialDir = `./${packageInfo.slug}`
|
98
|
-
fs.ensureDirSync(tutorialDir)
|
99
|
-
|
100
|
-
cli.action.start("Initializing package")
|
101
|
-
|
102
|
-
const languages = ["en", "es"]
|
294
|
+
fs.ensureDirSync(tutorialDir)
|
103
295
|
|
104
296
|
const templatesDir = path.resolve(
|
105
297
|
__dirname,
|
@@ -109,6 +301,12 @@ class InitComand extends BaseCommand {
|
|
109
301
|
throw ValidationError(`Template ${templatesDir} does not exists`)
|
110
302
|
await fs.copySync(templatesDir, tutorialDir)
|
111
303
|
|
304
|
+
if (choices.useAI === "yes") {
|
305
|
+
await handleAILogic(tutorialDir)
|
306
|
+
}
|
307
|
+
|
308
|
+
const languages = ["en", "es"]
|
309
|
+
|
112
310
|
// Creating README files
|
113
311
|
for (const language of languages) {
|
114
312
|
const readmeFilename = `README${language !== "en" ? `.${language}` : ""}`
|
@@ -128,6 +326,8 @@ class InitComand extends BaseCommand {
|
|
128
326
|
fs.removeSync(path.join(tutorialDir, `${readmeFilename}.ejs`))
|
129
327
|
}
|
130
328
|
|
329
|
+
cli.action.start("Initializing package")
|
330
|
+
|
131
331
|
if (!fs.existsSync(path.join(tutorialDir, ".gitignore")))
|
132
332
|
fs.copyFile(
|
133
333
|
path.resolve(__dirname, "../../src/utils/templates/gitignore.txt"),
|
@@ -0,0 +1,111 @@
|
|
1
|
+
import axios from "axios"
|
2
|
+
import { writeFile } from "fs"
|
3
|
+
|
4
|
+
const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
|
5
|
+
|
6
|
+
export const getExercisesNames = async (token: string, inputs: object) => {
|
7
|
+
const result = await fetch(`${RIGOBOT_HOST}/v1/prompting/completion/60/`, {
|
8
|
+
method: "POST",
|
9
|
+
headers: {
|
10
|
+
"Content-Type": "application/json",
|
11
|
+
Authorization: "Token " + token,
|
12
|
+
},
|
13
|
+
body: JSON.stringify({
|
14
|
+
inputs: inputs,
|
15
|
+
include_purpose_objective: false,
|
16
|
+
execute_async: false,
|
17
|
+
}),
|
18
|
+
})
|
19
|
+
const json = await result.json()
|
20
|
+
|
21
|
+
return json
|
22
|
+
}
|
23
|
+
|
24
|
+
type TCreateReadmeInputs = {
|
25
|
+
title: string
|
26
|
+
output_lang: string
|
27
|
+
list_of_exercises: string
|
28
|
+
tutorial_description: string
|
29
|
+
}
|
30
|
+
export const createReadme = async (
|
31
|
+
token: string,
|
32
|
+
inputs: TCreateReadmeInputs
|
33
|
+
) => {
|
34
|
+
const result = await fetch(`${RIGOBOT_HOST}/v1/prompting/completion/61/`, {
|
35
|
+
method: "POST",
|
36
|
+
headers: {
|
37
|
+
"Content-Type": "application/json",
|
38
|
+
Authorization: "Token " + token,
|
39
|
+
},
|
40
|
+
body: JSON.stringify({
|
41
|
+
inputs: inputs,
|
42
|
+
include_purpose_objective: false,
|
43
|
+
execute_async: false,
|
44
|
+
}),
|
45
|
+
})
|
46
|
+
const json = await result.json()
|
47
|
+
|
48
|
+
return json
|
49
|
+
}
|
50
|
+
|
51
|
+
export const hasCreatorPermission = async (token: string) => {
|
52
|
+
try {
|
53
|
+
const result = await fetch(
|
54
|
+
`${RIGOBOT_HOST}/v1/learnpack/permissions/creator`,
|
55
|
+
{
|
56
|
+
method: "GET",
|
57
|
+
headers: {
|
58
|
+
"Content-Type": "application/json",
|
59
|
+
Authorization: "Token " + token,
|
60
|
+
},
|
61
|
+
}
|
62
|
+
)
|
63
|
+
if (result.status === 403)
|
64
|
+
return false
|
65
|
+
|
66
|
+
return true
|
67
|
+
} catch {
|
68
|
+
return false
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
type TGenerateImageParams = {
|
73
|
+
prompt: string
|
74
|
+
}
|
75
|
+
|
76
|
+
export const generateImage = async (
|
77
|
+
token: string,
|
78
|
+
{ prompt }: TGenerateImageParams
|
79
|
+
) => {
|
80
|
+
try {
|
81
|
+
const result = await fetch(`${RIGOBOT_HOST}/v1/learnpack/tools/images`, {
|
82
|
+
method: "POST",
|
83
|
+
headers: {
|
84
|
+
"Content-Type": "application/json",
|
85
|
+
Authorization: "Token " + token,
|
86
|
+
},
|
87
|
+
body: JSON.stringify({ prompt }),
|
88
|
+
})
|
89
|
+
|
90
|
+
const json = await result.json()
|
91
|
+
return json
|
92
|
+
} catch (error) {
|
93
|
+
console.log(error)
|
94
|
+
|
95
|
+
return null
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
export async function downloadImage(
|
100
|
+
imageUrl: string,
|
101
|
+
savePath: string
|
102
|
+
): Promise<void> {
|
103
|
+
const response = await axios.get(imageUrl, { responseType: "arraybuffer" })
|
104
|
+
const buffer = Buffer.from(response.data, "binary")
|
105
|
+
|
106
|
+
writeFile(savePath, buffer, err => {
|
107
|
+
if (err) {
|
108
|
+
console.error("Error saving the image:", err)
|
109
|
+
}
|
110
|
+
})
|
111
|
+
}
|