@lxpack/cli 0.2.2 → 0.3.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/README.md CHANGED
@@ -7,7 +7,7 @@
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 (**v0.2.2**).
10
+ Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime (**v0.3.0**).
11
11
 
12
12
  | Related | Package |
13
13
  |---------|---------|
@@ -34,6 +34,8 @@ lxpack preview # http://127.0.0.1:3847 by default
34
34
  lxpack validate
35
35
  lxpack build --target scorm12
36
36
  lxpack build --target scorm2004
37
+ lxpack build --target xapi
38
+ lxpack build --target cmi5
37
39
  ```
38
40
 
39
41
  Output lands in `.lxpack/` unless overridden by `-o` or `lxpack.config.json`.
@@ -44,14 +46,14 @@ Output lands in `.lxpack/` unless overridden by `-o` or `lxpack.config.json`.
44
46
  |---------|-------------|
45
47
  | `init <name>` | Scaffold a new course (`-d, --dir <path>`, `-f, --force`) |
46
48
  | `preview` | Start local preview server (`-p, --port`, `-H, --host`) |
47
- | `validate` | Validate `course.yaml` and referenced files |
49
+ | `validate` | Validate `course.yaml` and referenced files (`-t, --target` for export checks) |
48
50
  | `build` | Package for LMS or standalone export |
49
51
 
50
52
  ### `build` options
51
53
 
52
54
  | Option | Description |
53
55
  |--------|-------------|
54
- | `-t, --target <target>` | `scorm12` (default), `scorm2004`, or `standalone` |
56
+ | `-t, --target <target>` | `scorm12` (default), `scorm2004`, `standalone`, `xapi`, or `cmi5` |
55
57
  | `-o, --output <path>` | Output ZIP file or directory |
56
58
  | `--dir` | Write an unpacked directory instead of a ZIP |
57
59
 
@@ -59,7 +61,19 @@ Output lands in `.lxpack/` unless overridden by `-o` or `lxpack.config.json`.
59
61
 
60
62
  **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
63
 
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.
64
+ **Preview** serves the runtime client and optional components bundle at `/runtime/components.js`. Configure SCORM simulators in `lxpack.config.json`:
65
+
66
+ ```json
67
+ { "preview": { "scormMode": "local" } }
68
+ ```
69
+
70
+ | `scormMode` | Behavior |
71
+ |-------------|----------|
72
+ | `local` | `localStorage` progress (default) |
73
+ | `scorm12` | SCORM 1.2 simulator on `window.API` |
74
+ | `scorm2004` | SCORM 2004 simulator on `window.API_1484_11` |
75
+
76
+ Direct HTTP access to `assessments/`, `course.yaml`, `lxpack.config.json`, and `.lxpack/` under `/course/` returns 404; quiz content is embedded in the preview page config only.
63
77
 
64
78
  ### Course discovery
65
79
 
package/dist/cli.d.ts CHANGED
@@ -3,11 +3,40 @@ 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", "scorm2004", "standalone"]>>;
6
+ defaultTarget: z.ZodOptional<z.ZodEnum<["scorm12", "scorm2004", "standalone", "xapi", "cmi5"]>>;
7
7
  }, "strip", z.ZodTypeAny, {
8
- defaultTarget?: "scorm12" | "scorm2004" | "standalone" | undefined;
8
+ defaultTarget?: "scorm12" | "scorm2004" | "standalone" | "xapi" | "cmi5" | undefined;
9
9
  }, {
10
- defaultTarget?: "scorm12" | "scorm2004" | "standalone" | undefined;
10
+ defaultTarget?: "scorm12" | "scorm2004" | "standalone" | "xapi" | "cmi5" | undefined;
11
+ }>>;
12
+ preview: z.ZodOptional<z.ZodObject<{
13
+ scormMode: z.ZodOptional<z.ZodEnum<["local", "scorm12", "scorm2004"]>>;
14
+ }, "strict", z.ZodTypeAny, {
15
+ scormMode?: "scorm12" | "scorm2004" | "local" | undefined;
16
+ }, {
17
+ scormMode?: "scorm12" | "scorm2004" | "local" | undefined;
18
+ }>>;
19
+ xapi: z.ZodOptional<z.ZodObject<{
20
+ preview: z.ZodOptional<z.ZodObject<{
21
+ logStatements: z.ZodOptional<z.ZodBoolean>;
22
+ mockLrs: z.ZodOptional<z.ZodBoolean>;
23
+ }, "strict", z.ZodTypeAny, {
24
+ logStatements?: boolean | undefined;
25
+ mockLrs?: boolean | undefined;
26
+ }, {
27
+ logStatements?: boolean | undefined;
28
+ mockLrs?: boolean | undefined;
29
+ }>>;
30
+ }, "strict", z.ZodTypeAny, {
31
+ preview?: {
32
+ logStatements?: boolean | undefined;
33
+ mockLrs?: boolean | undefined;
34
+ } | undefined;
35
+ }, {
36
+ preview?: {
37
+ logStatements?: boolean | undefined;
38
+ mockLrs?: boolean | undefined;
39
+ } | undefined;
11
40
  }>>;
12
41
  output: z.ZodOptional<z.ZodObject<{
13
42
  dir: z.ZodOptional<z.ZodString>;
@@ -17,15 +46,33 @@ declare const lxpackConfigSchema: z.ZodObject<{
17
46
  dir?: string | undefined;
18
47
  }>>;
19
48
  }, "strict", z.ZodTypeAny, {
49
+ preview?: {
50
+ scormMode?: "scorm12" | "scorm2004" | "local" | undefined;
51
+ } | undefined;
52
+ xapi?: {
53
+ preview?: {
54
+ logStatements?: boolean | undefined;
55
+ mockLrs?: boolean | undefined;
56
+ } | undefined;
57
+ } | undefined;
20
58
  exports?: {
21
- defaultTarget?: "scorm12" | "scorm2004" | "standalone" | undefined;
59
+ defaultTarget?: "scorm12" | "scorm2004" | "standalone" | "xapi" | "cmi5" | undefined;
22
60
  } | undefined;
23
61
  output?: {
24
62
  dir?: string | undefined;
25
63
  } | undefined;
26
64
  }, {
65
+ preview?: {
66
+ scormMode?: "scorm12" | "scorm2004" | "local" | undefined;
67
+ } | undefined;
68
+ xapi?: {
69
+ preview?: {
70
+ logStatements?: boolean | undefined;
71
+ mockLrs?: boolean | undefined;
72
+ } | undefined;
73
+ } | undefined;
27
74
  exports?: {
28
- defaultTarget?: "scorm12" | "scorm2004" | "standalone" | undefined;
75
+ defaultTarget?: "scorm12" | "scorm2004" | "standalone" | "xapi" | "cmi5" | undefined;
29
76
  } | undefined;
30
77
  output?: {
31
78
  dir?: string | undefined;
package/dist/cli.js CHANGED
@@ -85,8 +85,17 @@ import { z } from "zod";
85
85
  import { formatErrorMessage, isPathContained } from "@lxpack/validators";
86
86
  var lxpackConfigSchema = z.object({
87
87
  exports: z.object({
88
- defaultTarget: z.enum(["scorm12", "scorm2004", "standalone"]).optional()
88
+ defaultTarget: z.enum(["scorm12", "scorm2004", "standalone", "xapi", "cmi5"]).optional()
89
89
  }).optional(),
90
+ preview: z.object({
91
+ scormMode: z.enum(["local", "scorm12", "scorm2004"]).optional()
92
+ }).strict().optional(),
93
+ xapi: z.object({
94
+ preview: z.object({
95
+ logStatements: z.boolean().optional(),
96
+ mockLrs: z.boolean().optional()
97
+ }).strict().optional()
98
+ }).strict().optional(),
90
99
  output: z.object({
91
100
  dir: z.string().optional()
92
101
  }).optional()
@@ -178,7 +187,7 @@ assessments:
178
187
  - id: final_quiz
179
188
  file: assessments/final.yaml
180
189
 
181
- # Optional Phase 2 features (see examples/branching-demo):
190
+ # Optional branching (see examples/branching-demo):
182
191
  # variables:
183
192
  # path:
184
193
  # default: intro
@@ -188,6 +197,12 @@ assessments:
188
197
  # variable:
189
198
  # eq: [path, advanced]
190
199
  # goto: component_lesson
200
+
201
+ # Optional xAPI/cmi5 export (see examples/xapi-awareness):
202
+ # tracking:
203
+ # xapi:
204
+ # activityIri: "https://example.test/courses/my-course"
205
+ # displayName: My Course
191
206
  `;
