@learnpack/learnpack 4.0.12 → 4.0.13

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.
@@ -1,107 +1,181 @@
1
- import { prompt } from "enquirer"
2
- import SessionCommand from "../utils/SessionCommand"
3
- import Console from "../utils/console"
4
- import api from "../utils/api"
5
- import { validURL } from "../utils/validators"
6
-
7
- // eslint-disable-next-line
8
- const fetch = require("node-fetch");
9
-
10
- class PublishCommand extends SessionCommand {
11
- static description = `Describe the command here
12
- ...
13
- Extra documentation goes here
14
- `
15
-
16
- static flags: any = {
17
- // name: flags.string({char: 'n', description: 'name to print'}),
18
- }
19
-
20
- static args = [
21
- {
22
- name: "package", // name of arg to show in help and reference with args[name]
23
- required: false, // make the arg required with `required: true`
24
- description:
25
- "The unique string that identifies this package on learnpack", // help description
26
- hidden: false, // hide this arg from help
27
- },
28
- ]
29
-
30
- async init() {
31
- const { flags } = this.parse(PublishCommand)
32
- await this.initSession(flags, true)
33
- }
34
-
35
- async run() {
36
- const { flags, args } = this.parse(PublishCommand)
37
-
38
- // avoid annonymus sessions
39
- // eslint-disable-next-line
40
- if (!this.session) return;
41
-
42
- Console.info(
43
- `Session found for ${this.session.payload.email}, publishing the package...`
44
- )
45
-
46
- const configObject = this.configManager?.get()
47
- if (
48
- configObject?.config?.slug === undefined ||
49
- !configObject.config?.slug
50
- ) {
51
- throw new Error(
52
- "The package is missing a slug (unique name identifier), please check your learn.json file and make sure it has a 'slug'"
53
- )
54
- }
55
-
56
- if (!validURL(configObject?.config?.repository ?? "")) {
57
- throw new Error(
58
- "The package has a missing or invalid 'repository' on the configuration file, it needs to be a Github URL"
59
- )
60
- } else {
61
- const validateResp = await fetch(configObject.config?.repository, {
62
- method: "HEAD",
63
- })
64
- if (!validateResp.ok || validateResp.status !== 200) {
65
- throw new Error(
66
- `The specified repository URL on the configuration file does not exist or its private, only public repositories are allowed at the moment: ${configObject.config?.repository}`
67
- )
68
- }
69
- }
70
-
71
- // start watching for file changes
72
- try {
73
- await api.publish({
74
- ...configObject,
75
- author: this.session.payload.user_id,
76
- })
77
- Console.success(
78
- `Package updated and published successfully: ${configObject.config?.slug}`
79
- )
80
- } catch (error) {
81
- if ((error as any).status === 404) {
82
- const answer = await prompt([
83
- {
84
- type: "confirm",
85
- name: "create",
86
- message: `Package with slug ${configObject.config?.slug} does not exist, do you want to create it?`,
87
- },
88
- ])
89
- if (answer) {
90
- await api.update({
91
- ...configObject,
92
- author: this.session.payload.user_id,
93
- })
94
- Console.success(
95
- `Package created and published successfully: ${configObject.config?.slug}`
96
- )
97
- } else {
98
- Console.error("No answer from server")
99
- }
100
- } else {
101
- Console.error((error as TypeError).message)
102
- }
103
- }
104
- }
105
- }
106
-
107
- export default PublishCommand
1
+ /* eslint-disable arrow-parens */
2
+ /* eslint-disable unicorn/no-array-for-each */
3
+ import { flags } from "@oclif/command"
4
+ import SessionCommand from "../utils/SessionCommand"
5
+ import SessionManager from "../managers/session"
6
+ import * as fs from "fs"
7
+ import * as path from "path"
8
+ import * as archiver from "archiver"
9
+ import axios from "axios"
10
+ import FormData = require("form-data")
11
+ import Console from "../utils/console"
12
+
13
+ // const RIGOBOT_HOST = "https://rigobot-test-cca7d841c9d8.herokuapp.com"
14
+ const RIGOBOT_HOST =
15
+ // "https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us116.gitpod.io"
16
+ "https://rigobot.herokuapp.com"
17
+ const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"
18
+
19
+ export default class BuildCommand extends SessionCommand {
20
+ static description =
21
+ "Builds the project by copying necessary files and directories into a zip file"
22
+
23
+ static flags = {
24
+ help: flags.help({ char: "h" }),
25
+ }
26
+
27
+ async init() {
28
+ const { flags } = this.parse(BuildCommand)
29
+ await this.initSession(flags)
30
+ }
31
+
32
+ async run() {
33
+ const buildDir = path.join(process.cwd(), "build")
34
+ const sessionPayload = await SessionManager.getPayload()
35
+ if (!sessionPayload || !sessionPayload.rigobot) {
36
+ Console.error(
37
+ "You must be logged in to upload a LearnPack packge, please run: \n$ learnpack login"
38
+ )
39
+ return
40
+ }
41
+
42
+ const rigoToken = sessionPayload.rigobot.key
43
+ // const rigoToken = "417d612d226a1606ad3a4e94b1881a9f0124b667"
44
+
45
+ // Read learn.json to get the slug
46
+ const learnJsonPath = path.join(process.cwd(), "learn.json")
47
+ if (!fs.existsSync(learnJsonPath)) {
48
+ this.error("learn.json not found")
49
+ }
50
+
51
+ const learnJson = JSON.parse(fs.readFileSync(learnJsonPath, "utf-8"))
52
+
53
+ const zipFilePath = path.join(process.cwd(), `${learnJson.slug}.zip`)
54
+
55
+ // Ensure build directory exists
56
+ if (!fs.existsSync(buildDir)) {
57
+ fs.mkdirSync(buildDir)
58
+ }
59
+
60
+ // Copy config.json
61
+ const configPath = path.join(process.cwd(), ".learn", "config.json")
62
+ if (fs.existsSync(configPath)) {
63
+ fs.copyFileSync(configPath, path.join(buildDir, "config.json"))
64
+ } else {
65
+ this.error("config.json not found")
66
+ }
67
+
68
+ // Copy .learn/assets directory
69
+ const assetsDir = path.join(process.cwd(), ".learn", "assets")
70
+ if (fs.existsSync(assetsDir)) {
71
+ this.copyDirectory(assetsDir, path.join(buildDir, ".learn", "assets"))
72
+ } else {
73
+ this.error(".learn/assets directory not found")
74
+ }
75
+
76
+ // Copy .learn/_app directory files to the same level as config.json
77
+ const appDir = path.join(process.cwd(), ".learn", "_app")
78
+ if (fs.existsSync(appDir)) {
79
+ this.copyDirectory(appDir, buildDir)
80
+ } else {
81
+ this.error(".learn/_app directory not found")
82
+ }
83
+
84
+ // Copy exercises directory
85
+ const exercisesDir = path.join(process.cwd(), "exercises")
86
+ const learnExercisesDir = path.join(process.cwd(), ".learn", "exercises")
87
+
88
+ if (fs.existsSync(exercisesDir)) {
89
+ this.copyDirectory(exercisesDir, path.join(buildDir, "exercises"))
90
+ } else if (fs.existsSync(learnExercisesDir)) {
91
+ this.copyDirectory(learnExercisesDir, path.join(buildDir, "exercises"))
92
+ } else {
93
+ this.error("exercises directory not found in either location")
94
+ }
95
+
96
+ // Copy learn.json
97
+ fs.copyFileSync(learnJsonPath, path.join(buildDir, "learn.json"))
98
+
99
+ // Create zip file
100
+ const output = fs.createWriteStream(zipFilePath)
101
+ const archive = archiver("zip", {
102
+ zlib: { level: 9 },
103
+ })
104
+
105
+ output.on("close", async () => {
106
+ this.log(
107
+ `Build completed: ${zipFilePath} (${archive.pointer()} total bytes)`
108
+ )
109
+ // Remove build directory after zip is created
110
+ this.removeDirectory(buildDir)
111
+ console.log("Zip file saved in project root")
112
+
113
+ const formData = new FormData()
114
+ formData.append("file", fs.createReadStream(zipFilePath))
115
+ formData.append("config", JSON.stringify(learnJson))
116
+
117
+ try {
118
+ const res = await axios.post(uploadZipEndpont, formData, {
119
+ headers: {
120
+ ...formData.getHeaders(),
121
+ Authorization: `Token ${rigoToken}`,
122
+ },
123
+ })
124
+ console.log(res.data)
125
+ } catch (error) {
126
+ if (axios.isAxiosError(error)) {
127
+ if (error.response && error.response.status === 403) {
128
+ console.error("Error 403:", error.response.data.error)
129
+ } else if (error.response && error.response.status === 400) {
130
+ console.error(error.response.data.error)
131
+ } else {
132
+ console.error("Error uploading file:", error.message)
133
+ }
134
+ } else {
135
+ console.error("Error uploading file:", error)
136
+ }
137
+ }
138
+ })
139
+
140
+ archive.on("error", (err: any) => {
141
+ throw err
142
+ })
143
+
144
+ archive.pipe(output)
145
+ archive.directory(buildDir, false)
146
+ await archive.finalize()
147
+ }
148
+
149
+ copyDirectory(src: string, dest: string) {
150
+ if (!fs.existsSync(dest)) {
151
+ fs.mkdirSync(dest, { recursive: true })
152
+ }
153
+
154
+ const entries = fs.readdirSync(src, { withFileTypes: true })
155
+
156
+ for (const entry of entries) {
157
+ const srcPath = path.join(src, entry.name)
158
+ const destPath = path.join(dest, entry.name)
159
+
160
+ if (entry.isDirectory()) {
161
+ this.copyDirectory(srcPath, destPath)
162
+ } else {
163
+ fs.copyFileSync(srcPath, destPath)
164
+ }
165
+ }
166
+ }
167
+
168
+ removeDirectory(dir: string) {
169
+ if (fs.existsSync(dir)) {
170
+ fs.readdirSync(dir).forEach((file) => {
171
+ const currentPath = path.join(dir, file)
172
+ if (fs.lstatSync(currentPath).isDirectory()) {
173
+ this.removeDirectory(currentPath)
174
+ } else {
175
+ fs.unlinkSync(currentPath)
176
+ }
177
+ })
178
+ fs.rmdirSync(dir)
179
+ }
180
+ }
181
+ }
@@ -1,145 +1,145 @@
1
- import Console from "../utils/console"
2
- import api from "../utils/api"
3
-
4
- import v from "validator"
5
- import { ValidationError, InternalError } from "../utils/errors"
6
-
7
- import * as fs from "fs"
8
- import cli from "cli-ux"
9
- import * as storage from "node-persist"
10
-
11
- import { IPayload, ISession, IStartProps } from "../models/session"
12
- import { IConfigObj } from "../models/config"
13
- import TelemetryManager from "./telemetry"
14
-
15
- const Session: ISession = {
16
- sessionStarted: false,
17
- token: null,
18
- config: null,
19
- currentCohort: null,
20
- initialize: async function () {
21
- if (!this.sessionStarted) {
22
- if (!this.config) {
23
- throw InternalError("Configuration not found")
24
- }
25
-
26
- if (!fs.existsSync(this.config.dirPath)) {
27
- fs.mkdirSync(this.config.dirPath)
28
- }
29
-
30
- await storage.init({ dir: `${this.config.dirPath}/.session` })
31
- this.sessionStarted = true
32
- }
33
-
34
- return true
35
- },
36
-
37
- setRigoToken: async function (token: string) {
38
- await this.initialize()
39
- const payload = await storage.getItem("bc-payload")
40
- await storage.setItem("bc-payload", {
41
- ...payload,
42
- rigobot: { key: token },
43
- })
44
- Console.debug("Rigobot token successfuly set")
45
- return true
46
- },
47
- setPayload: async function (value: IPayload) {
48
- await this.initialize()
49
- await storage.setItem("bc-payload", { token: this.token, ...value })
50
- Console.debug("Payload successfuly found and set for " + value.email)
51
- return true
52
- },
53
- getPayload: async function () {
54
- await this.initialize()
55
- let payload = null
56
- try {
57
- payload = await storage.getItem("bc-payload")
58
- } catch {
59
- Console.debug("Error retriving session payload")
60
- }
61
-
62
- return payload
63
- },
64
- isActive: function () {
65
- /* if (this.token) {
66
- return true
67
- } else {
68
- return false
69
- } */
70
- return !!this.token
71
- },
72
- get: async function (configObj?: IConfigObj) {
73
- if (configObj && configObj.config) {
74
- this.config = configObj.config
75
- }
76
-
77
- await this.sync()
78
- if (!this.isActive()) {
79
- return null
80
- }
81
-
82
- const payload = await this.getPayload()
83
-
84
- return {
85
- payload,
86
- token: this.token,
87
- }
88
- },
89
- login: async function () {
90
- const email = await cli.prompt("What is your email?")
91
- if (!v.isEmail(email)) {
92
- throw ValidationError("Invalid email")
93
- }
94
-
95
- const password = await cli.prompt("What is your password?", {
96
- type: "hide",
97
- })
98
-
99
- const data = await api.login(email, password)
100
- if (data) {
101
- // cli.log(data)
102
- this.start({ token: data.token, payload: data })
103
- }
104
- },
105
- loginWeb: async function (email, password) {
106
- if (!v.isEmail(email)) {
107
- throw ValidationError("Invalid email")
108
- }
109
-
110
- const data = await api.login(email, password)
111
- if (data) {
112
- this.start({ token: data.token, payload: data })
113
- TelemetryManager.setStudent({
114
- user_id: data.user_id,
115
- email: data.email,
116
- token: data.token,
117
- })
118
- return data
119
- }
120
- },
121
- sync: async function () {
122
- const payload = await this.getPayload()
123
- if (payload) {
124
- this.token = payload.token
125
- }
126
- },
127
- start: async function ({ token, payload = null }: IStartProps) {
128
- if (!token) {
129
- throw new Error("A token and email is needed to start a session")
130
- }
131
-
132
- this.token = token
133
-
134
- if (payload && (await this.setPayload(payload))) {
135
- Console.success(`Successfully logged in as ${payload.email}`)
136
- }
137
- },
138
- destroy: async function () {
139
- await storage.clear()
140
- this.token = null
141
- Console.success("You have logged out")
142
- },
143
- }
144
-
145
- export default Session
1
+ import Console from "../utils/console"
2
+ import api from "../utils/api"
3
+
4
+ import v from "validator"
5
+ import { ValidationError, InternalError } from "../utils/errors"
6
+
7
+ import * as fs from "fs"
8
+ import cli from "cli-ux"
9
+ import * as storage from "node-persist"
10
+
11
+ import { IPayload, ISession, IStartProps } from "../models/session"
12
+ import { IConfigObj } from "../models/config"
13
+ import TelemetryManager from "./telemetry"
14
+
15
+ const Session: ISession = {
16
+ sessionStarted: false,
17
+ token: null,
18
+ config: null,
19
+ currentCohort: null,
20
+ initialize: async function () {
21
+ if (!this.sessionStarted) {
22
+ if (!this.config) {
23
+ throw InternalError("Configuration not found")
24
+ }
25
+
26
+ if (!fs.existsSync(this.config.dirPath)) {
27
+ fs.mkdirSync(this.config.dirPath)
28
+ }
29
+
30
+ await storage.init({ dir: `${this.config.dirPath}/.session` })
31
+ this.sessionStarted = true
32
+ }
33
+
34
+ return true
35
+ },
36
+
37
+ setRigoToken: async function (token: string) {
38
+ await this.initialize()
39
+ const payload = await storage.getItem("bc-payload")
40
+ await storage.setItem("bc-payload", {
41
+ ...payload,
42
+ rigobot: { key: token },
43
+ })
44
+ Console.debug("Rigobot token successfuly set")
45
+ return true
46
+ },
47
+ setPayload: async function (value: IPayload) {
48
+ await this.initialize()
49
+ await storage.setItem("bc-payload", { token: this.token, ...value })
50
+ Console.debug("Payload successfuly found and set for " + value.email)
51
+ return true
52
+ },
53
+ getPayload: async function () {
54
+ await this.initialize()
55
+ let payload = null
56
+ try {
57
+ payload = await storage.getItem("bc-payload")
58
+ } catch {
59
+ Console.debug("Error retriving session payload")
60
+ }
61
+
62
+ return payload
63
+ },
64
+ isActive: function () {
65
+ /* if (this.token) {
66
+ return true
67
+ } else {
68
+ return false
69
+ } */
70
+ return !!this.token
71
+ },
72
+ get: async function (configObj?: IConfigObj) {
73
+ if (configObj && configObj.config) {
74
+ this.config = configObj.config
75
+ }
76
+
77
+ await this.sync()
78
+ if (!this.isActive()) {
79
+ return null
80
+ }
81
+
82
+ const payload = await this.getPayload()
83
+
84
+ return {
85
+ payload,
86
+ token: this.token,
87
+ }
88
+ },
89
+ login: async function () {
90
+ const email = await cli.prompt("What is your email?")
91
+ if (!v.isEmail(email)) {
92
+ throw ValidationError("Invalid email")
93
+ }
94
+
95
+ const password = await cli.prompt("What is your password?", {
96
+ type: "hide",
97
+ })
98
+
99
+ const data = await api.login(email, password)
100
+ if (data) {
101
+ // cli.log(data)
102
+ this.start({ token: data.token, payload: data })
103
+ }
104
+ },
105
+ loginWeb: async function (email, password) {
106
+ if (!v.isEmail(email)) {
107
+ throw ValidationError("Invalid email")
108
+ }
109
+
110
+ const data = await api.login(email, password)
111
+ if (data) {
112
+ this.start({ token: data.token, payload: data })
113
+ TelemetryManager.setStudent({
114
+ user_id: data.user_id,
115
+ email: data.email,
116
+ token: data.token,
117
+ })
118
+ return data
119
+ }
120
+ },
121
+ sync: async function () {
122
+ const payload = await this.getPayload()
123
+ if (payload) {
124
+ this.token = payload.token
125
+ }
126
+ },
127
+ start: async function ({ token, payload = null }: IStartProps) {
128
+ if (!token) {
129
+ throw new Error("A token and email is needed to start a session")
130
+ }
131
+
132
+ this.token = token
133
+
134
+ if (payload && (await this.setPayload(payload))) {
135
+ Console.success(`Successfully logged in as ${payload.email}`)
136
+ }
137
+ },
138
+ destroy: async function () {
139
+ await storage.clear()
140
+ this.token = null
141
+ Console.success("You have logged out")
142
+ },
143
+ }
144
+
145
+ export default Session
@@ -1,11 +0,0 @@
1
- import SessionCommand from "../utils/SessionCommand";
2
- export default class BuildCommand extends SessionCommand {
3
- static description: string;
4
- static flags: {
5
- help: import("@oclif/parser/lib/flags").IBooleanFlag<void>;
6
- };
7
- init(): Promise<void>;
8
- run(): Promise<void>;
9
- copyDirectory(src: string, dest: string): void;
10
- removeDirectory(dir: string): void;
11
- }