@lxpack/cli 0.1.0 → 0.1.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 +114 -0
- package/dist/cli.js +84 -41
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @lxpack/cli
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@lxpack/cli)
|
|
4
|
+
[](https://github.com/eddiethedean/lxpack/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/eddiethedean/lxpack/blob/main/LICENSE)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
Command-line tool for scaffolding, previewing, validating, and packaging LXPack courses.
|
|
9
|
+
|
|
10
|
+
Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime.
|
|
11
|
+
|
|
12
|
+
| Related | Package |
|
|
13
|
+
|---------|---------|
|
|
14
|
+
| Validation | [`@lxpack/validators`](../validators/README.md) |
|
|
15
|
+
| Browser runtime | [`@lxpack/runtime`](../runtime/README.md) |
|
|
16
|
+
| Export / ZIP | [`@lxpack/scorm`](../scorm/README.md) |
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @lxpack/cli
|
|
22
|
+
# or: pnpm add -g @lxpack/cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires Node.js 20+.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
lxpack init my-course
|
|
31
|
+
cd my-course
|
|
32
|
+
lxpack preview # http://127.0.0.1:3847 by default
|
|
33
|
+
lxpack validate
|
|
34
|
+
lxpack build --target scorm12
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Output lands in `.lxpack/` unless overridden by `-o` or `lxpack.config.json`.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
|---------|-------------|
|
|
43
|
+
| `init <name>` | Scaffold a new course (`-d, --dir <path>`, `-f, --force`) |
|
|
44
|
+
| `preview` | Start local preview server (`-p, --port`, `-H, --host`) |
|
|
45
|
+
| `validate` | Validate `course.yaml` and referenced files |
|
|
46
|
+
| `build` | Package for LMS or standalone export |
|
|
47
|
+
|
|
48
|
+
### `build` options
|
|
49
|
+
|
|
50
|
+
| Option | Description |
|
|
51
|
+
|--------|-------------|
|
|
52
|
+
| `-t, --target <target>` | `scorm12` (default) or `standalone` |
|
|
53
|
+
| `-o, --output <path>` | Output ZIP file or directory |
|
|
54
|
+
| `--dir` | Write an unpacked directory instead of a ZIP |
|
|
55
|
+
|
|
56
|
+
`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
|
+
|
|
58
|
+
### Course discovery
|
|
59
|
+
|
|
60
|
+
Commands walk up from the current working directory until they find `course.yaml`. Run them from inside your course project (or a subdirectory).
|
|
61
|
+
|
|
62
|
+
### Path safety
|
|
63
|
+
|
|
64
|
+
- `init --dir` must be a relative path that stays inside the current working directory.
|
|
65
|
+
- `lxpack.config.json` `output.dir` is resolved relative to the course root with the same containment rules.
|
|
66
|
+
|
|
67
|
+
## Course layout
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
my-course/
|
|
71
|
+
course.yaml
|
|
72
|
+
lxpack.config.json # optional: defaultTarget, output
|
|
73
|
+
lessons/
|
|
74
|
+
interactions/
|
|
75
|
+
assessments/ # authoring only — omitted from export ZIPs
|
|
76
|
+
assets/
|
|
77
|
+
.lxpack/ # build output (generated)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `lxpack.config.json`
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"defaultTarget": "scorm12",
|
|
85
|
+
"output": {
|
|
86
|
+
"dir": ".lxpack"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See the [root README](https://github.com/eddiethedean/lxpack#course-structure) for a full `course.yaml` example.
|
|
92
|
+
|
|
93
|
+
## Programmatic use
|
|
94
|
+
|
|
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/runtime` directly.
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
From the monorepo root:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pnpm --filter @lxpack/cli build
|
|
103
|
+
pnpm --filter @lxpack/cli test
|
|
104
|
+
pnpm --filter @lxpack/cli typecheck
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Links
|
|
108
|
+
|
|
109
|
+
- [LXPack repository](https://github.com/eddiethedean/lxpack)
|
|
110
|
+
- [Changelog](https://github.com/eddiethedean/lxpack/blob/main/CHANGELOG.md)
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
Apache-2.0
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,11 @@ import { createRequire } from "module";
|
|
|
17
17
|
import { dirname, join, resolve } from "path";
|
|
18
18
|
import { stringify as stringifyYaml } from "yaml";
|
|
19
19
|
import { z } from "zod";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
loadManifest,
|
|
22
|
+
formatErrorMessage,
|
|
23
|
+
isPathContained
|
|
24
|
+
} from "@lxpack/validators";
|
|
21
25
|
var require2 = createRequire(import.meta.url);
|
|
22
26
|
function findCourseDir(startDir = process.cwd()) {
|
|
23
27
|
let dir = resolve(startDir);
|
|
@@ -31,13 +35,6 @@ function findCourseDir(startDir = process.cwd()) {
|
|
|
31
35
|
"No course.yaml found. Run from a course directory or use lxpack init."
|
|
32
36
|
);
|
|
33
37
|
}
|
|
34
|
-
async function loadCourseManifest(courseDir) {
|
|
35
|
-
const loaded = await loadManifest(courseDir);
|
|
36
|
-
if (Array.isArray(loaded)) {
|
|
37
|
-
throw new Error(loaded.map((i) => i.message).join("; "));
|
|
38
|
-
}
|
|
39
|
-
return loaded.manifest;
|
|
40
|
-
}
|
|
41
38
|
function getRuntimeAssetsDir() {
|
|
42
39
|
return dirname(require2.resolve("@lxpack/runtime/client"));
|
|
43
40
|
}
|
|
@@ -112,6 +109,27 @@ function getCliVersion() {
|
|
|
112
109
|
const pkg = require2("../package.json");
|
|
113
110
|
return pkg.version;
|
|
114
111
|
}
|
|
112
|
+
function resolvePathInCwd(relativePath) {
|
|
113
|
+
const cwd = resolve(process.cwd());
|
|
114
|
+
if (relativePath.startsWith("/") || /^[a-zA-Z]:\\/.test(relativePath)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Use a relative path for the output directory (must stay inside the current working directory)"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const target = resolve(cwd, relativePath);
|
|
120
|
+
if (!isPathContained(cwd, target)) {
|
|
121
|
+
throw new Error("Path must be inside the current working directory");
|
|
122
|
+
}
|
|
123
|
+
return target;
|
|
124
|
+
}
|
|
125
|
+
function resolveOutputDir(courseDir, outputDir) {
|
|
126
|
+
const root = resolve(courseDir);
|
|
127
|
+
const target = resolve(root, outputDir);
|
|
128
|
+
if (!isPathContained(root, target)) {
|
|
129
|
+
throw new Error("output.dir in lxpack.config.json must stay inside the course directory");
|
|
130
|
+
}
|
|
131
|
+
return target;
|
|
132
|
+
}
|
|
115
133
|
|
|
116
134
|
// src/commands/init.ts
|
|
117
135
|
var COURSE_YAML_TEMPLATE = `title: {{title}}
|
|
@@ -234,7 +252,7 @@ function formatTitle(projectName) {
|
|
|
234
252
|
return projectName.split(/[-_\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
235
253
|
}
|
|
236
254
|
async function initCommand(projectName, options = {}) {
|
|
237
|
-
const targetDir = options.dir ?? projectName;
|
|
255
|
+
const targetDir = resolvePathInCwd(options.dir ?? projectName);
|
|
238
256
|
const title = formatTitle(projectName);
|
|
239
257
|
const yamlTitle = formatCourseTitleForYaml(title);
|
|
240
258
|
if (existsSync2(join2(targetDir, "course.yaml")) && !options.force) {
|
|
@@ -281,11 +299,23 @@ async function initCommand(projectName, options = {}) {
|
|
|
281
299
|
import Fastify from "fastify";
|
|
282
300
|
import fastifyStatic from "@fastify/static";
|
|
283
301
|
import pc2 from "picocolors";
|
|
284
|
-
import { validateCourse } from "@lxpack/validators";
|
|
302
|
+
import { validateCourse, buildRuntimeAssessmentBundle } from "@lxpack/validators";
|
|
303
|
+
import { safeJsonForHtml } from "@lxpack/scorm";
|
|
285
304
|
async function loadPreviewStyles(assetsDir = getRuntimeAssetsDir()) {
|
|
286
305
|
return loadRuntimeStyles(assetsDir);
|
|
287
306
|
}
|
|
288
|
-
|
|
307
|
+
function buildPreviewConfig(manifest, assessmentBundle) {
|
|
308
|
+
return safeJsonForHtml({
|
|
309
|
+
manifest,
|
|
310
|
+
baseUrl: "/course",
|
|
311
|
+
mode: "preview",
|
|
312
|
+
...assessmentBundle ? {
|
|
313
|
+
assessments: assessmentBundle.assessments,
|
|
314
|
+
answerKeys: assessmentBundle.answerKeys
|
|
315
|
+
} : {}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async function createPreviewServer(courseDir, manifest, assessmentBundle) {
|
|
289
319
|
const runtimeDir = getRuntimeAssetsDir();
|
|
290
320
|
const app = Fastify({ logger: false });
|
|
291
321
|
await app.register(fastifyStatic, {
|
|
@@ -299,12 +329,8 @@ async function createPreviewServer(courseDir, manifest) {
|
|
|
299
329
|
decorateReply: false
|
|
300
330
|
});
|
|
301
331
|
const stylesCss = await loadPreviewStyles(runtimeDir);
|
|
332
|
+
const config = buildPreviewConfig(manifest, assessmentBundle);
|
|
302
333
|
app.get("/", async (_req, reply) => {
|
|
303
|
-
const config = JSON.stringify({
|
|
304
|
-
manifest,
|
|
305
|
-
baseUrl: "/course",
|
|
306
|
-
mode: "preview"
|
|
307
|
-
});
|
|
308
334
|
const html = `<!DOCTYPE html>
|
|
309
335
|
<html lang="en">
|
|
310
336
|
<head>
|
|
@@ -315,8 +341,9 @@ async function createPreviewServer(courseDir, manifest) {
|
|
|
315
341
|
</head>
|
|
316
342
|
<body>
|
|
317
343
|
<div id="lxpack-app"></div>
|
|
344
|
+
<script type="application/json" id="lxpack-config">${config}</script>
|
|
318
345
|
<script>
|
|
319
|
-
window.__LXPACK_CONFIG__ =
|
|
346
|
+
window.__LXPACK_CONFIG__ = JSON.parse(document.getElementById('lxpack-config').textContent);
|
|
320
347
|
</script>
|
|
321
348
|
<script type="module" src="/runtime/client.js"></script>
|
|
322
349
|
</body>
|
|
@@ -336,13 +363,21 @@ async function startPreview(courseDir, _options = {}) {
|
|
|
336
363
|
process.exit(1);
|
|
337
364
|
}
|
|
338
365
|
if (!validation.valid) {
|
|
339
|
-
console.
|
|
366
|
+
console.error(pc2.red("Cannot preview: course validation failed"));
|
|
340
367
|
for (const issue of validation.issues) {
|
|
341
|
-
console.
|
|
368
|
+
console.error(` ${issue.path}: ${issue.message}`);
|
|
342
369
|
}
|
|
343
|
-
|
|
370
|
+
process.exit(1);
|
|
344
371
|
}
|
|
345
|
-
const
|
|
372
|
+
const assessmentBundle = await buildRuntimeAssessmentBundle(
|
|
373
|
+
courseDir,
|
|
374
|
+
validation.manifest
|
|
375
|
+
);
|
|
376
|
+
const app = await createPreviewServer(
|
|
377
|
+
courseDir,
|
|
378
|
+
validation.manifest,
|
|
379
|
+
assessmentBundle
|
|
380
|
+
);
|
|
346
381
|
return { app, validation };
|
|
347
382
|
}
|
|
348
383
|
function resolvePreviewDeps(deps) {
|
|
@@ -415,8 +450,11 @@ async function validateCommand() {
|
|
|
415
450
|
// src/commands/build.ts
|
|
416
451
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
417
452
|
import { join as join3 } from "path";
|
|
418
|
-
import { packageCourse, packageStandaloneDir } from "@lxpack/scorm";
|
|
419
|
-
import {
|
|
453
|
+
import { packageCourse, packageStandaloneDir, courseSlug } from "@lxpack/scorm";
|
|
454
|
+
import {
|
|
455
|
+
validateCourse as validateCourse3,
|
|
456
|
+
buildRuntimeAssessmentBundle as buildRuntimeAssessmentBundle2
|
|
457
|
+
} from "@lxpack/validators";
|
|
420
458
|
import pc4 from "picocolors";
|
|
421
459
|
var VALID_TARGETS = ["scorm12", "standalone"];
|
|
422
460
|
async function buildCommand(options) {
|
|
@@ -432,41 +470,46 @@ async function buildCommand(options) {
|
|
|
432
470
|
process.exit(1);
|
|
433
471
|
}
|
|
434
472
|
const validation = await validateCourse3(courseDir);
|
|
435
|
-
if (!validation.valid) {
|
|
473
|
+
if (!validation.valid || !validation.manifest) {
|
|
436
474
|
console.error(pc4.red("Cannot build: course validation failed"));
|
|
437
475
|
for (const issue of validation.issues) {
|
|
438
476
|
console.error(` ${issue.path}: ${issue.message}`);
|
|
439
477
|
}
|
|
440
478
|
process.exit(1);
|
|
441
479
|
}
|
|
442
|
-
const manifest =
|
|
480
|
+
const manifest = validation.manifest;
|
|
481
|
+
const assessmentBundle = await buildRuntimeAssessmentBundle2(
|
|
482
|
+
courseDir,
|
|
483
|
+
manifest
|
|
484
|
+
);
|
|
443
485
|
const { clientJs, css } = await readRuntimeBundle();
|
|
444
|
-
const slug = manifest
|
|
486
|
+
const slug = courseSlug(manifest);
|
|
445
487
|
const outputBase = config?.output?.dir ?? ".lxpack";
|
|
446
|
-
|
|
488
|
+
const outputRoot = resolveOutputDir(courseDir, outputBase);
|
|
489
|
+
await mkdir2(outputRoot, { recursive: true });
|
|
490
|
+
const packageOptions = {
|
|
491
|
+
courseDir,
|
|
492
|
+
manifest,
|
|
493
|
+
target,
|
|
494
|
+
runtimeClientJs: clientJs,
|
|
495
|
+
runtimeCss: css,
|
|
496
|
+
assessmentBundle
|
|
497
|
+
};
|
|
447
498
|
if (options.dir) {
|
|
448
|
-
const outputDir = options.output ?? join3(
|
|
499
|
+
const outputDir = options.output ?? join3(outputRoot, target);
|
|
449
500
|
const result = await packageStandaloneDir({
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
outputDir,
|
|
453
|
-
target,
|
|
454
|
-
runtimeClientJs: clientJs,
|
|
455
|
-
runtimeCss: css
|
|
501
|
+
...packageOptions,
|
|
502
|
+
outputDir
|
|
456
503
|
});
|
|
457
504
|
console.log(pc4.green(`\u2713 Built ${target} package`));
|
|
458
505
|
console.log(` Output: ${result.outputDir}`);
|
|
459
506
|
console.log(` Files: ${result.fileCount}`);
|
|
460
507
|
} else {
|
|
461
508
|
const defaultName = target === "standalone" ? `${slug}-standalone.zip` : `${slug}-scorm12.zip`;
|
|
462
|
-
const outputPath = options.output ?? join3(
|
|
509
|
+
const outputPath = options.output ?? join3(outputRoot, defaultName);
|
|
463
510
|
const result = await packageCourse({
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
outputPath,
|
|
467
|
-
target,
|
|
468
|
-
runtimeClientJs: clientJs,
|
|
469
|
-
runtimeCss: css
|
|
511
|
+
...packageOptions,
|
|
512
|
+
outputPath
|
|
470
513
|
});
|
|
471
514
|
console.log(pc4.green(`\u2713 Built ${target} package`));
|
|
472
515
|
console.log(` Output: ${result.outputPath}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lxpack/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "CLI for building, validating, and packaging LXPack courses",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"picocolors": "^1.1.1",
|
|
40
40
|
"yaml": "^2.7.0",
|
|
41
41
|
"zod": "^3.24.2",
|
|
42
|
-
"@lxpack/runtime": "0.1.
|
|
43
|
-
"@lxpack/scorm": "0.1.
|
|
44
|
-
"@lxpack/validators": "0.1.
|
|
42
|
+
"@lxpack/runtime": "0.1.1",
|
|
43
|
+
"@lxpack/scorm": "0.1.1",
|
|
44
|
+
"@lxpack/validators": "0.1.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.13.10",
|