@lessonkit/cli 1.4.0 → 1.6.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
@@ -1,11 +1,12 @@
1
1
  // src/index.ts
2
- import { createRequire as createRequire2 } from "module";
2
+ import { createRequire as createRequire3 } from "module";
3
3
  import { Command } from "commander";
4
4
 
5
5
  // src/commands/init.ts
6
6
  import { slugifyId } from "@lessonkit/core";
7
- import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
7
+ import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
8
8
  import { existsSync } from "fs";
9
+ import { randomUUID } from "crypto";
9
10
  import { basename, dirname, join, resolve } from "path";
10
11
  import { fileURLToPath } from "url";
11
12
 
@@ -122,7 +123,7 @@ async function runNpmInstall(cwd) {
122
123
  }
123
124
 
124
125
  // src/commands/init.ts
125
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git"]);
126
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".lxpack", ".git", "coverage", ".nyc_output"]);
126
127
  var SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
127
128
  function getTemplateDir() {
128
129
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -146,7 +147,7 @@ async function isDirEmptyOrDotfilesOnly(dir) {
146
147
  return entries.every((name) => name.startsWith("."));
147
148
  }
148
149
  function escapeJsxString(value) {
149
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\r\n|\n|\r/g, "\\n");
150
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/\r\n|\n|\r/g, "\\n");
150
151
  }
151
152
  async function copyTemplate(src, dest) {
152
153
  await mkdir(dest, { recursive: true });
@@ -167,7 +168,7 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
167
168
  const pkgPath = join(projectDir, "package.json");
168
169
  const lessonkitPath = join(projectDir, "lessonkit.json");
169
170
  const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
170
- pkg.name = projectName;
171
+ pkg.name = slug;
171
172
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
172
173
  `, "utf8");
173
174
  const lessonkit = JSON.parse(await readFile(lessonkitPath, "utf8"));
@@ -175,11 +176,11 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
175
176
  const course = lessonkit.course;
176
177
  course.courseId = slug;
177
178
  course.title = projectName;
178
- const tracking = lessonkit.tracking ?? {};
179
- const xapi = tracking.xapi ?? {};
180
- xapi.activityIri = `https://example.com/courses/${slug}`;
181
- tracking.xapi = xapi;
182
- lessonkit.tracking = tracking;
179
+ const courseTracking = course.tracking ?? {};
180
+ const courseXapi = courseTracking.xapi ?? {};
181
+ courseXapi.activityIri = `https://example.com/courses/${slug}`;
182
+ courseTracking.xapi = courseXapi;
183
+ course.tracking = courseTracking;
183
184
  await writeFile(lessonkitPath, `${JSON.stringify(lessonkit, null, 2)}
184
185
  `, "utf8");
185
186
  const courseConfigPath = join(projectDir, "src", "courseConfig.ts");
@@ -192,6 +193,75 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
192
193
  appSource = appSource.replace(/\{\{courseTitle\}\}/g, escapeJsxString(projectName));
193
194
  await writeFile(appPath, appSource, "utf8");
194
195
  }
