@learnpack/learnpack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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