192
207
  var WELCOME_MD = `# Welcome
193
208
 
@@ -274,6 +289,9 @@ var LXPACK_CONFIG = `{
274
289
  "exports": {
275
290
  "defaultTarget": "scorm12"
276
291
  },
292
+ "preview": {
293
+ "scormMode": "local"
294
+ },
277
295
  "output": {
278
296
  "dir": ".lxpack"
279
297
  }
@@ -361,14 +379,44 @@ function printValidationIssues(validation) {
361
379
 
362
380
  // src/commands/preview.ts
363
381
  import { buildLearnerPageHtml, safeJsonForHtml } from "@lxpack/scorm";
382
+ import { getCourseActivityIri } from "@lxpack/validators";
383
+
384
+ // src/lib/preview-paths.ts
385
+ function isPreviewBlockedCoursePath(urlPath) {
386
+ const path = urlPath.split("?")[0] ?? "";
387
+ if (!path.startsWith("/course/")) {
388
+ return false;
389
+ }
390
+ const rel = path.slice("/course/".length);
391
+ if (rel.startsWith("assessments/")) {
392
+ return true;
393
+ }
394
+ if (rel === "course.yaml" || rel === "lxpack.config.json") {
395
+ return true;
396
+ }
397
+ if (rel.startsWith(".lxpack/") || rel === ".lxpack") {
398
+ return true;
399
+ }
400
+ return false;
401
+ }
402
+
403
+ // src/commands/preview.ts
364
404
  async function loadPreviewStyles(assetsDir = getRuntimeAssetsDir()) {
365
405
  return loadRuntimeStyles(assetsDir);
366
406
  }
367
- function buildPreviewConfig(manifest, assessmentBundle) {
407
+ function buildPreviewConfig(manifest, assessmentBundle, options) {
368
408
  return safeJsonForHtml({
369
409
  manifest,
370
410
  baseUrl: "/course",
371
411
  mode: "preview",
412
+ ...options?.previewScormMode && options.previewScormMode !== "local" ? { previewScormMode: options.previewScormMode } : {},
413
+ ...options?.activityIri ? { activityIri: options.activityIri } : {},
414
+ ...options?.xapiPreview ? {
415
+ xapi: {
416
+ previewLog: options.xapiPreview.logStatements ?? true,
417
+ mockLrs: options.xapiPreview.mockLrs ?? true
418
+ }
419
+ } : {},
372
420
  ...assessmentBundle ? {
373
421
  assessments: assessmentBundle.assessments,
374
422
  answerKeys: assessmentBundle.answerKeys,
@@ -382,7 +430,7 @@ async function createPreviewServer(courseDir, manifest, assessmentBundle) {
382
430
  const app = Fastify({ logger: false });
383
431
  app.addHook("onRequest", async (request, reply) => {
384
432
  const path = request.url.split("?")[0] ?? "";
385
- if (path.startsWith("/course/assessments/")) {
433
+ if (isPreviewBlockedCoursePath(path)) {
386
434
  return reply.code(404).send("Not found");
387
435
  }
388
436
  });
@@ -397,7 +445,14 @@ async function createPreviewServer(courseDir, manifest, assessmentBundle) {
397
445
  decorateReply: false
398
446
  });
399
447
  const stylesCss = await loadPreviewStyles(runtimeDir);
400
- const config = buildPreviewConfig(manifest, assessmentBundle);
448
+ const lxpackConfig = await loadLxpackConfig(courseDir);
449
+ const activityIri = getCourseActivityIri(manifest);
450
+ const previewScormMode = lxpackConfig?.preview?.scormMode ?? "local";
451
+ const config = buildPreviewConfig(manifest, assessmentBundle, {
452
+ activityIri,
453
+ previewScormMode,
454
+ xapiPreview: lxpackConfig?.xapi?.preview
455
+ });
401
456
  const componentsJs = await readComponentsBundle();
402
457
  if (componentsJs) {
403
458
  app.get("/runtime/components.js", async (_req, reply) => {
@@ -484,11 +539,42 @@ function logPreviewStarted(host, port) {
484
539
  }
485
540
 
486
541
  // src/commands/validate.ts
487
- import { validateCourse as validateCourse3 } from "@lxpack/validators";
542
+ import {
543
+ validateCourse as validateCourse3,
544
+ validateXapiTracking
545
+ } from "@lxpack/validators";
488
546
  import pc3 from "picocolors";
489
- async function validateCommand() {
547
+
548
+ // src/lib/targets.ts
549
+ var VALID_EXPORT_TARGETS = [
550
+ "scorm12",
551
+ "scorm2004",
552
+ "standalone",
553
+ "xapi",
554
+ "cmi5"
555
+ ];
556
+ function isValidExportTarget(target) {
557
+ return VALID_EXPORT_TARGETS.includes(target);
558
+ }
559
+ function formatInvalidTargetMessage(target) {
560
+ return `Invalid target: ${target}. Valid targets: ${VALID_EXPORT_TARGETS.join(", ")}`;
561
+ }
562
+
563
+ // src/commands/validate.ts
564
+ var XAPI_TARGETS = ["xapi", "cmi5"];
565
+ async function validateCommand(options) {
566
+ if (options?.target !== void 0 && !isValidExportTarget(options.target)) {
567
+ console.error(pc3.red(formatInvalidTargetMessage(options.target)));
568
+ process.exit(1);
569
+ }
490
570
  const courseDir = findCourseDir();
491
571
  const result = await validateCourse3(courseDir);
572
+ const issues = [...result.issues];
573
+ const target = options?.target;
574
+ const needsXapiCheck = target && XAPI_TARGETS.includes(target) || Boolean(result.manifest?.tracking?.xapi);
575
+ if (needsXapiCheck && result.manifest) {
576
+ issues.push(...validateXapiTracking(result.manifest));
577
+ }
492
578
  if (result.manifest) {
493
579
  console.log(
494
580
  pc3.dim(`Course: ${result.manifest.title} v${result.manifest.version}`)
@@ -496,11 +582,13 @@ async function validateCommand() {
496
582
  console.log(pc3.dim(`Lessons: ${result.manifest.lessons.length}`));
497
583
  console.log();
498
584
  }
499
- for (const issue of result.issues) {
585
+ const hasErrors = issues.some((i) => i.severity === "error");
586
+ const valid = result.valid && !hasErrors;
587
+ for (const issue of issues) {
500
588
  const icon = issue.severity === "error" ? pc3.red("\u2717") : pc3.yellow("!");
501
589
  console.log(`${icon} ${issue.path}: ${issue.message}`);
502
590
  }
503
- if (result.valid) {
591
+ if (valid) {
504
592
  console.log(pc3.green("\u2713 Course validation passed"));
505
593
  process.exit(0);
506
594
  } else {
@@ -524,12 +612,16 @@ import {
524
612
  var zipPackagers = {
525
613
  scorm12: { package: packageCourse },
526
614
  scorm2004: { package: packageCourse },
527
- standalone: { package: packageCourse }
615
+ standalone: { package: packageCourse },
616
+ xapi: { package: packageCourse },
617
+ cmi5: { package: packageCourse }
528
618
  };
529
619
  var dirPackagers = {
530
620
  scorm12: { package: packageStandaloneDir },
531
621
  scorm2004: { package: packageScorm2004Dir },
532
- standalone: { package: packageStandaloneDir }
622
+ standalone: { package: packageStandaloneDir },
623
+ xapi: { package: packageStandaloneDir },
624
+ cmi5: { package: packageStandaloneDir }
533
625
  };
534
626
  function getZipPackager(target) {
535
627
  return zipPackagers[target];
@@ -539,18 +631,13 @@ function getDirPackager(target) {
539
631
  }
540
632
 
541
633
  // src/commands/build.ts
542
- import { validateCourse as validateCourse4 } from "@lxpack/validators";
543
- var VALID_TARGETS = ["scorm12", "scorm2004", "standalone"];
634
+ import { validateCourse as validateCourse4, validateXapiTracking as validateXapiTracking2 } from "@lxpack/validators";
544
635
  async function buildCommand(options) {
545
636
  const courseDir = findCourseDir();
546
637
  const config = await loadLxpackConfig(courseDir);
547
638
  const target = options.target ?? config?.exports?.defaultTarget ?? "scorm12";
548
- if (!VALID_TARGETS.includes(target)) {
549
- console.error(
550
- pc4.red(
551
- `Invalid target: ${target}. Valid targets: ${VALID_TARGETS.join(", ")}`
552
- )
553
- );
639
+ if (!isValidExportTarget(target)) {
640
+ console.error(pc4.red(formatInvalidTargetMessage(target)));
554
641
  process.exit(1);
555
642
  }
556
643
  const ctx = await loadValidatedCourseContext(courseDir);
@@ -561,6 +648,16 @@ async function buildCommand(options) {
561
648
  process.exit(1);
562
649
  }
563
650
  const { manifest, assessmentBundle } = ctx;
651
+ if (target === "xapi" || target === "cmi5") {
652
+ const xapiIssues = validateXapiTracking2(manifest);
653
+ if (xapiIssues.length > 0) {
654
+ console.error(pc4.red("Cannot build: course validation failed"));
655
+ for (const issue of xapiIssues) {
656
+ console.error(` ${issue.path}: ${issue.message}`);
657
+ }
658
+ process.exit(1);
659
+ }
660
+ }
564
661
  const [{ clientJs, css }, componentsBundleJs] = await Promise.all([
565
662
  readRuntimeBundle(),
566
663
  readComponentsBundle()
@@ -588,7 +685,7 @@ async function buildCommand(options) {
588
685
  console.log(` Output: ${result.outputDir}`);
589
686
  console.log(` Files: ${result.fileCount}`);
590
687
  } else {
591
- const defaultName = target === "standalone" ? `${slug}-standalone.zip` : target === "scorm2004" ? `${slug}-scorm2004.zip` : `${slug}-scorm12.zip`;
688
+ const defaultName = target === "standalone" ? `${slug}-standalone.zip` : target === "scorm2004" ? `${slug}-scorm2004.zip` : target === "xapi" ? `${slug}-xapi.zip` : target === "cmi5" ? `${slug}-cmi5.zip` : `${slug}-scorm12.zip`;
592
689
  const outputPath = options.output ?? join5(outputRoot, defaultName);
593
690
  const result = await getZipPackager(target).package({
594
691
  ...packageOptions,
@@ -615,12 +712,15 @@ function createCliProgram() {
615
712
  host: options.host
616
713
  });
617
714
  });
618
- program.command("validate").description("Validate course structure and assets").action(async () => {
619
- await validateCommand();
715
+ program.command("validate").description("Validate course structure and assets").option(
716
+ "-t, --target <target>",
717
+ "Also validate export requirements for scorm12, scorm2004, standalone, xapi, or cmi5"
718
+ ).action(async (options) => {
719
+ await validateCommand(options);
620
720
  });
621
721
  program.command("build").description("Build LMS-compatible package").option(
622
722
  "-t, --target <target>",
623
- "Export target: scorm12, scorm2004, standalone",
723
+ "Export target: scorm12, scorm2004, standalone, xapi, cmi5",
624
724
  void 0
625
725
  ).option("-o, --output <path>", "Output file or directory path").option("--dir", "Output as directory instead of ZIP").action(
626
726
  async (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lxpack/cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for building, validating, and packaging LXPack courses",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -39,10 +39,10 @@
39
39
  "picocolors": "^1.1.1",
40
40
  "yaml": "^2.7.0",
41
41
  "zod": "^3.24.2",
42
- "@lxpack/components": "0.2.2",
43
- "@lxpack/runtime": "0.2.2",
44
- "@lxpack/validators": "0.2.2",
45
- "@lxpack/scorm": "0.2.2"
42
+ "@lxpack/components": "0.3.0",
43
+ "@lxpack/runtime": "0.3.0",
44
+ "@lxpack/scorm": "0.3.0",
45
+ "@lxpack/validators": "0.3.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.13.10",