@learnpack/learnpack 4.0.12 → 4.0.13

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