@lxpack/cli 0.1.1 → 0.2.1
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/README.md +16 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +195 -112
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
Command-line tool for scaffolding, previewing, validating, and packaging LXPack courses.
|
|
9
9
|
|
|
10
|
-
Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime.
|
|
10
|
+
Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime (**v0.2.1**).
|
|
11
11
|
|
|
12
12
|
| Related | Package |
|
|
13
13
|
|---------|---------|
|
|
14
14
|
| Validation | [`@lxpack/validators`](../validators/README.md) |
|
|
15
15
|
| Browser runtime | [`@lxpack/runtime`](../runtime/README.md) |
|
|
16
16
|
| Export / ZIP | [`@lxpack/scorm`](../scorm/README.md) |
|
|
17
|
+
| Lesson widgets | [`@lxpack/components`](../components/README.md) |
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
|
@@ -32,6 +33,7 @@ cd my-course
|
|
|
32
33
|
lxpack preview # http://127.0.0.1:3847 by default
|
|
33
34
|
lxpack validate
|
|
34
35
|
lxpack build --target scorm12
|
|
36
|
+
lxpack build --target scorm2004
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
Output lands in `.lxpack/` unless overridden by `-o` or `lxpack.config.json`.
|
|
@@ -49,12 +51,16 @@ Output lands in `.lxpack/` unless overridden by `-o` or `lxpack.config.json`.
|
|
|
49
51
|
|
|
50
52
|
| Option | Description |
|
|
51
53
|
|--------|-------------|
|
|
52
|
-
| `-t, --target <target>` | `scorm12` (default) or `standalone` |
|
|
54
|
+
| `-t, --target <target>` | `scorm12` (default), `scorm2004`, or `standalone` |
|
|
53
55
|
| `-o, --output <path>` | Output ZIP file or directory |
|
|
54
56
|
| `--dir` | Write an unpacked directory instead of a ZIP |
|
|
55
57
|
|
|
56
58
|
`build` and `preview` use the same validation rules: errors fail the command (exit code 1). `build` reuses the validated manifest and bakes a sanitized [assessment bundle](../validators/README.md#assessment-packaging) into the exported HTML config.
|
|
57
59
|
|
|
60
|
+
**SCORM 2004** builds produce a multi-SCO ZIP: one launch page per activity under `sco/<activityId>/index.html`, plus shared `lxpack-runtime.js` and `lxpack-components.js`.
|
|
61
|
+
|
|
62
|
+
**Preview** serves the runtime client, optional components bundle at `/runtime/components.js`, and installs SCORM API simulators (1.2 and 2004) for local testing. Direct HTTP access to `assessments/*.yaml` under `/course/` returns 404; quiz content is embedded in the preview page config only.
|
|
63
|
+
|
|
58
64
|
### Course discovery
|
|
59
65
|
|
|
60
66
|
Commands walk up from the current working directory until they find `course.yaml`. Run them from inside your course project (or a subdirectory).
|
|
@@ -73,10 +79,13 @@ my-course/
|
|
|
73
79
|
lessons/
|
|
74
80
|
interactions/
|
|
75
81
|
assessments/ # authoring only — omitted from export ZIPs
|
|
82
|
+
components/ # optional widget overrides
|
|
76
83
|
assets/
|
|
77
84
|
.lxpack/ # build output (generated)
|
|
78
85
|
```
|
|
79
86
|
|
|
87
|
+
`init` scaffolds commented examples for `variables`, `flow`, and `type: component` lessons. See [branching-demo](https://github.com/eddiethedean/lxpack/tree/main/examples/branching-demo) for a full v0.2 course.
|
|
88
|
+
|
|
80
89
|
### `lxpack.config.json`
|
|
81
90
|
|
|
82
91
|
```json
|
|
@@ -88,11 +97,13 @@ my-course/
|
|
|
88
97
|
}
|
|
89
98
|
```
|
|
90
99
|
|
|
100
|
+
Use `"defaultTarget": "scorm2004"` when your LMS expects SCORM 2004 4th Edition packages.
|
|
101
|
+
|
|
91
102
|
See the [root README](https://github.com/eddiethedean/lxpack#course-structure) for a full `course.yaml` example.
|
|
92
103
|
|
|
93
104
|
## Programmatic use
|
|
94
105
|
|
|
95
|
-
The CLI is built with [Commander](https://github.com/tj/commander.js). For library integration, import from the built package or depend on `@lxpack/validators`, `@lxpack/scorm`, and `@lxpack/
|
|
106
|
+
The CLI is built with [Commander](https://github.com/tj/commander.js). For library integration, import from the built package or depend on `@lxpack/validators`, `@lxpack/scorm`, `@lxpack/runtime`, and `@lxpack/components` directly.
|
|
96
107
|
|
|
97
108
|
## Development
|
|
98
109
|
|
|
@@ -107,6 +118,8 @@ pnpm --filter @lxpack/cli typecheck
|
|
|
107
118
|
## Links
|
|
108
119
|
|
|
109
120
|
- [LXPack repository](https://github.com/eddiethedean/lxpack)
|
|
121
|
+
- [Documentation index](https://github.com/eddiethedean/lxpack/blob/main/docs/README.md)
|
|
122
|
+
- [Roadmap & phases](https://github.com/eddiethedean/lxpack/blob/main/docs/ROADMAP.md)
|
|
110
123
|
- [Changelog](https://github.com/eddiethedean/lxpack/blob/main/CHANGELOG.md)
|
|
111
124
|
|
|
112
125
|
## License
|
package/dist/cli.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { z } from 'zod';
|
|
|
3
3
|
|
|
4
4
|
declare const lxpackConfigSchema: z.ZodObject<{
|
|
5
5
|
exports: z.ZodOptional<z.ZodObject<{
|
|
6
|
-
defaultTarget: z.ZodOptional<z.ZodEnum<["scorm12", "standalone"]>>;
|
|
6
|
+
defaultTarget: z.ZodOptional<z.ZodEnum<["scorm12", "scorm2004", "standalone"]>>;
|
|
7
7
|
}, "strip", z.ZodTypeAny, {
|
|
8
|
-
defaultTarget?: "scorm12" | "standalone" | undefined;
|
|
8
|
+
defaultTarget?: "scorm12" | "scorm2004" | "standalone" | undefined;
|
|
9
9
|
}, {
|
|
10
|
-
defaultTarget?: "scorm12" | "standalone" | undefined;
|
|
10
|
+
defaultTarget?: "scorm12" | "scorm2004" | "standalone" | undefined;
|
|
11
11
|
}>>;
|
|
12
12
|
output: z.ZodOptional<z.ZodObject<{
|
|
13
13
|
dir: z.ZodOptional<z.ZodString>;
|
|
@@ -18,14 +18,14 @@ declare const lxpackConfigSchema: z.ZodObject<{
|
|
|
18
18
|
}>>;
|
|
19
19
|
}, "strict", z.ZodTypeAny, {
|
|
20
20
|
exports?: {
|
|
21
|
-
defaultTarget?: "scorm12" | "standalone" | undefined;
|
|
21
|
+
defaultTarget?: "scorm12" | "scorm2004" | "standalone" | undefined;
|
|
22
22
|
} | undefined;
|
|
23
23
|
output?: {
|
|
24
24
|
dir?: string | undefined;
|
|
25
25
|
} | undefined;
|
|
26
26
|
}, {
|
|
27
27
|
exports?: {
|
|
28
|
-
defaultTarget?: "scorm12" | "standalone" | undefined;
|
|
28
|
+
defaultTarget?: "scorm12" | "scorm2004" | "standalone" | undefined;
|
|
29
29
|
} | undefined;
|
|
30
30
|
output?: {
|
|
31
31
|
dir?: string | undefined;
|
package/dist/cli.js
CHANGED
|
@@ -5,24 +5,19 @@ import { Command } from "commander";
|
|
|
5
5
|
import pc5 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/commands/init.ts
|
|
8
|
-
import { existsSync as
|
|
8
|
+
import { existsSync as existsSync4 } from "fs";
|
|
9
9
|
import { mkdir, writeFile } from "fs/promises";
|
|
10
|
-
import { join as
|
|
10
|
+
import { join as join4 } from "path";
|
|
11
11
|
import pc from "picocolors";
|
|
12
12
|
|
|
13
13
|
// src/utils.ts
|
|
14
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
15
|
+
import { createRequire as createRequire2 } from "module";
|
|
16
|
+
|
|
17
|
+
// src/lib/course-discovery.ts
|
|
14
18
|
import { existsSync } from "fs";
|
|
15
|
-
import { readFile } from "fs/promises";
|
|
16
|
-
import { createRequire } from "module";
|
|
17
19
|
import { dirname, join, resolve } from "path";
|
|
18
|
-
import {
|
|
19
|
-
import { z } from "zod";
|
|
20
|
-
import {
|
|
21
|
-
loadManifest,
|
|
22
|
-
formatErrorMessage,
|
|
23
|
-
isPathContained
|
|
24
|
-
} from "@lxpack/validators";
|
|
25
|
-
var require2 = createRequire(import.meta.url);
|
|
20
|
+
import { loadManifest } from "@lxpack/validators";
|
|
26
21
|
function findCourseDir(startDir = process.cwd()) {
|
|
27
22
|
let dir = resolve(startDir);
|
|
28
23
|
while (dir !== dirname(dir)) {
|
|
@@ -35,22 +30,29 @@ function findCourseDir(startDir = process.cwd()) {
|
|
|
35
30
|
"No course.yaml found. Run from a course directory or use lxpack init."
|
|
36
31
|
);
|
|
37
32
|
}
|
|
33
|
+
|
|
34
|
+
// src/lib/bundle-io.ts
|
|
35
|
+
import { existsSync as existsSync2 } from "fs";
|
|
36
|
+
import { readFile } from "fs/promises";
|
|
37
|
+
import { createRequire } from "module";
|
|
38
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
39
|
+
var require2 = createRequire(import.meta.url);
|
|
38
40
|
function getRuntimeAssetsDir() {
|
|
39
|
-
return
|
|
41
|
+
return dirname2(require2.resolve("@lxpack/runtime/client"));
|
|
40
42
|
}
|
|
41
43
|
function getEmbeddedStyles() {
|
|
42
44
|
return `:root { --lxpack-bg: #0f1419; } body { margin: 0; }`;
|
|
43
45
|
}
|
|
44
46
|
async function loadRuntimeStyles(assetsDir) {
|
|
45
47
|
try {
|
|
46
|
-
return await readFile(
|
|
48
|
+
return await readFile(join2(assetsDir, "styles.css"), "utf-8");
|
|
47
49
|
} catch {
|
|
48
50
|
return getEmbeddedStyles();
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
async function readRuntimeBundle(assetsDir = getRuntimeAssetsDir()) {
|
|
52
|
-
const clientPath =
|
|
53
|
-
if (!
|
|
54
|
+
const clientPath = join2(assetsDir, "client.js");
|
|
55
|
+
if (!existsSync2(clientPath)) {
|
|
54
56
|
throw new Error(
|
|
55
57
|
"Runtime bundle not found. Run `pnpm build` from the LXPack repo root, or reinstall @lxpack/runtime."
|
|
56
58
|
);
|
|
@@ -66,19 +68,34 @@ async function readRuntimeBundle(assetsDir = getRuntimeAssetsDir()) {
|
|
|
66
68
|
}
|
|
67
69
|
return { clientJs, css };
|
|
68
70
|
}
|
|
71
|
+
async function readComponentsBundle() {
|
|
72
|
+
try {
|
|
73
|
+
const bundlePath = require2.resolve("@lxpack/components/bundle");
|
|
74
|
+
return await readFile(bundlePath, "utf-8");
|
|
75
|
+
} catch {
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/lib/lxpack-config.ts
|
|
81
|
+
import { existsSync as existsSync3 } from "fs";
|
|
82
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
83
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
84
|
+
import { z } from "zod";
|
|
85
|
+
import { formatErrorMessage, isPathContained } from "@lxpack/validators";
|
|
69
86
|
var lxpackConfigSchema = z.object({
|
|
70
87
|
exports: z.object({
|
|
71
|
-
defaultTarget: z.enum(["scorm12", "standalone"]).optional()
|
|
88
|
+
defaultTarget: z.enum(["scorm12", "scorm2004", "standalone"]).optional()
|
|
72
89
|
}).optional(),
|
|
73
90
|
output: z.object({
|
|
74
91
|
dir: z.string().optional()
|
|
75
92
|
}).optional()
|
|
76
93
|
}).strict();
|
|
77
94
|
async function loadLxpackConfig(courseDir) {
|
|
78
|
-
const configPath =
|
|
79
|
-
if (!
|
|
95
|
+
const configPath = join3(courseDir, "lxpack.config.json");
|
|
96
|
+
if (!existsSync3(configPath)) return null;
|
|
80
97
|
try {
|
|
81
|
-
const content = await
|
|
98
|
+
const content = await readFile2(configPath, "utf-8");
|
|
82
99
|
const raw = JSON.parse(content);
|
|
83
100
|
const parsed = lxpackConfigSchema.safeParse(raw);
|
|
84
101
|
if (!parsed.success) {
|
|
@@ -93,44 +110,47 @@ async function loadLxpackConfig(courseDir) {
|
|
|
93
110
|
);
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
|
-
function escapeHtml(text) {
|
|
97
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
98
|
-
}
|
|
99
|
-
function formatCourseTitleForYaml(title) {
|
|
100
|
-
const doc = { title, version: "1.0.0" };
|
|
101
|
-
const yaml = stringifyYaml(doc);
|
|
102
|
-
const titleLine = yaml.split("\n").find((l) => l.startsWith("title:"));
|
|
103
|
-
if (!titleLine) {
|
|
104
|
-
return JSON.stringify(title);
|
|
105
|
-
}
|
|
106
|
-
return titleLine.replace(/^title:\s*/, "");
|
|
107
|
-
}
|
|
108
|
-
function getCliVersion() {
|
|
109
|
-
const pkg = require2("../package.json");
|
|
110
|
-
return pkg.version;
|
|
111
|
-
}
|
|
112
113
|
function resolvePathInCwd(relativePath) {
|
|
113
|
-
const cwd =
|
|
114
|
+
const cwd = resolve2(process.cwd());
|
|
114
115
|
if (relativePath.startsWith("/") || /^[a-zA-Z]:\\/.test(relativePath)) {
|
|
115
116
|
throw new Error(
|
|
116
117
|
"Use a relative path for the output directory (must stay inside the current working directory)"
|
|
117
118
|
);
|
|
118
119
|
}
|
|
119
|
-
const target =
|
|
120
|
+
const target = resolve2(cwd, relativePath);
|
|
120
121
|
if (!isPathContained(cwd, target)) {
|
|
121
122
|
throw new Error("Path must be inside the current working directory");
|
|
122
123
|
}
|
|
123
124
|
return target;
|
|
124
125
|
}
|
|
125
126
|
function resolveOutputDir(courseDir, outputDir) {
|
|
126
|
-
const root =
|
|
127
|
-
const target =
|
|
127
|
+
const root = resolve2(courseDir);
|
|
128
|
+
const target = resolve2(root, outputDir);
|
|
128
129
|
if (!isPathContained(root, target)) {
|
|
129
130
|
throw new Error("output.dir in lxpack.config.json must stay inside the course directory");
|
|
130
131
|
}
|
|
131
132
|
return target;
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// src/lib/html.ts
|
|
136
|
+
import { escapeHtml } from "@lxpack/validators";
|
|
137
|
+
|
|
138
|
+
// src/utils.ts
|
|
139
|
+
var require3 = createRequire2(import.meta.url);
|
|
140
|
+
function formatCourseTitleForYaml(title) {
|
|
141
|
+
const doc = { title, version: "1.0.0" };
|
|
142
|
+
const yaml = stringifyYaml(doc);
|
|
143
|
+
const titleLine = yaml.split("\n").find((l) => l.startsWith("title:"));
|
|
144
|
+
if (!titleLine) {
|
|
145
|
+
return JSON.stringify(title);
|
|
146
|
+
}
|
|
147
|
+
return titleLine.replace(/^title:\s*/, "");
|
|
148
|
+
}
|
|
149
|
+
function getCliVersion() {
|
|
150
|
+
const pkg = require3("../package.json");
|
|
151
|
+
return pkg.version;
|
|
152
|
+
}
|
|
153
|
+
|
|
134
154
|
// src/commands/init.ts
|
|
135
155
|
var COURSE_YAML_TEMPLATE = `title: {{title}}
|
|
136
156
|
version: 1.0.0
|
|
@@ -157,6 +177,17 @@ lessons:
|
|
|
157
177
|
assessments:
|
|
158
178
|
- id: final_quiz
|
|
159
179
|
file: assessments/final.yaml
|
|
180
|
+
|
|
181
|
+
# Optional Phase 2 features (see examples/branching-demo):
|
|
182
|
+
# variables:
|
|
183
|
+
# path:
|
|
184
|
+
# default: intro
|
|
185
|
+
# type: string
|
|
186
|
+
# flow:
|
|
187
|
+
# - when:
|
|
188
|
+
# variable:
|
|
189
|
+
# eq: [path, advanced]
|
|
190
|
+
# goto: component_lesson
|
|
160
191
|
`;
|
|
161
192
|
var WELCOME_MD = `# Welcome
|
|
162
193
|
|
|
@@ -255,7 +286,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
255
286
|
const targetDir = resolvePathInCwd(options.dir ?? projectName);
|
|
256
287
|
const title = formatTitle(projectName);
|
|
257
288
|
const yamlTitle = formatCourseTitleForYaml(title);
|
|
258
|
-
if (
|
|
289
|
+
if (existsSync4(join4(targetDir, "course.yaml")) && !options.force) {
|
|
259
290
|
console.error(
|
|
260
291
|
pc.red(
|
|
261
292
|
`Directory already contains a course. Use --force to overwrite: ${targetDir}`
|
|
@@ -265,27 +296,29 @@ async function initCommand(projectName, options = {}) {
|
|
|
265
296
|
}
|
|
266
297
|
const dirs = [
|
|
267
298
|
targetDir,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
299
|
+
join4(targetDir, "lessons"),
|
|
300
|
+
join4(targetDir, "interactions", "phishing-lab"),
|
|
301
|
+
join4(targetDir, "assets"),
|
|
302
|
+
join4(targetDir, "assessments"),
|
|
303
|
+
join4(targetDir, "theme"),
|
|
304
|
+
join4(targetDir, "components")
|
|
273
305
|
];
|
|
274
306
|
for (const dir of dirs) {
|
|
275
307
|
await mkdir(dir, { recursive: true });
|
|
276
308
|
}
|
|
277
309
|
await writeFile(
|
|
278
|
-
|
|
310
|
+
join4(targetDir, "course.yaml"),
|
|
279
311
|
COURSE_YAML_TEMPLATE.replace("{{title}}", yamlTitle)
|
|
280
312
|
);
|
|
281
|
-
await writeFile(
|
|
313
|
+
await writeFile(join4(targetDir, "lessons", "welcome.md"), WELCOME_MD);
|
|
282
314
|
await writeFile(
|
|
283
|
-
|
|
315
|
+
join4(targetDir, "interactions", "phishing-lab", "index.html"),
|
|
284
316
|
PHISHING_HTML
|
|
285
317
|
);
|
|
286
|
-
await writeFile(
|
|
287
|
-
await writeFile(
|
|
288
|
-
await writeFile(
|
|
318
|
+
await writeFile(join4(targetDir, "assessments", "final.yaml"), FINAL_ASSESSMENT);
|
|
319
|
+
await writeFile(join4(targetDir, "lxpack.config.json"), LXPACK_CONFIG);
|
|
320
|
+
await writeFile(join4(targetDir, "theme", ".gitkeep"), "");
|
|
321
|
+
await writeFile(join4(targetDir, "components", ".gitkeep"), "");
|
|
289
322
|
console.log(pc.green(`\u2713 Created LXPack course: ${targetDir}`));
|
|
290
323
|
console.log();
|
|
291
324
|
console.log("Next steps:");
|
|
@@ -299,8 +332,35 @@ async function initCommand(projectName, options = {}) {
|
|
|
299
332
|
import Fastify from "fastify";
|
|
300
333
|
import fastifyStatic from "@fastify/static";
|
|
301
334
|
import pc2 from "picocolors";
|
|
302
|
-
import { validateCourse
|
|
303
|
-
|
|
335
|
+
import { validateCourse as validateCourse2 } from "@lxpack/validators";
|
|
336
|
+
|
|
337
|
+
// src/lib/validated-course.ts
|
|
338
|
+
import {
|
|
339
|
+
validateCourse,
|
|
340
|
+
buildRuntimeAssessmentBundleFromParsed
|
|
341
|
+
} from "@lxpack/validators";
|
|
342
|
+
async function loadValidatedCourseContext(courseDir) {
|
|
343
|
+
const validation = await validateCourse(courseDir);
|
|
344
|
+
if (!validation.valid || !validation.manifest) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const parsed = validation.parsedAssessments ?? /* @__PURE__ */ new Map();
|
|
348
|
+
const assessmentBundle = buildRuntimeAssessmentBundleFromParsed(parsed);
|
|
349
|
+
return {
|
|
350
|
+
courseDir,
|
|
351
|
+
validation,
|
|
352
|
+
manifest: validation.manifest,
|
|
353
|
+
assessmentBundle
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function printValidationIssues(validation) {
|
|
357
|
+
for (const issue of validation.issues) {
|
|
358
|
+
console.error(` ${issue.path}: ${issue.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/commands/preview.ts
|
|
363
|
+
import { buildLearnerPageHtml, safeJsonForHtml } from "@lxpack/scorm";
|
|
304
364
|
async function loadPreviewStyles(assetsDir = getRuntimeAssetsDir()) {
|
|
305
365
|
return loadRuntimeStyles(assetsDir);
|
|
306
366
|
}
|
|
@@ -311,13 +371,21 @@ function buildPreviewConfig(manifest, assessmentBundle) {
|
|
|
311
371
|
mode: "preview",
|
|
312
372
|
...assessmentBundle ? {
|
|
313
373
|
assessments: assessmentBundle.assessments,
|
|
314
|
-
answerKeys: assessmentBundle.answerKeys
|
|
374
|
+
answerKeys: assessmentBundle.answerKeys,
|
|
375
|
+
assessmentConfigs: assessmentBundle.configs,
|
|
376
|
+
assessmentFeedback: assessmentBundle.feedback
|
|
315
377
|
} : {}
|
|
316
378
|
});
|
|
317
379
|
}
|
|
318
380
|
async function createPreviewServer(courseDir, manifest, assessmentBundle) {
|
|
319
381
|
const runtimeDir = getRuntimeAssetsDir();
|
|
320
382
|
const app = Fastify({ logger: false });
|
|
383
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
384
|
+
const path = request.url.split("?")[0] ?? "";
|
|
385
|
+
if (path.startsWith("/course/assessments/")) {
|
|
386
|
+
return reply.code(404).send("Not found");
|
|
387
|
+
}
|
|
388
|
+
});
|
|
321
389
|
await app.register(fastifyStatic, {
|
|
322
390
|
root: courseDir,
|
|
323
391
|
prefix: "/course/",
|
|
@@ -330,52 +398,46 @@ async function createPreviewServer(courseDir, manifest, assessmentBundle) {
|
|
|
330
398
|
});
|
|
331
399
|
const stylesCss = await loadPreviewStyles(runtimeDir);
|
|
332
400
|
const config = buildPreviewConfig(manifest, assessmentBundle);
|
|
401
|
+
const componentsJs = await readComponentsBundle();
|
|
402
|
+
if (componentsJs) {
|
|
403
|
+
app.get("/runtime/components.js", async (_req, reply) => {
|
|
404
|
+
return reply.type("application/javascript").send(componentsJs);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
333
407
|
app.get("/", async (_req, reply) => {
|
|
334
|
-
const html =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
</head>
|
|
342
|
-
<body>
|
|
343
|
-
<div id="lxpack-app"></div>
|
|
344
|
-
<script type="application/json" id="lxpack-config">${config}</script>
|
|
345
|
-
<script>
|
|
346
|
-
window.__LXPACK_CONFIG__ = JSON.parse(document.getElementById('lxpack-config').textContent);
|
|
347
|
-
</script>
|
|
348
|
-
<script type="module" src="/runtime/client.js"></script>
|
|
349
|
-
</body>
|
|
350
|
-
</html>`;
|
|
408
|
+
const html = buildLearnerPageHtml({
|
|
409
|
+
title: `${manifest.title} \u2014 Preview`,
|
|
410
|
+
runtimeCss: stylesCss,
|
|
411
|
+
configJson: config,
|
|
412
|
+
runtimeScript: "/runtime/client.js",
|
|
413
|
+
componentsScript: componentsJs ? "/runtime/components.js" : void 0
|
|
414
|
+
});
|
|
351
415
|
return reply.type("text/html").send(html);
|
|
352
416
|
});
|
|
353
417
|
app.get("/health", async () => ({ status: "ok" }));
|
|
354
418
|
return app;
|
|
355
419
|
}
|
|
356
420
|
async function startPreview(courseDir, _options = {}) {
|
|
357
|
-
const
|
|
358
|
-
if (!
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
console.error(
|
|
421
|
+
const ctx = await loadValidatedCourseContext(courseDir);
|
|
422
|
+
if (!ctx) {
|
|
423
|
+
const validation2 = await validateCourse2(courseDir);
|
|
424
|
+
if (!validation2.manifest) {
|
|
425
|
+
console.error(pc2.red("Cannot preview: course manifest is invalid"));
|
|
426
|
+
for (const issue of validation2.issues) {
|
|
427
|
+
console.error(` ${issue.path}: ${issue.message}`);
|
|
428
|
+
}
|
|
429
|
+
process.exit(1);
|
|
362
430
|
}
|
|
363
|
-
process.exit(1);
|
|
364
|
-
}
|
|
365
|
-
if (!validation.valid) {
|
|
366
431
|
console.error(pc2.red("Cannot preview: course validation failed"));
|
|
367
|
-
for (const issue of
|
|
432
|
+
for (const issue of validation2.issues) {
|
|
368
433
|
console.error(` ${issue.path}: ${issue.message}`);
|
|
369
434
|
}
|
|
370
435
|
process.exit(1);
|
|
371
436
|
}
|
|
372
|
-
const assessmentBundle =
|
|
373
|
-
courseDir,
|
|
374
|
-
validation.manifest
|
|
375
|
-
);
|
|
437
|
+
const { validation, manifest, assessmentBundle } = ctx;
|
|
376
438
|
const app = await createPreviewServer(
|
|
377
439
|
courseDir,
|
|
378
|
-
|
|
440
|
+
manifest,
|
|
379
441
|
assessmentBundle
|
|
380
442
|
);
|
|
381
443
|
return { app, validation };
|
|
@@ -422,11 +484,11 @@ function logPreviewStarted(host, port) {
|
|
|
422
484
|
}
|
|
423
485
|
|
|
424
486
|
// src/commands/validate.ts
|
|
425
|
-
import { validateCourse as
|
|
487
|
+
import { validateCourse as validateCourse3 } from "@lxpack/validators";
|
|
426
488
|
import pc3 from "picocolors";
|
|
427
489
|
async function validateCommand() {
|
|
428
490
|
const courseDir = findCourseDir();
|
|
429
|
-
const result = await
|
|
491
|
+
const result = await validateCourse3(courseDir);
|
|
430
492
|
if (result.manifest) {
|
|
431
493
|
console.log(
|
|
432
494
|
pc3.dim(`Course: ${result.manifest.title} v${result.manifest.version}`)
|
|
@@ -449,14 +511,36 @@ async function validateCommand() {
|
|
|
449
511
|
|
|
450
512
|
// src/commands/build.ts
|
|
451
513
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
452
|
-
import { join as
|
|
453
|
-
import {
|
|
454
|
-
import {
|
|
455
|
-
validateCourse as validateCourse3,
|
|
456
|
-
buildRuntimeAssessmentBundle as buildRuntimeAssessmentBundle2
|
|
457
|
-
} from "@lxpack/validators";
|
|
514
|
+
import { join as join5 } from "path";
|
|
515
|
+
import { courseSlug } from "@lxpack/scorm";
|
|
458
516
|
import pc4 from "picocolors";
|
|
459
|
-
|
|
517
|
+
|
|
518
|
+
// src/packagers/index.ts
|
|
519
|
+
import {
|
|
520
|
+
packageCourse,
|
|
521
|
+
packageScorm2004Dir,
|
|
522
|
+
packageStandaloneDir
|
|
523
|
+
} from "@lxpack/scorm";
|
|
524
|
+
var zipPackagers = {
|
|
525
|
+
scorm12: { package: packageCourse },
|
|
526
|
+
scorm2004: { package: packageCourse },
|
|
527
|
+
standalone: { package: packageCourse }
|
|
528
|
+
};
|
|
529
|
+
var dirPackagers = {
|
|
530
|
+
scorm12: { package: packageStandaloneDir },
|
|
531
|
+
scorm2004: { package: packageScorm2004Dir },
|
|
532
|
+
standalone: { package: packageStandaloneDir }
|
|
533
|
+
};
|
|
534
|
+
function getZipPackager(target) {
|
|
535
|
+
return zipPackagers[target];
|
|
536
|
+
}
|
|
537
|
+
function getDirPackager(target) {
|
|
538
|
+
return dirPackagers[target];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/commands/build.ts
|
|
542
|
+
import { validateCourse as validateCourse4 } from "@lxpack/validators";
|
|
543
|
+
var VALID_TARGETS = ["scorm12", "scorm2004", "standalone"];
|
|
460
544
|
async function buildCommand(options) {
|
|
461
545
|
const courseDir = findCourseDir();
|
|
462
546
|
const config = await loadLxpackConfig(courseDir);
|
|
@@ -469,20 +553,18 @@ async function buildCommand(options) {
|
|
|
469
553
|
);
|
|
470
554
|
process.exit(1);
|
|
471
555
|
}
|
|
472
|
-
const
|
|
473
|
-
if (!
|
|
556
|
+
const ctx = await loadValidatedCourseContext(courseDir);
|
|
557
|
+
if (!ctx) {
|
|
474
558
|
console.error(pc4.red("Cannot build: course validation failed"));
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
559
|
+
const validation = await validateCourse4(courseDir);
|
|
560
|
+
printValidationIssues(validation);
|
|
478
561
|
process.exit(1);
|
|
479
562
|
}
|
|
480
|
-
const manifest =
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
);
|
|
485
|
-
const { clientJs, css } = await readRuntimeBundle();
|
|
563
|
+
const { manifest, assessmentBundle } = ctx;
|
|
564
|
+
const [{ clientJs, css }, componentsBundleJs] = await Promise.all([
|
|
565
|
+
readRuntimeBundle(),
|
|
566
|
+
readComponentsBundle()
|
|
567
|
+
]);
|
|
486
568
|
const slug = courseSlug(manifest);
|
|
487
569
|
const outputBase = config?.output?.dir ?? ".lxpack";
|
|
488
570
|
const outputRoot = resolveOutputDir(courseDir, outputBase);
|
|
@@ -493,11 +575,12 @@ async function buildCommand(options) {
|
|
|
493
575
|
target,
|
|
494
576
|
runtimeClientJs: clientJs,
|
|
495
577
|
runtimeCss: css,
|
|
578
|
+
componentsBundleJs,
|
|
496
579
|
assessmentBundle
|
|
497
580
|
};
|
|
498
581
|
if (options.dir) {
|
|
499
|
-
const outputDir = options.output ??
|
|
500
|
-
const result = await
|
|
582
|
+
const outputDir = options.output ?? join5(outputRoot, target);
|
|
583
|
+
const result = await getDirPackager(target).package({
|
|
501
584
|
...packageOptions,
|
|
502
585
|
outputDir
|
|
503
586
|
});
|
|
@@ -505,9 +588,9 @@ async function buildCommand(options) {
|
|
|
505
588
|
console.log(` Output: ${result.outputDir}`);
|
|
506
589
|
console.log(` Files: ${result.fileCount}`);
|
|
507
590
|
} else {
|
|
508
|
-
const defaultName = target === "standalone" ? `${slug}-standalone.zip` : `${slug}-scorm12.zip`;
|
|
509
|
-
const outputPath = options.output ??
|
|
510
|
-
const result = await
|
|
591
|
+
const defaultName = target === "standalone" ? `${slug}-standalone.zip` : target === "scorm2004" ? `${slug}-scorm2004.zip` : `${slug}-scorm12.zip`;
|
|
592
|
+
const outputPath = options.output ?? join5(outputRoot, defaultName);
|
|
593
|
+
const result = await getZipPackager(target).package({
|
|
511
594
|
...packageOptions,
|
|
512
595
|
outputPath
|
|
513
596
|
});
|
|
@@ -537,7 +620,7 @@ function createCliProgram() {
|
|
|
537
620
|
});
|
|
538
621
|
program.command("build").description("Build LMS-compatible package").option(
|
|
539
622
|
"-t, --target <target>",
|
|
540
|
-
"Export target: scorm12, standalone",
|
|
623
|
+
"Export target: scorm12, scorm2004, standalone",
|
|
541
624
|
void 0
|
|
542
625
|
).option("-o, --output <path>", "Output file or directory path").option("--dir", "Output as directory instead of ZIP").action(
|
|
543
626
|
async (options) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lxpack/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI for building, validating, and packaging LXPack courses",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -39,9 +39,10 @@
|
|
|
39
39
|
"picocolors": "^1.1.1",
|
|
40
40
|
"yaml": "^2.7.0",
|
|
41
41
|
"zod": "^3.24.2",
|
|
42
|
-
"@lxpack/
|
|
43
|
-
"@lxpack/
|
|
44
|
-
"@lxpack/
|
|
42
|
+
"@lxpack/components": "0.2.1",
|
|
43
|
+
"@lxpack/validators": "0.2.1",
|
|
44
|
+
"@lxpack/runtime": "0.2.1",
|
|
45
|
+
"@lxpack/scorm": "0.2.1"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/node": "^22.13.10",
|