@lessonkit/lxpack 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.cjs CHANGED
@@ -33,21 +33,28 @@ __export(index_exports, {
33
33
  LESSONKIT_TELEMETRY_EVENTS: () => import_tracking_schema2.LESSONKIT_TELEMETRY_EVENTS,
34
34
  assessmentDescriptorToLxpack: () => assessmentDescriptorToLxpack,
35
35
  buildLessonkitProject: () => buildLessonkitProject,
36
+ buildStagingPackage: () => buildStagingPackage,
36
37
  descriptorToInterchange: () => descriptorToInterchange,
38
+ ensureOutDirParent: () => ensureOutDirParent,
37
39
  extractAssessments: () => extractAssessments,
38
40
  lessonkitInterchangeSchema: () => import_validators2.lessonkitInterchangeSchema,
41
+ loadLessonkitManifestFromFile: () => loadLessonkitManifestFromFile,
39
42
  mapLessonkitIds: () => mapLessonkitIds,
40
43
  mapLessonkitTelemetryToBridgeAction: () => import_tracking_schema2.mapLessonkitTelemetryToBridgeAction,
41
44
  mapLessonkitTelemetryToLxpack: () => import_tracking_schema2.mapLessonkitTelemetryToLxpack,
42
45
  materializeLessonkitProject: () => import_validators2.materializeLessonkitProject,
43
46
  packageLessonkitCourse: () => packageLessonkitCourse,
44
47
  parseLessonkitInterchange: () => import_validators2.parseLessonkitInterchange,
48
+ parseLessonkitManifest: () => parseLessonkitManifest,
49
+ promoteStagingToOutDir: () => promoteStagingToOutDir,
50
+ remapArtifactPaths: () => remapArtifactPaths,
45
51
  resolveSafePackageOutputOverride: () => resolveSafePackageOutputOverride,
46
52
  resolveSpaLessons: () => resolveSpaLessons,
47
53
  telemetryEventToLessonkit: () => telemetryEventToLessonkit,
48
54
  themeToLxpackRuntime: () => themeToLxpackRuntime,
49
55
  validateDescriptor: () => validateDescriptor,
50
56
  validateLessonkitProject: () => validateLessonkitProject,
57
+ validatePackageInputs: () => validatePackageInputs,
51
58
  validateProjectPaths: () => validateProjectPaths,
52
59
  writeLxpackProject: () => writeLxpackProject
53
60
  });
@@ -57,7 +64,14 @@ module.exports = __toCommonJS(index_exports);
57
64
  var import_core = require("@lessonkit/core");
58
65
 
59
66
  // src/spaPath.ts
67
+ var import_node_fs = require("fs");
60
68
  var import_node_path = require("path");
69
+ function resolveComparablePath(p) {
70
+ if (/^[a-zA-Z]:[/\\]/.test(p)) {
71
+ return import_node_path.win32.resolve(p);
72
+ }
73
+ return (0, import_node_path.resolve)(p);
74
+ }
61
75
  function isSafeRelativeSpaPath(spaPath) {
62
76
  if (!spaPath.length || spaPath.includes("\0")) return false;
63
77
  if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
@@ -67,13 +81,43 @@ function isSafeRelativeSpaPath(spaPath) {
67
81
  return true;
68
82
  }
69
83
  function assertResolvedPathUnderRoot(root, target) {
70
- const rootResolved = (0, import_node_path.resolve)(root);
71
- const targetResolved = (0, import_node_path.resolve)(target);
84
+ const rootResolved = resolveComparablePath(root);
85
+ const targetResolved = resolveComparablePath(target);
72
86
  const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
73
- if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix)) {
87
+ const win32Prefix = rootResolved.endsWith(import_node_path.win32.sep) ? rootResolved : rootResolved + import_node_path.win32.sep;
88
+ if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && !targetResolved.startsWith(win32Prefix)) {
74
89
  throw new Error(`unsafe path escapes project root: ${target}`);
75
90
  }
76
91
  }
