@learnpack/learnpack 5.0.27 → 5.0.28
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 +27 -11
- package/lib/commands/breakToken.d.ts +10 -0
- package/lib/commands/breakToken.js +15 -0
- package/lib/commands/init.js +9 -6
- package/lib/commands/publish.js +29 -2
- package/lib/managers/session.js +8 -0
- package/lib/models/session.d.ts +1 -0
- package/lib/utils/api.d.ts +11 -1
- package/lib/utils/api.js +49 -11
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/src/commands/breakToken.ts +25 -0
- package/src/commands/init.ts +10 -9
- package/src/commands/publish.ts +34 -2
- package/src/managers/session.ts +11 -0
- package/src/models/session.ts +1 -0
- package/src/utils/api.ts +60 -12
- package/src/utils/creatorUtilities.ts +147 -147
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.
|
24
|
+
@learnpack/learnpack/5.0.28 win32-x64 node-v20.16.0
|
25
25
|
$ learnpack --help [COMMAND]
|
26
26
|
USAGE
|
27
27
|
$ learnpack COMMAND
|
@@ -33,6 +33,7 @@ USAGE
|
|
33
33
|
|
34
34
|
<!-- commands -->
|
35
35
|
* [`learnpack audit`](#learnpack-audit)
|
36
|
+
* [`learnpack breakToken`](#learnpack-breaktoken)
|
36
37
|
* [`learnpack clean`](#learnpack-clean)
|
37
38
|
* [`learnpack download [PACKAGE]`](#learnpack-download-package)
|
38
39
|
* [`learnpack help [COMMAND]`](#learnpack-help-command)
|
@@ -75,7 +76,22 @@ DESCRIPTION
|
|
75
76
|
12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
|
76
77
|
```
|
77
78
|
|
78
|
-
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
79
|
+
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\audit.ts)_
|
80
|
+
|
81
|
+
## `learnpack breakToken`
|
82
|
+
|
83
|
+
Break the token
|
84
|
+
|
85
|
+
```
|
86
|
+
USAGE
|
87
|
+
$ learnpack breakToken
|
88
|
+
|
89
|
+
OPTIONS
|
90
|
+
-h, --grading show CLI help
|
91
|
+
-y, --yes Skip all prompts and initialize an empty project
|
92
|
+
```
|
93
|
+
|
94
|
+
_See code: [src\commands\breakToken.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\breakToken.ts)_
|
79
95
|
|
80
96
|
## `learnpack clean`
|
81
97
|
|
@@ -90,7 +106,7 @@ DESCRIPTION
|
|
90
106
|
Extra documentation goes here
|
91
107
|
```
|
92
108
|
|
93
|
-
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
109
|
+
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\clean.ts)_
|
94
110
|
|
95
111
|
## `learnpack download [PACKAGE]`
|
96
112
|
|
@@ -108,7 +124,7 @@ DESCRIPTION
|
|
108
124
|
Extra documentation goes here
|
109
125
|
```
|
110
126
|
|
111
|
-
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
127
|
+
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\download.ts)_
|
112
128
|
|
113
129
|
## `learnpack help [COMMAND]`
|
114
130
|
|
@@ -140,7 +156,7 @@ OPTIONS
|
|
140
156
|
-y, --yes Skip all prompts and initialize an empty project
|
141
157
|
```
|
142
158
|
|
143
|
-
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
159
|
+
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\init.ts)_
|
144
160
|
|
145
161
|
## `learnpack login [PACKAGE]`
|
146
162
|
|
@@ -158,7 +174,7 @@ DESCRIPTION
|
|
158
174
|
Extra documentation goes here
|
159
175
|
```
|
160
176
|
|
161
|
-
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
177
|
+
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\login.ts)_
|
162
178
|
|
163
179
|
## `learnpack logout [PACKAGE]`
|
164
180
|
|
@@ -176,7 +192,7 @@ DESCRIPTION
|
|
176
192
|
Extra documentation goes here
|
177
193
|
```
|
178
194
|
|
179
|
-
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
195
|
+
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\logout.ts)_
|
180
196
|
|
181
197
|
## `learnpack plugins`
|
182
198
|
|
@@ -307,7 +323,7 @@ OPTIONS
|
|
307
323
|
-h, --help show CLI help
|
308
324
|
```
|
309
325
|
|
310
|
-
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
326
|
+
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\publish.ts)_
|
311
327
|
|
312
328
|
## `learnpack start`
|
313
329
|
|
@@ -329,7 +345,7 @@ OPTIONS
|
|
329
345
|
-y, --yes Skip all prompts and initialize an empty project
|
330
346
|
```
|
331
347
|
|
332
|
-
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
348
|
+
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\start.ts)_
|
333
349
|
|
334
350
|
## `learnpack test [EXERCISESLUG]`
|
335
351
|
|
@@ -346,7 +362,7 @@ OPTIONS
|
|
346
362
|
-y, --yes Skip all prompts and initialize an empty project
|
347
363
|
```
|
348
364
|
|
349
|
-
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
365
|
+
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\test.ts)_
|
350
366
|
|
351
367
|
## `learnpack translate`
|
352
368
|
|
@@ -360,7 +376,7 @@ OPTIONS
|
|
360
376
|
-y, --yes Skip all prompts and initialize an empty project
|
361
377
|
```
|
362
378
|
|
363
|
-
_See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
379
|
+
_See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.28/src\commands\translate.ts)_
|
364
380
|
<!-- commandsstop -->
|
365
381
|
|
366
382
|
> > > > > > > 0cb3e56d84c197f9d008836bb573eade212b7e57
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import BaseCommand from "../utils/BaseCommand";
|
2
|
+
declare class BreakTokenCommand extends BaseCommand {
|
3
|
+
static description: string;
|
4
|
+
static flags: {
|
5
|
+
grading: import("@oclif/parser/lib/flags").IBooleanFlag<void>;
|
6
|
+
yes: import("@oclif/parser/lib/flags").IBooleanFlag<boolean>;
|
7
|
+
};
|
8
|
+
run(): Promise<void>;
|
9
|
+
}
|
10
|
+
export default BreakTokenCommand;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const command_1 = require("@oclif/command");
|
4
|
+
const BaseCommand_1 = require("../utils/BaseCommand");
|
5
|
+
const session_1 = require("../managers/session");
|
6
|
+
class BreakTokenCommand extends BaseCommand_1.default {
|
7
|
+
async run() {
|
8
|
+
const { flags } = this.parse(BreakTokenCommand);
|
9
|
+
await session_1.default.breakToken();
|
10
|
+
process.exit(0);
|
11
|
+
}
|
12
|
+
}
|
13
|
+
BreakTokenCommand.description = "Break the token";
|
14
|
+
BreakTokenCommand.flags = Object.assign(Object.assign({}, BaseCommand_1.default.flags), { grading: command_1.flags.help({ char: "h" }) });
|
15
|
+
exports.default = BreakTokenCommand;
|
package/lib/commands/init.js
CHANGED
@@ -7,11 +7,12 @@ 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");
|
13
14
|
const rigoActions_1 = require("../utils/rigoActions");
|
14
|
-
const
|
15
|
+
const api_2 = require("../utils/api");
|
15
16
|
const creatorUtilities_1 = require("../utils/creatorUtilities");
|
16
17
|
const session_1 = require("../managers/session");
|
17
18
|
const initializeInteractiveCreation = async (rigoToken, courseInfo) => {
|
@@ -65,7 +66,9 @@ const initializeInteractiveCreation = async (rigoToken, courseInfo) => {
|
|
65
66
|
const handleAILogic = async (tutorialDir, packageInfo) => {
|
66
67
|
fs.removeSync(path.join(tutorialDir, "exercises", "01-hello-world"));
|
67
68
|
let sessionPayload = await session_1.default.getPayload();
|
68
|
-
if (!sessionPayload ||
|
69
|
+
if (!sessionPayload ||
|
70
|
+
!sessionPayload.rigobot ||
|
71
|
+
(sessionPayload.token && !(await api_1.default.validateToken(sessionPayload.token)))) {
|
69
72
|
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
73
|
try {
|
71
74
|
sessionPayload = await session_1.default.login();
|
@@ -76,10 +79,10 @@ const handleAILogic = async (tutorialDir, packageInfo) => {
|
|
76
79
|
}
|
77
80
|
}
|
78
81
|
const rigoToken = sessionPayload.rigobot.key;
|
79
|
-
const
|
80
|
-
if (
|
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
|
82
|
-
process.exit(1)
|
82
|
+
const consumable = await (0, api_2.getConsumable)(sessionPayload.token, "ai-generation");
|
83
|
+
if (consumable.count === 0) {
|
84
|
+
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");
|
85
|
+
// process.exit(1)
|
83
86
|
}
|
84
87
|
const isCreator = await (0, rigoActions_1.hasCreatorPermission)(rigoToken);
|
85
88
|
if (!isCreator) {
|
package/lib/commands/publish.js
CHANGED
@@ -13,6 +13,8 @@ const axios_1 = require("axios");
|
|
13
13
|
const FormData = require("form-data");
|
14
14
|
const console_1 = require("../utils/console");
|
15
15
|
const file_1 = require("../managers/file");
|
16
|
+
const api_1 = require("../utils/api");
|
17
|
+
const prompts = require("prompts");
|
16
18
|
const RIGOBOT_HOST = "https://rigobot.herokuapp.com";
|
17
19
|
// const RIGOBOT_HOST =
|
18
20
|
// "https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us116.gitpod.io"
|
@@ -37,6 +39,26 @@ const runAudit = () => {
|
|
37
39
|
// Continuar con el proceso de build solo si `learnpack publish` fue exitoso
|
38
40
|
console_1.default.info("Learnpack publish completed successfully. Proceeding with build...");
|
39
41
|
};
|
42
|
+
const selectAcademy = async (academies) => {
|
43
|
+
if (academies.length === 0) {
|
44
|
+
return null;
|
45
|
+
}
|
46
|
+
if (academies.length === 1) {
|
47
|
+
return academies[0];
|
48
|
+
}
|
49
|
+
// prompts the user to select an academy to upload the assets
|
50
|
+
console_1.default.info("In which academy do you want to publish the asset?");
|
51
|
+
const response = await prompts({
|
52
|
+
type: "select",
|
53
|
+
name: "academy",
|
54
|
+
message: "Select an academy",
|
55
|
+
choices: academies.map((academy) => ({
|
56
|
+
title: academy.name,
|
57
|
+
value: academy,
|
58
|
+
})),
|
59
|
+
});
|
60
|
+
return response.academy;
|
61
|
+
};
|
40
62
|
class BuildCommand extends SessionCommand_1.default {
|
41
63
|
async init() {
|
42
64
|
const { flags } = this.parse(BuildCommand);
|
@@ -48,7 +70,9 @@ class BuildCommand extends SessionCommand_1.default {
|
|
48
70
|
// this.configManager?.clean()
|
49
71
|
const configObject = (_a = this.configManager) === null || _a === void 0 ? void 0 : _a.get();
|
50
72
|
let sessionPayload = await session_1.default.getPayload();
|
51
|
-
if (!sessionPayload ||
|
73
|
+
if (!sessionPayload ||
|
74
|
+
!sessionPayload.rigobot ||
|
75
|
+
(sessionPayload.token && !(await api_1.default.validateToken(sessionPayload.token)))) {
|
52
76
|
console_1.default.error("You must be logged in to upload a LearnPack package");
|
53
77
|
try {
|
54
78
|
sessionPayload = await session_1.default.login();
|
@@ -67,7 +91,10 @@ class BuildCommand extends SessionCommand_1.default {
|
|
67
91
|
console_1.default.debug("Building exercises");
|
68
92
|
(_c = this.configManager) === null || _c === void 0 ? void 0 : _c.buildIndex();
|
69
93
|
}
|
70
|
-
// const
|
94
|
+
// const academies = await api.listUserAcademies(sessionPayload.token)
|
95
|
+
// console.log(academies, "academies")
|
96
|
+
// const academy = await selectAcademy(academies)
|
97
|
+
// console.log(academy, "academy")
|
71
98
|
// Read learn.json to get the slug
|
72
99
|
const learnJsonPath = path.join(process.cwd(), "learn.json");
|
73
100
|
if (!fs.existsSync(learnJsonPath)) {
|
package/lib/managers/session.js
CHANGED
@@ -138,5 +138,13 @@ const Session = {
|
|
138
138
|
this.token = null;
|
139
139
|
console_1.default.success("You have logged out");
|
140
140
|
},
|
141
|
+
breakToken: async function () {
|
142
|
+
const payload = await this.getPayload();
|
143
|
+
if (payload) {
|
144
|
+
this.token = "asdasdasdasd";
|
145
|
+
await storage.setItem("bc-payload", Object.assign(Object.assign({}, payload), { token: "asdasdsad" }));
|
146
|
+
console_1.default.success("Token broken successfully");
|
147
|
+
}
|
148
|
+
},
|
141
149
|
};
|
142
150
|
exports.default = Session;
|
package/lib/models/session.d.ts
CHANGED
package/lib/utils/api.d.ts
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
type TConsumableSlug = "ai-conversation-message" | "ai-compilation" | "ai-tutorial-generation" | "ai-generation";
|
2
2
|
export declare const countConsumables: (consumables: any, consumableSlug?: TConsumableSlug) => any;
|
3
|
-
export declare const
|
3
|
+
export declare const getConsumable: (token: string, consumableSlug?: TConsumableSlug) => Promise<any>;
|
4
|
+
export interface TAcademy {
|
5
|
+
id: number;
|
6
|
+
name: string;
|
7
|
+
slug: string;
|
8
|
+
timezone: string;
|
9
|
+
}
|
10
|
+
export declare const listUserAcademies: (breathecodeToken: string) => Promise<TAcademy[]>;
|
11
|
+
export declare const validateToken: (token: string) => Promise<any>;
|
4
12
|
declare const _default: {
|
5
13
|
login: (identification: string, password: string) => Promise<any>;
|
6
14
|
publish: (config: any) => Promise<any>;
|
@@ -13,5 +21,7 @@ declare const _default: {
|
|
13
21
|
}) => Promise<any>;
|
14
22
|
sendBatchTelemetry: (url: string, body: any) => Promise<void>;
|
15
23
|
sendStreamTelemetry: (url: string, body: object) => Promise<void>;
|
24
|
+
listUserAcademies: (breathecodeToken: string) => Promise<TAcademy[]>;
|
25
|
+
validateToken: (token: string) => Promise<any>;
|
16
26
|
};
|
17
27
|
export default _default;
|
package/lib/utils/api.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.
|
3
|
+
exports.validateToken = exports.listUserAcademies = exports.getConsumable = exports.countConsumables = void 0;
|
4
4
|
const console_1 = require("../utils/console");
|
5
5
|
const storage = require("node-persist");
|
6
6
|
const cli_ux_1 = require("cli-ux");
|
@@ -63,13 +63,11 @@ const login = async (identification, password) => {
|
|
63
63
|
cli_ux_1.default.action.start(`Looking for credentials with ${identification}`);
|
64
64
|
await cli_ux_1.default.wait(1000);
|
65
65
|
const url = `${HOST}/v1/auth/login/`;
|
66
|
-
const
|
67
|
-
|
68
|
-
|
69
|
-
password: password,
|
70
|
-
}),
|
71
|
-
method: "post",
|
66
|
+
const res = await axios_1.default.post(url, {
|
67
|
+
email: identification,
|
68
|
+
password: password,
|
72
69
|
});
|
70
|
+
const data = res.data;
|
73
71
|
cli_ux_1.default.action.stop("ready");
|
74
72
|
let rigoPayload = null;
|
75
73
|
try {
|
@@ -258,22 +256,60 @@ const countConsumables = (consumables, consumableSlug = "ai-tutorial-generation"
|
|
258
256
|
return consumable ? consumable.balance.unit : 0;
|
259
257
|
};
|
260
258
|
exports.countConsumables = countConsumables;
|
261
|
-
const
|
259
|
+
const getConsumable = async (token, consumableSlug = "ai-generation") => {
|
262
260
|
const url = `${HOST}/v1/payments/me/service/consumable?virtual=true`;
|
263
261
|
const headers = {
|
264
262
|
Authorization: `Token ${token}`,
|
265
263
|
};
|
266
264
|
try {
|
267
265
|
const response = await axios_1.default.get(url, { headers });
|
268
|
-
const
|
269
|
-
return {
|
266
|
+
const count = (0, exports.countConsumables)(response.data, consumableSlug);
|
267
|
+
return { count };
|
270
268
|
}
|
271
269
|
catch (error) {
|
272
270
|
console.error("Error fetching consumables:", error);
|
273
271
|
throw error;
|
274
272
|
}
|
275
273
|
};
|
276
|
-
exports.
|
274
|
+
exports.getConsumable = getConsumable;
|
275
|
+
const listUserAcademies = async (breathecodeToken) => {
|
276
|
+
const url = "https://breathecode.herokuapp.com/v1/auth/user/me";
|
277
|
+
try {
|
278
|
+
const response = await axios_1.default.get(url, {
|
279
|
+
headers: {
|
280
|
+
Authorization: `Token ${breathecodeToken}`,
|
281
|
+
},
|
282
|
+
});
|
283
|
+
const data = response.data;
|
284
|
+
const academiesMap = new Map();
|
285
|
+
for (const role of data.roles) {
|
286
|
+
const academy = role.academy;
|
287
|
+
if (!academiesMap.has(academy.id)) {
|
288
|
+
academiesMap.set(academy.id, academy);
|
289
|
+
}
|
290
|
+
}
|
291
|
+
return [...academiesMap.values()];
|
292
|
+
}
|
293
|
+
catch (error) {
|
294
|
+
console.error("Failed to fetch user academies:", error);
|
295
|
+
return [];
|
296
|
+
}
|
297
|
+
};
|
298
|
+
exports.listUserAcademies = listUserAcademies;
|
299
|
+
const validateToken = async (token) => {
|
300
|
+
const url = "https://breathecode.herokuapp.com/v1/auth/user/me";
|
301
|
+
const headers = {
|
302
|
+
Authorization: `Token ${token}`,
|
303
|
+
};
|
304
|
+
try {
|
305
|
+
const response = await axios_1.default.get(url, { headers });
|
306
|
+
return response.data;
|
307
|
+
}
|
308
|
+
catch (_a) {
|
309
|
+
return false;
|
310
|
+
}
|
311
|
+
};
|
312
|
+
exports.validateToken = validateToken;
|
277
313
|
exports.default = {
|
278
314
|
login,
|
279
315
|
publish,
|
@@ -283,4 +319,6 @@ exports.default = {
|
|
283
319
|
getAllPackages,
|
284
320
|
sendBatchTelemetry,
|
285
321
|
sendStreamTelemetry,
|
322
|
+
listUserAcademies: exports.listUserAcademies,
|
323
|
+
validateToken: exports.validateToken,
|
286
324
|
};
|
package/oclif.manifest.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":"5.0.
|
1
|
+
{"version":"5.0.28","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":[]},"breakToken":{"id":"breakToken","description":"Break the token","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":[]},"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.
|
4
|
+
"version": "5.0.28",
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
6
6
|
"contributors": [
|
7
7
|
{
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { flags } from "@oclif/command"
|
2
|
+
import BaseCommand from "../utils/BaseCommand"
|
3
|
+
// eslint-disable-next-line
|
4
|
+
import * as fs from "fs-extra"
|
5
|
+
import Console from "../utils/console"
|
6
|
+
|
7
|
+
import SessionManager from "../managers/session"
|
8
|
+
|
9
|
+
class BreakTokenCommand extends BaseCommand {
|
10
|
+
static description = "Break the token"
|
11
|
+
|
12
|
+
static flags = {
|
13
|
+
...BaseCommand.flags,
|
14
|
+
grading: flags.help({ char: "h" }),
|
15
|
+
}
|
16
|
+
|
17
|
+
async run() {
|
18
|
+
const { flags } = this.parse(BreakTokenCommand)
|
19
|
+
|
20
|
+
await SessionManager.breakToken()
|
21
|
+
process.exit(0)
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
export default BreakTokenCommand
|
package/src/commands/init.ts
CHANGED
@@ -20,7 +20,7 @@ import {
|
|
20
20
|
readmeCreator,
|
21
21
|
createPreviewReadme,
|
22
22
|
} from "../utils/rigoActions"
|
23
|
-
import {
|
23
|
+
import { getConsumable } from "../utils/api"
|
24
24
|
import {
|
25
25
|
checkReadingTime,
|
26
26
|
PackageInfo,
|
@@ -98,7 +98,11 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
|
|
98
98
|
|
99
99
|
let sessionPayload = await SessionManager.getPayload()
|
100
100
|
|
101
|
-
if (
|
101
|
+
if (
|
102
|
+
!sessionPayload ||
|
103
|
+
!sessionPayload.rigobot ||
|
104
|
+
(sessionPayload.token && !(await api.validateToken(sessionPayload.token)))
|
105
|
+
) {
|
102
106
|
Console.info(
|
103
107
|
"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
108
|
)
|
@@ -112,16 +116,13 @@ const handleAILogic = async (tutorialDir: string, packageInfo: PackageInfo) => {
|
|
112
116
|
|
113
117
|
const rigoToken = sessionPayload.rigobot.key
|
114
118
|
|
115
|
-
const
|
116
|
-
sessionPayload.token,
|
117
|
-
"ai-generation"
|
118
|
-
)
|
119
|
+
const consumable = await getConsumable(sessionPayload.token, "ai-generation")
|
119
120
|
|
120
|
-
if (
|
121
|
+
if (consumable.count === 0) {
|
121
122
|
Console.error(
|
122
|
-
"It seems you cannot generate tutorials with AI. Make sure you creator subscription is up to date here: https://4geeks.com/profile/subscriptions
|
123
|
+
"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"
|
123
124
|
)
|
124
|
-
process.exit(1)
|
125
|
+
// process.exit(1)
|
125
126
|
}
|
126
127
|
|
127
128
|
const isCreator = await hasCreatorPermission(rigoToken)
|
package/src/commands/publish.ts
CHANGED
@@ -15,6 +15,8 @@ import {
|
|
15
15
|
downloadEditor,
|
16
16
|
checkIfDirectoryExists,
|
17
17
|
} from "../managers/file"
|
18
|
+
import api, { TAcademy } from "../utils/api"
|
19
|
+
import * as prompts from "prompts"
|
18
20
|
|
19
21
|
const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
|
20
22
|
// const RIGOBOT_HOST =
|
@@ -45,6 +47,29 @@ const runAudit = () => {
|
|
45
47
|
)
|
46
48
|
}
|
47
49
|
|
50
|
+
const selectAcademy = async (academies: TAcademy[]) => {
|
51
|
+
if (academies.length === 0) {
|
52
|
+
return null
|
53
|
+
}
|
54
|
+
|
55
|
+
if (academies.length === 1) {
|
56
|
+
return academies[0]
|
57
|
+
}
|
58
|
+
|
59
|
+
// prompts the user to select an academy to upload the assets
|
60
|
+
Console.info("In which academy do you want to publish the asset?")
|
61
|
+
const response = await prompts({
|
62
|
+
type: "select",
|
63
|
+
name: "academy",
|
64
|
+
message: "Select an academy",
|
65
|
+
choices: academies.map((academy) => ({
|
66
|
+
title: academy.name,
|
67
|
+
value: academy,
|
68
|
+
})),
|
69
|
+
})
|
70
|
+
return response.academy
|
71
|
+
}
|
72
|
+
|
48
73
|
export default class BuildCommand extends SessionCommand {
|
49
74
|
static description =
|
50
75
|
"Builds the project by copying necessary files and directories into a zip file"
|
@@ -65,7 +90,11 @@ export default class BuildCommand extends SessionCommand {
|
|
65
90
|
const configObject = this.configManager?.get()
|
66
91
|
|
67
92
|
let sessionPayload = await SessionManager.getPayload()
|
68
|
-
if (
|
93
|
+
if (
|
94
|
+
!sessionPayload ||
|
95
|
+
!sessionPayload.rigobot ||
|
96
|
+
(sessionPayload.token && !(await api.validateToken(sessionPayload.token)))
|
97
|
+
) {
|
69
98
|
Console.error("You must be logged in to upload a LearnPack package")
|
70
99
|
try {
|
71
100
|
sessionPayload = await SessionManager.login()
|
@@ -87,7 +116,10 @@ export default class BuildCommand extends SessionCommand {
|
|
87
116
|
this.configManager?.buildIndex()
|
88
117
|
}
|
89
118
|
|
90
|
-
// const
|
119
|
+
// const academies = await api.listUserAcademies(sessionPayload.token)
|
120
|
+
// console.log(academies, "academies")
|
121
|
+
// const academy = await selectAcademy(academies)
|
122
|
+
// console.log(academy, "academy")
|
91
123
|
|
92
124
|
// Read learn.json to get the slug
|
93
125
|
const learnJsonPath = path.join(process.cwd(), "learn.json")
|
package/src/managers/session.ts
CHANGED
@@ -167,6 +167,17 @@ const Session: ISession = {
|
|
167
167
|
this.token = null
|
168
168
|
Console.success("You have logged out")
|
169
169
|
},
|
170
|
+
breakToken: async function () {
|
171
|
+
const payload = await this.getPayload()
|
172
|
+
if (payload) {
|
173
|
+
this.token = "asdasdasdasd"
|
174
|
+
await storage.setItem("bc-payload", {
|
175
|
+
...payload,
|
176
|
+
token: "asdasdsad",
|
177
|
+
})
|
178
|
+
Console.success("Token broken successfully")
|
179
|
+
}
|
180
|
+
},
|
170
181
|
}
|
171
182
|
|
172
183
|
export default Session
|
package/src/models/session.ts
CHANGED
package/src/utils/api.ts
CHANGED
@@ -79,13 +79,12 @@ const login = async (identification: string, password: string) => {
|
|
79
79
|
await cli.wait(1000)
|
80
80
|
const url = `${HOST}/v1/auth/login/`
|
81
81
|
|
82
|
-
const
|
83
|
-
|
84
|
-
|
85
|
-
password: password,
|
86
|
-
}),
|
87
|
-
method: "post",
|
82
|
+
const res = await axios.post(url, {
|
83
|
+
email: identification,
|
84
|
+
password: password,
|
88
85
|
})
|
86
|
+
const data = res.data
|
87
|
+
|
89
88
|
cli.action.stop("ready")
|
90
89
|
let rigoPayload = null
|
91
90
|
try {
|
@@ -320,7 +319,7 @@ export const countConsumables = (
|
|
320
319
|
return consumable ? consumable.balance.unit : 0
|
321
320
|
}
|
322
321
|
|
323
|
-
export const
|
322
|
+
export const getConsumable = async (
|
324
323
|
token: string,
|
325
324
|
consumableSlug: TConsumableSlug = "ai-generation"
|
326
325
|
): Promise<any> => {
|
@@ -333,18 +332,65 @@ export const getConsumables = async (
|
|
333
332
|
try {
|
334
333
|
const response = await axios.get(url, { headers })
|
335
334
|
|
336
|
-
const
|
337
|
-
response.data,
|
338
|
-
consumableSlug
|
339
|
-
)
|
335
|
+
const count = countConsumables(response.data, consumableSlug)
|
340
336
|
|
341
|
-
return {
|
337
|
+
return { count }
|
342
338
|
} catch (error) {
|
343
339
|
console.error("Error fetching consumables:", error)
|
344
340
|
throw error
|
345
341
|
}
|
346
342
|
}
|
347
343
|
|
344
|
+
export interface TAcademy {
|
345
|
+
id: number
|
346
|
+
name: string
|
347
|
+
slug: string
|
348
|
+
timezone: string
|
349
|
+
}
|
350
|
+
|
351
|
+
export const listUserAcademies = async (
|
352
|
+
breathecodeToken: string
|
353
|
+
): Promise<TAcademy[]> => {
|
354
|
+
const url = "https://breathecode.herokuapp.com/v1/auth/user/me"
|
355
|
+
|
356
|
+
try {
|
357
|
+
const response = await axios.get(url, {
|
358
|
+
headers: {
|
359
|
+
Authorization: `Token ${breathecodeToken}`,
|
360
|
+
},
|
361
|
+
})
|
362
|
+
|
363
|
+
const data = response.data
|
364
|
+
|
365
|
+
const academiesMap = new Map<number, TAcademy>()
|
366
|
+
for (const role of data.roles) {
|
367
|
+
const academy = role.academy
|
368
|
+
if (!academiesMap.has(academy.id)) {
|
369
|
+
academiesMap.set(academy.id, academy)
|
370
|
+
}
|
371
|
+
}
|
372
|
+
|
373
|
+
return [...academiesMap.values()]
|
374
|
+
} catch (error) {
|
375
|
+
console.error("Failed to fetch user academies:", error)
|
376
|
+
return []
|
377
|
+
}
|
378
|
+
}
|
379
|
+
|
380
|
+
export const validateToken = async (token: string) => {
|
381
|
+
const url = "https://breathecode.herokuapp.com/v1/auth/user/me"
|
382
|
+
const headers = {
|
383
|
+
Authorization: `Token ${token}`,
|
384
|
+
}
|
385
|
+
|
386
|
+
try {
|
387
|
+
const response = await axios.get(url, { headers })
|
388
|
+
return response.data
|
389
|
+
} catch {
|
390
|
+
return false
|
391
|
+
}
|
392
|
+
}
|
393
|
+
|
348
394
|
export default {
|
349
395
|
login,
|
350
396
|
publish,
|
@@ -354,4 +400,6 @@ export default {
|
|
354
400
|
getAllPackages,
|
355
401
|
sendBatchTelemetry,
|
356
402
|
sendStreamTelemetry,
|
403
|
+
listUserAcademies,
|
404
|
+
validateToken,
|
357
405
|
}
|
@@ -1,147 +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
|
+
// 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
|
+
}
|