@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/dist/index.js CHANGED
@@ -207,18 +207,13 @@ async function runInit(opts, logger) {
207
207
  import { readFileSync, existsSync as existsSync2 } from "fs";
208
208
  import { readFile as readFile2 } from "fs/promises";
209
209
  import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
210
- import { validateDescriptor, validateProjectPaths } from "@lessonkit/lxpack";
210
+ import { parseLessonkitManifest } from "@lessonkit/lxpack";
211
211
  var LESSONKIT_JSON = "lessonkit.json";
212
212
  var PACKAGE_JSON = "package.json";
213
- var DEFAULT_PATHS = {
214
- spaDistDir: "dist",
215
- lxpackOutDir: ".lxpack/course",
216
- outputBaseDir: ".lxpack/out"
217
- };
218
213
  function isProjectManifest(configPath) {
219
214
  try {
220
215
  const raw = JSON.parse(readFileSync(configPath, "utf8"));
221
- return raw.schemaVersion === 1 && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object";
216
+ return raw.schemaVersion === 1 && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
222
217
  } catch {
223
218
  return false;
224
219
  }
@@ -251,124 +246,66 @@ async function loadLessonkitJson(projectRoot) {
251
246
  exitCode: EXIT_INVALID_PROJECT
252
247
  });
253
248
  }