92
+ function assertRealPathUnderRoot(root, target) {
93
+ const rootResolved = resolveComparablePath(root);
94
+ const targetResolved = resolveComparablePath(target);
95
+ let rootReal;
96
+ try {
97
+ rootReal = (0, import_node_fs.realpathSync)(rootResolved);
98
+ } catch {
99
+ rootReal = rootResolved;
100
+ }
101
+ let targetCheck;
102
+ try {
103
+ targetCheck = (0, import_node_fs.realpathSync)(targetResolved);
104
+ } catch {
105
+ const rel = (0, import_node_path.relative)(rootResolved, targetResolved);
106
+ if (rel.startsWith("..") || rel.includes(`..${import_node_path.sep}`)) {
107
+ throw new Error(`unsafe path escapes project root: ${target}`);
108
+ }
109
+ targetCheck = (0, import_node_path.resolve)(rootReal, rel);
110
+ }
111
+ assertResolvedPathUnderRoot(rootReal, targetCheck);
112
+ }
113
+ function isResolvedPathUnderRoot(root, target) {
114
+ const rootResolved = resolveComparablePath(root);
115
+ const targetResolved = resolveComparablePath(target);
116
+ if (targetResolved === rootResolved) return true;
117
+ const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
118
+ const win32Prefix = rootResolved.endsWith(import_node_path.win32.sep) ? rootResolved : rootResolved + import_node_path.win32.sep;
119
+ return targetResolved.startsWith(prefix) || targetResolved.startsWith(win32Prefix);
120
+ }
77
121
 
78
122
  // src/theme.ts
79
123
  var import_themes = require("@lessonkit/themes");
@@ -249,7 +293,7 @@ function validateDescriptor(input) {
249
293
  issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
250
294
  }
251
295
  const passingScore = assessment.passingScore;
