@learnpack/learnpack 5.0.320 → 5.0.322

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/src/ui/app.tar.gz CHANGED
Binary file
@@ -8,10 +8,10 @@ import { cli } from "cli-ux"
8
8
  import Console from "./console"
9
9
 
10
10
  type TNeededPlugins = {
11
- needed: string[]
12
- notInstalled: string[]
13
- }
14
- type TPackageManager = "npm" | "pip"
11
+ needed: string[];
12
+ notInstalled: string[];
13
+ };
14
+ type TPackageManager = "npm" | "pip";
15
15
 
16
16
  export const checkNotInstalledPlugins = async (
17
17
  exercises: IExercise[],
@@ -45,8 +45,7 @@ export const checkNotInstalledPlugins = async (
45
45
 
46
46
  for (const f of notReadmeFiles) {
47
47
  const ext = f.name.split(".").pop()
48
- if (!ext)
49
- continue
48
+ if (!ext) continue
50
49
 
51
50
  if (f.name.includes("test")) {
52
51
  !testingExtensions.includes(ext) && testingExtensions.push(ext)
@@ -116,7 +115,9 @@ continue
116
115
  Console.info("Run: $ learnpack start")
117
116
  command.exit(0)
118
117
  } else {
119
- Console.error("You need to install the plugins to complete this exercise")
118
+ Console.error(
119
+ "You need to install the plugins to complete this exercise"
120
+ )
120
121
  Console.info(
121
122
  "To install the plugins run each of the following commands: "
122
123
  )
@@ -149,15 +150,52 @@ const installDependencies = async (
149
150
  command = `pip install ${deps.join(" ")}`
150
151
  }
151
152
 
152
- const { stdout, stderr } = await exec(command)
153
- if (stderr && (stderr.includes("npm ERR!") || stderr.includes("Traceback"))) {
154
- Console.error(`Error executing ${command}.`)
155
- Console.error(stderr)
156
- return
157
- }
153
+ try {
154
+ const { stdout, stderr } = await exec(command)
155
+ if (
156
+ stderr &&
157
+ (stderr.includes("npm ERR!") || stderr.includes("Traceback"))
158
+ ) {
159
+ Console.error(`Error executing ${command}.`)
160
+ Console.error(stderr)
161
+ return
162
+ }
158
163
 
159
- Console.info(`Dependencies ${deps.join(" ")} installed...`)
160
- return true
164
+ Console.info(`Dependencies ${deps.join(" ")} installed...`)
165
+ return true
166
+ } catch (error) {
167
+ // Si npm/pip retorna código > 0, exec lanzará una excepción
168
+ const execError = error as any
169
+ const stdout = execError.stdout || ""
170
+ const stderr = execError.stderr || ""
171
+ const errorMessage = stderr || execError.message || String(error)
172
+
173
+ // Verificar si es un error real
174
+ if (
175
+ errorMessage.includes("npm ERR!") ||
176
+ errorMessage.includes("Traceback")
177
+ ) {
178
+ Console.error(`Error executing ${command}.`)
179
+ Console.error(errorMessage)
180
+ return
181
+ }
182
+
183
+ // Si no es un error real, podría ser solo un warning
184
+ // Verificar si la instalación fue exitosa a pesar del warning
185
+ if (
186
+ stdout &&
187
+ (stdout.includes("added") || stdout.includes("Successfully installed"))
188
+ ) {
189
+ Console.info(`Dependencies ${deps.join(" ")} installed...`)
190
+ return true
191
+ }
192
+
193
+ // Si no hay indicios de éxito, se asume fallo (retorna undefined implícitamente)
194
+ Console.debug(
195
+ `${command} returned non-zero exit code, but may be just a warning:`,
196
+ errorMessage
197
+ )
198
+ }
161
199
  }
162
200
 
163
201
  export const checkNotInstalledDependencies = async (
@@ -165,7 +203,10 @@ export const checkNotInstalledDependencies = async (
165
203
  ) => {
166
204
  Console.info("Checking needed dependencies...")
167
205
 
168
- const jsPluginsDependencies = ["jest@29.7.0", "jest-environment-jsdom@29.7.0"]
206
+ const jsPluginsDependencies = [
207
+ "jest@29.7.0",
208
+ "jest-environment-jsdom@29.7.0",
209
+ ]
169
210
  // pytest up to 6.2.5
170
211
  const pyPluginsDependencies = ["pytest", "pytest-testdox", "mock"]
171
212
 
@@ -188,15 +229,33 @@ export const checkNotInstalledDependencies = async (
188
229
  }
189
230
 
190
231
  if (jestNeeded) {
191
- const { stdout, stderr } = await exec("npm ls -g")
192
- if (stderr) {
193
- Console.error(`Error executing ${npmLsCommand}. Use debug for more info`)
194
- Console.debug(stderr)
195
- return false
196
- }
232
+ try {
233
+ const { stdout, stderr } = await exec("npm ls -g")
234
+ // Solo considerar errores reales, no warnings
235
+ if (stderr && stderr.includes("npm ERR!")) {
236
+ Console.error(`Error executing npm ls -g. Use debug for more info`)
237
+ Console.debug(stderr)
238
+ return false
239
+ }
240
+
241
+ if (includesAll(stdout, jsPluginsDependencies)) return true
242
+ } catch (error) {
243
+ // Si npm retorna código > 0, exec lanzará una excepción
244
+ // Verificar si es un error real o solo un warning
245
+ const errorMessage =
246
+ (error as any)?.stderr || (error as Error)?.message || String(error)
247
+ if (errorMessage.includes("npm ERR!")) {
248
+ Console.error(`Error executing npm ls -g. Use debug for more info`)
249
+ Console.debug(errorMessage)
250
+ return false
251
+ }
197
252
 
198
- if (includesAll(stdout, jsPluginsDependencies))
199
- return true
253
+ // Si no es un error real, continuar (podría ser solo un warning)
254
+ Console.debug(
255
+ "npm ls -g returned non-zero exit code, but may be just a warning:",
256
+ errorMessage
257
+ )
258
+ }
200
259
 
201
260
  Console.error("The jest dependencies are not installed")
202
261
  const confirmInstall = await cli.confirm(
@@ -231,15 +290,32 @@ return true
231
290
  Console.debug(error)
232
291
  }
233
292
 
234
- const { stdout, stderr } = await exec("pip list")
235
- if (stderr) {
236
- Console.error(`Error executing pip list. Use debug for more info`)
237
- Console.debug(stderr)
238
- return
239
- }
293
+ try {
294
+ const { stdout, stderr } = await exec("pip list")
295
+ // Solo considerar errores reales, no warnings
296
+ if (stderr && stderr.includes("Traceback")) {
297
+ Console.error(`Error executing pip list. Use debug for more info`)
298
+ Console.debug(stderr)
299
+ return false
300
+ }
301
+
302
+ if (includesAll(stdout, pyPluginsDependencies)) return true
303
+ } catch (error) {
304
+ // Si pip retorna código > 0, verificar si es un error real
305
+ const errorMessage =
306
+ (error as any)?.stderr || (error as Error)?.message || String(error)
307
+ if (errorMessage.includes("Traceback")) {
308
+ Console.error(`Error executing pip list. Use debug for more info`)
309
+ Console.debug(errorMessage)
310
+ return false
311
+ }
240
312
 
241
- if (includesAll(stdout, pyPluginsDependencies))
242
- return true
313
+ // Si no es un error real, continuar
314
+ Console.debug(
315
+ "pip list returned non-zero exit code, but may be just a warning:",
316
+ errorMessage
317
+ )
318
+ }
243
319
 
244
320
  Console.error("The pytest dependencies are not installed")
245
321
  const confirmInstall = await cli.confirm(
@@ -0,0 +1,295 @@
1
+ import { Request, Response, NextFunction } from "express"
2
+
3
+ /**
4
+ * Interface for application errors with additional information
5
+ */
6
+ export interface AppError extends Error {
7
+ statusCode?: number;
8
+ code?: string;
9
+ isOperational?: boolean;
10
+ details?: any;
11
+ }
12
+
13
+ /**
14
+ * Base class for operational errors (expected errors)
15
+ */
16
+ export class OperationalError extends Error implements AppError {
17
+ statusCode: number
18
+ code: string
19
+ isOperational: boolean
20
+ details?: any
21
+
22
+ constructor(
23
+ message: string,
24
+ details?: any,
25
+ statusCode = 500,
26
+ code = "OPERATIONAL_ERROR"
27
+ ) {
28
+ super(message)
29
+ this.name = this.constructor.name
30
+ this.statusCode = statusCode
31
+ this.code = code
32
+ this.isOperational = true
33
+ this.details = details
34
+
35
+ // Maintains correct stack trace
36
+ Error.captureStackTrace(this, this.constructor)
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Validation error (400)
42
+ */
43
+ export class ValidationError extends OperationalError {
44
+ constructor(message: string, details?: any) {
45
+ super(message, details, 400, "VALIDATION_ERROR")
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Authentication error (401)
51
+ */
52
+ export class UnauthorizedError extends OperationalError {
53
+ constructor(message: string, details?: any) {
54
+ super(message, details, 401, "UNAUTHORIZED")
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Authorization error (403)
60
+ */
61
+ export class ForbiddenError extends OperationalError {
62
+ constructor(message: string, details?: any) {
63
+ super(message, details, 403, "FORBIDDEN")
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Resource not found error (404)
69
+ */
70
+ export class NotFoundError extends OperationalError {
71
+ constructor(message: string, details?: any) {
72
+ super(message, details, 404, "NOT_FOUND")
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Conflict error (409)
78
+ */
79
+ export class ConflictError extends OperationalError {
80
+ constructor(message: string, details?: any) {
81
+ super(message, details, 409, "CONFLICT")
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Internal server error (500)
87
+ */
88
+ export class InternalServerError extends OperationalError {
89
+ constructor(message: string, details?: any) {
90
+ super(message, details, 500, "INTERNAL_SERVER_ERROR")
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Determines if an error is operational (expected) or programming (unexpected)
96
+ * @param error - The error to check
97
+ * @returns true if the error is operational, false otherwise
98
+ */
99
+ function isOperationalError(error: any): boolean {
100
+ if (error instanceof OperationalError) {
101
+ return error.isOperational === true
102
+ }
103
+
104
+ return false
105
+ }
106
+
107
+ /**
108
+ * Formats the error for HTTP response
109
+ * @param error - The error to format
110
+ * @param isDevelopment - Whether in development mode
111
+ * @returns Object with the formatted response
112
+ */
113
+ function formatError(error: AppError | Error, isDevelopment = false) {
114
+ const isOperational = isOperationalError(error)
115
+ const appError = error as AppError
116
+
117
+ const baseResponse: any = {
118
+ status: "error",
119
+ message: appError.message || "An unexpected error occurred",
120
+ code: appError.code || "INTERNAL_ERROR",
121
+ }
122
+
123
+ // In development, include more details
124
+ if (isDevelopment) {
125
+ baseResponse.stack = appError.stack
126
+ baseResponse.details = appError.details
127
+ }
128
+
129
+ // If it's an operational error, include details if they exist
130
+ if (isOperational && appError.details) {
131
+ baseResponse.details = appError.details
132
+ }
133
+
134
+ // If not operational, don't expose details in production
135
+ if (!isOperational && !isDevelopment) {
136
+ baseResponse.message = "An unexpected error occurred"
137
+ baseResponse.code = "INTERNAL_ERROR"
138
+ }
139
+
140
+ return baseResponse
141
+ }
142
+
143
+ /**
144
+ * Determines the appropriate HTTP status code
145
+ * @param error - The error from which to get the status code
146
+ * @returns The HTTP status code
147
+ */
148
+ function getStatusCode(error: AppError | Error): number {
149
+ if ((error as AppError).statusCode) {
150
+ return (error as AppError).statusCode!
151
+ }
152
+
153
+ // Common Node.js errors
154
+ if (error.name === "ValidationError") return 400
155
+ if (error.name === "UnauthorizedError") return 401
156
+ if (error.name === "ForbiddenError") return 403
157
+ if (error.name === "NotFoundError") return 404
158
+ if (error.name === "ConflictError") return 409
159
+
160
+ // Default to server error
161
+ return 500
162
+ }
163
+
164
+ /**
165
+ * Logs the error in a structured format
166
+ * @param error - The error to log
167
+ * @param req - Express Request object
168
+ * @returns void
169
+ */
170
+ function logError(error: AppError | Error, req: Request): void {
171
+ const appError = error as AppError
172
+ const isOperational = isOperationalError(error)
173
+
174
+ const logData = {
175
+ timestamp: new Date().toISOString(),
176
+ method: req.method,
177
+ url: req.originalUrl,
178
+ statusCode: getStatusCode(error),
179
+ error: {
180
+ name: error.name,
181
+ message: error.message,
182
+ code: appError.code,
183
+ isOperational,
184
+ stack: error.stack,
185
+ details: appError.details,
186
+ },
187
+ ip: req.ip || req.socket.remoteAddress,
188
+ userAgent: req.get("user-agent"),
189
+ }
190
+
191
+ // Different log based on error type
192
+ if (isOperational) {
193
+ console.error("⚠️ Operational Error:", JSON.stringify(logData, null, 2))
194
+ } else {
195
+ console.error("❌ Unexpected Error:", JSON.stringify(logData, null, 2))
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Wrapper for async handlers that automatically catches errors
201
+ *
202
+ * Usage:
203
+ * app.get('/route', asyncHandler(async (req, res) => {
204
+ * // your async code here
205
+ * }))
206
+ * @param fn - Async handler function to wrap
207
+ * @returns Express middleware that automatically catches errors
208
+ */
209
+ export const asyncHandler = (
210
+ fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
211
+ ) => {
212
+ return (req: Request, res: Response, next: NextFunction) => {
213
+ Promise.resolve(fn(req, res, next)).catch(next)
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Middleware to handle not found routes (404)
219
+ * Must be placed after all routes but before the error handler
220
+ * @param req - Express Request object
221
+ * @param res - Express Response object
222
+ * @param next - Express next function
223
+ * @returns void
224
+ */
225
+ export const notFoundHandler = (
226
+ req: Request,
227
+ res: Response,
228
+ next: NextFunction
229
+ ): void => {
230
+ const error = new NotFoundError(
231
+ `Route ${req.method} ${req.originalUrl} not found`
232
+ )
233
+ next(error)
234
+ }
235
+
236
+ /**
237
+ * Global error handling middleware
238
+ * Must be the last middleware in the chain
239
+ *
240
+ * Catches all unhandled synchronous and asynchronous errors
241
+ * @param error - The caught error
242
+ * @param req - Express Request object
243
+ * @param res - Express Response object
244
+ * @param next - Express next function
245
+ * @returns void
246
+ */
247
+ export const errorHandler = (
248
+ error: AppError | Error,
249
+ req: Request,
250
+ res: Response,
251
+ next: NextFunction
252
+ ): void => {
253
+ // If response already sent, delegate to Express default handler
254
+ if (res.headersSent) {
255
+ return next(error)
256
+ }
257
+
258
+ // Determine if we're in development mode
259
+ const isDevelopment =
260
+ process.env.NODE_ENV === "development" || process.env.DEBUG === "true"
261
+
262
+ // Log the error
263
+ logError(error, req)
264
+
265
+ // Get status code
266
+ const statusCode = getStatusCode(error)
267
+
268
+ // Format response
269
+ const response = formatError(error, isDevelopment)
270
+
271
+ // Send response
272
+ res.status(statusCode).json(response)
273
+ }
274
+
275
+ /**
276
+ * Helper to quickly create operational errors
277
+ * @param message - Error message
278
+ * @param details - Additional error details
279
+ * @param statusCode - HTTP status code
280
+ * @param code - Custom error code
281
+ * @returns New OperationalError instance
282
+ */
283
+ export const createError = (
284
+ message: string,
285
+ details?: any,
286
+ statusCode?: number,
287
+ code?: string
288
+ ): OperationalError => {
289
+ return new OperationalError(
290
+ message,
291
+ details,
292
+ statusCode || 500,
293
+ code || "OPERATIONAL_ERROR"
294
+ )
295
+ }