196
+ async function backupConflictingFiles(stagingDir, projectDir) {
197
+ const backups = /* @__PURE__ */ new Map();
198
+ const stagingEntries = await readdir(stagingDir, { withFileTypes: true });
199
+ for (const entry of stagingEntries) {
200
+ const destPath = join(projectDir, entry.name);
201
+ if (!existsSync(destPath)) continue;
202
+ const destStat = await stat(destPath);
203
+ if (destStat.isFile()) {
204
+ backups.set(entry.name, await readFile(destPath));
205
+ }
206
+ }
207
+ return backups;
208
+ }
209
+ async function rollbackPromotedFiles(projectDir, stagingDir, preExisting, backups) {
210
+ const failures = [];
211
+ let stagingEntries;
212
+ try {
213
+ stagingEntries = await readdir(stagingDir, { withFileTypes: true });
214
+ } catch {
215
+ return;
216
+ }
217
+ for (const entry of stagingEntries) {
218
+ if (preExisting.has(entry.name)) continue;
219
+ try {
220
+ await rm(join(projectDir, entry.name), { recursive: true, force: true });
221
+ } catch (err) {
222
+ failures.push(
223
+ `remove ${entry.name}: ${err instanceof Error ? err.message : String(err)}`
224
+ );
225
+ }
226
+ }
227
+ for (const [name, content] of backups) {
228
+ try {
229
+ await writeFile(join(projectDir, name), content);
230
+ } catch (err) {
231
+ failures.push(`restore ${name}: ${err instanceof Error ? err.message : String(err)}`);
232
+ }
233
+ }
234
+ if (failures.length > 0) {
235
+ throw new CliError(`Init rollback failed: ${failures.join("; ")}`, {
236
+ code: "RUNTIME",
237
+ exitCode: EXIT_INVALID_PROJECT
238
+ });
239
+ }
240
+ }
241
+ async function promoteStagingToProjectDir(stagingDir, projectDir) {
242
+ await mkdir(projectDir, { recursive: true });
243
+ const entries = await readdir(stagingDir, { withFileTypes: true });
244
+ for (const entry of entries) {
245
+ const srcPath = join(stagingDir, entry.name);
246
+ const destPath = join(projectDir, entry.name);
247
+ if (entry.isDirectory()) {
248
+ await cp(srcPath, destPath, { recursive: true });
249
+ } else if (entry.isFile()) {
250
+ await cp(srcPath, destPath);
251
+ } else {
252
+ }
253
+ }
254
+ }
255
+ var __testInitHelpers = {
256
+ getTemplateDir,
257
+ isDirEmpty,
258
+ isDirEmptyOrDotfilesOnly,
259
+ escapeJsxString,
260
+ copyTemplate,
261
+ promoteStagingToProjectDir,
262
+ rollbackPromotedFiles,
263
+ backupConflictingFiles
264
+ };
195
265
  async function runInit(opts, logger) {
196
266
  const cwd = process.cwd();
197
267
  const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
@@ -219,11 +289,14 @@ async function runInit(opts, logger) {
219
289
  }
220
290
  );
221
291
  }
