@lessonkit/cli 0.9.2 → 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
@@ -5,6 +5,7 @@ import { createRequire } from "module";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/init.ts
8
+ import { slugifyId } from "@lessonkit/core";
8
9
  import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
9
10
  import { existsSync } from "fs";
10
11
  import { basename, dirname, join, resolve } from "path";
@@ -101,9 +102,6 @@ function getTemplateDir() {
101
102
  }
102
103
  return candidates[0];
103
104
  }
104
- function slugifyName(name) {
105
- return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "my-course";
106
- }
107
105
  async function isDirEmpty(dir) {
108
106
  if (!existsSync(dir)) return true;
109
107
  const entries = await readdir(dir);
@@ -153,14 +151,14 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
153
151
  }
154
152
  async function runInit(opts, logger) {
155
153
  const cwd = process.cwd();
156
- const rawName = opts.name ?? (opts.here ? slugifyName(basename(process.cwd()) || "my-course") : void 0);
154
+ const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
157
155
  if (!rawName && !opts.here) {
158
156
  throw new CliError("Project name is required. Usage: lessonkit init <name> or lessonkit init --here", {
159
157
  code: "INVALID_PROJECT",
160
158
  exitCode: EXIT_INVALID_PROJECT
161
159
  });
162
160
  }
163
- const slug = slugifyName(rawName ?? "my-course");
161
+ const slug = slugifyId(rawName ?? "my-course");
164
162
  const projectName = rawName ?? slug;
165
163
  const projectDir = opts.here ? cwd : resolve(cwd, slug);
166
164
  if (!opts.here && existsSync(projectDir)) {
@@ -211,18 +209,13 @@ async function runInit(opts, logger) {
211
209
  import { readFileSync, existsSync as existsSync2 } from "fs";
212
210
  import { readFile as readFile2 } from "fs/promises";
213
211
  import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
214
- import { validateDescriptor, validateProjectPaths } from "@lessonkit/lxpack";
212
+ import { parseLessonkitManifest } from "@lessonkit/lxpack";
215
213
  var LESSONKIT_JSON = "lessonkit.json";
216
214
  var PACKAGE_JSON = "package.json";
217
- var DEFAULT_PATHS = {
218
- spaDistDir: "dist",
219
- lxpackOutDir: ".lxpack/course",
220
- outputBaseDir: ".lxpack/out"
221
- };
222
215
  function isProjectManifest(configPath) {
223
216
  try {
224
217
  const raw = JSON.parse(readFileSync(configPath, "utf8"));
225
- 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);
226
219
  } catch {
227
220
  return false;
228
221
  }
@@ -255,124 +248,66 @@ async function loadLessonkitJson(projectRoot) {
255
248
  exitCode: EXIT_INVALID_PROJECT
256
249
  });
257
250
  }
258
- if (!raw || typeof raw !== "object") {
259
- throw new CliError(`${configPath} must be a JSON object.`, {
260
- code: "INVALID_PROJECT",
261
- exitCode: EXIT_INVALID_PROJECT
262
- });
263
- }
264
- const config = raw;
265
- const schemaVersion = config.schemaVersion;
266
- if (schemaVersion !== 1) {
267
- throw new CliError(`${configPath}: schemaVersion must be 1 (got ${String(schemaVersion)}).`, {
268
- code: "INVALID_PROJECT",
269
- exitCode: EXIT_INVALID_PROJECT
270
- });
251
+ const parsed = parseLessonkitManifest(raw, configPath, projectRoot);
252
+ if (!parsed.ok) {
253
+ throwManifestCliError(configPath, parsed.issues);
271
254
  }
272
- const name = config.name;
273
- if (typeof name !== "string" || !name.trim()) {
274
- throw new CliError(`${configPath}: "name" must be a non-empty string.`, {
275
- code: "INVALID_PROJECT",
276
- exitCode: EXIT_INVALID_PROJECT
277
- });
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
+ );
278
270
  }
279
- const courseRaw = config.course;
280
- if (!courseRaw || typeof courseRaw !== "object") {
281
- 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.`, {
282
274
  code: "INVALID_PROJECT",
283
275
  exitCode: EXIT_INVALID_PROJECT
284
276
  });
285
277
  }
286
- const courseObj = courseRaw;
287
- if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
288
- 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.`, {
289
281
  code: "INVALID_PROJECT",
290
282
  exitCode: EXIT_INVALID_PROJECT
291
283
  });
292
284
  }
