@telebort/question-banks 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.
- package/LICENSE +21 -0
- package/README.md +415 -0
- package/bin/question-banks.js +16 -0
- package/dist/assessment-EEJoYtXy.d.cts +351 -0
- package/dist/assessment-EEJoYtXy.d.ts +351 -0
- package/dist/cli/index.cjs +855 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +18 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.js +852 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index-BPlb4Ksx.d.cts +76 -0
- package/dist/index-BdbmdJgH.d.ts +76 -0
- package/dist/index.cjs +452 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +213 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.js +429 -0
- package/dist/index.js.map +1 -0
- package/dist/premium/index.cjs +116 -0
- package/dist/premium/index.cjs.map +1 -0
- package/dist/premium/index.d.cts +96 -0
- package/dist/premium/index.d.ts +96 -0
- package/dist/premium/index.js +114 -0
- package/dist/premium/index.js.map +1 -0
- package/dist/schemas/index.cjs +242 -0
- package/dist/schemas/index.cjs.map +1 -0
- package/dist/schemas/index.d.cts +1465 -0
- package/dist/schemas/index.d.ts +1465 -0
- package/dist/schemas/index.js +220 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/validation/index.cjs +306 -0
- package/dist/validation/index.cjs.map +1 -0
- package/dist/validation/index.d.cts +47 -0
- package/dist/validation/index.d.ts +47 -0
- package/dist/validation/index.js +304 -0
- package/dist/validation/index.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var commander = require('commander');
|
|
5
|
+
var promises = require('fs/promises');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
var zod = require('zod');
|
|
8
|
+
|
|
9
|
+
var FeedbackSchema = zod.z.object({
|
|
10
|
+
short: zod.z.string().min(1).max(200),
|
|
11
|
+
detailed: zod.z.string().min(1).max(1e3),
|
|
12
|
+
socraticHint: zod.z.string().max(300).optional()
|
|
13
|
+
});
|
|
14
|
+
var OptionKeySchema = zod.z.enum(["A", "B", "C", "D"]);
|
|
15
|
+
var OptionSchema = zod.z.object({
|
|
16
|
+
key: OptionKeySchema,
|
|
17
|
+
text: zod.z.string().min(1),
|
|
18
|
+
isCorrect: zod.z.boolean(),
|
|
19
|
+
// v1.1 fields - optional for backward compatibility
|
|
20
|
+
misconceptionId: zod.z.string().optional(),
|
|
21
|
+
feedback: FeedbackSchema.optional()
|
|
22
|
+
});
|
|
23
|
+
var QuestionTypeSchema = zod.z.enum([
|
|
24
|
+
"vocabulary",
|
|
25
|
+
"code_understanding",
|
|
26
|
+
"problem_solving",
|
|
27
|
+
"application",
|
|
28
|
+
"reflection"
|
|
29
|
+
]);
|
|
30
|
+
var QuestionArchetypeSchema = zod.z.enum([
|
|
31
|
+
"vocabulary",
|
|
32
|
+
"trace",
|
|
33
|
+
"bebras",
|
|
34
|
+
"blockmodel",
|
|
35
|
+
"parsons"
|
|
36
|
+
]);
|
|
37
|
+
var DifficultySchema = zod.z.enum(["easy", "medium", "hard", "challenge"]);
|
|
38
|
+
var BloomsTaxonomySchema = zod.z.enum([
|
|
39
|
+
"remember",
|
|
40
|
+
"understand",
|
|
41
|
+
"apply",
|
|
42
|
+
"analyze",
|
|
43
|
+
"evaluate",
|
|
44
|
+
"create"
|
|
45
|
+
]);
|
|
46
|
+
var QuestionMetadataSchema = zod.z.object({
|
|
47
|
+
difficulty: DifficultySchema,
|
|
48
|
+
estimatedTime: zod.z.number().int().positive(),
|
|
49
|
+
// seconds
|
|
50
|
+
bloomsTaxonomy: BloomsTaxonomySchema,
|
|
51
|
+
tags: zod.z.array(zod.z.string()),
|
|
52
|
+
source: zod.z.string().default("exit-ticket"),
|
|
53
|
+
version: zod.z.string().default("1.1"),
|
|
54
|
+
createdDate: zod.z.string().optional(),
|
|
55
|
+
lastModified: zod.z.string().optional()
|
|
56
|
+
});
|
|
57
|
+
var QuestionSchema = zod.z.object({
|
|
58
|
+
// Identifiers
|
|
59
|
+
questionId: zod.z.string().regex(/^[a-z0-9-]+-l\d+-q\d+$/),
|
|
60
|
+
globalId: zod.z.string().regex(/^exit-ticket-\d{4}$/),
|
|
61
|
+
questionNumber: zod.z.number().int().positive().max(5),
|
|
62
|
+
// Classification
|
|
63
|
+
questionType: QuestionTypeSchema,
|
|
64
|
+
questionTypeLabel: zod.z.string(),
|
|
65
|
+
questionArchetype: QuestionArchetypeSchema.optional(),
|
|
66
|
+
// v1.1
|
|
67
|
+
// Content
|
|
68
|
+
prompt: zod.z.string().min(10),
|
|
69
|
+
hasCodeBlock: zod.z.boolean(),
|
|
70
|
+
codeLanguage: zod.z.string().nullable(),
|
|
71
|
+
codeContent: zod.z.string().nullable(),
|
|
72
|
+
// Misconception targeting (v1.1)
|
|
73
|
+
misconceptionTargets: zod.z.array(zod.z.string()).optional(),
|
|
74
|
+
// Answer options (exactly 4)
|
|
75
|
+
options: zod.z.array(OptionSchema).length(4),
|
|
76
|
+
// Correct answer
|
|
77
|
+
correctAnswer: OptionKeySchema,
|
|
78
|
+
correctAnswerText: zod.z.string(),
|
|
79
|
+
// Metadata
|
|
80
|
+
metadata: QuestionMetadataSchema
|
|
81
|
+
});
|
|
82
|
+
QuestionSchema.omit({
|
|
83
|
+
correctAnswer: true,
|
|
84
|
+
correctAnswerText: true
|
|
85
|
+
}).extend({
|
|
86
|
+
options: zod.z.array(
|
|
87
|
+
OptionSchema.omit({
|
|
88
|
+
isCorrect: true,
|
|
89
|
+
feedback: true,
|
|
90
|
+
misconceptionId: true
|
|
91
|
+
})
|
|
92
|
+
).length(4)
|
|
93
|
+
});
|
|
94
|
+
var CourseDomainSchema = zod.z.enum([
|
|
95
|
+
// AI & Data Science
|
|
96
|
+
"ai_data_science",
|
|
97
|
+
"ai_ml_cv",
|
|
98
|
+
"ai_generative",
|
|
99
|
+
"ai_advanced",
|
|
100
|
+
// Web Development
|
|
101
|
+
"web_development",
|
|
102
|
+
// Mobile Development
|
|
103
|
+
"mobile_development",
|
|
104
|
+
"mobile",
|
|
105
|
+
// Block-based Programming
|
|
106
|
+
"block_based",
|
|
107
|
+
"python_programming",
|
|
108
|
+
"design",
|
|
109
|
+
// Foundation
|
|
110
|
+
"foundation",
|
|
111
|
+
"creative_computing"
|
|
112
|
+
]);
|
|
113
|
+
var CourseTierSchema = zod.z.enum([
|
|
114
|
+
"foundation",
|
|
115
|
+
"intermediate",
|
|
116
|
+
"advanced"
|
|
117
|
+
]);
|
|
118
|
+
var LessonSchema = zod.z.object({
|
|
119
|
+
lessonId: zod.z.string(),
|
|
120
|
+
// e.g., "ai-1-lesson-1"
|
|
121
|
+
lessonNumber: zod.z.number().int().positive(),
|
|
122
|
+
lessonTitle: zod.z.string(),
|
|
123
|
+
lessonSlug: zod.z.string().optional(),
|
|
124
|
+
totalQuestions: zod.z.number().int().positive().default(5),
|
|
125
|
+
questions: zod.z.array(QuestionSchema)
|
|
126
|
+
});
|
|
127
|
+
var CourseSchema = zod.z.object({
|
|
128
|
+
courseId: zod.z.string(),
|
|
129
|
+
// e.g., "ai-1"
|
|
130
|
+
courseName: zod.z.string(),
|
|
131
|
+
// e.g., "AI-1 Data Analysis and Data Science"
|
|
132
|
+
courseCode: zod.z.string(),
|
|
133
|
+
// e.g., "AI1"
|
|
134
|
+
domain: CourseDomainSchema,
|
|
135
|
+
tier: CourseTierSchema,
|
|
136
|
+
difficulty: zod.z.number().int().min(1).max(5),
|
|
137
|
+
totalLessons: zod.z.number().int().positive(),
|
|
138
|
+
totalQuestions: zod.z.number().int().positive(),
|
|
139
|
+
sourceFile: zod.z.string().optional(),
|
|
140
|
+
lessons: zod.z.array(LessonSchema)
|
|
141
|
+
});
|
|
142
|
+
CourseSchema.omit({ lessons: true }).extend({
|
|
143
|
+
lessons: zod.z.array(
|
|
144
|
+
LessonSchema.omit({ questions: true })
|
|
145
|
+
).optional()
|
|
146
|
+
});
|
|
147
|
+
var UserResponseSchema = zod.z.object({
|
|
148
|
+
questionId: zod.z.string(),
|
|
149
|
+
selectedAnswer: OptionKeySchema,
|
|
150
|
+
timeSpent: zod.z.number().int().nonnegative().optional(),
|
|
151
|
+
// milliseconds
|
|
152
|
+
submittedAt: zod.z.string().datetime().optional()
|
|
153
|
+
});
|
|
154
|
+
zod.z.object({
|
|
155
|
+
assessmentId: zod.z.string().uuid().optional(),
|
|
156
|
+
// Generated if not provided
|
|
157
|
+
userId: zod.z.string().optional(),
|
|
158
|
+
// For authenticated users
|
|
159
|
+
sessionId: zod.z.string().optional(),
|
|
160
|
+
// For anonymous sessions
|
|
161
|
+
courseId: zod.z.string(),
|
|
162
|
+
lessonId: zod.z.string().optional(),
|
|
163
|
+
responses: zod.z.array(UserResponseSchema).min(1),
|
|
164
|
+
submittedAt: zod.z.string().datetime(),
|
|
165
|
+
metadata: zod.z.record(zod.z.unknown()).optional()
|
|
166
|
+
// Custom metadata
|
|
167
|
+
});
|
|
168
|
+
var GradedResponseSchema = zod.z.object({
|
|
169
|
+
questionId: zod.z.string(),
|
|
170
|
+
selectedAnswer: OptionKeySchema,
|
|
171
|
+
correctAnswer: OptionKeySchema,
|
|
172
|
+
isCorrect: zod.z.boolean(),
|
|
173
|
+
misconceptionId: zod.z.string().nullable(),
|
|
174
|
+
// For incorrect answers
|
|
175
|
+
feedback: FeedbackSchema.optional(),
|
|
176
|
+
timeSpent: zod.z.number().int().nonnegative().optional()
|
|
177
|
+
});
|
|
178
|
+
var MisconceptionReportSchema = zod.z.object({
|
|
179
|
+
misconceptionId: zod.z.string(),
|
|
180
|
+
count: zod.z.number().int().positive(),
|
|
181
|
+
percentage: zod.z.number().min(0).max(100),
|
|
182
|
+
questionIds: zod.z.array(zod.z.string()),
|
|
183
|
+
description: zod.z.string().optional()
|
|
184
|
+
});
|
|
185
|
+
zod.z.object({
|
|
186
|
+
assessmentId: zod.z.string().uuid(),
|
|
187
|
+
userId: zod.z.string().optional(),
|
|
188
|
+
courseId: zod.z.string(),
|
|
189
|
+
lessonId: zod.z.string().optional(),
|
|
190
|
+
// Scores
|
|
191
|
+
totalQuestions: zod.z.number().int().positive(),
|
|
192
|
+
correctCount: zod.z.number().int().nonnegative(),
|
|
193
|
+
incorrectCount: zod.z.number().int().nonnegative(),
|
|
194
|
+
score: zod.z.number().min(0).max(100),
|
|
195
|
+
// Percentage
|
|
196
|
+
passed: zod.z.boolean(),
|
|
197
|
+
// Based on threshold (default 70%)
|
|
198
|
+
// Detailed results
|
|
199
|
+
responses: zod.z.array(GradedResponseSchema),
|
|
200
|
+
// Misconception analysis (v1.1 feature)
|
|
201
|
+
misconceptions: zod.z.array(MisconceptionReportSchema).optional(),
|
|
202
|
+
topMisconceptions: zod.z.array(zod.z.string()).max(3).optional(),
|
|
203
|
+
// Timing
|
|
204
|
+
totalTimeSpent: zod.z.number().int().nonnegative().optional(),
|
|
205
|
+
// milliseconds
|
|
206
|
+
averageTimePerQuestion: zod.z.number().nonnegative().optional(),
|
|
207
|
+
submittedAt: zod.z.string().datetime(),
|
|
208
|
+
gradedAt: zod.z.string().datetime()
|
|
209
|
+
});
|
|
210
|
+
zod.z.object({
|
|
211
|
+
valid: zod.z.boolean(),
|
|
212
|
+
errors: zod.z.array(zod.z.object({
|
|
213
|
+
path: zod.z.array(zod.z.union([zod.z.string(), zod.z.number()])),
|
|
214
|
+
message: zod.z.string(),
|
|
215
|
+
code: zod.z.string().optional()
|
|
216
|
+
})).optional(),
|
|
217
|
+
isComplete: zod.z.boolean(),
|
|
218
|
+
// All questions answered
|
|
219
|
+
questionCount: zod.z.number().int(),
|
|
220
|
+
answeredCount: zod.z.number().int()
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// src/cli/utils/formatters.ts
|
|
224
|
+
var colors = {
|
|
225
|
+
reset: "\x1B[0m",
|
|
226
|
+
bold: "\x1B[1m",
|
|
227
|
+
dim: "\x1B[2m",
|
|
228
|
+
red: "\x1B[31m",
|
|
229
|
+
green: "\x1B[32m",
|
|
230
|
+
yellow: "\x1B[33m",
|
|
231
|
+
magenta: "\x1B[35m",
|
|
232
|
+
cyan: "\x1B[36m",
|
|
233
|
+
white: "\x1B[37m"
|
|
234
|
+
};
|
|
235
|
+
function green(text) {
|
|
236
|
+
return `${colors.green}${text}${colors.reset}`;
|
|
237
|
+
}
|
|
238
|
+
function red(text) {
|
|
239
|
+
return `${colors.red}${text}${colors.reset}`;
|
|
240
|
+
}
|
|
241
|
+
function cyan(text) {
|
|
242
|
+
return `${colors.cyan}${text}${colors.reset}`;
|
|
243
|
+
}
|
|
244
|
+
function bold(text) {
|
|
245
|
+
return `${colors.bold}${text}${colors.reset}`;
|
|
246
|
+
}
|
|
247
|
+
function formatQuestion(q, index) {
|
|
248
|
+
const difficultyColors = {
|
|
249
|
+
easy: colors.green,
|
|
250
|
+
medium: colors.yellow,
|
|
251
|
+
hard: colors.red,
|
|
252
|
+
challenge: colors.magenta
|
|
253
|
+
};
|
|
254
|
+
const diffColor = difficultyColors[q.metadata.difficulty] || colors.white;
|
|
255
|
+
const diffLabel = `${diffColor}${q.metadata.difficulty}${colors.reset}`;
|
|
256
|
+
let output = `
|
|
257
|
+
${colors.bold}Q${index + 1}${colors.reset} [${diffLabel}] ${q.prompt}
|
|
258
|
+
`;
|
|
259
|
+
if (q.hasCodeBlock && q.codeContent) {
|
|
260
|
+
output += `${colors.dim}\u250C\u2500 ${q.codeLanguage || "code"} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${colors.reset}
|
|
261
|
+
`;
|
|
262
|
+
const codeLines = q.codeContent.split("\n");
|
|
263
|
+
for (const line of codeLines) {
|
|
264
|
+
output += `${colors.dim}\u2502${colors.reset} ${line}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
output += `${colors.dim}\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${colors.reset}
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
for (const opt of q.options) {
|
|
271
|
+
output += ` ${colors.cyan}${opt.key})${colors.reset} ${opt.text}
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
return output;
|
|
275
|
+
}
|
|
276
|
+
function formatStats(stats, title) {
|
|
277
|
+
let output = "\n";
|
|
278
|
+
if (title) {
|
|
279
|
+
output += `${colors.bold}${colors.cyan}${title}${colors.reset}
|
|
280
|
+
`;
|
|
281
|
+
output += `${"\u2500".repeat(40)}
|
|
282
|
+
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
output += `${colors.bold}Overview${colors.reset}
|
|
286
|
+
`;
|
|
287
|
+
output += ` Total Courses: ${stats.totalCourses}
|
|
288
|
+
`;
|
|
289
|
+
output += ` Total Lessons: ${stats.totalLessons}
|
|
290
|
+
`;
|
|
291
|
+
output += ` Total Questions: ${stats.totalQuestions}
|
|
292
|
+
|
|
293
|
+
`;
|
|
294
|
+
output += `${colors.bold}Questions by Difficulty${colors.reset}
|
|
295
|
+
`;
|
|
296
|
+
const difficulties = ["easy", "medium", "hard", "challenge"];
|
|
297
|
+
for (const diff of difficulties) {
|
|
298
|
+
const count = stats.byDifficulty[diff] || 0;
|
|
299
|
+
const pct = stats.totalQuestions > 0 ? Math.round(count / stats.totalQuestions * 100) : 0;
|
|
300
|
+
const bar = "\u2588".repeat(Math.floor(pct / 5));
|
|
301
|
+
output += ` ${diff.padEnd(10)} ${String(count).padStart(4)} ${colors.dim}${bar}${colors.reset} ${pct}%
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
304
|
+
output += "\n";
|
|
305
|
+
output += `${colors.bold}Questions by Type${colors.reset}
|
|
306
|
+
`;
|
|
307
|
+
const types = Object.entries(stats.byQuestionType).sort((a, b) => b[1] - a[1]);
|
|
308
|
+
for (const [type, count] of types) {
|
|
309
|
+
output += ` ${type.padEnd(20)} ${count}
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
output += "\n";
|
|
313
|
+
output += `${colors.bold}Code Blocks${colors.reset}
|
|
314
|
+
`;
|
|
315
|
+
output += ` With Code: ${stats.withCodeBlocks}
|
|
316
|
+
`;
|
|
317
|
+
output += ` Without Code: ${stats.withoutCodeBlocks}
|
|
318
|
+
`;
|
|
319
|
+
return output;
|
|
320
|
+
}
|
|
321
|
+
function formatValidationError(filePath, errors) {
|
|
322
|
+
let output = `
|
|
323
|
+
${colors.red}\u2717${colors.reset} ${filePath}
|
|
324
|
+
`;
|
|
325
|
+
for (const error of errors) {
|
|
326
|
+
const path = error.path.join(".");
|
|
327
|
+
output += ` ${colors.dim}\u2192${colors.reset} ${path}: ${error.message}
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
return output;
|
|
331
|
+
}
|
|
332
|
+
function formatValidationSuccess(filePath, lessonCount, questionCount) {
|
|
333
|
+
return `${colors.green}\u2713${colors.reset} ${filePath}
|
|
334
|
+
${colors.dim}${lessonCount} lessons, ${questionCount} questions${colors.reset}
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
function formatSummary(passed, failed) {
|
|
338
|
+
const total = passed + failed;
|
|
339
|
+
if (failed === 0) {
|
|
340
|
+
return `
|
|
341
|
+
${colors.green}${colors.bold}All ${total} files valid${colors.reset}
|
|
342
|
+
`;
|
|
343
|
+
}
|
|
344
|
+
return `
|
|
345
|
+
${colors.red}${colors.bold}${failed}/${total} files failed validation${colors.reset}
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/cli/utils/error-handler.ts
|
|
350
|
+
var EXIT_CODES = {
|
|
351
|
+
SUCCESS: 0,
|
|
352
|
+
VALIDATION_ERROR: 1,
|
|
353
|
+
USAGE_ERROR: 2,
|
|
354
|
+
FATAL_ERROR: 3
|
|
355
|
+
};
|
|
356
|
+
function setupErrorHandlers() {
|
|
357
|
+
process.on("uncaughtException", (error) => {
|
|
358
|
+
console.error(`
|
|
359
|
+
${red(bold("Uncaught Exception:"))} ${error.message}`);
|
|
360
|
+
if (process.env.DEBUG) {
|
|
361
|
+
console.error(error.stack);
|
|
362
|
+
}
|
|
363
|
+
process.exit(EXIT_CODES.FATAL_ERROR);
|
|
364
|
+
});
|
|
365
|
+
process.on("unhandledRejection", (reason) => {
|
|
366
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
367
|
+
console.error(`
|
|
368
|
+
${red(bold("Unhandled Rejection:"))} ${message}`);
|
|
369
|
+
if (process.env.DEBUG && reason instanceof Error) {
|
|
370
|
+
console.error(reason.stack);
|
|
371
|
+
}
|
|
372
|
+
process.exit(EXIT_CODES.FATAL_ERROR);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/cli/commands/validate.ts
|
|
377
|
+
async function validateFile(filePath, verbose) {
|
|
378
|
+
try {
|
|
379
|
+
const content = await promises.readFile(filePath, "utf-8");
|
|
380
|
+
const data = JSON.parse(content);
|
|
381
|
+
const result = CourseSchema.safeParse(data);
|
|
382
|
+
if (result.success) {
|
|
383
|
+
const lessonCount = result.data.lessons?.length ?? 0;
|
|
384
|
+
const questionCount = result.data.lessons?.reduce(
|
|
385
|
+
(sum, l) => sum + (l.questions?.length ?? 0),
|
|
386
|
+
0
|
|
387
|
+
) ?? 0;
|
|
388
|
+
console.log(formatValidationSuccess(path.basename(filePath), lessonCount, questionCount));
|
|
389
|
+
return true;
|
|
390
|
+
} else {
|
|
391
|
+
const errors = result.error.issues.map((issue) => ({
|
|
392
|
+
path: issue.path,
|
|
393
|
+
message: issue.message
|
|
394
|
+
}));
|
|
395
|
+
console.log(formatValidationError(path.basename(filePath), verbose ? errors : errors.slice(0, 5)));
|
|
396
|
+
if (!verbose && errors.length > 5) {
|
|
397
|
+
console.log(` ... and ${errors.length - 5} more errors (use --verbose to see all)
|
|
398
|
+
`);
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
404
|
+
console.log(formatValidationError(path.basename(filePath), [
|
|
405
|
+
{ path: [], message: `Parse error: ${message}` }
|
|
406
|
+
]));
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function validateDirectory(dirPath, verbose) {
|
|
411
|
+
const files = await promises.readdir(dirPath);
|
|
412
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
413
|
+
if (jsonFiles.length === 0) {
|
|
414
|
+
console.log(`No JSON files found in ${dirPath}`);
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
console.log(`
|
|
418
|
+
${cyan(bold("Validating"))} ${jsonFiles.length} files in ${dirPath}
|
|
419
|
+
`);
|
|
420
|
+
let passed = 0;
|
|
421
|
+
let failed = 0;
|
|
422
|
+
for (const file of jsonFiles.sort()) {
|
|
423
|
+
const filePath = path.join(dirPath, file);
|
|
424
|
+
const success = await validateFile(filePath, verbose);
|
|
425
|
+
if (success) {
|
|
426
|
+
passed++;
|
|
427
|
+
} else {
|
|
428
|
+
failed++;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
console.log(formatSummary(passed, failed));
|
|
432
|
+
return failed === 0;
|
|
433
|
+
}
|
|
434
|
+
async function validateCommand(fileOrDir, options) {
|
|
435
|
+
try {
|
|
436
|
+
const stats = await promises.stat(fileOrDir);
|
|
437
|
+
if (stats.isDirectory()) {
|
|
438
|
+
const success = await validateDirectory(fileOrDir, options.verbose);
|
|
439
|
+
process.exit(success ? EXIT_CODES.SUCCESS : EXIT_CODES.VALIDATION_ERROR);
|
|
440
|
+
} else if (stats.isFile()) {
|
|
441
|
+
console.log(`
|
|
442
|
+
${cyan(bold("Validating"))} ${path.basename(fileOrDir)}
|
|
443
|
+
`);
|
|
444
|
+
const success = await validateFile(fileOrDir, options.verbose);
|
|
445
|
+
if (success) {
|
|
446
|
+
console.log(`
|
|
447
|
+
${green(bold("Valid!"))} File passed schema validation.
|
|
448
|
+
`);
|
|
449
|
+
}
|
|
450
|
+
process.exit(success ? EXIT_CODES.SUCCESS : EXIT_CODES.VALIDATION_ERROR);
|
|
451
|
+
} else {
|
|
452
|
+
console.error(`Invalid path: ${fileOrDir}`);
|
|
453
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (error.code === "ENOENT") {
|
|
457
|
+
console.error(`File or directory not found: ${fileOrDir}`);
|
|
458
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
459
|
+
}
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/index.ts
|
|
465
|
+
function createExitTicketsSDK(config) {
|
|
466
|
+
let courses = null;
|
|
467
|
+
const questionsById = /* @__PURE__ */ new Map();
|
|
468
|
+
const questionsByGlobalId = /* @__PURE__ */ new Map();
|
|
469
|
+
async function ensureLoaded() {
|
|
470
|
+
if (courses === null) {
|
|
471
|
+
courses = await config.dataSource.loadCourses();
|
|
472
|
+
for (const course of courses) {
|
|
473
|
+
for (const lesson of course.lessons) {
|
|
474
|
+
for (const question of lesson.questions) {
|
|
475
|
+
questionsById.set(question.questionId, question);
|
|
476
|
+
questionsByGlobalId.set(question.globalId, question.questionId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return courses;
|
|
482
|
+
}
|
|
483
|
+
const data = {
|
|
484
|
+
async getCourses() {
|
|
485
|
+
return ensureLoaded();
|
|
486
|
+
},
|
|
487
|
+
async getCourse(courseId) {
|
|
488
|
+
const all = await ensureLoaded();
|
|
489
|
+
return all.find((c) => c.courseId === courseId) ?? null;
|
|
490
|
+
},
|
|
491
|
+
async getLesson(courseId, lessonNumber) {
|
|
492
|
+
const course = await data.getCourse(courseId);
|
|
493
|
+
return course?.lessons.find((l) => l.lessonNumber === lessonNumber) ?? null;
|
|
494
|
+
},
|
|
495
|
+
async getQuestion(questionId) {
|
|
496
|
+
await ensureLoaded();
|
|
497
|
+
return questionsById.get(questionId) ?? null;
|
|
498
|
+
},
|
|
499
|
+
async getAllQuestions() {
|
|
500
|
+
await ensureLoaded();
|
|
501
|
+
return Array.from(questionsById.values());
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
const query = {
|
|
505
|
+
async filterQuestions(filters) {
|
|
506
|
+
const all = await data.getAllQuestions();
|
|
507
|
+
let filtered = all;
|
|
508
|
+
if (filters.courseId) {
|
|
509
|
+
filtered = filtered.filter((q) => q.questionId.startsWith(filters.courseId));
|
|
510
|
+
}
|
|
511
|
+
if (filters.lessonNumber) {
|
|
512
|
+
filtered = filtered.filter((q) => q.questionId.includes(`-l${filters.lessonNumber}-`));
|
|
513
|
+
}
|
|
514
|
+
if (filters.questionType) {
|
|
515
|
+
filtered = filtered.filter((q) => q.questionType === filters.questionType);
|
|
516
|
+
}
|
|
517
|
+
if (filters.difficulty) {
|
|
518
|
+
filtered = filtered.filter((q) => q.metadata.difficulty === filters.difficulty);
|
|
519
|
+
}
|
|
520
|
+
if (filters.hasCodeBlock !== void 0) {
|
|
521
|
+
filtered = filtered.filter((q) => q.hasCodeBlock === filters.hasCodeBlock);
|
|
522
|
+
}
|
|
523
|
+
if (filters.tags?.length) {
|
|
524
|
+
filtered = filtered.filter(
|
|
525
|
+
(q) => filters.tags.some((tag) => q.metadata.tags.includes(tag))
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const total = filtered.length;
|
|
529
|
+
const offset = filters.offset ?? 0;
|
|
530
|
+
const limit = filters.limit ?? 20;
|
|
531
|
+
const paginated = filtered.slice(offset, offset + limit);
|
|
532
|
+
return {
|
|
533
|
+
data: paginated,
|
|
534
|
+
total,
|
|
535
|
+
limit,
|
|
536
|
+
offset,
|
|
537
|
+
hasMore: offset + limit < total
|
|
538
|
+
};
|
|
539
|
+
},
|
|
540
|
+
async getRandomQuestions(count, filters) {
|
|
541
|
+
const result = await query.filterQuestions({ ...filters, limit: 1e3 });
|
|
542
|
+
const shuffled = [...result.data].sort(() => Math.random() - 0.5);
|
|
543
|
+
return shuffled.slice(0, count);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const quiz = {
|
|
547
|
+
async generateQuiz(criteria) {
|
|
548
|
+
const questions = await query.getRandomQuestions(criteria.questionCount, {
|
|
549
|
+
courseId: criteria.courseIds?.[0],
|
|
550
|
+
difficulty: criteria.difficulty,
|
|
551
|
+
questionType: criteria.questionTypes?.[0],
|
|
552
|
+
hasCodeBlock: criteria.includeCodeBlocks
|
|
553
|
+
});
|
|
554
|
+
return {
|
|
555
|
+
questions,
|
|
556
|
+
metadata: {
|
|
557
|
+
distribution: questions.reduce((acc, q) => {
|
|
558
|
+
acc[q.questionType] = (acc[q.questionType] ?? 0) + 1;
|
|
559
|
+
return acc;
|
|
560
|
+
}, {}),
|
|
561
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
async getLessonQuiz(courseId, lessonNumber) {
|
|
566
|
+
const lesson = await data.getLesson(courseId, lessonNumber);
|
|
567
|
+
return lesson?.questions ?? [];
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
const search = {
|
|
571
|
+
async search(options) {
|
|
572
|
+
const all = await data.getAllQuestions();
|
|
573
|
+
const query2 = options.query.toLowerCase();
|
|
574
|
+
const scored = all.map((q) => {
|
|
575
|
+
let score = 0;
|
|
576
|
+
if (q.prompt.toLowerCase().includes(query2)) score += 3;
|
|
577
|
+
if (q.options.some((o) => o.text.toLowerCase().includes(query2))) score += 1;
|
|
578
|
+
return { ...q, relevanceScore: score };
|
|
579
|
+
}).filter((q) => q.relevanceScore > 0).sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
580
|
+
const offset = options.offset ?? 0;
|
|
581
|
+
const limit = options.limit ?? 20;
|
|
582
|
+
return {
|
|
583
|
+
data: scored.slice(offset, offset + limit),
|
|
584
|
+
total: scored.length,
|
|
585
|
+
limit,
|
|
586
|
+
offset,
|
|
587
|
+
hasMore: offset + limit < scored.length
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
async searchByTags(tags, matchAll = false) {
|
|
591
|
+
const all = await data.getAllQuestions();
|
|
592
|
+
return all.filter((q) => {
|
|
593
|
+
if (matchAll) {
|
|
594
|
+
return tags.every((tag) => q.metadata.tags.includes(tag));
|
|
595
|
+
}
|
|
596
|
+
return tags.some((tag) => q.metadata.tags.includes(tag));
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
const analytics = {
|
|
601
|
+
async getStatistics() {
|
|
602
|
+
const all = await ensureLoaded();
|
|
603
|
+
const questions = await data.getAllQuestions();
|
|
604
|
+
const stats = {
|
|
605
|
+
totalCourses: all.length,
|
|
606
|
+
totalLessons: all.reduce((sum, c) => sum + c.lessons.length, 0),
|
|
607
|
+
totalQuestions: questions.length,
|
|
608
|
+
byDifficulty: {},
|
|
609
|
+
byQuestionType: {},
|
|
610
|
+
byBloomsTaxonomy: {},
|
|
611
|
+
withCodeBlocks: 0,
|
|
612
|
+
withoutCodeBlocks: 0
|
|
613
|
+
};
|
|
614
|
+
for (const q of questions) {
|
|
615
|
+
stats.byDifficulty[q.metadata.difficulty] = (stats.byDifficulty[q.metadata.difficulty] ?? 0) + 1;
|
|
616
|
+
stats.byQuestionType[q.questionType] = (stats.byQuestionType[q.questionType] ?? 0) + 1;
|
|
617
|
+
stats.byBloomsTaxonomy[q.metadata.bloomsTaxonomy] = (stats.byBloomsTaxonomy[q.metadata.bloomsTaxonomy] ?? 0) + 1;
|
|
618
|
+
if (q.hasCodeBlock) stats.withCodeBlocks++;
|
|
619
|
+
else stats.withoutCodeBlocks++;
|
|
620
|
+
}
|
|
621
|
+
return stats;
|
|
622
|
+
},
|
|
623
|
+
async getCourseStatistics(courseId) {
|
|
624
|
+
const course = await data.getCourse(courseId);
|
|
625
|
+
if (!course) throw new Error(`Course not found: ${courseId}`);
|
|
626
|
+
const questions = course.lessons.flatMap((l) => l.questions);
|
|
627
|
+
const stats = {
|
|
628
|
+
courseId,
|
|
629
|
+
totalCourses: 1,
|
|
630
|
+
totalLessons: course.lessons.length,
|
|
631
|
+
totalQuestions: questions.length,
|
|
632
|
+
byDifficulty: {},
|
|
633
|
+
byQuestionType: {},
|
|
634
|
+
byBloomsTaxonomy: {},
|
|
635
|
+
withCodeBlocks: 0,
|
|
636
|
+
withoutCodeBlocks: 0
|
|
637
|
+
};
|
|
638
|
+
for (const q of questions) {
|
|
639
|
+
stats.byDifficulty[q.metadata.difficulty] = (stats.byDifficulty[q.metadata.difficulty] ?? 0) + 1;
|
|
640
|
+
stats.byQuestionType[q.questionType] = (stats.byQuestionType[q.questionType] ?? 0) + 1;
|
|
641
|
+
stats.byBloomsTaxonomy[q.metadata.bloomsTaxonomy] = (stats.byBloomsTaxonomy[q.metadata.bloomsTaxonomy] ?? 0) + 1;
|
|
642
|
+
if (q.hasCodeBlock) stats.withCodeBlocks++;
|
|
643
|
+
else stats.withoutCodeBlocks++;
|
|
644
|
+
}
|
|
645
|
+
return stats;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
const validate = {
|
|
649
|
+
async checkAnswer(questionId, answer, mode) {
|
|
650
|
+
const question = await data.getQuestion(questionId);
|
|
651
|
+
if (!question) throw new Error(`Question not found: ${questionId}`);
|
|
652
|
+
const selectedOption = question.options.find((o) => o.key === answer);
|
|
653
|
+
if (!selectedOption) throw new Error(`Invalid answer: ${answer}`);
|
|
654
|
+
const isCorrect = selectedOption.isCorrect;
|
|
655
|
+
return {
|
|
656
|
+
isCorrect,
|
|
657
|
+
feedback: selectedOption.feedback,
|
|
658
|
+
misconceptionId: !isCorrect ? selectedOption.misconceptionId : null,
|
|
659
|
+
correctAnswer: mode === "formative" ? question.correctAnswer : void 0
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
return {
|
|
664
|
+
data,
|
|
665
|
+
query,
|
|
666
|
+
quiz,
|
|
667
|
+
search,
|
|
668
|
+
analytics,
|
|
669
|
+
validate
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/cli/utils/sdk-loader.ts
|
|
674
|
+
var sdkCache = /* @__PURE__ */ new Map();
|
|
675
|
+
async function detectDataDir() {
|
|
676
|
+
const possiblePaths = [
|
|
677
|
+
// Relative to SDK
|
|
678
|
+
"../exit-tickets-api/data/courses",
|
|
679
|
+
// Relative to current working directory
|
|
680
|
+
"./data/courses",
|
|
681
|
+
"../data/courses",
|
|
682
|
+
// Absolute common locations
|
|
683
|
+
path.join(process.cwd(), "data", "courses")
|
|
684
|
+
];
|
|
685
|
+
for (const path$1 of possiblePaths) {
|
|
686
|
+
try {
|
|
687
|
+
const resolved = path.resolve(path$1);
|
|
688
|
+
const files = await promises.readdir(resolved);
|
|
689
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
690
|
+
if (jsonFiles.length > 0) {
|
|
691
|
+
return resolved;
|
|
692
|
+
}
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
throw new Error(
|
|
697
|
+
"Could not find data directory. Use --data-dir to specify the path to course JSON files."
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
async function loadCoursesFromDir(dir) {
|
|
701
|
+
const files = await promises.readdir(dir);
|
|
702
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
703
|
+
if (jsonFiles.length === 0) {
|
|
704
|
+
throw new Error(`No JSON files found in ${dir}`);
|
|
705
|
+
}
|
|
706
|
+
const courses = [];
|
|
707
|
+
for (const file of jsonFiles) {
|
|
708
|
+
try {
|
|
709
|
+
const content = await promises.readFile(path.join(dir, file), "utf-8");
|
|
710
|
+
const data = JSON.parse(content);
|
|
711
|
+
courses.push(data);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.warn(`Warning: Could not parse ${file}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return courses;
|
|
717
|
+
}
|
|
718
|
+
async function loadSDK(options) {
|
|
719
|
+
const dataDir = options?.dataDir || await detectDataDir();
|
|
720
|
+
const cacheKey = path.resolve(dataDir);
|
|
721
|
+
if (sdkCache.has(cacheKey)) {
|
|
722
|
+
return sdkCache.get(cacheKey);
|
|
723
|
+
}
|
|
724
|
+
const sdk = createExitTicketsSDK({
|
|
725
|
+
dataSource: {
|
|
726
|
+
async loadCourses() {
|
|
727
|
+
return loadCoursesFromDir(dataDir);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
sdkCache.set(cacheKey, sdk);
|
|
732
|
+
return sdk;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/cli/commands/stats.ts
|
|
736
|
+
async function statsCommand(options) {
|
|
737
|
+
try {
|
|
738
|
+
const dataDir = options.dataDir || await detectDataDir();
|
|
739
|
+
const sdk = await loadSDK({ dataDir });
|
|
740
|
+
if (options.course) {
|
|
741
|
+
try {
|
|
742
|
+
const stats = await sdk.analytics.getCourseStatistics(options.course);
|
|
743
|
+
if (options.json) {
|
|
744
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
745
|
+
} else {
|
|
746
|
+
console.log(formatStats(stats, `Course: ${options.course}`));
|
|
747
|
+
}
|
|
748
|
+
} catch (error) {
|
|
749
|
+
console.error(`${red("Error:")} Course not found: ${options.course}`);
|
|
750
|
+
console.error("Use --data-dir to specify the path to course files.");
|
|
751
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
752
|
+
}
|
|
753
|
+
} else {
|
|
754
|
+
const stats = await sdk.analytics.getStatistics();
|
|
755
|
+
if (options.json) {
|
|
756
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
757
|
+
} else {
|
|
758
|
+
console.log(formatStats(stats, "Exit Tickets Statistics"));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
764
|
+
console.error(`${red("Error:")} ${message}`);
|
|
765
|
+
process.exit(EXIT_CODES.FATAL_ERROR);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/cli/commands/quiz.ts
|
|
770
|
+
async function quizCommand(options) {
|
|
771
|
+
try {
|
|
772
|
+
const dataDir = options.dataDir || await detectDataDir();
|
|
773
|
+
const sdk = await loadSDK({ dataDir });
|
|
774
|
+
const quiz = await sdk.quiz.generateQuiz({
|
|
775
|
+
questionCount: options.count,
|
|
776
|
+
courseIds: options.course ? [options.course] : void 0,
|
|
777
|
+
difficulty: options.difficulty,
|
|
778
|
+
questionTypes: options.type ? [options.type] : void 0
|
|
779
|
+
});
|
|
780
|
+
if (quiz.questions.length === 0) {
|
|
781
|
+
console.error(`${red("Error:")} No questions found matching criteria.`);
|
|
782
|
+
if (options.course) {
|
|
783
|
+
console.error(`Check if course "${options.course}" exists.`);
|
|
784
|
+
}
|
|
785
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
786
|
+
}
|
|
787
|
+
if (options.json) {
|
|
788
|
+
const output = {
|
|
789
|
+
questions: options.showAnswers ? quiz.questions : quiz.questions.map((q) => ({
|
|
790
|
+
...q,
|
|
791
|
+
correctAnswer: void 0,
|
|
792
|
+
correctAnswerText: void 0,
|
|
793
|
+
options: q.options.map((o) => ({
|
|
794
|
+
key: o.key,
|
|
795
|
+
text: o.text
|
|
796
|
+
}))
|
|
797
|
+
})),
|
|
798
|
+
metadata: quiz.metadata
|
|
799
|
+
};
|
|
800
|
+
console.log(JSON.stringify(output, null, 2));
|
|
801
|
+
} else {
|
|
802
|
+
const title = options.course ? `Sample Quiz (${quiz.questions.length} questions from ${options.course})` : `Sample Quiz (${quiz.questions.length} questions)`;
|
|
803
|
+
console.log(`
|
|
804
|
+
${cyan(bold(title))}
|
|
805
|
+
`);
|
|
806
|
+
for (let i = 0; i < quiz.questions.length; i++) {
|
|
807
|
+
const q = quiz.questions[i];
|
|
808
|
+
console.log(formatQuestion(q, i));
|
|
809
|
+
if (options.showAnswers) {
|
|
810
|
+
console.log(` ${green("Answer:")} ${q.correctAnswer}) ${q.correctAnswerText}
|
|
811
|
+
`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
console.log(`
|
|
815
|
+
${bold("Distribution:")}`);
|
|
816
|
+
for (const [type, count] of Object.entries(quiz.metadata.distribution)) {
|
|
817
|
+
console.log(` ${type}: ${count}`);
|
|
818
|
+
}
|
|
819
|
+
console.log();
|
|
820
|
+
}
|
|
821
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
822
|
+
} catch (error) {
|
|
823
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
824
|
+
console.error(`${red("Error:")} ${message}`);
|
|
825
|
+
process.exit(EXIT_CODES.FATAL_ERROR);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/cli/index.ts
|
|
830
|
+
setupErrorHandlers();
|
|
831
|
+
var program = new commander.Command();
|
|
832
|
+
program.name("exit-tickets").description("CLI tools for Exit Tickets SDK").version("1.0.0");
|
|
833
|
+
program.command("validate <path>").description("Validate course JSON files against schemas").option("-v, --verbose", "Show all validation errors").action(async (path, options) => {
|
|
834
|
+
await validateCommand(path, options);
|
|
835
|
+
});
|
|
836
|
+
program.command("stats").description("Show exit ticket statistics").option("-c, --course <id>", "Show statistics for a specific course").option("--json", "Output as JSON").option("-d, --data-dir <path>", "Path to course data directory").action(async (options) => {
|
|
837
|
+
await statsCommand(options);
|
|
838
|
+
});
|
|
839
|
+
program.command("quiz").description("Generate a sample quiz").option("-c, --course <id>", "Filter by course ID").option("-n, --count <number>", "Number of questions", "5").option("--difficulty <level>", "Filter by difficulty (easy, medium, hard, challenge)").option("--type <type>", "Filter by question type").option("--json", "Output as JSON").option("-a, --show-answers", "Show correct answers").option("-d, --data-dir <path>", "Path to course data directory").action(async (options) => {
|
|
840
|
+
await quizCommand({
|
|
841
|
+
...options,
|
|
842
|
+
count: parseInt(options.count, 10)
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
function createCLI() {
|
|
846
|
+
return program;
|
|
847
|
+
}
|
|
848
|
+
async function run(argv) {
|
|
849
|
+
await program.parseAsync(argv ?? process.argv);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
exports.createCLI = createCLI;
|
|
853
|
+
exports.run = run;
|
|
854
|
+
//# sourceMappingURL=index.cjs.map
|
|
855
|
+
//# sourceMappingURL=index.cjs.map
|