@lessonkit/cli 0.9.3 → 1.0.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
@@ -1,55 +1,41 @@
1
- # `@lessonkit/cli`
1
+ # @lessonkit/cli
2
2
 
3
- [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
4
- [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/)
5
3
  [![npm](https://img.shields.io/npm/v/@lessonkit/cli.svg)](https://www.npmjs.com/package/@lessonkit/cli)
4
+ [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/reference/cli.html)
6
5
  [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
7
6
 
8
- LessonKit CLI — scaffold, dev, build, and package learning experiences.
9
-
10
- **Docs:** [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [Packaging & CLI guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [Vibe coding: shipping to LMS](https://lessonkit.readthedocs.io/en/latest/guides/vibe-coding/shipping-to-lms.html)
7
+ Scaffold, develop, build, and package LessonKit courses. Node.js **18+**.
11
8
 
12
9
  ## Install
13
10
 
14
11
  ```bash
15
12
  npm install -g @lessonkit/cli
16
- # or
13
+ # or one-shot:
17
14
  npx @lessonkit/cli init my-course
18
15
  ```
19
16
 
20
- **Node.js:** dev/build on Node 18+. LMS packaging targets require **Node.js 20+**.
21
-
22
- ## Quick start
17
+ ## Commands
23
18
 
24
19
  ```bash
25
- lessonkit init my-course
26
- cd my-course
27
- lessonkit dev
28
- lessonkit build
29
- lessonkit package --target scorm12
20
+ lessonkit init my-course # scaffold Vite + React project
21
+ lessonkit dev # Vite dev server
22
+ lessonkit build # production build → dist/
23
+ lessonkit package --target scorm12 # LMS artifact
30
24
  ```
31
25
 
32
- ## Commands
33
-
34
- | Command | Description |
35
- |---------|-------------|
36
- | `lessonkit init [name]` | Scaffold a Vite + React project |
37
- | `lessonkit dev` | Run Vite dev server |
38
- | `lessonkit build` | Production Vite build |
39
- | `lessonkit package --target <target>` | Build or package for web / LMS |
40
- | `lessonkit publish` | Stub — see [`RELEASING.md`](https://github.com/eddiethedean/lessonkit/blob/main/RELEASING.md) |
41
-
42
- ### Package targets
26
+ | Target | Output |
27
+ | --- | --- |
28
+ | `react-vite` | Vite build only |
29
+ | `scorm12`, `scorm2004` | SCORM package |
30
+ | `standalone` | Self-contained web bundle |
31
+ | `xapi`, `cmi5` | xAPI / cmi5 packages |
43
32
 
44
- - `react-vite` Vite production build `dist/`
45
- - `scorm12`, `scorm2004`, `xapi`, `cmi5`, `standalone` — via `@lessonkit/lxpack`
33
+ Every project includes a root `lessonkit.json` manifest (`schemaVersion: 1`).
46
34
 
47
- ## Project manifest
35
+ ## Docs
48
36
 
49
- Projects include a `lessonkit.json` at the root. See the [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) for the schema, flags, exit codes, and JSON output mode.
37
+ [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [Packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html) · [Template source](https://github.com/eddiethedean/lessonkit/tree/main/templates/vite-react)
50
38
 
51
- ## Related
39
+ ## License
52
40
 
53
- - [Packaging reference](https://lessonkit.readthedocs.io/en/latest/reference/packaging.html) — LXPack output layout
54
- - [React quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html)
55
- - [`templates/vite-react`](https://github.com/eddiethedean/lessonkit/tree/main/templates/vite-react) — starter template
41
+ Apache-2.0
package/dist/bin.js CHANGED
@@ -209,18 +209,13 @@ async function runInit(opts, logger) {
209
209
  import { readFileSync, existsSync as existsSync2 } from "fs";
210
210
  import { readFile as readFile2 } from "fs/promises";
211
211
  import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
212
- import { validateDescriptor, validateProjectPaths } from "@lessonkit/lxpack";
212
+ import { parseLessonkitManifest } from "@lessonkit/lxpack";
213
213
  var LESSONKIT_JSON = "lessonkit.json";
214
214
  var PACKAGE_JSON = "package.json";
215
- var DEFAULT_PATHS = {
216
- spaDistDir: "dist",
217
- lxpackOutDir: ".lxpack/course",
218
- outputBaseDir: ".lxpack/out"
219
- };
220
215
  function isProjectManifest(configPath) {
221
216
  try {
222
217
  const raw = JSON.parse(readFileSync(configPath, "utf8"));
223
- return raw.schemaVersion === 1 && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object";
218
+ return raw.schemaVersion === 1 && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
224
219
  } catch {
225
220
  return false;
226
221
  }
@@ -253,124 +248,66 @@ async function loadLessonkitJson(projectRoot) {
253
248
  exitCode: EXIT_INVALID_PROJECT
254
249
  });
255
250
  }
256
- if (!raw || typeof raw !== "object") {
257
- throw new CliError(`${configPath} must be a JSON object.`, {
258
- code: "INVALID_PROJECT",
259
- exitCode: EXIT_INVALID_PROJECT
260
- });
261
- }
262
- const config = raw;
263
- const schemaVersion = config.schemaVersion;
264
- if (schemaVersion !== 1) {
265
- throw new CliError(`${configPath}: schemaVersion must be 1 (got ${String(schemaVersion)}).`, {
266
- code: "INVALID_PROJECT",
267
- exitCode: EXIT_INVALID_PROJECT
268
- });
251
+ const parsed = parseLessonkitManifest(raw, configPath, projectRoot);
252
+ if (!parsed.ok) {
253
+ throwManifestCliError(configPath, parsed.issues);
269
254
  }
270
- const name = config.name;
271
- if (typeof name !== "string" || !name.trim()) {
272
- throw new CliError(`${configPath}: "name" must be a non-empty string.`, {
273
- code: "INVALID_PROJECT",
274
- exitCode: EXIT_INVALID_PROJECT
275
- });
255
+ return {
256
+ root: projectRoot,
257
+ schemaVersion: 1,
258
+ name: parsed.manifest.name,
259
+ course: parsed.manifest.course,
260
+ paths: parsed.manifest.paths
261
+ };
262
+ }
263
+ function throwManifestCliError(configPath, issues) {
264
+ const layoutIssue = issues.find((i) => i.path === "course.layout");
265
+ if (layoutIssue?.message.includes("per-lesson-spa")) {
266
+ throw new CliError(
267
+ `${configPath}: per-lesson-spa layout is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly.`,
268
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
269
+ );
276
270
  }
277
- const courseRaw = config.course;
278
- if (!courseRaw || typeof courseRaw !== "object") {
279
- throw new CliError(`${configPath}: "course" must be an object.`, {
271
+ const lessonsIssue = issues.find((i) => i.path === "course.lessons");
272
+ if (lessonsIssue) {
273
+ throw new CliError(`${configPath}: "course.lessons" must be an array.`, {
280
274
  code: "INVALID_PROJECT",
281
275
  exitCode: EXIT_INVALID_PROJECT
282
276
  });
283
277
  }
284
- const courseObj = courseRaw;
285
- if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
286
- throw new CliError(`${configPath}: "course.lessons" must be an array.`, {
278
+ const spaDistTypeIssue = issues.find((i) => i.path === "paths.spaDistDir");
279
+ if (spaDistTypeIssue && spaDistTypeIssue.message.includes("non-empty string")) {
280
+ throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
287
281
  code: "INVALID_PROJECT",
288
282
  exitCode: EXIT_INVALID_PROJECT
289
283
  });
290
284
  }
291
- if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
292
- throw new CliError(`${configPath}: "course.assessments" must be an array.`, {
285
+ const courseSpaIssue = issues.find((i) => i.path === "course.spaDistDir");
286
+ if (courseSpaIssue) {
287
+ throw new CliError(`${configPath}: ${courseSpaIssue.message}`, {
293
288
  code: "INVALID_PROJECT",
294
289
  exitCode: EXIT_INVALID_PROJECT
295
290
  });
296
291
  }
297
- const validation = validateDescriptor(courseRaw);
298
- if (!validation.ok) {
299
- throw new CliError(`${configPath}: invalid course descriptor.`, {
292
+ if (issues.some((i) => i.path.startsWith("paths."))) {
293
+ throw new CliError(`${configPath}: invalid paths.`, {
300
294
  code: "INVALID_PROJECT",
301
295
  exitCode: EXIT_INVALID_PROJECT,
302
- issues: validation.issues.map((i) => ({
303
- path: i.path,
304
- message: i.message
305
- }))
296
+ issues: issues.map((i) => ({ path: i.path, message: i.message }))
306
297
  });
307
298
  }
308
- if (validation.descriptor.layout === "per-lesson-spa") {
309
- throw new CliError(
310
- `${configPath}: per-lesson-spa layout is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly.`,
311
- { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
312
- );
313
- }
314
- const pathsRaw = config.paths;
315
- const paths = { ...DEFAULT_PATHS };
316
- if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
317
- throw new CliError(`${configPath}: "paths" must be an object.`, {
299
+ const schemaIssue = issues.find((i) => i.path === "schemaVersion");
300
+ if (schemaIssue) {
301
+ throw new CliError(`${configPath}: schemaVersion must be 1 (got ${schemaIssue.message.replace(/^must be 1 \(got /, "").replace(/\)$/, "")}).`, {
318
302
  code: "INVALID_PROJECT",
319
303
  exitCode: EXIT_INVALID_PROJECT
320
304
  });
321
305
  }
322
- if (pathsRaw && typeof pathsRaw === "object") {
323
- const p = pathsRaw;
324
- if (p.spaDistDir !== void 0) {
325
- if (typeof p.spaDistDir !== "string" || !p.spaDistDir.trim()) {
326
- throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
327
- code: "INVALID_PROJECT",
328
- exitCode: EXIT_INVALID_PROJECT
329
- });
330
- }
331
- paths.spaDistDir = p.spaDistDir;
332
- }
333
- if (p.lxpackOutDir !== void 0) {
334
- if (typeof p.lxpackOutDir !== "string" || !p.lxpackOutDir.trim()) {
335
- throw new CliError(`${configPath}: "paths.lxpackOutDir" must be a non-empty string.`, {
336
- code: "INVALID_PROJECT",
337
- exitCode: EXIT_INVALID_PROJECT
338
- });
339
- }
340
- paths.lxpackOutDir = p.lxpackOutDir;
341
- }
342
- if (p.outputBaseDir !== void 0) {
343
- if (typeof p.outputBaseDir !== "string" || !p.outputBaseDir.trim()) {
344
- throw new CliError(`${configPath}: "paths.outputBaseDir" must be a non-empty string.`, {
345
- code: "INVALID_PROJECT",
346
- exitCode: EXIT_INVALID_PROJECT
347
- });
348
- }
349
- paths.outputBaseDir = p.outputBaseDir;
350
- }
351
- }
352
- const courseSpaDistDir = validation.descriptor.spaDistDir?.trim();
353
- if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
354
- throw new CliError(
355
- `${configPath}: "course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`,
356
- { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
357
- );
358
- }
359
- const pathIssues = validateProjectPaths(projectRoot, paths);
360
- if (pathIssues.length) {
361
- throw new CliError(`${configPath}: invalid paths.`, {
362
- code: "INVALID_PROJECT",
363
- exitCode: EXIT_INVALID_PROJECT,
364
- issues: pathIssues
365
- });
366
- }
367
- return {
368
- root: projectRoot,
369
- schemaVersion: 1,
370
- name,
371
- course: validation.descriptor,
372
- paths
373
- };
306
+ throw new CliError(`${configPath}: invalid lessonkit manifest.`, {
307
+ code: "INVALID_PROJECT",
308
+ exitCode: EXIT_INVALID_PROJECT,
309
+ issues: issues.map((i) => ({ path: i.path, message: i.message }))
310
+ });
374
311
  }
375
312
  async function loadProject(cwd = process.cwd()) {
376
313
  const root = findProjectRoot(cwd);
@@ -388,7 +325,7 @@ async function readPackageJson(projectRoot) {
388
325
  }
389
326
  }
390
327
  function assertViteProject(pkg, projectRoot) {
391
- const vite = pkg.devDependencies?.vite ?? pkg.dependencies?.vite ?? (existsSync2(join2(projectRoot, "node_modules", ".bin", "vite")) || existsSync2(join2(projectRoot, "node_modules", ".bin", "vite.cmd")) ? "present" : void 0);
328
+ const vite = pkg.devDependencies?.vite ?? pkg.dependencies?.vite ?? (existsSync2(join2(projectRoot, "node_modules", "vite", "bin", "vite.js")) || existsSync2(join2(projectRoot, "node_modules", ".bin", "vite")) || existsSync2(join2(projectRoot, "node_modules", ".bin", "vite.cmd")) ? "present" : void 0);
392
329
  if (!vite) {
393
330
  throw new CliError(
394
331
  `No Vite dependency found in ${join2(projectRoot, PACKAGE_JSON)}. LessonKit projects require Vite.`,
@@ -396,28 +333,25 @@ function assertViteProject(pkg, projectRoot) {
396
333
  );
397
334
  }
398
335
  }
399
- function resolveViteBin(projectRoot) {
336
+ function resolveViteJs(projectRoot) {
400
337
  let dir = resolve2(projectRoot);
401
338
  const fsRoot = parse(dir).root;
402
339
  while (true) {
403
- const binDir = join2(dir, "node_modules", ".bin");
404
- const bin = join2(binDir, "vite");
405
- if (existsSync2(bin)) return bin;
406
- const binCmd = join2(binDir, "vite.cmd");
407
- if (existsSync2(binCmd)) return binCmd;
340
+ const viteJs = join2(dir, "node_modules", "vite", "bin", "vite.js");
341
+ if (existsSync2(viteJs)) return viteJs;
408
342
  if (dir === fsRoot) break;
409
343
  dir = dirname2(dir);
410
344
  }
411
345
  throw new CliError(
412
- `Vite binary not found near ${projectRoot}. Run npm install in the project first.`,
346
+ `Vite not found near ${projectRoot}. Run npm install in the project first.`,
413
347
  { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
414
348
  );
415
349
  }
416
- function assertNode20ForLxpack() {
350
+ function assertNode18ForLxpack() {
417
351
  const major = Number(process.versions.node.split(".")[0]);
418
- if (major < 20) {
352
+ if (major < 18) {
419
353
  throw new CliError(
420
- `LMS packaging requires Node.js 20+ (current: ${process.versions.node}). See docs/PACKAGING.md.`,
354
+ `LMS packaging requires Node.js 18+ (current: ${process.versions.node}). See docs/PACKAGING.md.`,
421
355
  { code: "NODE_VERSION", exitCode: EXIT_INVALID_PROJECT }
422
356
  );
423
357
  }
@@ -479,17 +413,19 @@ async function runDev(opts) {
479
413
  const project = await loadProject(opts.cwd ?? process.cwd());
480
414
  const pkg = await readPackageJson(project.root);
481
415
  assertViteProject(pkg, project.root);
482
- const viteBin = resolveViteBin(project.root);
483
- await runCommand(viteBin, opts.viteArgs ?? [], { cwd: project.root });
416
+ const viteJs = resolveViteJs(project.root);
417
+ await runCommand(process.execPath, [viteJs, ...opts.viteArgs ?? []], { cwd: project.root });
484
418
  return { ok: true, command: "dev", projectRoot: project.root };
485
419
  }
486
420
  async function runBuild(opts) {
487
421
  const project = await loadProject(opts.cwd ?? process.cwd());
488
422
  const pkg = await readPackageJson(project.root);
489
423
  assertViteProject(pkg, project.root);
490
- const viteBin = resolveViteBin(project.root);
424
+ const viteJs = resolveViteJs(project.root);
491
425
  const buildArgs = resolveViteBuildArgs(project);
492
- await runCommand(viteBin, [...buildArgs, ...opts.viteArgs ?? []], { cwd: project.root });
426
+ await runCommand(process.execPath, [viteJs, ...buildArgs, ...opts.viteArgs ?? []], {
427
+ cwd: project.root
428
+ });
493
429
  return { ok: true, command: "build", projectRoot: project.root };
494
430
  }
495
431
 
@@ -524,7 +460,7 @@ async function runPackage(opts) {
524
460
  }
525
461
  return { ok: true, target, projectRoot: project.root, distDir };
526
462
  }
527
- assertNode20ForLxpack();
463
+ assertNode18ForLxpack();
528
464
  if (!opts.noBuild || !existsSync3(distDir)) {
529
465
  await runBuild({ cwd: project.root, json: opts.json });
530
466
  }
@@ -598,7 +534,10 @@ async function handleCommand(fn, logger, json) {
598
534
  function createProgram(baseLogger = console) {
599
535
  const program = new Command();
600
536
  program.name("lessonkit").description("LessonKit CLI").version(version);
601
- program.command("init").description("Initialize a LessonKit project from the Vite + React template").argument("[name]", "Project directory name").option("--here", "Initialize in the current directory").option("--skip-install", "Skip npm install").option("--force", "Initialize into a non-empty directory").option("--json", "Emit structured JSON result").action(async (name, opts) => {
537
+ program.command("init").description("Initialize a LessonKit project from the Vite + React template").argument("[name]", "Project directory name").option("--here", "Initialize in the current directory").option("--skip-install", "Skip npm install").option(
538
+ "--force",
539
+ "With --here, allow init when the directory is empty or contains only dotfiles"
540
+ ).option("--json", "Emit structured JSON result").action(async (name, opts) => {
602
541
  const logger = createLogger({ json: opts.json });
603
542
  await handleCommand(
604
543
  () => runInit({ name, here: opts.here, skipInstall: opts.skipInstall, force: opts.force, json: opts.json }, logger),
@@ -632,13 +571,27 @@ function createProgram(baseLogger = console) {
632
571
  program.command("package").description("Build or package for web / LMS delivery").requiredOption("--target <target>", `Export target (${PACKAGE_TARGETS.join(", ")})`).option("--cwd <dir>", "Project root directory").option("--no-build", "Skip implicit Vite build for LMS targets").option("--out <path>", "Override output artifact path").option("--json", "Emit structured JSON result").action(async (opts) => {
633
572
  const logger = createLogger({ json: opts.json });
634
573
  await handleCommand(
635
- () => runPackage({
636
- target: opts.target,
637
- cwd: opts.cwd,
638
- noBuild: opts.build === false,
639
- out: opts.out,
640
- json: opts.json
641
- }),
574
+ async () => {
575
+ const result = await runPackage({
576
+ target: opts.target,
577
+ cwd: opts.cwd,
578
+ noBuild: opts.build === false,
579
+ out: opts.out,
580
+ json: opts.json
581
+ });
582
+ if (!opts.json && result.ok) {
583
+ if (result.target === "react-vite" && "distDir" in result) {
584
+ logger.log(`Built react-vite \u2192 ${result.distDir}`);
585
+ } else if ("outputPath" in result || "outputDir" in result) {
586
+ const dest = result.outputPath ?? result.outputDir;
587
+ const count = "fileCount" in result ? result.fileCount : void 0;
588
+ logger.log(
589
+ `Packaged ${result.target}${dest ? ` \u2192 ${dest}` : ""}${count != null ? ` (${count} files)` : ""}`
590
+ );
591
+ }
592
+ }
593
+ return result;
594
+ },
642
595
  logger,
643
596
  Boolean(opts.json)
644
597
  );