@a-company/university 3.1.2

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.
@@ -0,0 +1,292 @@
1
+ // src/server/index.ts
2
+ import express from "express";
3
+ import * as path3 from "path";
4
+ import * as fs3 from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import chalk from "chalk";
7
+
8
+ // src/server/routes/courses.ts
9
+ import { Router } from "express";
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ function createCoursesRouter(contentDir) {
13
+ const router = Router();
14
+ router.get("/", (_req, res) => {
15
+ const coursesDir = path.join(contentDir, "courses");
16
+ if (!fs.existsSync(coursesDir)) {
17
+ return res.json({ courses: [] });
18
+ }
19
+ const files = fs.readdirSync(coursesDir).filter((f) => f.endsWith(".json"));
20
+ const courses = files.map((f) => {
21
+ const data = JSON.parse(fs.readFileSync(path.join(coursesDir, f), "utf-8"));
22
+ return {
23
+ id: data.id,
24
+ title: data.title,
25
+ description: data.description,
26
+ lessonCount: data.lessons?.length || 0,
27
+ lessons: (data.lessons || []).map((l) => ({
28
+ id: l.id,
29
+ title: l.title
30
+ }))
31
+ };
32
+ });
33
+ courses.sort((a, b) => a.id.localeCompare(b.id));
34
+ return res.json({ courses });
35
+ });
36
+ router.get("/:id", (req, res) => {
37
+ const courseFile = path.join(contentDir, "courses", `${req.params.id}.json`);
38
+ if (!fs.existsSync(courseFile)) {
39
+ return res.status(404).json({ error: `Course '${req.params.id}' not found` });
40
+ }
41
+ const data = JSON.parse(fs.readFileSync(courseFile, "utf-8"));
42
+ return res.json(data);
43
+ });
44
+ router.get("/:id/lessons/:lessonId", (req, res) => {
45
+ const courseFile = path.join(contentDir, "courses", `${req.params.id}.json`);
46
+ if (!fs.existsSync(courseFile)) {
47
+ return res.status(404).json({ error: `Course '${req.params.id}' not found` });
48
+ }
49
+ const data = JSON.parse(fs.readFileSync(courseFile, "utf-8"));
50
+ const lesson = (data.lessons || []).find((l) => l.id === req.params.lessonId);
51
+ if (!lesson) {
52
+ return res.status(404).json({ error: `Lesson '${req.params.lessonId}' not found` });
53
+ }
54
+ return res.json(lesson);
55
+ });
56
+ return router;
57
+ }
58
+
59
+ // src/server/routes/plsat.ts
60
+ import { Router as Router2 } from "express";
61
+ import * as fs2 from "fs";
62
+ import * as path2 from "path";
63
+ function pickVariant(variants) {
64
+ return variants[Math.floor(Math.random() * variants.length)];
65
+ }
66
+ function fisherYatesShuffle(arr) {
67
+ for (let i = arr.length - 1; i > 0; i--) {
68
+ const j = Math.floor(Math.random() * (i + 1));
69
+ [arr[i], arr[j]] = [arr[j], arr[i]];
70
+ }
71
+ return arr;
72
+ }
73
+ function resolveV3(data) {
74
+ const passages = {};
75
+ const blocks = [];
76
+ for (const item of data.items) {
77
+ if (item.type === "standalone") {
78
+ const v = pickVariant(item.variants);
79
+ blocks.push([{
80
+ id: v.id,
81
+ course: item.course,
82
+ scenario: v.scenario,
83
+ question: v.question,
84
+ choices: v.choices,
85
+ correct: v.correct,
86
+ explanation: v.explanation
87
+ }]);
88
+ } else {
89
+ passages[item.slot] = item.passage;
90
+ const group = item.questions.map((pq) => {
91
+ const v = pickVariant(pq.variants);
92
+ return {
93
+ id: v.id,
94
+ course: item.course,
95
+ scenario: v.scenario,
96
+ question: v.question,
97
+ choices: v.choices,
98
+ correct: v.correct,
99
+ explanation: v.explanation,
100
+ passageId: item.slot
101
+ };
102
+ });
103
+ blocks.push(group);
104
+ }
105
+ }
106
+ fisherYatesShuffle(blocks);
107
+ return { questions: blocks.flat(), passages };
108
+ }
109
+ function countV3Questions(data) {
110
+ let count = 0;
111
+ for (const item of data.items) {
112
+ if (item.type === "standalone") {
113
+ count += 1;
114
+ } else {
115
+ count += item.questions.length;
116
+ }
117
+ }
118
+ return count;
119
+ }
120
+ function createPlsatRouter(contentDir) {
121
+ const router = Router2();
122
+ router.get("/", (_req, res) => {
123
+ const plsatDir = path2.join(contentDir, "plsat");
124
+ if (!fs2.existsSync(plsatDir)) {
125
+ return res.json({ versions: [] });
126
+ }
127
+ const files = fs2.readdirSync(plsatDir).filter((f) => f.endsWith(".json"));
128
+ const versions = files.map((f) => {
129
+ const data = JSON.parse(fs2.readFileSync(path2.join(plsatDir, f), "utf-8"));
130
+ const questionCount = data.items ? countV3Questions(data) : data.questions?.length || 0;
131
+ return {
132
+ version: data.version,
133
+ frameworkVersion: data.frameworkVersion,
134
+ questionCount,
135
+ timeLimit: data.timeLimit,
136
+ passThreshold: data.passThreshold
137
+ };
138
+ });
139
+ versions.sort((a, b) => b.version.localeCompare(a.version));
140
+ return res.json({ versions });
141
+ });
142
+ router.get("/:version", (req, res) => {
143
+ const examFile = path2.join(contentDir, "plsat", `v${req.params.version}.json`);
144
+ if (!fs2.existsSync(examFile)) {
145
+ return res.status(404).json({ error: `PLSAT version '${req.params.version}' not found` });
146
+ }
147
+ const data = JSON.parse(fs2.readFileSync(examFile, "utf-8"));
148
+ if (data.items) {
149
+ const { questions, passages } = resolveV3(data);
150
+ return res.json({
151
+ version: data.version,
152
+ frameworkVersion: data.frameworkVersion,
153
+ timeLimit: data.timeLimit,
154
+ passThreshold: data.passThreshold,
155
+ title: data.title,
156
+ description: data.description,
157
+ questions,
158
+ ...Object.keys(passages).length > 0 ? { passages } : {}
159
+ });
160
+ }
161
+ const shuffled = [...data.questions].sort(() => Math.random() - 0.5);
162
+ return res.json({
163
+ ...data,
164
+ questions: shuffled
165
+ });
166
+ });
167
+ return router;
168
+ }
169
+
170
+ // src/server/index.ts
171
+ var __filename = fileURLToPath(import.meta.url);
172
+ var __dirname = path3.dirname(__filename);
173
+ function findPackageRoot(startDir) {
174
+ let dir = startDir;
175
+ while (dir !== path3.dirname(dir)) {
176
+ if (fs3.existsSync(path3.join(dir, "package.json"))) {
177
+ const pkg = JSON.parse(fs3.readFileSync(path3.join(dir, "package.json"), "utf-8"));
178
+ if (pkg.name === "@a-company/university") return dir;
179
+ }
180
+ dir = path3.dirname(dir);
181
+ }
182
+ return startDir;
183
+ }
184
+ var log = {
185
+ component(name) {
186
+ const symbol = chalk.magenta(`#${name}`);
187
+ return {
188
+ info: (msg, data) => {
189
+ const dataStr = data ? chalk.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
190
+ console.log(`${chalk.blue("\u2139")} ${symbol} ${msg}${dataStr}`);
191
+ },
192
+ success: (msg, data) => {
193
+ const dataStr = data ? chalk.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
194
+ console.log(`${chalk.green("\u2714")} ${symbol} ${msg}${dataStr}`);
195
+ },
196
+ warn: (msg, data) => {
197
+ const dataStr = data ? chalk.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
198
+ console.log(`${chalk.yellow("\u26A0")} ${symbol} ${msg}${dataStr}`);
199
+ },
200
+ error: (msg, data) => {
201
+ const dataStr = data ? chalk.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
202
+ console.error(`${chalk.red("\u2716")} ${symbol} ${msg}${dataStr}`);
203
+ }
204
+ };
205
+ }
206
+ };
207
+ function resolveAssetPaths(options) {
208
+ if (options?.contentDir && options?.uiDistPath) {
209
+ return { contentDir: options.contentDir, uiDistPath: options.uiDistPath };
210
+ }
211
+ const bundledContent = path3.join(__dirname, "university-content");
212
+ const bundledUi = path3.join(__dirname, "university-ui");
213
+ if (fs3.existsSync(bundledContent) && fs3.existsSync(bundledUi)) {
214
+ return {
215
+ contentDir: options?.contentDir || bundledContent,
216
+ uiDistPath: options?.uiDistPath || bundledUi
217
+ };
218
+ }
219
+ const packageRoot = findPackageRoot(__dirname);
220
+ return {
221
+ contentDir: options?.contentDir || path3.join(packageRoot, "src", "content"),
222
+ uiDistPath: options?.uiDistPath || path3.join(packageRoot, "ui", "dist")
223
+ };
224
+ }
225
+ function createApp(options) {
226
+ const app = express();
227
+ app.use(express.json());
228
+ app.use((_req, res, next) => {
229
+ res.header("Access-Control-Allow-Origin", "*");
230
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
231
+ res.header("Access-Control-Allow-Headers", "Content-Type");
232
+ next();
233
+ });
234
+ const { contentDir, uiDistPath } = resolveAssetPaths(options);
235
+ app.use("/api/courses", createCoursesRouter(contentDir));
236
+ app.use("/api/plsat", createPlsatRouter(contentDir));
237
+ app.get("/api/reference", (_req, res) => {
238
+ const refPath = path3.join(contentDir, "reference.json");
239
+ if (fs3.existsSync(refPath)) {
240
+ const data = JSON.parse(fs3.readFileSync(refPath, "utf-8"));
241
+ res.json(data);
242
+ } else {
243
+ res.status(404).json({ error: "Reference data not found" });
244
+ }
245
+ });
246
+ app.get("/api/health", (_req, res) => {
247
+ res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
248
+ });
249
+ if (fs3.existsSync(uiDistPath)) {
250
+ app.use(express.static(uiDistPath));
251
+ app.get("{*path}", (req, res) => {
252
+ if (!req.path.startsWith("/api")) {
253
+ res.sendFile(path3.join(uiDistPath, "index.html"));
254
+ }
255
+ });
256
+ }
257
+ return app;
258
+ }
259
+ async function startServer(options) {
260
+ const app = createApp({
261
+ contentDir: options.contentDir,
262
+ uiDistPath: options.uiDistPath
263
+ });
264
+ log.component("university-server").info("Starting server", { port: options.port });
265
+ return new Promise((resolve, reject) => {
266
+ const server = app.listen(options.port, () => {
267
+ log.component("university-server").success("Server running", { url: `http://localhost:${options.port}` });
268
+ if (options.open) {
269
+ import("open").then((openModule) => {
270
+ openModule.default(`http://localhost:${options.port}`);
271
+ log.component("university-server").info("Opened browser");
272
+ }).catch(() => {
273
+ log.component("university-server").warn("Could not open browser automatically");
274
+ });
275
+ }
276
+ resolve();
277
+ });
278
+ server.on("error", (err) => {
279
+ if (err.code === "EADDRINUSE") {
280
+ log.component("university-server").error("Port already in use", { port: options.port });
281
+ } else {
282
+ log.component("university-server").error("Server error", { error: err.message });
283
+ }
284
+ reject(err);
285
+ });
286
+ });
287
+ }
288
+
289
+ export {
290
+ createApp,
291
+ startServer
292
+ };
@@ -0,0 +1,2 @@
1
+ export { ServerOptions, createApp, startServer } from './server/index.js';
2
+ import 'express';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import {
2
+ createApp,
3
+ startServer
4
+ } from "./chunk-3ZAP26AP.js";
5
+ export {
6
+ createApp,
7
+ startServer
8
+ };
@@ -0,0 +1,25 @@
1
+ import { Express } from 'express';
2
+
3
+ /**
4
+ * University Server - Express server for the learning platform UI
5
+ */
6
+
7
+ interface ServerOptions {
8
+ port: number;
9
+ open?: boolean;
10
+ contentDir?: string;
11
+ uiDistPath?: string;
12
+ }
13
+ /**
14
+ * Create the Express application with all routes configured
15
+ */
16
+ declare function createApp(options?: {
17
+ contentDir?: string;
18
+ uiDistPath?: string;
19
+ }): Express;
20
+ /**
21
+ * Start the University server
22
+ */
23
+ declare function startServer(options: ServerOptions): Promise<void>;
24
+
25
+ export { type ServerOptions, createApp, startServer };
@@ -0,0 +1,8 @@
1
+ import {
2
+ createApp,
3
+ startServer
4
+ } from "../chunk-3ZAP26AP.js";
5
+ export {
6
+ createApp,
7
+ startServer
8
+ };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@a-company/university",
3
+ "version": "3.1.2",
4
+ "description": "Interactive learning platform for the Paradigm framework — courses, quizzes, and the PLSAT certification exam",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./server": {
14
+ "types": "./dist/server/index.d.ts",
15
+ "import": "./dist/server/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src/content",
21
+ "ui/dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "npm run build:core && npm run build:ui",
25
+ "build:core": "tsup src/index.ts src/server/index.ts --format esm --dts --clean",
26
+ "build:ui": "cd ui && npm run build",
27
+ "dev": "tsup src/index.ts src/server/index.ts --format esm --dts --watch",
28
+ "dev:ui": "cd ui && npm run dev",
29
+ "typecheck": "tsc --noEmit",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "dependencies": {
33
+ "chalk": "^5.3.0",
34
+ "express": "^4.18.2",
35
+ "open": "^10.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/express": "^4.17.21",
39
+ "@types/node": "^20.10.0",
40
+ "tsup": "^8.0.1",
41
+ "typescript": "^5.3.3"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "author": "ascend42",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/ascend42/a-paradigm.git",
51
+ "directory": "packages/university"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/ascend42/a-paradigm/issues"
55
+ },
56
+ "homepage": "https://github.com/ascend42/a-paradigm#readme",
57
+ "keywords": [
58
+ "paradigm",
59
+ "university",
60
+ "learning",
61
+ "courses",
62
+ "plsat",
63
+ "certification",
64
+ "interactive"
65
+ ],
66
+ "engines": {
67
+ "node": ">=18.0.0"
68
+ }
69
+ }