252
- if (passingScore !== void 0 && !(passingScore > 0)) {
296
+ if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
253
297
  issues.push({
254
298
  path: `${path}.passingScore`,
255
299
  message: "passingScore must be greater than 0 (absolute point threshold)"
@@ -415,13 +459,18 @@ async function resolveSpaDirs(options) {
415
459
  const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
416
460
  const srcDist = projectRoot ? (0, import_node_path3.resolve)(projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
417
461
  if (projectRoot) {
418
- assertResolvedPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
462
+ assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
419
463
  }
420
464
  try {
421
465
  await (0, import_promises.access)(srcDist);
422
466
  } catch {
423
467
  throw new Error(`spaDistDir not found: ${srcDist}`);
424
468
  }
469
+ try {
470
+ await (0, import_promises.access)((0, import_node_path3.join)(srcDist, "index.html"));
471
+ } catch {
472
+ throw new Error(`spaDistDir must contain index.html: ${(0, import_node_path3.join)(srcDist, "index.html")}`);
473
+ }
425
474
  const lessonId = spaLessons[0]?.id ?? "main";
426
475
  return { [lessonId]: srcDist };
427
476
  }
@@ -434,7 +483,19 @@ async function resolveSpaDirs(options) {
434
483
  }
435
484
  const resolved = projectRoot ? (0, import_node_path3.resolve)(projectRoot, src) : (0, import_node_path3.resolve)(src);
436
485
  if (projectRoot) {
437
- assertResolvedPathUnderRoot((0, import_node_path3.resolve)(projectRoot), resolved);
486
+ assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), resolved);
487
+ }
488
+ try {
489
+ await (0, import_promises.access)(resolved);
490
+ } catch {
491
+ throw new Error(`lessonSpaDirs path not found for lesson "${lesson.id}": ${resolved}`);
492
+ }
493
+ try {
494
+ await (0, import_promises.access)((0, import_node_path3.join)(resolved, "index.html"));
495
+ } catch {
496
+ throw new Error(
497
+ `lessonSpaDirs must contain index.html for lesson "${lesson.id}": ${(0, import_node_path3.join)(resolved, "index.html")}`
498
+ );
438
499
  }
439
500
  dirs[lesson.id] = resolved;
440
501
  }
@@ -476,26 +537,105 @@ async function writeLxpackProject(options) {
476
537
  }
477
538
 
478
539
  // src/packageCourse.ts
479
- var fsp = __toESM(require("fs/promises"), 1);
540
+ var import_node_path7 = require("path");
541
+ var fsp3 = __toESM(require("fs/promises"), 1);
542
+ var import_api2 = require("@lxpack/api");
543
+
544
+ // src/packaging/validateInputs.ts
480
545
  var import_node_path5 = require("path");
481
- var import_node_os = require("os");
482
- var import_api = require("@lxpack/api");
483
- async function validateLessonkitProject(options) {
484
- return (0, import_api.validateCourse)({
485
- courseDir: (0, import_node_path5.resolve)(options.courseDir),
486
- target: options.target
487
- });
546
+ function validatePackageInputs(options) {
547
+ const { target, output, outputBaseDir } = options;
548
+ const outDir = (0, import_node_path5.resolve)(options.outDir);
549
+ const projectRoot = options.projectRoot ? (0, import_node_path5.resolve)(options.projectRoot) : void 0;
550
+ if (projectRoot) {
551
+ try {
552
+ assertResolvedPathUnderRoot(projectRoot, outDir);
553
+ } catch (err) {
554
+ return {
555
+ ok: false,
556
+ courseDir: outDir,
557
+ target,
558
+ issues: [{ path: "outDir", message: err instanceof Error ? err.message : String(err) }]
559
+ };
560
+ }
561
+ }
562
+ if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
563
+ return {
564
+ ok: false,
565
+ courseDir: outDir,
566
+ target,
567
+ issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
568
+ };
569
+ }
570
+ if (output && !projectRoot && !isSafeRelativeSpaPath(output)) {
571
+ return {
572
+ ok: false,
573
+ courseDir: outDir,
574
+ target,
575
+ issues: [{ path: "output", message: `unsafe output: ${output}` }]
576
+ };
577
+ }
578
+ if (projectRoot && outputBaseDir) {
579
+ const resolvedOutputBase = (0, import_node_path5.resolve)(projectRoot, outputBaseDir);
580
+ try {
581
+ assertRealPathUnderRoot(projectRoot, resolvedOutputBase);
582
+ } catch (err) {
583
+ return {
584
+ ok: false,
585
+ courseDir: outDir,
586
+ target,
587
+ issues: [
588
+ {
589
+ path: "outputBaseDir",
590
+ message: err instanceof Error ? err.message : String(err)
591
+ }
592
+ ]
593
+ };
594
+ }
595
+ }
596
+ if (projectRoot && output) {
597
+ const resolvedOutput = (0, import_node_path5.resolve)(projectRoot, output);
598
+ try {
599
+ assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
600
+ } catch (err) {
601
+ return {
602
+ ok: false,
603
+ courseDir: outDir,
604
+ target,
605
+ issues: [{ path: "output", message: err instanceof Error ? err.message : String(err) }]
606
+ };
607
+ }
608
+ }
609
+ return { ok: true, outDir, projectRoot };
488
610
  }
489
- async function buildLessonkitProject(options) {
490
- return (0, import_api.buildCourse)({
491
- courseDir: (0, import_node_path5.resolve)(options.courseDir),
492
- target: options.target,
493
- output: options.output,
494
- dir: options.dir,
495
- outputBaseDir: options.outputBaseDir,
496
- assessments: options.assessments
497
- });
611
+ function validateArtifactInStaging(stagingRoot, artifactPath, field) {
612
+ if (!artifactPath) return null;
613
+ const resolved = resolveComparablePath(artifactPath);
614
+ if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
615
+ return {
616
+ path: field,
617
+ message: `${field} is outside the staging directory: ${artifactPath}`
618
+ };
619
+ }
620
+ return null;
498
621
  }
622
+ function remapArtifactPaths(stagingRoot, outDir, artifactPath) {
623
+ if (!artifactPath) return void 0;
624
+ const resolved = resolveComparablePath(artifactPath);
625
+ if (!isResolvedPathUnderRoot(stagingRoot, resolved)) {
626
+ return artifactPath;
627
+ }
628
+ const stagingResolved = resolveComparablePath(stagingRoot);
629
+ const relative2 = resolved === stagingResolved ? "" : resolved.slice(stagingResolved.length).replace(/^[/\\]/, "");
630
+ if (!relative2) return outDir;
631
+ if (/^[a-zA-Z]:[/\\]/.test(outDir)) {
632
+ return import_node_path5.win32.join(outDir, relative2.replace(/\//g, import_node_path5.win32.sep));
633
+ }
634
+ return (0, import_node_path5.join)(outDir, relative2);
635
+ }
636
+
637
+ // src/packaging/promote.ts
638
+ var fsp = __toESM(require("fs/promises"), 1);
499
639
  async function pathExists(path) {
500
640
  try {
501
641
  await fsp.access(path);
@@ -504,82 +644,76 @@ async function pathExists(path) {
504
644
  return false;
505
645
  }
506
646
  }
647
+ async function renameOrCopy(from, to) {
648
+ try {
649
+ await fsp.rename(from, to);
650
+ } catch (err) {
651
+ const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
652
+ if (code !== "EXDEV") throw err;
653
+ await fsp.cp(from, to, { recursive: true });
654
+ await fsp.rm(from, { recursive: true, force: true });
655
+ }
656
+ }
507
657
  async function promoteStagingToOutDir(stagingDir, outDir) {
508
658
  const tmpPromote = `${outDir}.tmp-promote`;
509
659
  const backup = `${outDir}.bak`;
510
- await fsp.rename(stagingDir, tmpPromote);
660
+ await renameOrCopy(stagingDir, tmpPromote);
511
661
  const hadOutDir = await pathExists(outDir);
512
662
  if (hadOutDir) {
513
- await fsp.rename(outDir, backup);
663
+ await renameOrCopy(outDir, backup);
514
664
  }
515
665
  try {
516
- await fsp.rename(tmpPromote, outDir);
666
+ await renameOrCopy(tmpPromote, outDir);
517
667
  } catch (promoteError) {
518
668
  if (hadOutDir) {
519
669
  try {
520
- await fsp.rename(backup, outDir);
670
+ await renameOrCopy(backup, outDir);
671
+ } catch (restoreError) {
672
+ const failedPromote2 = `${outDir}.failed-promote-${Date.now()}`;
673
+ try {
674
+ await renameOrCopy(tmpPromote, failedPromote2);
675
+ } catch {
676
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
677
+ }
678
+ const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
679
+ const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
680
+ throw new Error(
681
+ `[lessonkit/lxpack] promote failed (${promoteMsg}) and could not restore ${outDir} (${restoreMsg}). Recovery: previous output may be in ${backup}; staged package may be in ${failedPromote2}.`
682
+ );
683
+ }
684
+ } else {
685
+ try {
686
+ await renameOrCopy(tmpPromote, stagingDir);
521
687
  } catch (restoreError) {
522
688
  console.warn(
523
- `[lessonkit/lxpack] failed to restore ${outDir} after promote error:`,
689
+ `[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
524
690
  restoreError instanceof Error ? restoreError.message : restoreError
525
691
  );
692
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
526
693
  }
694
+ throw promoteError;
695
+ }
696
+ const failedPromote = `${outDir}.failed-promote-${Date.now()}`;
697
+ try {
698
+ await renameOrCopy(tmpPromote, failedPromote);
699
+ } catch {
700
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
527
701
  }
528
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
529
702
  throw promoteError;
530
703
  }
531
704
  if (hadOutDir) {
532
705
  await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
533
706
  }
534
707
  }
535
- async function packageLessonkitCourse(options) {
536
- const { target, output, dir, outputBaseDir, ...writeOpts } = options;
537
- const outDir = (0, import_node_path5.resolve)(writeOpts.outDir);
538
- const projectRoot = writeOpts.projectRoot ? (0, import_node_path5.resolve)(writeOpts.projectRoot) : void 0;
539
- if (projectRoot) {
540
- assertResolvedPathUnderRoot(projectRoot, outDir);
541
- }
542
- if (outputBaseDir && !isSafeRelativeSpaPath(outputBaseDir)) {
543
- return {
544
- ok: false,
545
- courseDir: outDir,
546
- target,
547
- issues: [{ path: "outputBaseDir", message: `unsafe outputBaseDir: ${outputBaseDir}` }]
548
- };
549
- }
550
- if (projectRoot && output) {
551
- const resolvedOutput = (0, import_node_path5.resolve)(projectRoot, output);
552
- try {
553
- assertResolvedPathUnderRoot(projectRoot, resolvedOutput);
554
- } catch (err) {
555
- return {
556
- ok: false,
557
- courseDir: outDir,
558
- target,
559
- issues: [
560
- {
561
- path: "output",
562
- message: err instanceof Error ? err.message : String(err)
563
- }
564
- ]
565
- };
566
- }
567
- }
568
- const descriptorValidation = validateDescriptor(writeOpts.descriptor);
569
- if (!descriptorValidation.ok) {
570
- return {
571
- ok: false,
572
- courseDir: outDir,
573
- target,
574
- issues: descriptorValidation.issues.map((i) => ({
575
- path: i.path,
576
- message: i.message
577
- }))
578
- };
579
- }
580
- const descriptor = descriptorValidation.descriptor;
581
- const stagingDir = await fsp.mkdtemp((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
582
- let promoted = false;
708
+
709
+ // src/packaging/staging.ts
710
+ var fsp2 = __toESM(require("fs/promises"), 1);
711
+ var import_node_path6 = require("path");
712
+ var import_node_os = require("os");
713
+ var import_api = require("@lxpack/api");
714
+ async function buildStagingPackage(options) {
715
+ const { target, output, dir, outputBaseDir, descriptor, ...writeOpts } = options;
716
+ const stagingDir = await fsp2.mkdtemp((0, import_node_path6.join)((0, import_node_os.tmpdir)(), "lessonkit-lxpack-"));
583
717
  try {
584
718
  let spaDirs;
585
719
  try {
@@ -587,8 +721,7 @@ async function packageLessonkitCourse(options) {
587
721
  } catch (err) {
588
722
  return {
589
723
  ok: false,
590
- courseDir: outDir,
591
- target,
724
+ stagingDir,
592
725
  issues: [
593
726
  {
594
727
  path: "spaDirs",
@@ -599,8 +732,8 @@ async function packageLessonkitCourse(options) {
599
732
  }
600
733
  const interchange = descriptorToInterchange(descriptor);
601
734
  const outputBase = outputBaseDir ?? ".lxpack/out";
602
- await fsp.mkdir((0, import_node_path5.join)(stagingDir, outputBase), { recursive: true });
603
- const defaultOutput = output ?? (dir ? (0, import_node_path5.join)(outputBase, target) : (0, import_node_path5.join)(outputBase, `course-${target}.zip`));
735
+ await fsp2.mkdir((0, import_node_path6.join)(stagingDir, outputBase), { recursive: true });
736
+ const defaultOutput = output ?? (dir ? (0, import_node_path6.join)(outputBase, target) : (0, import_node_path6.join)(outputBase, `course-${target}.zip`));
604
737
  const build = await (0, import_api.packageLessonkit)({
605
738
  interchange,
606
739
  spaDirs,
@@ -613,15 +746,9 @@ async function packageLessonkitCourse(options) {
613
746
  writeAuthoringFiles: true
614
747
  });
615
748
  if (!build.ok) {
616
- const validation2 = {
617
- ok: false,
618
- issues: build.issues
619
- };
620
749
  return {
621
750
  ok: false,
622
- courseDir: outDir,
623
- target,
624
- validation: validation2,
751
+ stagingDir,
625
752
  build,
626
753
  issues: build.issues.map((i) => ({
627
754
  path: i.path,
@@ -630,48 +757,266 @@ async function packageLessonkitCourse(options) {
630
757
  }))
631
758
  };
632
759
  }
633
- const validation = {
760
+ return {
634
761
  ok: true,
635
- manifest: build.manifest,
636
- issues: build.issues
762
+ stagingDir,
763
+ build,
764
+ outputPath: "outputPath" in build ? build.outputPath : void 0,
765
+ outputDir: "outputDir" in build ? build.outputDir : void 0
637
766
  };
638
- const stagingRoot = await fsp.realpath(stagingDir);
639
- const remapArtifactPath = (artifactPath) => {
640
- if (!artifactPath) return void 0;
641
- const resolved = (0, import_node_path5.resolve)(artifactPath);
642
- if (resolved === stagingRoot || resolved.startsWith(`${stagingRoot}/`)) {
643
- return (0, import_node_path5.join)(outDir, resolved.slice(stagingRoot.length + 1));
644
- }
645
- return artifactPath;
767
+ } catch (err) {
768
+ await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
769
+ throw err;
770
+ }
771
+ }
772
+ async function ensureOutDirParent(outDir) {
773
+ await fsp2.mkdir((0, import_node_path6.dirname)(outDir), { recursive: true });
774
+ }
775
+
776
+ // src/packageCourse.ts
777
+ async function validateLessonkitProject(options) {
778
+ return (0, import_api2.validateCourse)({
779
+ courseDir: (0, import_node_path7.resolve)(options.courseDir),
780
+ target: options.target
781
+ });
782
+ }
783
+ async function buildLessonkitProject(options) {
784
+ return (0, import_api2.buildCourse)({
785
+ courseDir: (0, import_node_path7.resolve)(options.courseDir),
786
+ target: options.target,
787
+ output: options.output,
788
+ dir: options.dir,
789
+ outputBaseDir: options.outputBaseDir,
790
+ assessments: options.assessments
791
+ });
792
+ }
793
+ async function packageLessonkitCourse(options) {
794
+ const { target, output, dir, outputBaseDir, ...writeOpts } = options;
795
+ const inputValidation = validatePackageInputs({
796
+ target,
797
+ output,
798
+ outputBaseDir,
799
+ outDir: writeOpts.outDir,
800
+ projectRoot: writeOpts.projectRoot
801
+ });
802
+ if (!inputValidation.ok) {
803
+ return {
804
+ ok: false,
805
+ courseDir: inputValidation.courseDir,
806
+ target: inputValidation.target,
807
+ issues: inputValidation.issues
646
808
  };
647
- const remappedOutputPath = remapArtifactPath(
648
- "outputPath" in build ? build.outputPath : void 0
649
- );
650
- const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
651
- await fsp.mkdir((0, import_node_path5.dirname)(outDir), { recursive: true });
652
- await promoteStagingToOutDir(stagingDir, outDir);
653
- promoted = true;
654
- const remappedBuild = { ...build };
655
- if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
656
- remappedBuild.outputPath = remappedOutputPath;
657
- }
658
- if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
659
- remappedBuild.outputDir = remappedOutputDir;
809
+ }
810
+ const outDir = inputValidation.outDir;
811
+ const descriptorValidation = validateDescriptor(writeOpts.descriptor);
812
+ if (!descriptorValidation.ok) {
813
+ return {
814
+ ok: false,
815
+ courseDir: outDir,
816
+ target,
817
+ issues: descriptorValidation.issues.map((i) => ({
818
+ path: i.path,
819
+ message: i.message
820
+ }))
821
+ };
822
+ }
823
+ const descriptor = descriptorValidation.descriptor;
824
+ if (target === "xapi" || target === "cmi5") {
825
+ const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
826
+ if (!activityIri) {
827
+ return {
828
+ ok: false,
829
+ courseDir: outDir,
830
+ target,
831
+ issues: [
832
+ {
833
+ path: "course.tracking.xapi.activityIri",
834
+ message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
835
+ }
836
+ ]
837
+ };
660
838
  }
839
+ }
840
+ const staged = await buildStagingPackage({
841
+ ...writeOpts,
842
+ descriptor,
843
+ target,
844
+ output,
845
+ dir,
846
+ outputBaseDir
847
+ });
848
+ if (!staged.ok) {
849
+ await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(() => void 0);
850
+ const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
661
851
  return {
662
- ok: true,
852
+ ok: false,
853
+ courseDir: outDir,
854
+ target,
855
+ validation: validation2,
856
+ build: staged.build,
857
+ issues: staged.issues
858
+ };
859
+ }
860
+ const { stagingDir, build } = staged;
861
+ const stagingRoot = await fsp3.realpath(stagingDir);
862
+ const artifactIssues = [
863
+ validateArtifactInStaging(stagingRoot, staged.outputPath, "outputPath"),
864
+ validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
865
+ ].filter((issue) => issue != null);
866
+ if (artifactIssues.length > 0) {
867
+ await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
868
+ return {
869
+ ok: false,
870
+ courseDir: outDir,
871
+ target,
872
+ validation: { ok: true, manifest: build.manifest, issues: build.issues },
873
+ build,
874
+ issues: artifactIssues
875
+ };
876
+ }
877
+ const remappedOutputPath = remapArtifactPaths(stagingRoot, outDir, staged.outputPath);
878
+ const remappedOutputDir = remapArtifactPaths(stagingRoot, outDir, staged.outputDir);
879
+ const validation = {
880
+ ok: true,
881
+ manifest: build.manifest,
882
+ issues: build.issues
883
+ };
884
+ try {
885
+ await ensureOutDirParent(outDir);
886
+ await promoteStagingToOutDir(stagingDir, outDir);
887
+ } catch (err) {
888
+ return {
889
+ ok: false,
663
890
  courseDir: outDir,
664
891
  target,
665
- outputPath: remappedOutputPath,
666
- outputDir: remappedOutputDir,
667
- fileCount: build.fileCount,
668
892
  validation,
669
- build: remappedBuild
893
+ build,
894
+ issues: [
895
+ {
896
+ path: "promote",
897
+ message: err instanceof Error ? err.message : String(err)
898
+ }
899
+ ]
670
900
  };
671
- } finally {
672
- if (!promoted) {
673
- await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
901
+ }
902
+ const remappedBuild = { ...build };
903
+ if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
904
+ remappedBuild.outputPath = remappedOutputPath;
905
+ }
906
+ if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
907
+ remappedBuild.outputDir = remappedOutputDir;
908
+ }
909
+ return {
910
+ ok: true,
911
+ courseDir: outDir,
912
+ target,
913
+ outputPath: remappedOutputPath,
914
+ outputDir: remappedOutputDir,
915
+ fileCount: build.fileCount,
916
+ validation,
917
+ build: remappedBuild
918
+ };
919
+ }
920
+
921
+ // src/manifest.ts
922
+ var DEFAULT_PATHS = {
923
+ spaDistDir: "dist",
924
+ lxpackOutDir: ".lxpack/course",
925
+ outputBaseDir: ".lxpack/out"
926
+ };
927
+ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
928
+ if (!raw || typeof raw !== "object") {
929
+ return { ok: false, issues: [{ path: label, message: "must be a JSON object" }] };
930
+ }
931
+ const config = raw;
932
+ const issues = [];
933
+ if (config.schemaVersion !== 1) {
934
+ issues.push({
935
+ path: "schemaVersion",
936
+ message: `must be 1 (got ${String(config.schemaVersion)})`
937
+ });
938
+ }
939
+ const name = config.name;
940
+ if (typeof name !== "string" || !name.trim()) {
941
+ issues.push({ path: "name", message: "must be a non-empty string" });
942
+ }
943
+ const courseRaw = config.course;
944
+ if (Array.isArray(courseRaw)) {
945
+ issues.push({ path: "course", message: "must be an object, not an array" });
946
+ return { ok: false, issues };
947
+ }
948
+ if (!courseRaw || typeof courseRaw !== "object") {
949
+ issues.push({ path: "course", message: "must be an object" });
950
+ return { ok: false, issues };
951
+ }
952
+ const courseObj = courseRaw;
953
+ if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
954
+ issues.push({ path: "course.lessons", message: "must be an array" });
955
+ }
956
+ if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
957
+ issues.push({ path: "course.assessments", message: "must be an array" });
958
+ }
959
+ if (issues.length) return { ok: false, issues };
960
+ const validation = validateDescriptor(courseRaw);
961
+ if (!validation.ok) {
962
+ for (const i of validation.issues) {
963
+ issues.push({
964
+ path: i.path.startsWith("course.") ? i.path : `course.${i.path}`,
965
+ message: i.message
966
+ });
674
967
  }
968
+ } else if (validation.descriptor.layout === "per-lesson-spa") {
969
+ issues.push({
970
+ path: "course.layout",
971
+ message: "per-lesson-spa is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly."
972
+ });
973
+ }
974
+ const paths = { ...DEFAULT_PATHS };
975
+ const pathsRaw = config.paths;
976
+ if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
977
+ issues.push({ path: "paths", message: "must be an object" });
978
+ } else if (pathsRaw && typeof pathsRaw === "object") {
979
+ const p = pathsRaw;
980
+ for (const key of ["spaDistDir", "lxpackOutDir", "outputBaseDir"]) {
981
+ if (p[key] !== void 0) {
982
+ if (typeof p[key] !== "string" || !p[key].trim()) {
983
+ issues.push({ path: `paths.${key}`, message: "must be a non-empty string" });
984
+ } else {
985
+ paths[key] = p[key].trim();
986
+ }
987
+ }
988
+ }
989
+ }
990
+ const courseSpaDistDir = validation.ok ? validation.descriptor.spaDistDir?.trim() : void 0;
991
+ if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
992
+ issues.push({
993
+ path: "course.spaDistDir",
994
+ message: `"course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`
995
+ });
996
+ }
997
+ if (projectRoot) {
998
+ const pathIssues = validateProjectPaths(projectRoot, paths);
999
+ for (const pi of pathIssues) {
1000
+ issues.push({ path: pi.path, message: pi.message });
1001
+ }
1002
+ }
1003
+ if (issues.length) return { ok: false, issues };
1004
+ if (!validation.ok) return { ok: false, issues };
1005
+ return {
1006
+ ok: true,
1007
+ manifest: {
1008
+ schemaVersion: 1,
1009
+ name,
1010
+ course: validation.descriptor,
1011
+ paths
1012
+ }
1013
+ };
1014
+ }
1015
+ async function loadLessonkitManifestFromFile(readJson, label = "lessonkit.json", projectRoot) {
1016
+ try {
1017
+ return parseLessonkitManifest(await readJson(), label, projectRoot);
1018
+ } catch {
1019
+ return { ok: false, issues: [{ path: label, message: "failed to read or parse JSON" }] };
675
1020
  }
676
1021
  }
677
1022
 
@@ -714,21 +1059,28 @@ var import_validators2 = require("@lxpack/validators");
714
1059
  LESSONKIT_TELEMETRY_EVENTS,
715
1060
  assessmentDescriptorToLxpack,
716
1061
  buildLessonkitProject,
1062
+ buildStagingPackage,
717
1063
  descriptorToInterchange,
1064
+ ensureOutDirParent,
718
1065
  extractAssessments,
719
1066
  lessonkitInterchangeSchema,
1067
+ loadLessonkitManifestFromFile,
720
1068
  mapLessonkitIds,
721
1069
  mapLessonkitTelemetryToBridgeAction,
722
1070
  mapLessonkitTelemetryToLxpack,
723
1071
  materializeLessonkitProject,
724
1072
  packageLessonkitCourse,
725
1073
  parseLessonkitInterchange,
1074
+ parseLessonkitManifest,
1075
+ promoteStagingToOutDir,
1076
+ remapArtifactPaths,
726
1077
  resolveSafePackageOutputOverride,
727
1078
  resolveSpaLessons,
728
1079
  telemetryEventToLessonkit,
729
1080
  themeToLxpackRuntime,
730
1081
  validateDescriptor,
731
1082
  validateLessonkitProject,
1083
+ validatePackageInputs,
732
1084
  validateProjectPaths,
733
1085
  writeLxpackProject
734
1086
  });