222
- if (opts.here && !await isDirEmpty(projectDir) && !opts.force) {
223
- throw new CliError(`Directory is not empty: ${projectDir}. Use --force to initialize anyway.`, {
224
- code: "INVALID_PROJECT",
225
- exitCode: EXIT_INVALID_PROJECT
226
- });
292
+ if (opts.here && !await isDirEmptyOrDotfilesOnly(projectDir) && !opts.force) {
293
+ throw new CliError(
294
+ `Directory is not empty: ${projectDir}. Use --here --force only when the directory is empty or contains dotfiles only (e.g. .git).`,
295
+ {
296
+ code: "INVALID_PROJECT",
297
+ exitCode: EXIT_INVALID_PROJECT
298
+ }
299
+ );
227
300
  }
228
301
  if (opts.here && opts.force && !await isDirEmptyOrDotfilesOnly(projectDir)) {
229
302
  throw new CliError(
@@ -241,11 +314,42 @@ async function runInit(opts, logger) {
241
314
  exitCode: EXIT_INVALID_PROJECT
242
315
  });
243
316
  }
244
- await copyTemplate(templateDir, projectDir);
245
- await applyTemplateSubstitutions(projectDir, projectName, slug);
246
- if (!opts.skipInstall) {
247
- if (!opts.json) logger.log(`Installing dependencies in ${projectDir}\u2026`);
248
- await runNpmInstall(projectDir);
317
+ const stagingDir = opts.here ? join(cwd, `.lessonkit-init-${randomUUID()}`) : join(cwd, `.${slug}-init-${randomUUID()}`);
318
+ try {
319
+ await copyTemplate(templateDir, stagingDir);
320
+ await applyTemplateSubstitutions(stagingDir, projectName, slug);
321
+ if (!opts.skipInstall) {
322
+ if (!opts.json) logger.log(`Installing dependencies in ${stagingDir}\u2026`);
323
+ await runNpmInstall(stagingDir);
324
+ }
325
+ if (opts.here) {
326
+ const preExisting = new Set(await readdir(projectDir));
327
+ const backups = await backupConflictingFiles(stagingDir, projectDir);
328
+ try {
329
+ await __testInitHelpers.promoteStagingToProjectDir(stagingDir, projectDir);
330
+ } catch (promoteErr) {
331
+ try {
332
+ await rollbackPromotedFiles(projectDir, stagingDir, preExisting, backups);
333
+ } catch (rollbackErr) {
334
+ const promoteMessage = promoteErr instanceof Error ? promoteErr.message : String(promoteErr);
335
+ const rollbackMessage = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
336
+ throw new CliError(`${promoteMessage}; ${rollbackMessage}`, {
337
+ code: "RUNTIME",
338
+ exitCode: EXIT_INVALID_PROJECT
339
+ });
340
+ }
341
+ throw promoteErr;
342
+ }
343
+ await rm(stagingDir, { recursive: true, force: true });
344
+ } else {
345
+ await rename(stagingDir, projectDir);
346
+ }
347
+ } catch (err) {
348
+ await rm(stagingDir, { recursive: true, force: true }).catch(
349
+ /* v8 ignore next */
350
+ () => void 0
351
+ );
352
+ throw err;
249
353
  }
250
354
  if (!opts.json) {
251
355
  logger.log(`Created LessonKit project at ${projectDir}`);
@@ -254,6 +358,12 @@ async function runInit(opts, logger) {
254
358
  return { ok: true, command: "init", projectRoot: projectDir };
255
359
  }
256
360
 
361
+ // src/commands/dev.ts
362
+ import { existsSync as existsSync3 } from "fs";
363
+ import { mkdir as mkdir2 } from "fs/promises";
364
+ import { dirname as dirname3, join as join3 } from "path";
365
+ import { assertSpaDistContentsSafe } from "@lessonkit/lxpack";
366
+
257
367
  // src/lib/project.ts
258
368
  import { readFileSync, existsSync as existsSync2 } from "fs";
259
369
  import { readFile as readFile2 } from "fs/promises";
@@ -262,10 +372,13 @@ import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "
262
372
  import { parseLessonkitManifest } from "@lessonkit/lxpack";
263
373
  var LESSONKIT_JSON = "lessonkit.json";
264
374
  var PACKAGE_JSON = "package.json";
375
+ function isSchemaVersionOne(value) {
376
+ return value === 1 || value === "1";
377
+ }
265
378
  function isProjectManifest(configPath) {
266
379
  try {
267
380
  const raw = JSON.parse(readFileSync(configPath, "utf8"));
268
- return raw.schemaVersion === 1 && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
381
+ return isSchemaVersionOne(raw.schemaVersion) && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
269
382
  } catch {
270
383
  return false;
271
384
  }
@@ -438,7 +551,12 @@ function resolvePackageOutput(project, target, override) {
438
551
  if (override) {
439
552
  try {
440
553
  const resolved = resolveSafePackageOutputOverride(project.root, override);
441
- return { output: resolved, dir: target === "standalone", outputBaseDir };
554
+ const isZipOutput = override.trim().toLowerCase().endsWith(".zip");
555
+ return {
556
+ output: resolved,
557
+ dir: target === "standalone" && !isZipOutput,
558
+ outputBaseDir
559
+ };
442
560
  } catch (err) {
443
561
  const message = err instanceof Error ? err.message : String(err);
444
562
  throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
@@ -449,13 +567,24 @@ function resolvePackageOutput(project, target, override) {
449
567
  }
450
568
  return { output: `${outputBaseDir}/course-${target}.zip`, dir: false, outputBaseDir };
451
569
  }
452
- var DEFAULT_SPA_DIST_DIR = "dist";
453
- function resolveViteBuildArgs(project) {
454
- const args = ["build"];
455
- if (project.paths.spaDistDir !== DEFAULT_SPA_DIST_DIR) {
456
- args.push("--outDir", project.paths.spaDistDir);
570
+ function stripOutDirFromViteArgs(viteArgs) {
571
+ const stripped = [];
572
+ for (let i = 0; i < viteArgs.length; i++) {
573
+ const arg = viteArgs[i];
574
+ if (arg === "--outDir" || arg === "-o") {
575
+ i++;
576
+ continue;
577
+ }
578
+ if (arg.startsWith("--outDir=")) {
579
+ continue;
580
+ }
581
+ stripped.push(arg);
457
582
  }
458
- return args;
583
+ return stripped;
584
+ }
585
+ function resolveViteBuildArgv(project, viteArgs = []) {
586
+ const passthrough = stripOutDirFromViteArgs(viteArgs);
587
+ return ["build", ...passthrough, "--outDir", project.paths.spaDistDir];
459
588
  }
460
589
  function parsePackageTarget(value) {
461
590
  if (!value) {
@@ -473,7 +602,8 @@ async function runDev(opts) {
473
602
  const pkg = await readPackageJson(project.root);
474
603
  assertViteProject(pkg, project.root);
475
604
  const viteJs = resolveViteJs(project.root);
476
- await runCommand(process.execPath, [viteJs, ...opts.viteArgs ?? []], {
605
+ const devArgs = stripOutDirFromViteArgs(opts.viteArgs ?? []);
606
+ await runCommand(process.execPath, [viteJs, ...devArgs], {
477
607
  cwd: project.root,
478
608
  timeoutMs: 0
479
609
  });
@@ -484,15 +614,28 @@ async function runBuild(opts) {
484
614
  const pkg = await readPackageJson(project.root);
485
615
  assertViteProject(pkg, project.root);
486
616
  const viteJs = resolveViteJs(project.root);
487
- const buildArgs = resolveViteBuildArgs(project);
488
- await runCommand(process.execPath, [viteJs, ...buildArgs, ...opts.viteArgs ?? []], {
617
+ const distDir = resolveDistDir(project);
618
+ await mkdir2(dirname3(distDir), { recursive: true });
619
+ if (existsSync3(distDir)) {
620
+ await assertSpaDistContentsSafe({ main: distDir }, project.root);
621
+ }
622
+ const buildArgs = resolveViteBuildArgv(project, opts.viteArgs);
623
+ await runCommand(process.execPath, [viteJs, ...buildArgs], {
489
624
  cwd: project.root
490
625
  });
626
+ const indexHtml = join3(distDir, "index.html");
627
+ if (!existsSync3(indexHtml)) {
628
+ throw new CliError(
629
+ `Build did not produce index.html at ${indexHtml}. Check paths.spaDistDir in lessonkit.json.`,
630
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
631
+ );
632
+ }
491
633
  return { ok: true, command: "build", projectRoot: project.root };
492
634
  }
493
635
 
494
636
  // src/commands/package.ts
495
- import { existsSync as existsSync3 } from "fs";
637
+ import { existsSync as existsSync4 } from "fs";
638
+ import { isAbsolute } from "path";
496
639
  import { packageLessonkitCourse } from "@lessonkit/lxpack";
497
640
  async function runPackage(opts) {
498
641
  let target;
@@ -510,7 +653,7 @@ async function runPackage(opts) {
510
653
  }
511
654
  const project = await loadProject(opts.cwd ?? process.cwd());
512
655
  const distDir = resolveDistDir(project);
513
- if (opts.noBuild && !existsSync3(distDir)) {
656
+ if (opts.noBuild && !existsSync4(distDir)) {
514
657
  throw new CliError(
515
658
  `dist directory not found at ${distDir}. Run lessonkit build before packaging with --no-build.`,
516
659
  {
@@ -523,7 +666,7 @@ async function runPackage(opts) {
523
666
  if (!opts.noBuild) {
524
667
  await runBuild({ cwd: project.root, json: opts.json });
525
668
  }
526
- if (!existsSync3(distDir)) {
669
+ if (!existsSync4(distDir)) {
527
670
  throw new CliError(`Build completed but dist directory not found at ${distDir}.`, {
528
671
  code: "INVALID_PROJECT",
529
672
  exitCode: EXIT_INVALID_PROJECT
@@ -535,14 +678,20 @@ async function runPackage(opts) {
535
678
  if (!opts.noBuild) {
536
679
  await runBuild({ cwd: project.root, json: opts.json });
537
680
  }
538
- if (!existsSync3(distDir)) {
681
+ if (!existsSync4(distDir)) {
539
682
  throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
540
683
  code: "INVALID_PROJECT",
541
684
  exitCode: EXIT_INVALID_PROJECT
542
685
  });
543
686
  }
544
687
  const outDir = resolveLxpackOutDir(project);
545
- const { output, dir, outputBaseDir } = resolvePackageOutput(project, target, opts.out);
688
+ const { output: resolvedOutput, dir, outputBaseDir } = resolvePackageOutput(
689
+ project,
690
+ target,
691
+ opts.out
692
+ );
693
+ const trimmedOut = opts.out?.trim();
694
+ const output = trimmedOut && !isAbsolute(trimmedOut) ? trimmedOut : resolvedOutput;
546
695
  const result = await packageLessonkitCourse({
547
696
  descriptor: project.course,
548
697
  outDir,
@@ -551,7 +700,9 @@ async function runPackage(opts) {
551
700
  target,
552
701
  output,
553
702
  dir,
554
- outputBaseDir
703
+ outputBaseDir,
704
+ strictParity: opts.strictParity,
705
+ strictBuild: opts.strict
555
706
  });
556
707
  if (!result.ok) {
557
708
  throw new CliError("Packaging failed.", {
@@ -583,6 +734,112 @@ async function runPackage(opts) {
583
734
  };
584
735
  }
585
736
 
737
+ // src/commands/export.ts
738
+ import { existsSync as existsSync5 } from "fs";
739
+ import { relative, resolve as resolve4 } from "path";
740
+ import { exportLkcourse, resolveSafePackageOutputOverride as resolveSafePackageOutputOverride2 } from "@lessonkit/lxpack";
741
+ function resolveExportOutput(projectRoot, override, defaultName) {
742
+ if (override) {
743
+ try {
744
+ return resolveSafePackageOutputOverride2(projectRoot, override);
745
+ } catch (err) {
746
+ const message = err instanceof Error ? err.message : String(err);
747
+ throw new CliError(message, { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT });
748
+ }
749
+ }
750
+ return resolve4(projectRoot, `${defaultName ?? "course"}.lkcourse`);
751
+ }
752
+ async function runExport(opts) {
753
+ const project = await loadProject(opts.cwd ?? process.cwd());
754
+ const distDir = resolve4(project.root, project.paths.spaDistDir);
755
+ if (opts.noBuild && !existsSync5(distDir)) {
756
+ throw new CliError(
757
+ `dist directory not found at ${distDir}. Run lessonkit build before export with --no-build.`,
758
+ {
759
+ code: "INVALID_PROJECT",
760
+ exitCode: EXIT_INVALID_PROJECT
761
+ }
762
+ );
763
+ }
764
+ if (!opts.noBuild) {
765
+ await runBuild({ cwd: project.root, json: opts.json });
766
+ }
767
+ if (!existsSync5(distDir)) {
768
+ throw new CliError(`dist directory not found at ${distDir}. Run lessonkit build first.`, {
769
+ code: "INVALID_PROJECT",
770
+ exitCode: EXIT_INVALID_PROJECT
771
+ });
772
+ }
773
+ const resolvedOut = resolveExportOutput(project.root, opts.out, project.name);
774
+ const outRelative = relative(project.root, resolvedOut).replace(/\\/g, "/");
775
+ const result = await exportLkcourse({
776
+ projectRoot: project.root,
777
+ manifest: project,
778
+ outPath: outRelative,
779
+ includeBlockTree: Boolean(opts.withBlockTree)
780
+ });
781
+ if (!result.ok) {
782
+ throw new CliError(
783
+ result.issues.map((i) => `${i.path}: ${i.message}`).join("; "),
784
+ { code: "EXPORT_FAILED", exitCode: EXIT_PACKAGING }
785
+ );
786
+ }
787
+ return {
788
+ ok: true,
789
+ command: "export",
790
+ projectRoot: project.root,
791
+ archivePath: result.archivePath,
792
+ fileCount: result.fileCount,
793
+ includeBlockTree: result.includeBlockTree
794
+ };
795
+ }
796
+
797
+ // src/commands/blocks.ts
798
+ import { readFileSync as readFileSync2 } from "fs";
799
+ import { createRequire as createRequire2 } from "module";
800
+ function loadBlockCatalog() {
801
+ const require3 = createRequire2(import.meta.url);
802
+ const catalogPath = require3.resolve("@lessonkit/react/block-catalog.v3.json");
803
+ return JSON.parse(readFileSync2(catalogPath, "utf8"));
804
+ }
805
+ function filterEntries(entries, opts) {
806
+ return entries.filter((entry) => {
807
+ if (opts.category && entry.category !== opts.category) return false;
808
+ if (opts.tier && entry.tier !== opts.tier) return false;
809
+ return true;
810
+ });
811
+ }
812
+ async function runBlocksList(opts) {
813
+ const catalog = loadBlockCatalog();
814
+ const entries = filterEntries(catalog.entries, opts);
815
+ if (!opts.json) {
816
+ const lines = [
817
+ "type category h5pMachineName",
818
+ ...entries.map(
819
+ (entry) => [
820
+ entry.type,
821
+ entry.category ?? "\u2014",
822
+ entry.h5pMachineName ?? "\u2014"
823
+ ].join(" ")
824
+ )
825
+ ];
826
+ return {
827
+ ok: true,
828
+ command: "blocks list",
829
+ schemaVersion: catalog.schemaVersion,
830
+ count: entries.length,
831
+ text: lines.join("\n")
832
+ };
833
+ }
834
+ return {
835
+ ok: true,
836
+ command: "blocks list",
837
+ schemaVersion: catalog.schemaVersion,
838
+ count: entries.length,
839
+ entries
840
+ };
841
+ }
842
+
586
843
  // src/lib/logger.ts
587
844
  function createLogger(opts) {
588
845
  if (opts?.json) {
@@ -597,7 +854,7 @@ function createLogger(opts) {
597
854
  }
598
855
 
599
856
  // src/index.ts
600
- var require2 = createRequire2(import.meta.url);
857
+ var require2 = createRequire3(import.meta.url);
601
858
  var { version } = require2("../package.json");
602
859
  async function handleCommand(fn, logger, json) {
603
860
  try {
@@ -654,7 +911,7 @@ function createProgram(baseLogger = console) {
654
911
  );
655
912
  }
656
913
  );
657
- 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) => {
914
+ 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("--strict-parity", "Treat React ID parity warnings as packaging errors").option("--strict", "Treat Vite build warnings as packaging failures").option("--json", "Emit structured JSON result").action(async (opts) => {
658
915
  const logger = createLogger({ json: opts.json });
659
916
  await handleCommand(
660
917
  async () => {
@@ -663,7 +920,9 @@ function createProgram(baseLogger = console) {
663
920
  cwd: opts.cwd,
664
921
  noBuild: opts.build === false,
665
922
  out: opts.out,
666
- json: opts.json
923
+ json: opts.json,
924
+ strictParity: opts.strictParity,
925
+ strict: opts.strict
667
926
  });
668
927
  if (!opts.json && result.ok && result.command === "package") {
669
928
  if (result.target === "react-vite") {
@@ -681,8 +940,50 @@ function createProgram(baseLogger = console) {
681
940
  Boolean(opts.json)
682
941
  );
683
942
  });
684
- program.command("publish").description("Publish package artifacts (stub)").action(() => {
685
- baseLogger.log("lessonkit publish is not implemented. See RELEASING.md for npm publish workflow.");
943
+ addCwdAndJson(
944
+ program.command("export").description("Export a portable .lkcourse archive (manifest + interchange + dist)").option("--out <path>", "Output .lkcourse path (relative to project root)").option("--no-build", "Skip implicit Vite build").option("--with-block-tree", "Include optional block-tree.json from src scan")
945
+ ).action(async (opts) => {
946
+ const logger = createLogger({ json: opts.json });
947
+ await handleCommand(
948
+ async () => {
949
+ const result = await runExport({
950
+ cwd: opts.cwd,
951
+ out: opts.out,
952
+ noBuild: opts.build === false,
953
+ withBlockTree: opts.withBlockTree,
954
+ json: opts.json
955
+ });
956
+ if (!opts.json && result.ok && result.command === "export") {
957
+ logger.log(`Exported .lkcourse \u2192 ${result.archivePath} (${result.fileCount} files)`);
958
+ }
959
+ return result;
960
+ },
961
+ logger,
962
+ Boolean(opts.json)
963
+ );
964
+ });
965
+ program.command("blocks").description("Block registry commands").command("list").description("List runtime blocks from block-catalog.v3.json").option("--json", "Emit structured JSON result").option("--category <category>", "Filter by category (container, assessment, content, compound)").option("--tier <tier>", "Filter by tier (A, B, C, D, E)").action(async (opts) => {
966
+ const logger = createLogger({ json: opts.json });
967
+ await handleCommand(
968
+ async () => {
969
+ const result = await runBlocksList({
970
+ json: opts.json,
971
+ category: opts.category,
972
+ tier: opts.tier
973
+ });
974
+ if (!opts.json && result.ok && "text" in result && typeof result.text === "string") {
975
+ logger.log(result.text);
976
+ }
977
+ return result;
978
+ },
979
+ logger,
980
+ Boolean(opts.json)
981
+ );
982
+ });
983
+ program.command("publish").description("[maintainers] Not implemented \u2014 use Changesets (see RELEASING.md)").action(() => {
984
+ baseLogger.log(
985
+ "lessonkit publish is not implemented. Monorepo releases use Changesets: npm run changeset && npm run version-packages && npm run release. See RELEASING.md."
986
+ );
686
987
  });
687
988
  return program;
688
989
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/cli",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "description": "LessonKit CLI — init, dev, build, and package learning experiences.",
6
6
  "license": "Apache-2.0",
@@ -42,11 +42,12 @@
42
42
  "typecheck": "tsc -p tsconfig.json",
43
43
  "test": "vitest run --passWithNoTests",
44
44
  "test:coverage": "vitest run --coverage --passWithNoTests=false",
45
- "lint": "echo \"(no lint configured yet)\""
45
+ "lint": "eslint --max-warnings 0 \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\""
46
46
  },
47
47
  "dependencies": {
48
- "@lessonkit/core": "1.4.0",
49
- "@lessonkit/lxpack": "1.4.0",
48
+ "@lessonkit/core": "1.6.0",
49
+ "@lessonkit/lxpack": "1.6.0",
50
+ "@lessonkit/react": "1.6.0",
50
51
  "commander": "^15.0.0"
51
52
  },
52
53
  "engines": {
@@ -55,7 +56,7 @@
55
56
  "devDependencies": {
56
57
  "@types/node": "^25.9.2",
57
58
  "tsup": "^8.5.0",
58
- "typescript": "^5.8.3",
59
+ "typescript": "^6.0.3",
59
60
  "vitest": "^4.1.8"
60
61
  }
61
62
  }
@@ -1,5 +1,8 @@
1
- # Copy to .env.production before `npm run build`.
2
- # Point both URLs at your backend proxies never embed raw LRS credentials in the bundle.
1
+ # Copy to `.env` for production LMS export (see courseConfig.ts).
2
+ # Never commit real secretsuse short-lived tokens from your backend.
3
3
 
4
- VITE_XAPI_PROXY_URL=
4
+ # Analytics batch ingest proxy (createFetchBatchSink)
5
5
  VITE_ANALYTICS_URL=
6
+
7
+ # xAPI statement proxy (createFetchTransport)
8
+ VITE_XAPI_PROXY_URL=
@@ -18,10 +18,35 @@ npm run package:scorm12
18
18
  - `.env.example` — `VITE_XAPI_PROXY_URL` and `VITE_ANALYTICS_URL` for production builds
19
19
  - `lessonkit.json` — manifest for CLI and LXPack packaging
20
20
 
21
+ ## Before LMS packaging
22
+
23
+ 1. **LMS bridge** — In `src/courseConfig.ts`, enable the bridge for SCORM/xAPI/cmi5 export (template defaults to `"off"` for local preview):
24
+
25
+ ```ts
26
+ lxpack: {
27
+ bridge: "auto",
28
+ allowedParentOrigins: ["https://your-lms.example"], // required in production builds
29
+ },
30
+ ```
31
+
32
+ Development builds allow `bridge: "auto"` without an allowlist; **production builds do not**. Discover your LMS origin from the SCORM preview URL or browser devtools (`document.referrer`).
33
+
34
+ 2. **Production runtime** — Copy `.env.example` to `.env`, set `VITE_ANALYTICS_URL` and `VITE_XAPI_PROXY_URL`, and wire the observability hooks in `courseConfig.ts` (see comments there). Rebuild with `npm run build` before packaging. Alternatively, disable `tracking` and `xapi` for a first test export only.
35
+
36
+ 3. **Activity IRI** — Replace the `example.com` placeholder in `lessonkit.json` → `course.tracking.xapi.activityIri` before xAPI/cmi5 export (must be HTTPS).
37
+
38
+ ## SCORM output path
39
+
40
+ After `npm run package:scorm12`, the CLI prints the resolved ZIP path. Default:
41
+
42
+ **`.lxpack/course/.lxpack/out/course-scorm12.zip`**
43
+
44
+ (`paths.outputBaseDir` is resolved inside `paths.lxpackOutDir`, not at the project root.)
45
+
21
46
  ## Production
22
47
 
23
- Copy `.env.example` to `.env.production` and set your LRS/analytics proxy URLs before `npm run build`. See the [production checklist](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html).
48
+ Copy `.env.example` to `.env` and set your LRS/analytics proxy URLs before `npm run build`. See the [production checklist](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html) and [Ship to LMS checklist](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/ship-to-lms.html).
24
49
 
25
50
  ## Docs
26
51
 
27
- [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)
52
+ [5-minute guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/getting-started-in-5-minutes.html) · [First LMS export](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/first-lms-export.html) · [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)
@@ -16,23 +16,23 @@
16
16
  "test:coverage": "vitest run --coverage --passWithNoTests=false"
17
17
  },
18
18
  "dependencies": {
19
- "@lessonkit/core": "^1.4.0",
20
- "@lessonkit/react": "^1.4.0",
21
- "@lessonkit/themes": "^1.4.0",
22
- "@lessonkit/xapi": "^1.4.0",
19
+ "@lessonkit/core": "^1.6.0",
20
+ "@lessonkit/react": "^1.6.0",
21
+ "@lessonkit/themes": "^1.6.0",
22
+ "@lessonkit/xapi": "^1.6.0",
23
23
  "react": "^19.2.7",
24
24
  "react-dom": "^19.2.7"
25
25
  },
26
26
  "devDependencies": {
27
- "@lessonkit/cli": "^1.4.0",
28
- "@lessonkit/lxpack": "^1.4.0",
27
+ "@lessonkit/cli": "^1.6.0",
28
+ "@lessonkit/lxpack": "^1.6.0",
29
29
  "@testing-library/react": "^16.3.0",
30
30
  "@testing-library/dom": "^10.4.1",
31
31
  "@types/react": "^19.2.17",
32
32
  "@types/react-dom": "^19.2.3",
33
33
  "@vitejs/plugin-react": "^6.0.2",
34
34
  "jsdom": "^29.1.1",
35
- "typescript": "^5.8.3",
35
+ "typescript": "^6.0.3",
36
36
  "vite": "^8.0.11",
37
37
  "vitest": "^4.1.8"
38
38
  }