@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 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/runtime` directly.
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 existsSync2 } from "fs";
8
+ import { existsSync as existsSync4 } from "fs";
9
9
  import { mkdir, writeFile } from "fs/promises";
10
- import { join as join2 } from "path";
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 { stringify as stringifyYaml } from "yaml";
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 dirname(require2.resolve("@lxpack/runtime/client"));
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(join(assetsDir, "styles.css"), "utf-8");
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 = join(assetsDir, "client.js");
53
- if (!existsSync(clientPath)) {
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 = join(courseDir, "lxpack.config.json");
79
- if (!existsSync(configPath)) return null;
95
+ const configPath = join3(courseDir, "lxpack.config.json");
96
+ if (!existsSync3(configPath)) return null;
80
97
  try {
81
- const content = await readFile(configPath, "utf-8");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 = resolve(process.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 = resolve(cwd, relativePath);
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 = resolve(courseDir);
127
- const target = resolve(root, outputDir);
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 (existsSync2(join2(targetDir, "course.yaml")) && !options.force) {
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
- join2(targetDir, "lessons"),
269
- join2(targetDir, "interactions", "phishing-lab"),
270
- join2(targetDir, "assets"),
271
- join2(targetDir, "assessments"),
272
- join2(targetDir, "theme")
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
- join2(targetDir, "course.yaml"),
310
+ join4(targetDir, "course.yaml"),
279
311
  COURSE_YAML_TEMPLATE.replace("{{title}}", yamlTitle)
280
312
  );
281
- await writeFile(join2(targetDir, "lessons", "welcome.md"), WELCOME_MD);
313
+ await writeFile(join4(targetDir, "lessons", "welcome.md"), WELCOME_MD);
282
314
  await writeFile(
283
- join2(targetDir, "interactions", "phishing-lab", "index.html"),
315
+ join4(targetDir, "interactions", "phishing-lab", "index.html"),
284
316
  PHISHING_HTML
285
317
  );
286
- await writeFile(join2(targetDir, "assessments", "final.yaml"), FINAL_ASSESSMENT);
287
- await writeFile(join2(targetDir, "lxpack.config.json"), LXPACK_CONFIG);
288
- await writeFile(join2(targetDir, "theme", ".gitkeep"), "");
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, buildRuntimeAssessmentBundle } from "@lxpack/validators";
303
- import { safeJsonForHtml } from "@lxpack/scorm";
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 = `<!DOCTYPE html>
335
- <html lang="en">
336
- <head>
337
- <meta charset="UTF-8">
338
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
339
- <title>${escapeHtml(manifest.title)} \u2014 Preview</title>
340
- <style>${stylesCss}</style>
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 validation = await validateCourse(courseDir);
358
- if (!validation.manifest) {
359
- console.error(pc2.red("Cannot preview: course manifest is invalid"));
360
- for (const issue of validation.issues) {
361
- console.error(` ${issue.path}: ${issue.message}`);
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 validation.issues) {
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 = await buildRuntimeAssessmentBundle(
373
- courseDir,
374
- validation.manifest
375
- );
437
+ const { validation, manifest, assessmentBundle } = ctx;
376
438
  const app = await createPreviewServer(
377
439
  courseDir,
378
- validation.manifest,
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 validateCourse2 } from "@lxpack/validators";
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 validateCourse2(courseDir);
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 join3 } from "path";
453
- import { packageCourse, packageStandaloneDir, courseSlug } from "@lxpack/scorm";
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
- var VALID_TARGETS = ["scorm12", "standalone"];
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 validation = await validateCourse3(courseDir);
473
- if (!validation.valid || !validation.manifest) {
556
+ const ctx = await loadValidatedCourseContext(courseDir);
557
+ if (!ctx) {
474
558
  console.error(pc4.red("Cannot build: course validation failed"));
475
- for (const issue of validation.issues) {
476
- console.error(` ${issue.path}: ${issue.message}`);
477
- }
559
+ const validation = await validateCourse4(courseDir);
560
+ printValidationIssues(validation);
478
561
  process.exit(1);
479
562
  }
480
- const manifest = validation.manifest;
481
- const assessmentBundle = await buildRuntimeAssessmentBundle2(
482
- courseDir,
483
- manifest
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 ?? join3(outputRoot, target);
500
- const result = await packageStandaloneDir({
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 ?? join3(outputRoot, defaultName);
510
- const result = await packageCourse({
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.1.1",
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/runtime": "0.1.1",
43
- "@lxpack/scorm": "0.1.1",
44
- "@lxpack/validators": "0.1.1"
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",