254
- if (!raw || typeof raw !== "object") {
255
- throw new CliError(`${configPath} must be a JSON object.`, {
256
- code: "INVALID_PROJECT",
257
- exitCode: EXIT_INVALID_PROJECT
258
- });
259
- }
260
- const config = raw;
261
- const schemaVersion = config.schemaVersion;
262
- if (schemaVersion !== 1) {
263
- throw new CliError(`${configPath}: schemaVersion must be 1 (got ${String(schemaVersion)}).`, {
264
- code: "INVALID_PROJECT",
265
- exitCode: EXIT_INVALID_PROJECT
266
- });
249
+ const parsed = parseLessonkitManifest(raw, configPath, projectRoot);
250
+ if (!parsed.ok) {
251
+ throwManifestCliError(configPath, parsed.issues);
267
252
  }
268
- const name = config.name;
269
- if (typeof name !== "string" || !name.trim()) {
270
- throw new CliError(`${configPath}: "name" must be a non-empty string.`, {
271
- code: "INVALID_PROJECT",
272
- exitCode: EXIT_INVALID_PROJECT
273
- });
253
+ return {
254
+ root: projectRoot,
255
+ schemaVersion: 1,
256
+ name: parsed.manifest.name,
257
+ course: parsed.manifest.course,
258
+ paths: parsed.manifest.paths
259
+ };
260
+ }
261
+ function throwManifestCliError(configPath, issues) {
262
+ const layoutIssue = issues.find((i) => i.path === "course.layout");
263
+ if (layoutIssue?.message.includes("per-lesson-spa")) {
264
+ throw new CliError(
265
+ `${configPath}: per-lesson-spa layout is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly.`,
266
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
267
+ );
274
268
  }
275
- const courseRaw = config.course;
276
- if (!courseRaw || typeof courseRaw !== "object") {
277
- throw new CliError(`${configPath}: "course" must be an object.`, {
269
+ const lessonsIssue = issues.find((i) => i.path === "course.lessons");
270
+ if (lessonsIssue) {
271
+ throw new CliError(`${configPath}: "course.lessons" must be an array.`, {
278
272
  code: "INVALID_PROJECT",
279
273
  exitCode: EXIT_INVALID_PROJECT
280
274
  });
281
275
  }
282
- const courseObj = courseRaw;
283
- if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
284
- throw new CliError(`${configPath}: "course.lessons" must be an array.`, {
276
+ const spaDistTypeIssue = issues.find((i) => i.path === "paths.spaDistDir");
277
+ if (spaDistTypeIssue && spaDistTypeIssue.message.includes("non-empty string")) {
278
+ throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
285
279
  code: "INVALID_PROJECT",
286
280
  exitCode: EXIT_INVALID_PROJECT
287
281
  });
288
282
  }
289
- if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
290
- throw new CliError(`${configPath}: "course.assessments" must be an array.`, {
283
+ const courseSpaIssue = issues.find((i) => i.path === "course.spaDistDir");
284
+ if (courseSpaIssue) {
285
+ throw new CliError(`${configPath}: ${courseSpaIssue.message}`, {
291
286
  code: "INVALID_PROJECT",
292
287
  exitCode: EXIT_INVALID_PROJECT
293
288
  });
294
289
  }
295
- const validation = validateDescriptor(courseRaw);
296
- if (!validation.ok) {
297
- throw new CliError(`${configPath}: invalid course descriptor.`, {
290
+ if (issues.some((i) => i.path.startsWith("paths."))) {
291
+ throw new CliError(`${configPath}: invalid paths.`, {
298
292
  code: "INVALID_PROJECT",
299
293
  exitCode: EXIT_INVALID_PROJECT,
300
- issues: validation.issues.map((i) => ({
301
- path: i.path,
302
- message: i.message
303
- }))
294
+ issues: issues.map((i) => ({ path: i.path, message: i.message }))
304
295
  });
305
296
  }
306
- if (validation.descriptor.layout === "per-lesson-spa") {
307
- throw new CliError(
308
- `${configPath}: per-lesson-spa layout is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly.`,
309
- { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
310
- );
311
- }
312
- const pathsRaw = config.paths;
313
- const paths = { ...DEFAULT_PATHS };
314
- if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
315
- throw new CliError(`${configPath}: "paths" must be an object.`, {
297
+ const schemaIssue = issues.find((i) => i.path === "schemaVersion");
298
+ if (schemaIssue) {
299
+ throw new CliError(`${configPath}: schemaVersion must be 1 (got ${schemaIssue.message.replace(/^must be 1 \(got /, "").replace(/\)$/, "")}).`, {
316
300
  code: "INVALID_PROJECT",
317
301
  exitCode: EXIT_INVALID_PROJECT
318
302
  });
319
303
  }
320
- if (pathsRaw && typeof pathsRaw === "object") {
321
- const p = pathsRaw;
322
- if (p.spaDistDir !== void 0) {
323
- if (typeof p.spaDistDir !== "string" || !p.spaDistDir.trim()) {
324
- throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
325
- code: "INVALID_PROJECT",
326
- exitCode: EXIT_INVALID_PROJECT
327
- });
328
- }
329
- paths.spaDistDir = p.spaDistDir;
330
- }
331
- if (p.lxpackOutDir !== void 0) {
332
- if (typeof p.lxpackOutDir !== "string" || !p.lxpackOutDir.trim()) {
333
- throw new CliError(`${configPath}: "paths.lxpackOutDir" must be a non-empty string.`, {
334
- code: "INVALID_PROJECT",
335
- exitCode: EXIT_INVALID_PROJECT
336
- });
337
- }
338
- paths.lxpackOutDir = p.lxpackOutDir;
339
- }
340
- if (p.outputBaseDir !== void 0) {
341
- if (typeof p.outputBaseDir !== "string" || !p.outputBaseDir.trim()) {
342
- throw new CliError(`${configPath}: "paths.outputBaseDir" must be a non-empty string.`, {
343
- code: "INVALID_PROJECT",
344
- exitCode: EXIT_INVALID_PROJECT
345
- });
346
- }
347
- paths.outputBaseDir = p.outputBaseDir;
348
- }
349
- }
350
- const courseSpaDistDir = validation.descriptor.spaDistDir?.trim();
351
- if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
352
- throw new CliError(
353
- `${configPath}: "course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`,
354
- { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
355
- );
356
- }
357
- const pathIssues = validateProjectPaths(projectRoot, paths);
358
- if (pathIssues.length) {
359
- throw new CliError(`${configPath}: invalid paths.`, {
360
- code: "INVALID_PROJECT",
361
- exitCode: EXIT_INVALID_PROJECT,
362
- issues: pathIssues
363
- });
364
- }
365
- return {
366
- root: projectRoot,
367
- schemaVersion: 1,
368
- name,
369
- course: validation.descriptor,
370
- paths
371
- };
304
+ throw new CliError(`${configPath}: invalid lessonkit manifest.`, {
305
+ code: "INVALID_PROJECT",
306
+ exitCode: EXIT_INVALID_PROJECT,
307
+ issues: issues.map((i) => ({ path: i.path, message: i.message }))
308
+ });
372
309
  }
373
310
  async function loadProject(cwd = process.cwd()) {
374
311
  const root = findProjectRoot(cwd);
@@ -386,7 +323,7 @@ async function readPackageJson(projectRoot) {
386
323
  }
387
324
  }
388
325
  function assertViteProject(pkg, projectRoot) {
389
- 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);
326
+ 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);
390
327
  if (!vite) {
391
328
  throw new CliError(
392
329
  `No Vite dependency found in ${join2(projectRoot, PACKAGE_JSON)}. LessonKit projects require Vite.`,
@@ -394,28 +331,25 @@ function assertViteProject(pkg, projectRoot) {
394
331
  );
395
332
  }
396
333
  }
397
- function resolveViteBin(projectRoot) {
334
+ function resolveViteJs(projectRoot) {
398
335
  let dir = resolve2(projectRoot);
399
336
  const fsRoot = parse(dir).root;
400
337
  while (true) {
401
- const binDir = join2(dir, "node_modules", ".bin");
402
- const bin = join2(binDir, "vite");
403
- if (existsSync2(bin)) return bin;
404
- const binCmd = join2(binDir, "vite.cmd");
405
- if (existsSync2(binCmd)) return binCmd;
338
+ const viteJs = join2(dir, "node_modules", "vite", "bin", "vite.js");
339
+ if (existsSync2(viteJs)) return viteJs;
406
340
  if (dir === fsRoot) break;
407
341
  dir = dirname2(dir);
408
342
  }
409
343
  throw new CliError(
410
- `Vite binary not found near ${projectRoot}. Run npm install in the project first.`,
344
+ `Vite not found near ${projectRoot}. Run npm install in the project first.`,
411
345
  { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
412
346
  );
413
347
  }
414
- function assertNode20ForLxpack() {
348
+ function assertNode18ForLxpack() {
415
349
  const major = Number(process.versions.node.split(".")[0]);
416
- if (major < 20) {
350
+ if (major < 18) {
417
351
  throw new CliError(
418
- `LMS packaging requires Node.js 20+ (current: ${process.versions.node}). See docs/PACKAGING.md.`,
352
+ `LMS packaging requires Node.js 18+ (current: ${process.versions.node}). See docs/PACKAGING.md.`,
419
353
  { code: "NODE_VERSION", exitCode: EXIT_INVALID_PROJECT }
420
354
  );
421
355
  }
@@ -477,17 +411,19 @@ async function runDev(opts) {
477
411
  const project = await loadProject(opts.cwd ?? process.cwd());
478
412
  const pkg = await readPackageJson(project.root);
479
413
  assertViteProject(pkg, project.root);
480
- const viteBin = resolveViteBin(project.root);
481
- await runCommand(viteBin, opts.viteArgs ?? [], { cwd: project.root });
414
+ const viteJs = resolveViteJs(project.root);
415
+ await runCommand(process.execPath, [viteJs, ...opts.viteArgs ?? []], { cwd: project.root });
482
416
  return { ok: true, command: "dev", projectRoot: project.root };
483
417
  }
484
418
  async function runBuild(opts) {
485
419
  const project = await loadProject(opts.cwd ?? process.cwd());
486
420
  const pkg = await readPackageJson(project.root);
487
421
  assertViteProject(pkg, project.root);
488
- const viteBin = resolveViteBin(project.root);
422
+ const viteJs = resolveViteJs(project.root);
489
423
  const buildArgs = resolveViteBuildArgs(project);
490
- await runCommand(viteBin, [...buildArgs, ...opts.viteArgs ?? []], { cwd: project.root });
424
+ await runCommand(process.execPath, [viteJs, ...buildArgs, ...opts.viteArgs ?? []], {
425
+ cwd: project.root
426
+ });
491
427
  return { ok: true, command: "build", projectRoot: project.root };
492
428
  }
493
429
 
@@ -522,7 +458,7 @@ async function runPackage(opts) {
522
458
  }
523
459
  return { ok: true, target, projectRoot: project.root, distDir };
524
460
  }
525
- assertNode20ForLxpack();
461
+ assertNode18ForLxpack();
526
462
  if (!opts.noBuild || !existsSync3(distDir)) {
527
463
  await runBuild({ cwd: project.root, json: opts.json });
528
464
  }
@@ -596,7 +532,10 @@ async function handleCommand(fn, logger, json) {
596
532
  function createProgram(baseLogger = console) {
597
533
  const program = new Command();
598
534
  program.name("lessonkit").description("LessonKit CLI").version(version);
599
- 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) => {
535
+ 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(
536
+ "--force",
537
+ "With --here, allow init when the directory is empty or contains only dotfiles"
538
+ ).option("--json", "Emit structured JSON result").action(async (name, opts) => {
600
539
  const logger = createLogger({ json: opts.json });
601
540
  await handleCommand(
602
541
  () => runInit({ name, here: opts.here, skipInstall: opts.skipInstall, force: opts.force, json: opts.json }, logger),
@@ -630,13 +569,27 @@ function createProgram(baseLogger = console) {
630
569
  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) => {
631
570
  const logger = createLogger({ json: opts.json });
632
571
  await handleCommand(
633
- () => runPackage({
634
- target: opts.target,
635
- cwd: opts.cwd,
636
- noBuild: opts.build === false,
637
- out: opts.out,
638
- json: opts.json
639
- }),
572
+ async () => {
573
+ const result = await runPackage({
574
+ target: opts.target,
575
+ cwd: opts.cwd,
576
+ noBuild: opts.build === false,
577
+ out: opts.out,
578
+ json: opts.json
579
+ });
580
+ if (!opts.json && result.ok) {
581
+ if (result.target === "react-vite" && "distDir" in result) {
582
+ logger.log(`Built react-vite \u2192 ${result.distDir}`);
583
+ } else if ("outputPath" in result || "outputDir" in result) {
584
+ const dest = result.outputPath ?? result.outputDir;
585
+ const count = "fileCount" in result ? result.fileCount : void 0;
586
+ logger.log(
587
+ `Packaged ${result.target}${dest ? ` \u2192 ${dest}` : ""}${count != null ? ` (${count} files)` : ""}`
588
+ );
589
+ }
590
+ }
591
+ return result;
592
+ },
640
593
  logger,
641
594
  Boolean(opts.json)
642
595
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/cli",
3
- "version": "0.9.3",
3
+ "version": "1.0.0",
4
4
  "private": false,
5
5
  "description": "LessonKit CLI — init, dev, build, and package learning experiences.",
6
6
  "license": "Apache-2.0",
@@ -42,8 +42,8 @@
42
42
  "lint": "echo \"(no lint configured yet)\""
43
43
  },
44
44
  "dependencies": {
45
- "@lessonkit/core": "0.9.3",
46
- "@lessonkit/lxpack": "0.9.3",
45
+ "@lessonkit/core": "1.0.0",
46
+ "@lessonkit/lxpack": "1.0.0",
47
47
  "commander": "^14.0.1"
48
48
  },
49
49
  "engines": {
@@ -1,19 +1,21 @@
1
- # LessonKit Vite + React template
1
+ # LessonKit starter template
2
2
 
3
- [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/)
4
- [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
3
+ Vite + React scaffold for new LessonKit courses. Created by `lessonkit init`.
5
4
 
6
- Starter template copied by `lessonkit init`. See the [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) and [React quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html).
7
-
8
- ## Run
5
+ ## Commands
9
6
 
10
7
  ```bash
11
8
  npm install
12
- npm run dev
9
+ npm run dev # lessonkit dev
10
+ npm run build # lessonkit build
11
+ npm run package:scorm12
13
12
  ```
14
13
 
15
- ## Notes
14
+ ## Files
15
+
16
+ - `src/App.tsx` — course UI (IDs match `lessonkit.json`)
17
+ - `lessonkit.json` — manifest for CLI and LXPack packaging
18
+
19
+ ## Docs
16
20
 
17
- - Depends on [`@lessonkit/react`](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/components-and-hooks.html).
18
- - Copied by [`@lessonkit/cli`](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) when you run `lessonkit init`.
19
- - Package for an LMS with the [packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html).
21
+ [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [React quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html) · [Packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html)
@@ -15,7 +15,12 @@
15
15
  "passingScore": 1
16
16
  }
17
17
  ],
18
- "theme": { "preset": "default" }
18
+ "theme": { "preset": "default" },
19
+ "tracking": {
20
+ "xapi": {
21
+ "activityIri": "https://example.com/courses/my-course"
22
+ }
23
+ }
19
24
  },
20
25
  "paths": {
21
26
  "spaDistDir": "dist",
@@ -13,16 +13,16 @@
13
13
  "test:coverage": "vitest run --coverage --passWithNoTests=false"
14
14
  },
15
15
  "dependencies": {
16
- "@lessonkit/core": "^0.9.3",
17
- "@lessonkit/react": "^0.9.3",
18
- "@lessonkit/themes": "^0.9.3",
19
- "@lessonkit/xapi": "^0.9.3",
16
+ "@lessonkit/core": "^1.0.0",
17
+ "@lessonkit/react": "^1.0.0",
18
+ "@lessonkit/themes": "^1.0.0",
19
+ "@lessonkit/xapi": "^1.0.0",
20
20
  "react": "^18.3.1",
21
21
  "react-dom": "^18.3.1"
22
22
  },
23
23
  "devDependencies": {
24
- "@lessonkit/cli": "^0.9.3",
25
- "@lessonkit/lxpack": "^0.9.3",
24
+ "@lessonkit/cli": "^1.0.0",
25
+ "@lessonkit/lxpack": "^1.0.0",
26
26
  "@testing-library/react": "^16.3.0",
27
27
  "@types/react": "^18.3.23",
28
28
  "@types/react-dom": "^18.3.7",
@@ -1 +0,0 @@
1
- html,body{height:100%}body{margin:0;background:var(--lk-color-background);color:var(--lk-color-foreground);font-family:var(--lk-font-family);font-size:var(--lk-font-size-base);line-height:var(--lk-line-height-base)}.app-shell{margin:0 auto;padding:var(--lk-space-xl) var(--lk-space-lg);max-width:720px}section,article{border:1px solid var(--lk-color-border);border-radius:var(--lk-radius-lg);padding:var(--lk-space-lg);margin:var(--lk-space-md) 0;background:var(--lk-color-panel);box-shadow:var(--lk-shadow-md)}h1,h2{margin:0 0 var(--lk-space-sm);font-weight:var(--lk-font-weight-strong)}button{border:1px solid var(--lk-color-border);background:var(--lk-color-panel);color:var(--lk-color-foreground);border-radius:var(--lk-radius-md);padding:var(--lk-space-sm) var(--lk-space-md);font-weight:var(--lk-font-weight-strong);cursor:pointer}input[type=radio]{accent-color:var(--lk-color-primary)}label{display:block;margin:var(--lk-space-xs) 0}