@learnpack/learnpack 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +695 -0
  2. package/bin/run +5 -0
  3. package/bin/run.cmd +3 -0
  4. package/oclif.manifest.json +1 -0
  5. package/package.json +111 -0
  6. package/plugin/command/compile.js +17 -0
  7. package/plugin/command/test.js +29 -0
  8. package/plugin/index.js +6 -0
  9. package/plugin/plugin.js +71 -0
  10. package/plugin/utils.js +78 -0
  11. package/src/commands/audit.js +243 -0
  12. package/src/commands/clean.js +27 -0
  13. package/src/commands/download.js +52 -0
  14. package/src/commands/hello.js +20 -0
  15. package/src/commands/init.js +133 -0
  16. package/src/commands/login.js +45 -0
  17. package/src/commands/logout.js +39 -0
  18. package/src/commands/publish.js +78 -0
  19. package/src/commands/start.js +169 -0
  20. package/src/commands/test.js +85 -0
  21. package/src/index.js +1 -0
  22. package/src/managers/config/allowed_files.js +12 -0
  23. package/src/managers/config/defaults.js +32 -0
  24. package/src/managers/config/exercise.js +212 -0
  25. package/src/managers/config/index.js +342 -0
  26. package/src/managers/file.js +137 -0
  27. package/src/managers/server/index.js +62 -0
  28. package/src/managers/server/routes.js +151 -0
  29. package/src/managers/session.js +83 -0
  30. package/src/managers/socket.js +185 -0
  31. package/src/managers/test.js +77 -0
  32. package/src/ui/download.js +48 -0
  33. package/src/utils/BaseCommand.js +34 -0
  34. package/src/utils/SessionCommand.js +46 -0
  35. package/src/utils/api.js +164 -0
  36. package/src/utils/audit.js +114 -0
  37. package/src/utils/console.js +16 -0
  38. package/src/utils/errors.js +90 -0
  39. package/src/utils/exercisesQueue.js +45 -0
  40. package/src/utils/fileQueue.js +194 -0
  41. package/src/utils/misc.js +26 -0
  42. package/src/utils/templates/gitignore.txt +20 -0
  43. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +26 -0
  44. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +25 -0
  45. package/src/utils/templates/incremental/README.ejs +5 -0
  46. package/src/utils/templates/incremental/README.es.ejs +5 -0
  47. package/src/utils/templates/isolated/01-hello-world/README.es.md +27 -0
  48. package/src/utils/templates/isolated/01-hello-world/README.md +27 -0
  49. package/src/utils/templates/isolated/README.ejs +5 -0
  50. package/src/utils/templates/isolated/README.es.ejs +5 -0
  51. package/src/utils/templates/no-grading/README.ejs +5 -0
  52. package/src/utils/templates/no-grading/README.es.ejs +5 -0
  53. package/src/utils/validators.js +15 -0
  54. package/src/utils/watcher.js +24 -0