293
- if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
294
- 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}`, {
295
288
  code: "INVALID_PROJECT",
296
289
  exitCode: EXIT_INVALID_PROJECT
297
290
  });
298
291
  }
299
- const validation = validateDescriptor(courseRaw);
300
- if (!validation.ok) {
301
- throw new CliError(`${configPath}: invalid course descriptor.`, {
292
+ if (issues.some((i) => i.path.startsWith("paths."))) {
293
+ throw new CliError(`${configPath}: invalid paths.`, {
302
294
  code: "INVALID_PROJECT",
303
295
  exitCode: EXIT_INVALID_PROJECT,
304
- issues: validation.issues.map((i) => ({
305
- path: i.path,
306
- message: i.message
307
- }))
296
+ issues: issues.map((i) => ({ path: i.path, message: i.message }))
308
297
  });
309
298
  }
310
- if (validation.descriptor.layout === "per-lesson-spa") {
311
- throw new CliError(
312
- `${configPath}: per-lesson-spa layout is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly.`,
313
- { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
314
- );
315
- }
316
- const pathsRaw = config.paths;
317
- const paths = { ...DEFAULT_PATHS };
318
- if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
319
- 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(/\)$/, "")}).`, {
320
302
  code: "INVALID_PROJECT",
321
303
  exitCode: EXIT_INVALID_PROJECT
322
304
  });
323
305
  }
324
- if (pathsRaw && typeof pathsRaw === "object") {
325
- const p = pathsRaw;
326
- if (p.spaDistDir !== void 0) {
327
- if (typeof p.spaDistDir !== "string" || !p.spaDistDir.trim()) {
328
- throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
329
- code: "INVALID_PROJECT",
330
- exitCode: EXIT_INVALID_PROJECT
331
- });
332
- }
333
- paths.spaDistDir = p.spaDistDir;
334
- }
335
- if (p.lxpackOutDir !== void 0) {
336
- if (typeof p.lxpackOutDir !== "string" || !p.lxpackOutDir.trim()) {
337
- throw new CliError(`${configPath}: "paths.lxpackOutDir" must be a non-empty string.`, {
338
- code: "INVALID_PROJECT",
339
- exitCode: EXIT_INVALID_PROJECT
340
- });
341
- }
342
- paths.lxpackOutDir = p.lxpackOutDir;
343
- }
344
- if (p.outputBaseDir !== void 0) {
345
- if (typeof p.outputBaseDir !== "string" || !p.outputBaseDir.trim()) {
346
- throw new CliError(`${configPath}: "paths.outputBaseDir" must be a non-empty string.`, {
347
- code: "INVALID_PROJECT",
348
- exitCode: EXIT_INVALID_PROJECT
349
- });
350
- }
351
- paths.outputBaseDir = p.outputBaseDir;
352
- }
353
- }
354
- const courseSpaDistDir = validation.descriptor.spaDistDir?.trim();
355
- if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
356
- throw new CliError(
357
- `${configPath}: "course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`,
358
- { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
359
- );
360
- }
361
- const pathIssues = validateProjectPaths(projectRoot, paths);
362
- if (pathIssues.length) {
363
- throw new CliError(`${configPath}: invalid paths.`, {
364
- code: "INVALID_PROJECT",
365
- exitCode: EXIT_INVALID_PROJECT,
366
- issues: pathIssues
367
- });
368
- }
369
- return {
370
- root: projectRoot,
371
- schemaVersion: 1,
372
- name,
373
- course: validation.descriptor,
374
- paths
375
- };
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
+ });
376
311
  }
377
312
  async function loadProject(cwd = process.cwd()) {
378
313
  const root = findProjectRoot(cwd);
@@ -390,7 +325,7 @@ async function readPackageJson(projectRoot) {
390
325
  }
391
326
  }
392
327
  function assertViteProject(pkg, projectRoot) {
393
- 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);
394
329
  if (!vite) {
395
330
  throw new CliError(
396
331
  `No Vite dependency found in ${join2(projectRoot, PACKAGE_JSON)}. LessonKit projects require Vite.`,
@@ -398,28 +333,25 @@ function assertViteProject(pkg, projectRoot) {
398
333
  );
399
334
  }
400
335
  }
401
- function resolveViteBin(projectRoot) {
336
+ function resolveViteJs(projectRoot) {
402
337
  let dir = resolve2(projectRoot);
403
338
  const fsRoot = parse(dir).root;
404
339
  while (true) {
405
- const binDir = join2(dir, "node_modules", ".bin");
406
- const bin = join2(binDir, "vite");
407
- if (existsSync2(bin)) return bin;
408
- const binCmd = join2(binDir, "vite.cmd");
409
- if (existsSync2(binCmd)) return binCmd;
340
+ const viteJs = join2(dir, "node_modules", "vite", "bin", "vite.js");
341
+ if (existsSync2(viteJs)) return viteJs;
410
342
  if (dir === fsRoot) break;
411
343
  dir = dirname2(dir);
412
344
  }
413
345
  throw new CliError(
414
- `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.`,
415
347
  { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
416
348
  );
417
349
  }
418
- function assertNode20ForLxpack() {
350
+ function assertNode18ForLxpack() {
419
351
  const major = Number(process.versions.node.split(".")[0]);
420
- if (major < 20) {
352
+ if (major < 18) {
421
353
  throw new CliError(
422
- `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.`,
423
355
  { code: "NODE_VERSION", exitCode: EXIT_INVALID_PROJECT }
424
356
  );
425
357
  }
@@ -481,17 +413,19 @@ async function runDev(opts) {
481
413
  const project = await loadProject(opts.cwd ?? process.cwd());
482
414
  const pkg = await readPackageJson(project.root);
483
415
  assertViteProject(pkg, project.root);
484
- const viteBin = resolveViteBin(project.root);
485
- 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 });
486
418
  return { ok: true, command: "dev", projectRoot: project.root };
487
419
  }
488
420
  async function runBuild(opts) {
489
421
  const project = await loadProject(opts.cwd ?? process.cwd());
490
422
  const pkg = await readPackageJson(project.root);
491
423
  assertViteProject(pkg, project.root);
492
- const viteBin = resolveViteBin(project.root);
424
+ const viteJs = resolveViteJs(project.root);
493
425
  const buildArgs = resolveViteBuildArgs(project);
494
- await runCommand(viteBin, [...buildArgs, ...opts.viteArgs ?? []], { cwd: project.root });
426
+ await runCommand(process.execPath, [viteJs, ...buildArgs, ...opts.viteArgs ?? []], {
427
+ cwd: project.root
428
+ });
495
429
  return { ok: true, command: "build", projectRoot: project.root };
496
430
  }
497
431
 
@@ -526,7 +460,7 @@ async function runPackage(opts) {
526
460
  }
527
461
  return { ok: true, target, projectRoot: project.root, distDir };
528
462
  }
529
- assertNode20ForLxpack();
463
+ assertNode18ForLxpack();
530
464
  if (!opts.noBuild || !existsSync3(distDir)) {
531
465
  await runBuild({ cwd: project.root, json: opts.json });
532
466
  }
@@ -600,7 +534,10 @@ async function handleCommand(fn, logger, json) {
600
534
  function createProgram(baseLogger = console) {
601
535
  const program = new Command();
602
536
  program.name("lessonkit").description("LessonKit CLI").version(version);
603
- 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) => {
604
541
  const logger = createLogger({ json: opts.json });
605
542
  await handleCommand(
606
543
  () => runInit({ name, here: opts.here, skipInstall: opts.skipInstall, force: opts.force, json: opts.json }, logger),
@@ -634,13 +571,27 @@ function createProgram(baseLogger = console) {
634
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) => {
635
572
  const logger = createLogger({ json: opts.json });
636
573
  await handleCommand(
637
- () => runPackage({
638
- target: opts.target,
639
- cwd: opts.cwd,
640
- noBuild: opts.build === false,
641
- out: opts.out,
642
- json: opts.json
643
- }),
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
+ },
644
595
  logger,
645
596
  Boolean(opts.json)
646
597
  );