package/package.json ADDED
@@ -0,0 +1,111 @@
1
+ {
2
+ "name": "@learnpack/learnpack",
3
+ "description": "Seamlessly build or take interactive & auto-graded tutorials, start learning now or build a tutorial.",
4
+ "version": "1.0.0",
5
+ "author": "Alejandro Sanchez @alesanchezr",
6
+ "bin": {
7
+ "learnpack": "bin/run"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/learnpack/learnpack-cli/issues"
11
+ },
12
+ "dependencies": {
13
+ "@oclif/command": "^1.6.1",
14
+ "@oclif/config": "^1.15.1",
15
+ "@oclif/plugin-help": "^3.1.0",
16
+ "@oclif/plugin-plugins": "^1.8.0",
17
+ "@oclif/plugin-warn-if-update-available": "^1.7.0",
18
+ "body-parser": "^1.19.0",
19
+ "chalk": "^4.1.0",
20
+ "chokidar": "^3.4.0",
21
+ "cli-ux": "^5.4.6",
22
+ "cors": "^2.8.5",
23
+ "debounce": "^1.2.0",
24
+ "dotenv": "^8.2.0",
25
+ "enquirer": "^2.3.6",
26
+ "eta": "^1.2.0",
27
+ "express": "^4.17.1",
28
+ "front-matter": "^4.0.2",
29
+ "fs-extra": "^9.1.0",
30
+ "git-repo-info": "^2.1.1",
31
+ "moment": "^2.27.0",
32
+ "node-emoji": "^1.10.0",
33
+ "node-fetch": "^2.6.0",
34
+ "node-persist": "^3.1.0",
35
+ "prompts": "^2.3.2",
36
+ "request": "^2.88.2",
37
+ "shelljs": "^0.8.4",
38
+ "socket.io": "^2.3.0",
39
+ "targz": "^1.0.1",
40
+ "validator": "^13.1.1",
41
+ "xxhashjs": "^0.2.2"
42
+ },
43
+ "devDependencies": {
44
+ "@oclif/dev-cli": "^1.22.2",
45
+ "@oclif/test": "^1.2.6",
46
+ "chai": "^4.2.0",
47
+ "eslint": "^5.16.0",
48
+ "eslint-config-oclif": "^3.1.0",
49
+ "globby": "^10.0.2",
50
+ "mocha": "^5.2.0",
51
+ "nyc": "^14.1.1",
52
+ "pre-commit": "^1.2.2"
53
+ },
54
+ "engines": {
55
+ "node": ">=12.0.0"
56
+ },
57
+ "files": [
58
+ "/bin",
59
+ "/npm-shrinkwrap.json",
60
+ "/oclif.manifest.json",
61
+ "/plugin",
62
+ "/src"
63
+ ],
64
+ "homepage": "https://learnpack.co",
65
+ "keywords": [
66
+ "learn",
67
+ "learn to code",
68
+ "tutorial",
69
+ "tutorials",
70
+ "coding tutorials",
71
+ "education",
72
+ "coding education"
73
+ ],
74
+ "license": "UNLICENSED",
75
+ "main": "src/index.js",
76
+ "oclif": {
77
+ "commands": "./src/commands",
78
+ "bin": "learnpack",
79
+ "plugins": [
80
+ "@oclif/plugin-help",
81
+ "@oclif/plugin-plugins",
82
+ "@oclif/plugin-warn-if-update-available"
83
+ ],
84
+ "permanent_plugins": [
85
+ "@oclif/plugin-help",
86
+ "@oclif/plugin-plugins",
87
+ "@oclif/plugin-warn-if-update-available"
88
+ ]
89
+ },
90
+ "repository": {
91
+ "type": "git",
92
+ "url": "git+https://github.com/learnpack/learnpack-cli.git"
93
+ },
94
+ "pre-commit": {
95
+ "silent": true,
96
+ "run": [
97
+ "pre"
98
+ ]
99
+ },
100
+ "scripts": {
101
+ "postpack": "rm -f oclif.manifest.json",
102
+ "posttest": "eslint .",
103
+ "pre": "node ./test/precommit/index.js",
104
+ "prepack": "oclif-dev manifest && oclif-dev readme",
105
+ "test": "nyc mocha --forbid-only \"test/**/*.test.js\"",
106
+ "version": "oclif-dev readme && git add README.md"
107
+ },
108
+ "directories": {
109
+ "test": "test"
110
+ }
111
+ }
@@ -0,0 +1,17 @@
1
+
2
+ const CompilationError = (messages) => {
3
+ const _err = new Error(messages)
4
+ _err.status = 400
5
+ _err.stdout = messages
6
+ _err.type = 'compiler-error'
7
+ return _err
8
+ }
9
+
10
+ module.exports = {
11
+ CompilationError,
12
+ default: async ({ action, ...rest }) => {
13
+
14
+ const stdout = await action.run(rest)
15
+ return stdout
16
+ }
17
+ }
@@ -0,0 +1,29 @@
1
+ const fs = require('fs')
2
+
3
+ const TestingError = (messages) => {
4
+ const _err = new Error(messages)
5
+ _err.status = 400
6
+ _err.stdout = messages
7
+ _err.type = 'testing-error'
8
+ return _err
9
+ }
10
+
11
+ module.exports = {
12
+ TestingError,
13
+ default: async function(args){
14
+ const { action, configuration, socket, exercise } = args;
15
+
16
+ if (!fs.existsSync(`${configuration.dirPath}/reports`)){
17
+ // reports directory
18
+ fs.mkdirSync(`${configuration.dirPath}/reports`);
19
+ }
20
+
21
+ // compile
22
+ const stdout = await action.run(args)
23
+
24
+ // mark exercise as done
25
+ exercise.done = true;
26
+
27
+ return stdout
28
+ }
29
+ }
@@ -0,0 +1,6 @@
1
+ const { CompilationError } = require("./command/compile")
2
+ const { TestingError } = require("./command/test")
3
+ const utils = require("./utils")
4
+ const plugin = require("./plugin")
5
+
6
+ module.exports = { CompilationError, TestingError, Utils: utils, plugin }
@@ -0,0 +1,71 @@
1
+ const shell = require('shelljs')
2
+ /**
3
+ * Main Plugin Runner, it defines the behavior of a learnpack plugin
4
+ * dividing it in "actions" like: Compile, test, etc.
5
+ * @param {object} pluginConfig Configuration object that must defined language and each possible action.
6
+ */
7
+ module.exports = (pluginConfig) => {
8
+ return async (args) => {
9
+ const { action, exercise, socket, configuration } = args
10
+
11
+
12
+ if(pluginConfig.language === undefined) throw Error(`Missing language on the plugin configuration object`)
13
+
14
+ if(typeof action !== "string"){
15
+ throw Error("Missing action property on hook details")
16
+ }
17
+
18
+ if(!exercise || exercise === undefined){
19
+ throw Error("Missing exercise information")
20
+ }
21
+
22
+ // if the action does not exist I don't do anything
23
+ if(pluginConfig[action] === undefined){
24
+ console.log(`Ignoring ${action}`)
25
+ return () => null
26
+ }
27
+
28
+ // ignore if the plugin language its not the same as the exercise language
29
+ if(exercise.language !== pluginConfig.language){
30
+ return () => null
31
+ }
32
+
33
+ if( !exercise.files || exercise.files.length == 0){
34
+ throw Error(`No files to process`)
35
+ }
36
+
37
+ try{
38
+ const _action = pluginConfig[action]
39
+
40
+ if(_action == null || typeof _action != 'object') throw Error(`The ${pluginConfig.language} ${action} module must export an object configuration`)
41
+ if(_action.validate === undefined) throw Error(`Missing validate method for ${pluginConfig.language} ${action}`)
42
+ if(_action.run === undefined) throw Error(`Missing run method for ${pluginConfig.language} ${action}`)
43
+ if(_action.dependencies !== undefined){
44
+ if(!Array.isArray(_action.dependencies)) throw Error(`${action}.dependencies must be an array of package names`)
45
+
46
+ _action.dependencies.forEach(packageName => {
47
+ if (!shell.which(packageName)) {
48
+ throw Error(`🚫 You need to have ${packageName} installed to run test the exercises`);
49
+ }
50
+ })
51
+ }
52
+ const valid = await _action.validate(({ exercise, configuration }))
53
+ if(valid){
54
+ // look for the command standard implementation and execute it
55
+ const execute = require("./command/"+action+".js").default
56
+ // no matter the command, the response must always be a stdout
57
+ const stdout = await execute({ ...args, action: _action, configuration })
58
+
59
+ // Map the action names to socket messaging standards
60
+ const actionToSuccessMapper = { compile: 'compiler', test: 'testing' }
61
+
62
+ socket.success(actionToSuccessMapper[action], stdout)
63
+ return stdout
64
+ }
65
+ }
66
+ catch(error){
67
+ if(error.type == undefined) socket.fatal(error)
68
+ else socket.error(error.type, error.stdout)
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,78 @@
1
+ const chalk = require("chalk")
2
+
3
+ const getMatches = (reg, content) => {
4
+ let inputs = [];
5
+ let m;
6
+ while ((m = reg.exec(content)) !== null) {
7
+ // This is necessary to avoid infinite loops with zero-width matches
8
+ if (m.index === reg.lastIndex) reg.lastIndex++;
9
+
10
+ // The result can be accessed through the `m`-variable.
11
+ inputs.push(m[1] || null);
12
+ }
13
+ return inputs;
14
+ }
15
+
16
+ const cleanStdout = (buffer, inputs) => {
17
+ if(Array.isArray(inputs))
18
+ for(let i = 0; i < inputs.length; i++)
19
+ if(inputs[i]) buffer = buffer.replace(inputs[i],'');
20
+
21
+ return buffer;
22
+ }
23
+
24
+ const indent = (string, count = 1, options) => {
25
+ options = {
26
+ indent: ' ',
27
+ includeEmptyLines: false,
28
+ ...options
29
+ };
30
+
31
+ if (typeof string !== 'string') {
32
+ throw new TypeError(
33
+ `Expected \`input\` to be a \`string\`, got \`${typeof string}\``
34
+ );
35
+ }
36
+
37
+ if (typeof count !== 'number') {
38
+ throw new TypeError(
39
+ `Expected \`count\` to be a \`number\`, got \`${typeof count}\``
40
+ );
41
+ }
42
+
43
+ if (count < 0) {
44
+ throw new RangeError(
45
+ `Expected \`count\` to be at least 0, got \`${count}\``
46
+ );
47
+ }
48
+
49
+ if (typeof options.indent !== 'string') {
50
+ throw new TypeError(
51
+ `Expected \`options.indent\` to be a \`string\`, got \`${typeof options.indent}\``
52
+ );
53
+ }
54
+
55
+ if (count === 0) {
56
+ return string;
57
+ }
58
+
59
+ const regex = options.includeEmptyLines ? /^/gm : /^(?!\s*$)/gm;
60
+
61
+ return string.replace(regex, options.indent.repeat(count));
62
+ };
63
+
64
+ const Console = {
65
+ // _debug: true,
66
+ _debug: process.env.DEBUG == 'true',
67
+ startDebug: function(){ this._debug = true; },
68
+ log: (msg, ...args) => console.log(chalk.gray(msg), ...args),
69
+ error: (msg, ...args) => console.log(chalk.red('⨉ '+msg), ...args),
70
+ success: (msg, ...args) => console.log(chalk.green('✓ '+msg), ...args),
71
+ info: (msg, ...args) => console.log(chalk.blue('ⓘ '+msg), ...args),
72
+ help: (msg) => console.log(`${chalk.white.bold('⚠ help:')} ${chalk.white(msg)}`),
73
+ debug(...args){
74
+ this._debug && console.log(chalk.magentaBright(`⚠ debug: `), args)
75
+ }
76
+ }
77
+
78
+ module.exports = { getMatches, cleanStdout, indent, Console };
@@ -0,0 +1,243 @@
1
+ const fs = require('fs')
2
+ const fetch = require('node-fetch')
3
+ const {validateExerciseDirectoryName} = require('../managers/config/exercise.js')
4
+ const { flags } = require('@oclif/command')
5
+ const Console = require('../utils/console')
6
+ const { isUrl, findInFile, checkLearnpackClean, checkForEmptySpaces, showErrors, showWarnings } = require('../utils/audit')
7
+ const SessionCommand = require('../utils/SessionCommand');
8
+ const fm = require("front-matter")
9
+ const path = require('path')
10
+
11
+ class AuditCommand extends SessionCommand {
12
+ async init() {
13
+ const { flags } = this.parse(AuditCommand)
14
+ await this.initSession(flags)
15
+ }
16
+ async run() {
17
+ const { flags } = this.parse(AuditCommand)
18
+
19
+ Console.log("Running command audit...")
20
+
21
+ // Get configuration object.
22
+ const config = this.configManager.get();
23
+
24
+ let errors = []
25
+ let warnings = []
26
+ let counter = {
27
+ images: {
28
+ error: 0,
29
+ total: 0,
30
+ },
31
+ links: {
32
+ error: 0,
33
+ total: 0,
34
+ },
35
+ exercises: 0,
36
+ readmeFiles: 0
37
+ }
38
+
39
+ // Checks if learnpack clean has been run
40
+ checkLearnpackClean(config, errors)
41
+
42
+ // Build exercises if they are not built yet.
43
+ if (!this.configManager.get().exercises) this.configManager.buildIndex()
44
+
45
+ // Check if the exercises folder has some files within any ./exercise
46
+ const exercisesPath = config.config.exercisesPath
47
+ fs.readdir(exercisesPath, (err, files) => {
48
+ if (err) {
49
+ return console.log('Unable to scan directory: ' + err);
50
+ }
51
+ //listing all files using forEach
52
+ files.forEach(function (file) {
53
+ // Do whatever you want to do with the file
54
+ let filePath = path.join(exercisesPath, file)
55
+ if (fs.statSync(filePath).isFile()) warnings.push({ exercise: file, msg: `This file is not inside any exercise folder.` })
56
+ });
57
+ })
58
+
59
+ // This function checks that each of the url's are working.
60
+ const checkUrl = async (file, exercise) => {
61
+ if (!fs.existsSync(file.path)) return false
62
+ const content = fs.readFileSync(file.path).toString();
63
+ let isEmpty = checkForEmptySpaces(content);
64
+ if (isEmpty === true || content == false) errors.push({ exercise: exercise.title, msg: `This file (${file.name}) doesn't have any content inside.` })
65
+ const frontmatter = fm(content).attributes
66
+ for (const attribute in frontmatter) {
67
+ if (attribute === "intro" || attribute === "tutorial") {
68
+ counter.links.total++
69
+ try {
70
+ let res = await fetch(frontmatter[attribute], { method: "HEAD" });
71
+ if (!res.ok) {
72
+ counter.links.error++;
73
+ errors.push({ exercise: exercise.title, msg: `This link is broken (${res.ok}): ${frontmatter[attribute]}` })
74
+ }
75
+ }
76
+ catch (error) {
77
+ counter.links.error++;
78
+ errors.push({ exercise: exercise.title, msg: `This link is broken: ${frontmatter[attribute]}` })
79
+ }
80
+ }
81
+ }
82
+
83
+ // Check url's of each README file.
84
+ const findings = findInFile(["relative_images", "external_images", "markdown_links"], content);
85
+ for (const finding in findings) {
86
+ let obj = findings[finding];
87
+ // Valdites all the relative path images.
88
+ if (finding === "relative_images" && Object.keys(obj).length > 0) {
89
+ for (const img in obj) {
90
+ // Validates if the image is in the assets folder.
91
+ counter.images.total++
92
+ let relativePath = path.relative(exercise.path.replace(/\\/gm, "/"), `${config.config.dirPath}/assets/${obj[img].relUrl}`).replace(/\\/gm, "/")
93
+ if (relativePath != obj[img].absUrl.split("?").shift()) {
94
+ counter.images.error++;
95
+ errors.push({ exercise: exercise.title, msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.` })
96
+ }
97
+ if (!fs.existsSync(`${config.config.dirPath}/assets/${obj[img].relUrl}`)) {
98
+ counter.images.error++;
99
+ errors.push({ exercise: exercise.title, msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.` })
100
+ }
101
+ }
102
+ } else if (finding === "external_images" && Object.keys(obj).length > 0) {
103
+ // Valdites all the aboslute path images.
104
+ for (const img in obj) {
105
+ counter.images.total++
106
+ try {
107
+ let res = await fetch(obj[img].absUrl, { method: "HEAD" });
108
+ if (!res.ok) {
109
+ counter.images.error++;
110
+ errors.push({ exercise: exercise.title, msg: `This link is broken: ${obj[img].absUrl}` })
111
+ }
112
+ }
113
+ catch (error) {
114
+ counter.images.error++;
115
+ errors.push({ exercise: exercise.title, msg: `This link is broken: ${obj[img].absUrl}` })
116
+ }
117
+ }
118
+ } else if (finding === "markdown_links" && Object.keys(obj).length > 0) {
119
+ for (const link in obj) {
120
+ counter.links.total++
121
+ try {
122
+ let res = await fetch(obj[link].mdUrl, { method: "HEAD" });
123
+ if (res.status > 399 && res.status < 200) {
124
+ counter.links.error++;
125
+ errors.push({ exercise: exercise.title, msg: `This link is broken: ${obj[link].mdUrl}` })
126
+ }
127
+ }
128
+ catch (error) {
129
+ counter.links.error++;
130
+ errors.push({ exercise: exercise.title, msg: `This link is broken: ${obj[link].mdUrl}` })
131
+ }
132
+ }
133
+ }
134
+ }
135
+ return true
136
+ }
137
+
138
+ // This function is being created because the find method doesn't work with promises.
139
+ const find = async (file, lang, exercise) => {
140
+ if (file.name === lang) {
141
+ await checkUrl(file, exercise)
142
+ return true
143
+ }
144
+ return false
145
+ }
146
+
147
+ Console.info(' Checking if the config file is fine...')
148
+ // These two lines check if the 'slug' property is inside the configuration object.
149
+ Console.debug("Checking if the slug property is inside the configuration object...")
150
+ if (!config.config.slug) errors.push({ exercise: null, msg: "The slug property is not in the configuration object" })
151
+
152
+ // These two lines check if the 'repository' property is inside the configuration object.
153
+ Console.debug("Checking if the repository property is inside the configuration object...")
154
+ if (!config.config.repository) errors.push({ exercise: null, msg: "The repository property is not in the configuration object" })
155
+ else isUrl(config.config.repository, errors, counter)
156
+
157
+ // These two lines check if the 'description' property is inside the configuration object.
158
+ Console.debug("Checking if the description property is inside the configuration object...")
159
+ if (!config.config.description) errors.push({ exercise: null, msg: "The description property is not in the configuration object" })
160
+
161
+ if (errors.length == 0) Console.log("The config file is ok")
162
+
163
+ // Validates if images and links are working at every README file.
164
+ const exercises = config.exercises
165
+ let readmeFiles = []
166
+
167
+ if (exercises.length > 0) {
168
+ Console.info(' Checking if the images are working...')
169
+ for (const index in exercises) {
170
+ let exercise = exercises[index]
171
+ if (!validateExerciseDirectoryName(exercise.title)) errors.push({exercise: exercise.title, msg: `The exercise ${exercise.title} has an invalid name.`})
172
+ let readmeFilesCount = { exercise: exercise.title, count: 0 };
173
+ if (Object.keys(exercise.translations).length == 0) errors.push({ exercise: exercise.title, msg: `The exercise ${exercise.title} doesn't have a README.md file.` })
174
+
175
+ if (exercise.language == "python3" || exercise.language == "python") {
176
+ exercise.files.map(f => f).find(f => {
177
+ if (f.path.includes('test.py') || f.path.includes('tests.py')) {
178
+ const content = fs.readFileSync(f.path).toString();
179
+ let isEmpty = checkForEmptySpaces(content);
180
+ if (isEmpty === true || content == false) errors.push({ exercise: exercise.title, msg: `This file (${f.name}) doesn't have any content inside.` })
181
+ }
182
+ });
183
+ }
184
+ else {
185
+ exercise.files.map(f => f).find(f => {
186
+ if (f.path.includes('test.js') || f.path.includes('tests.js')) {
187
+ const content = fs.readFileSync(f.path).toString();
188
+ let isEmpty = checkForEmptySpaces(content);
189
+ if (isEmpty === true || content == false) errors.push({ exercise: exercise.title, msg: `This file (${f.name}) doesn't have any content inside.` })
190
+ }
191
+ });
192
+ }
193
+
194
+ for (const lang in exercise.translations) {
195
+ let files = []
196
+ for (const file of exercise.files) {
197
+ let found = await find(file, exercise.translations[lang], exercise)
198
+ if (found == true) readmeFilesCount = { ...readmeFilesCount, count: readmeFilesCount.count + 1 }
199
+ files.push(found)
200
+ }
201
+ if (!files.includes(true)) errors.push({ exercise: exercise.title, msg: `This exercise doesn't have a README.md file.` })
202
+
203
+ }
204
+ readmeFiles.push(readmeFilesCount)
205
+ }
206
+ } else errors.push({ exercise: null, msg: "The exercises array is empty." })
207
+
208
+ Console.log(`${counter.images.total - counter.images.error} images ok from ${counter.images.total}`)
209
+
210
+ Console.info(" Checking if important files are missing... (README's, translations, gitignore...)")
211
+ // Check if all the exercises has the same ammount of README's, this way we can check if they have the same ammount of translations.
212
+ let files = [];
213
+ readmeFiles.map((item, i, arr) => {
214
+ if (item.count !== arr[0].count) files.push(` ${item.exercise}`)
215
+ })
216
+ if (files.length > 0) {
217
+ files = files.join()
218
+ warnings.push({ exercise: null, msg: `These exercises are missing translations: ${files}` })
219
+ }
220
+
221
+ // Checks if the .gitignore file exists.
222
+ if (!fs.existsSync(`.gitignore`)) warnings.push(".gitignore file doesn't exist")
223
+
224
+ counter.exercises = exercises.length;
225
+ readmeFiles.forEach((readme) => {
226
+ counter.readmeFiles += readme.count
227
+ })
228
+
229
+ await showWarnings(warnings)
230
+ await showErrors(errors, counter)
231
+ }
232
+ }
233
+
234
+ AuditCommand.description = `Check if the configuration object has slug, description and repository property
235
+ ...
236
+ Extra documentation goes here
237
+ `
238
+
239
+ AuditCommand.flags = {
240
+ // name: flags.string({char: 'n', description: 'name to print'}),
241
+ }
242
+
243
+ module.exports = AuditCommand
@@ -0,0 +1,27 @@
1
+ const {flags} = require('@oclif/command')
2
+ const Console = require('../utils/console')
3
+ const SessionCommand = require('../utils/SessionCommand')
4
+ class CleanCommand extends SessionCommand {
5
+ async init() {
6
+ const {flags} = this.parse(CleanCommand)
7
+ await this.initSession(flags)
8
+ }
9
+ async run() {
10
+ const {flags} = this.parse(CleanCommand)
11
+
12
+ this.configManager.clean()
13
+
14
+ Console.success("Package cleaned successfully, ready to publish")
15
+ }
16
+ }
17
+
18
+ CleanCommand.description = `Clean the configuration object
19
+ ...
20
+ Extra documentation goes here
21
+ `
22
+
23
+ CleanCommand.flags = {
24
+ // name: flags.string({char: 'n', description: 'name to print'}),
25
+ }
26
+
27
+ module.exports = CleanCommand
@@ -0,0 +1,52 @@
1
+ const {Command, flags} = require('@oclif/command')
2
+ const fetch = require('node-fetch');
3
+ const { clone } = require('../managers/file.js')
4
+ const Console = require('../utils/console')
5
+ const api = require('../utils/api')
6
+ const getRepoInfo = require('git-repo-info')
7
+ const { askPackage } = require('../ui/download')
8
+
9
+ class DownloadCommand extends Command {
10
+ // async init() {
11
+ // const {flags} = this.parse(DownloadCommand)
12
+ // await this.initSession(flags)
13
+ // }
14
+ async run() {
15
+ const {flags, args} = this.parse(DownloadCommand)
16
+ // start watching for file changes
17
+ let _package = args.package
18
+ if(!_package) _package = await askPackage()
19
+
20
+ if(!_package) return null
21
+
22
+ try{
23
+ _package = _package.pack;
24
+ clone(_package.repository)
25
+ .then(result => {
26
+ Console.success(`Successfully downloaded`)
27
+ const folder = _package.repository.substring(_package.repository.lastIndexOf('/') + 1).split(".")[0];
28
+ Console.info(`You can now CD into the folder like this: $ cd ${folder}`)
29
+ })
30
+ .catch(error => Console.error(error.message || error))
31
+ }
32
+ catch(error){}
33
+ }
34
+ }
35
+
36
+ DownloadCommand.description = `Describe the command here
37
+ ...
38
+ Extra documentation goes here
39
+ `
40
+ DownloadCommand.flags = {
41
+ // name: flags.string({char: 'n', description: 'name to print'}),
42
+ }
43
+ DownloadCommand.args =[
44
+ {
45
+ name: 'package', // name of arg to show in help and reference with args[name]
46
+ required: false, // make the arg required with `required: true`
47
+ description: 'The unique string that identifies this package on learnpack', // help description
48
+ hidden: false // hide this arg from help
49
+ }
50
+ ]
51
+
52
+ module.exports = DownloadCommand
@@ -0,0 +1,20 @@
1
+ const {Command, flags} = require('@oclif/command')
2
+
3
+ class HelloCommand extends Command {
4
+ async run() {
5
+ const {flags} = this.parse(HelloCommand)
6
+ const name = flags.name || 'world'
7
+ this.log(`hello ${name} from ./src/commands/hello.js`)
8
+ }
9
+ }
10
+
11
+ HelloCommand.description = `Describe the command here
12
+ ...
13
+ Extra documentation goes here
14
+ `
15
+
16
+ HelloCommand.flags = {
17
+ name: flags.string({char: 'n', description: 'name to print'}),
18
+ }
19
+
20
+ module.exports = HelloCommand