@lessonkit/lxpack 0.8.0 → 0.9.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,3 +1,7 @@
1
+ import {
2
+ telemetryEventToLessonkit
3
+ } from "./chunk-PSUSESH3.js";
4
+
1
5
  // src/validateDescriptor.ts
2
6
  import { validateId } from "@lessonkit/core";
3
7
 
@@ -7,8 +11,8 @@ function isSafeRelativeSpaPath(spaPath) {
7
11
  if (!spaPath.length || spaPath.includes("\0")) return false;
8
12
  if (spaPath.startsWith("/") || spaPath.startsWith("\\")) return false;
9
13
  if (/^[a-zA-Z]:[/\\]/.test(spaPath)) return false;
10
- const segments = spaPath.split(/[/\\]/);
11
- if (segments.some((s) => s === "..")) return false;
14
+ const segments = spaPath.split(/[/\\]/).filter((s) => s.length > 0);
15
+ if (segments.some((s) => s === ".." || s === ".")) return false;
12
16
  return true;
13
17
  }
14
18
  function assertResolvedPathUnderRoot(root, target) {
@@ -20,6 +24,21 @@ function assertResolvedPathUnderRoot(root, target) {
20
24
  }
21
25
  }
22
26
 
27
+ // src/theme.ts
28
+ import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
29
+ function themeToLxpackRuntime(input) {
30
+ const theme = input.theme ?? getPresetTheme(input.preset ?? "default");
31
+ const raw = themeToCssVariables(theme);
32
+ const cssVariables = {};
33
+ for (const [key, value] of Object.entries(raw)) {
34
+ cssVariables[key] = String(value);
35
+ }
36
+ return {
37
+ theme: theme.name,
38
+ cssVariables
39
+ };
40
+ }
41
+
23
42
  // src/validateDescriptor.ts
24
43
  var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
25
44
  var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
@@ -81,6 +100,25 @@ function validateDescriptor(input) {
81
100
  message: `unknown preset; use one of: ${VALID_THEME_PRESETS.join(", ")}`
82
101
  });
83
102
  }
103
+ if (input.theme?.theme) {
104
+ try {
105
+ themeToLxpackRuntime({ preset: themePreset, theme: input.theme.theme });
106
+ } catch (err) {
107
+ issues.push({
108
+ path: "theme.theme",
109
+ message: err instanceof Error ? err.message : "invalid custom theme"
110
+ });
111
+ }
112
+ }
113
+ const completionThreshold = input.tracking?.completion?.threshold;
114
+ if (completionThreshold !== void 0) {
115
+ if (!Number.isFinite(completionThreshold) || completionThreshold < 0 || completionThreshold > 1) {
116
+ issues.push({
117
+ path: "tracking.completion.threshold",
118
+ message: "threshold must be a finite number between 0 and 1"
119
+ });
120
+ }
121
+ }
84
122
  if (layout === "single-spa" && (input.lessons?.length ?? 0) > 1) {
85
123
  issues.push({
86
124
  path: "lessons",
@@ -160,24 +198,69 @@ function validateDescriptor(input) {
160
198
  issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
161
199
  }
162
200
  const passingScore = assessment.passingScore;
163
- if (passingScore !== void 0) {
164
- if (!(passingScore > 0)) {
165
- issues.push({
166
- path: `${path}.passingScore`,
167
- message: "passingScore must be greater than 0"
168
- });
169
- } else if (trimmedChoices.length && passingScore > trimmedChoices.length) {
170
- issues.push({
171
- path: `${path}.passingScore`,
172
- message: "passingScore must not exceed the number of choices"
173
- });
174
- }
201
+ if (passingScore !== void 0 && !(passingScore > 0)) {
202
+ issues.push({
203
+ path: `${path}.passingScore`,
204
+ message: "passingScore must be greater than 0 (absolute point threshold)"
205
+ });
175
206
  }
176
207
  }
177
208
  if (issues.length) return { ok: false, issues };
178
209
  return { ok: true, descriptor: normalizeDescriptor(input) };
179
210
  }
180
211
 
212
+ // src/validateProjectPaths.ts
213
+ import { isAbsolute, resolve as resolve2 } from "path";
214
+ function validatePathField(value, fieldPath, projectRoot, issues) {
215
+ if (!isSafeRelativeSpaPath(value)) {
216
+ issues.push({
217
+ path: fieldPath,
218
+ message: "path must be relative without '..' segments or absolute prefixes"
219
+ });
220
+ return;
221
+ }
222
+ try {
223
+ assertResolvedPathUnderRoot(projectRoot, resolve2(projectRoot, value));
224
+ } catch {
225
+ issues.push({
226
+ path: fieldPath,
227
+ message: "path must resolve inside the project root"
228
+ });
229
+ }
230
+ }
231
+ function validateProjectPaths(projectRoot, paths) {
232
+ const issues = [];
233
+ const root = resolve2(projectRoot);
234
+ if (paths.spaDistDir?.trim()) {
235
+ validatePathField(paths.spaDistDir.trim(), "paths.spaDistDir", root, issues);
236
+ }
237
+ if (paths.lxpackOutDir?.trim()) {
238
+ validatePathField(paths.lxpackOutDir.trim(), "paths.lxpackOutDir", root, issues);
239
+ }
240
+ if (paths.outputBaseDir?.trim()) {
241
+ validatePathField(paths.outputBaseDir.trim(), "paths.outputBaseDir", root, issues);
242
+ }
243
+ return issues;
244
+ }
245
+ function resolveSafePackageOutputOverride(projectRoot, override) {
246
+ const root = resolve2(projectRoot);
247
+ const trimmed = override.trim();
248
+ if (!trimmed) {
249
+ throw new Error("output override must be a non-empty path");
250
+ }
251
+ if (isAbsolute(trimmed)) {
252
+ const resolved2 = resolve2(trimmed);
253
+ assertResolvedPathUnderRoot(root, resolved2);
254
+ return resolved2;
255
+ }
256
+ if (!isSafeRelativeSpaPath(trimmed)) {
257
+ throw new Error(`unsafe output path: ${override}`);
258
+ }
259
+ const resolved = resolve2(root, trimmed);
260
+ assertResolvedPathUnderRoot(root, resolved);
261
+ return resolved;
262
+ }
263
+
181
264
  // src/mapIds.ts
182
265
  import { assertValidId } from "@lessonkit/core";
183
266
  function mapLessonkitIds(descriptor) {
@@ -189,20 +272,36 @@ function mapLessonkitIds(descriptor) {
189
272
  return { courseId, lessonIds, checkIds };
190
273
  }
191
274
 
192
- // src/theme.ts
193
- import { getPresetTheme, themeToCssVariables } from "@lessonkit/themes";
194
- function themeToLxpackRuntime(input) {
195
- const theme = input.theme ?? getPresetTheme(input.preset ?? "default");
196
- const raw = themeToCssVariables(theme);
197
- const cssVariables = {};
198
- for (const [key, value] of Object.entries(raw)) {
199
- cssVariables[key] = String(value);
200
- }
275
+ // src/assessments.ts
276
+ function slugChoiceId(text, index) {
277
+ const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
278
+ const stem = base.length ? base : "choice";
279
+ return `${stem}-${index + 1}`;
280
+ }
281
+ function assessmentDescriptorToLxpack(assessment) {
282
+ const choices = assessment.choices.map((text, index) => {
283
+ const id = slugChoiceId(text, index);
284
+ return {
285
+ id,
286
+ text,
287
+ correct: text === assessment.answer
288
+ };
289
+ });
201
290
  return {
202
- theme: theme.name,
203
- cssVariables
291
+ id: assessment.checkId,
292
+ passingScore: assessment.passingScore ?? 1,
293
+ questions: [
294
+ {
295
+ id: "q1",
296
+ prompt: assessment.question,
297
+ choices
298
+ }
299
+ ]
204
300
  };
205
301
  }
302
+ function extractAssessments(descriptor) {
303
+ return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
304
+ }
206
305
 
207
306
  // src/interchange.ts
208
307
  function resolveSpaLessons(descriptor) {
@@ -227,6 +326,8 @@ function resolveSpaLessons(descriptor) {
227
326
  function descriptorToInterchange(descriptor) {
228
327
  const mapped = mapLessonkitIds(descriptor);
229
328
  const spaLessons = resolveSpaLessons(descriptor);
329
+ const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
330
+ const assessments = extractAssessments(descriptor);
230
331
  return {
231
332
  format: "lessonkit",
232
333
  version: "1",
@@ -240,121 +341,52 @@ function descriptorToInterchange(descriptor) {
240
341
  type: "spa",
241
342
  path: l.path
242
343
  })),
243
- tracking: descriptor.tracking
244
- };
245
- }
246
-
247
- // src/assessments.ts
248
- function slugChoiceId(text, index) {
249
- const base = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
250
- const stem = base.length ? base : "choice";
251
- return `${stem}-${index + 1}`;
252
- }
253
- function assessmentDescriptorToLxpack(assessment) {
254
- const choices = assessment.choices.map((text, index) => {
255
- const id = slugChoiceId(text, index);
256
- return {
257
- id,
258
- text,
259
- correct: text === assessment.answer
260
- };
261
- });
262
- return {
263
- id: assessment.checkId,
264
- passingScore: assessment.passingScore ?? 1,
265
- questions: [
266
- {
267
- id: "q1",
268
- prompt: assessment.question,
269
- choices
270
- }
271
- ]
344
+ tracking: descriptor.tracking,
345
+ runtime: runtime ? {
346
+ theme: runtime.theme,
347
+ cssVariables: runtime.cssVariables
348
+ } : void 0,
349
+ assessments: assessments.length ? assessments : void 0
272
350
  };
273
351
  }
274
- function extractAssessments(descriptor) {
275
- return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
276
- }
277
352
 
278
353
  // src/writeProject.ts
279
- import { access, cp, mkdir, rm, writeFile } from "fs/promises";
280
- import { dirname, join, resolve as resolve2 } from "path";
354
+ import { join, resolve as resolve4 } from "path";
355
+ import { materializeLessonkitProject } from "@lxpack/validators";
281
356
 
282
- // src/assessmentYaml.ts
283
- function yamlQuote(value) {
284
- return JSON.stringify(value);
285
- }
286
- function emitAssessmentYaml(assessment) {
287
- const lx = assessmentDescriptorToLxpack(assessment);
288
- const lines = [];
289
- lines.push(`id: ${lx.id}`);
290
- lines.push(`passingScore: ${lx.passingScore}`);
291
- lines.push("questions:");
292
- for (const question of lx.questions) {
293
- lines.push(` - id: ${question.id}`);
294
- lines.push(` prompt: ${yamlQuote(question.prompt)}`);
295
- lines.push(" choices:");
296
- for (const choice of question.choices) {
297
- lines.push(` - id: ${choice.id}`);
298
- lines.push(` text: ${yamlQuote(choice.text)}`);
299
- if (choice.correct) lines.push(" correct: true");
357
+ // src/spaDirs.ts
358
+ import { access } from "fs/promises";
359
+ import { resolve as resolve3 } from "path";
360
+ async function resolveSpaDirs(options) {
361
+ const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
362
+ const spaLessons = resolveSpaLessons(descriptor);
363
+ if (descriptor.layout === "single-spa") {
364
+ const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
365
+ const srcDist = projectRoot ? resolve3(projectRoot, spaDistRelative) : resolve3(spaDistRelative);
366
+ if (projectRoot) {
367
+ assertResolvedPathUnderRoot(resolve3(projectRoot), srcDist);
300
368
  }
301
- }
302
- return `${lines.join("\n")}
303
- `;
304
- }
305
-
306
- // src/yaml.ts
307
- function yamlQuote2(value) {
308
- if (/[:#\n\r]/.test(value) || value.startsWith(" ") || value.endsWith(" ")) {
309
- return JSON.stringify(value);
310
- }
311
- return value;
312
- }
313
- function emitCourseYaml(opts) {
314
- const lines = [];
315
- lines.push(`title: ${yamlQuote2(opts.title)}`);
316
- lines.push(`version: ${yamlQuote2(opts.version)}`);
317
- if (opts.description) lines.push(`description: ${yamlQuote2(opts.description)}`);
318
- if (opts.runtime) {
319
- lines.push("runtime:");
320
- lines.push(` theme: ${yamlQuote2(opts.runtime.theme)}`);
321
- if (opts.runtime.cssVariables && Object.keys(opts.runtime.cssVariables).length) {
322
- lines.push(" cssVariables:");
323
- for (const [key, value] of Object.entries(opts.runtime.cssVariables).sort(
324
- ([a], [b]) => a.localeCompare(b)
325
- )) {
326
- lines.push(` ${key}: ${JSON.stringify(String(value))}`);
327
- }
369
+ try {
370
+ await access(srcDist);
371
+ } catch {
372
+ throw new Error(`spaDistDir not found: ${srcDist}`);
328
373
  }
374
+ const lessonId = spaLessons[0]?.id ?? "main";
375
+ return { [lessonId]: srcDist };
329
376
  }
330
- if (opts.tracking?.completion?.threshold !== void 0) {
331
- lines.push("tracking:");
332
- lines.push(" completion:");
333
- lines.push(` threshold: ${opts.tracking.completion.threshold}`);
334
- }
335
- lines.push("lessons:");
336
- for (const lesson of opts.lessons) {
337
- lines.push(` - id: ${lesson.id}`);
338
- lines.push(` title: ${yamlQuote2(lesson.title)}`);
339
- lines.push(` type: spa`);
340
- lines.push(` path: ${lesson.path}`);
341
- }
342
- if (opts.assessments.length) {
343
- lines.push("assessments:");
344
- for (const assessment of opts.assessments) {
345
- lines.push(` - id: ${assessment.id}`);
346
- lines.push(` file: ${assessment.file}`);
377
+ const dirs = {};
378
+ const lessonDirs = lessonSpaDirs ?? {};
379
+ for (const lesson of descriptor.lessons) {
380
+ const src = lessonDirs[lesson.id];
381
+ if (!src) {
382
+ throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
347
383
  }
384
+ dirs[lesson.id] = resolve3(src);
348
385
  }
349
- return `${lines.join("\n")}
350
- `;
386
+ return dirs;
351
387
  }
352
388
 
353
389
  // src/writeProject.ts
354
- async function copyDir(src, dest) {
355
- await mkdir(dirname(dest), { recursive: true });
356
- await cp(src, dest, { recursive: true });
357
- }
358
390
  async function writeLxpackProject(options) {
359
391
  const validation = validateDescriptor(options.descriptor);
360
392
  if (!validation.ok) {
@@ -363,93 +395,46 @@ async function writeLxpackProject(options) {
363
395
  );
364
396
  }
365
397
  const descriptor = validation.descriptor;
366
- const outDir = resolve2(options.outDir);
367
- await mkdir(outDir, { recursive: true });
368
- const spaLessons = resolveSpaLessons(descriptor);
369
- const runtime = descriptor.theme ? themeToLxpackRuntime(descriptor.theme) : void 0;
370
- const assessments = (descriptor.assessments ?? []).map((a) => ({
371
- id: a.checkId,
372
- file: `assessments/${a.checkId}.yaml`
373
- }));
374
- if (descriptor.layout === "single-spa") {
375
- const srcDist = resolve2(options.spaDistDir ?? descriptor.spaDistDir ?? "dist");
376
- try {
377
- await access(srcDist);
378
- } catch {
379
- throw new Error(`spaDistDir not found: ${srcDist}`);
380
- }
381
- const destDist = join(outDir, "dist");
382
- await rm(destDist, { recursive: true, force: true });
383
- await copyDir(srcDist, destDist);
384
- } else {
385
- const lessonDirs = options.lessonSpaDirs ?? {};
386
- for (const lesson of descriptor.lessons) {
387
- const src = lessonDirs[lesson.id];
388
- if (!src) {
389
- throw new Error(`lessonSpaDirs missing build output for lesson "${lesson.id}"`);
390
- }
391
- const dest = join(outDir, lesson.spaPath);
392
- assertResolvedPathUnderRoot(outDir, dest);
393
- await rm(dest, { recursive: true, force: true });
394
- await copyDir(resolve2(src), dest);
395
- }
396
- }
397
- if (assessments.length) {
398
- const assessmentsDir = join(outDir, "assessments");
399
- await mkdir(assessmentsDir, { recursive: true });
400
- for (const assessment of descriptor.assessments ?? []) {
401
- await writeFile(
402
- join(outDir, `assessments/${assessment.checkId}.yaml`),
403
- emitAssessmentYaml(assessment),
404
- "utf-8"
405
- );
406
- }
398
+ const outDir = resolve4(options.outDir);
399
+ const spaDirs = await resolveSpaDirs({ ...options, descriptor });
400
+ const interchange = descriptorToInterchange(descriptor);
401
+ const materialized = await materializeLessonkitProject({
402
+ interchange,
403
+ spaDirs,
404
+ courseDir: outDir,
405
+ writeAuthoringFiles: true
406
+ });
407
+ if (!materialized.ok) {
408
+ throw new Error(
409
+ materialized.issues.map((i) => `${i.path ?? ""}: ${i.message}`.trim()).join("; ")
410
+ );
407
411
  }
408
- const courseYamlPath = join(outDir, "course.yaml");
409
- await writeFile(
410
- courseYamlPath,
411
- emitCourseYaml({
412
- title: descriptor.title,
413
- version: descriptor.version ?? "1.0.0",
414
- runtime,
415
- tracking: descriptor.tracking,
416
- lessons: spaLessons.map((l) => ({
417
- id: l.id,
418
- title: l.title,
419
- type: "spa",
420
- path: l.path
421
- })),
422
- assessments
423
- }),
424
- "utf-8"
425
- );
426
- const lessonkitJsonPath = join(outDir, "lessonkit.json");
427
- await writeFile(
428
- lessonkitJsonPath,
429
- `${JSON.stringify(descriptorToInterchange(descriptor), null, 2)}
430
- `,
431
- "utf-8"
432
- );
433
- return { outDir, courseYamlPath, lessonkitJsonPath };
412
+ const courseDir = materialized.courseDir;
413
+ return {
414
+ outDir: courseDir,
415
+ courseYamlPath: join(courseDir, "course.yaml"),
416
+ lessonkitJsonPath: join(courseDir, "lessonkit.json")
417
+ };
434
418
  }
435
419
 
436
420
  // src/packageCourse.ts
437
- import { mkdir as mkdir2, mkdtemp, rename, rm as rm2 } from "fs/promises";
438
- import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
421
+ import * as fsp from "fs/promises";
422
+ import { dirname, join as join2, resolve as resolve5 } from "path";
439
423
  import { tmpdir } from "os";
440
424
  import {
441
425
  buildCourse,
426
+ packageLessonkit,
442
427
  validateCourse
443
428
  } from "@lxpack/api";
444
429
  async function validateLessonkitProject(options) {
445
430
  return validateCourse({
446
- courseDir: resolve3(options.courseDir),
431
+ courseDir: resolve5(options.courseDir),
447
432
  target: options.target
448
433
  });
449
434
  }
450
435
  async function buildLessonkitProject(options) {
451
436
  return buildCourse({
452
- courseDir: resolve3(options.courseDir),
437
+ courseDir: resolve5(options.courseDir),
453
438
  target: options.target,
454
439
  output: options.output,
455
440
  dir: options.dir,
@@ -457,9 +442,38 @@ async function buildLessonkitProject(options) {
457
442
  assessments: options.assessments
458
443
  });
459
444
  }
445
+ async function pathExists(path) {
446
+ try {
447
+ await fsp.access(path);
448
+ return true;
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+ async function promoteStagingToOutDir(stagingDir, outDir) {
454
+ const tmpPromote = `${outDir}.tmp-promote`;
455
+ const backup = `${outDir}.bak`;
456
+ await fsp.rename(stagingDir, tmpPromote);
457
+ const hadOutDir = await pathExists(outDir);
458
+ if (hadOutDir) {
459
+ await fsp.rename(outDir, backup);
460
+ }
461
+ try {
462
+ await fsp.rename(tmpPromote, outDir);
463
+ } catch (promoteError) {
464
+ if (hadOutDir) {
465
+ await fsp.rename(backup, outDir).catch(() => void 0);
466
+ }
467
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
468
+ throw promoteError;
469
+ }
470
+ if (hadOutDir) {
471
+ await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
472
+ }
473
+ }
460
474
  async function packageLessonkitCourse(options) {
461
475
  const { target, output, dir, outputBaseDir, ...writeOpts } = options;
462
- const outDir = resolve3(writeOpts.outDir);
476
+ const outDir = resolve5(writeOpts.outDir);
463
477
  const descriptorValidation = validateDescriptor(writeOpts.descriptor);
464
478
  if (!descriptorValidation.ok) {
465
479
  return {
@@ -473,42 +487,50 @@ async function packageLessonkitCourse(options) {
473
487
  };
474
488
  }
475
489
  const descriptor = descriptorValidation.descriptor;
476
- const stagingDir = await mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
490
+ const stagingDir = await fsp.mkdtemp(join2(tmpdir(), "lessonkit-lxpack-"));
477
491
  let promoted = false;
478
492
  try {
479
- const written = await writeLxpackProject({ ...writeOpts, descriptor, outDir: stagingDir });
480
- const courseDir = written.outDir;
481
- const assessments = extractAssessments(descriptor);
482
- const validation = await validateLessonkitProject({ courseDir, target });
483
- if (!validation.ok) {
493
+ let spaDirs;
494
+ try {
495
+ spaDirs = await resolveSpaDirs({ ...writeOpts, descriptor });
496
+ } catch (err) {
484
497
  return {
485
498
  ok: false,
486
499
  courseDir: outDir,
487
500
  target,
488
- validation,
489
- issues: validation.issues.map((i) => ({
490
- path: i.path,
491
- message: i.message,
492
- severity: i.severity
493
- }))
501
+ issues: [
502
+ {
503
+ path: "spaDirs",
504
+ message: err instanceof Error ? err.message : String(err)
505
+ }
506
+ ]
494
507
  };
495
508
  }
509
+ const interchange = descriptorToInterchange(descriptor);
496
510
  const outputBase = outputBaseDir ?? ".lxpack/out";
497
- await mkdir2(join2(courseDir, outputBase), { recursive: true });
511
+ await fsp.mkdir(join2(stagingDir, outputBase), { recursive: true });
498
512
  const defaultOutput = output ?? (dir ? join2(outputBase, target) : join2(outputBase, `course-${target}.zip`));
499
- const build = await buildLessonkitProject({
500
- courseDir,
513
+ const build = await packageLessonkit({
514
+ interchange,
515
+ spaDirs,
501
516
  target,
502
- output: defaultOutput.startsWith("/") ? defaultOutput : join2(courseDir, defaultOutput),
517
+ courseDir: stagingDir,
518
+ output: defaultOutput,
503
519
  dir,
504
- assessments: assessments.length ? assessments : void 0
520
+ outputBaseDir,
521
+ outputAnchorDir: stagingDir,
522
+ writeAuthoringFiles: true
505
523
  });
506
524
  if (!build.ok) {
525
+ const validation2 = {
526
+ ok: false,
527
+ issues: build.issues
528
+ };
507
529
  return {
508
530
  ok: false,
509
531
  courseDir: outDir,
510
532
  target,
511
- validation,
533
+ validation: validation2,
512
534
  build,
513
535
  issues: build.issues.map((i) => ({
514
536
  path: i.path,
@@ -517,45 +539,81 @@ async function packageLessonkitCourse(options) {
517
539
  }))
518
540
  };
519
541
  }
520
- await rm2(outDir, { recursive: true, force: true });
521
- await mkdir2(dirname2(outDir), { recursive: true });
522
- await rename(stagingDir, outDir);
523
- promoted = true;
542
+ const validation = {
543
+ ok: true,
544
+ manifest: build.manifest,
545
+ issues: build.issues
546
+ };
547
+ const stagingRoot = await fsp.realpath(stagingDir);
524
548
  const remapArtifactPath = (artifactPath) => {
525
549
  if (!artifactPath) return void 0;
526
- const resolved = resolve3(artifactPath);
527
- const stagingResolved = resolve3(stagingDir);
528
- if (resolved === stagingResolved || resolved.startsWith(stagingResolved + "/")) {
529
- return join2(outDir, resolved.slice(stagingResolved.length + 1));
550
+ const resolved = resolve5(artifactPath);
551
+ if (resolved === stagingRoot || resolved.startsWith(`${stagingRoot}/`)) {
552
+ return join2(outDir, resolved.slice(stagingRoot.length + 1));
530
553
  }
531
554
  return artifactPath;
532
555
  };
556
+ const remappedOutputPath = remapArtifactPath(
557
+ "outputPath" in build ? build.outputPath : void 0
558
+ );
559
+ const remappedOutputDir = remapArtifactPath("outputDir" in build ? build.outputDir : void 0);
560
+ await fsp.mkdir(dirname(outDir), { recursive: true });
561
+ await promoteStagingToOutDir(stagingDir, outDir);
562
+ promoted = true;
563
+ const remappedBuild = { ...build };
564
+ if ("outputPath" in remappedBuild && remappedOutputPath !== void 0) {
565
+ remappedBuild.outputPath = remappedOutputPath;
566
+ }
567
+ if ("outputDir" in remappedBuild && remappedOutputDir !== void 0) {
568
+ remappedBuild.outputDir = remappedOutputDir;
569
+ }
533
570
  return {
534
571
  ok: true,
535
572
  courseDir: outDir,
536
573
  target,
537
- outputPath: remapArtifactPath("outputPath" in build ? build.outputPath : void 0),
538
- outputDir: remapArtifactPath("outputDir" in build ? build.outputDir : void 0),
574
+ outputPath: remappedOutputPath,
575
+ outputDir: remappedOutputDir,
539
576
  fileCount: build.fileCount,
540
577
  validation,
541
- build
578
+ build: remappedBuild
542
579
  };
543
580
  } finally {
544
581
  if (!promoted) {
545
- await rm2(stagingDir, { recursive: true, force: true }).catch(() => void 0);
582
+ await fsp.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
546
583
  }
547
584
  }
548
585
  }
586
+
587
+ // src/index.ts
588
+ import {
589
+ LESSONKIT_TELEMETRY_EVENTS,
590
+ mapLessonkitTelemetryToBridgeAction,
591
+ mapLessonkitTelemetryToLxpack
592
+ } from "@lxpack/tracking-schema";
593
+ import {
594
+ lessonkitInterchangeSchema,
595
+ materializeLessonkitProject as materializeLessonkitProject2,
596
+ parseLessonkitInterchange
597
+ } from "@lxpack/validators";
549
598
  export {
599
+ LESSONKIT_TELEMETRY_EVENTS,
550
600
  assessmentDescriptorToLxpack,
551
601
  buildLessonkitProject,
552
602
  descriptorToInterchange,
553
603
  extractAssessments,
604
+ lessonkitInterchangeSchema,
554
605
  mapLessonkitIds,
606
+ mapLessonkitTelemetryToBridgeAction,
607
+ mapLessonkitTelemetryToLxpack,
608
+ materializeLessonkitProject2 as materializeLessonkitProject,
555
609
  packageLessonkitCourse,
610
+ parseLessonkitInterchange,
611
+ resolveSafePackageOutputOverride,
556
612
  resolveSpaLessons,
613
+ telemetryEventToLessonkit,
557
614
  themeToLxpackRuntime,
558
615
  validateDescriptor,
559
616
  validateLessonkitProject,
617
+ validateProjectPaths,
560
618
  writeLxpackProject
561
619
  };
@@ -0,0 +1,9 @@
1
+ import { TelemetryEvent } from '@lessonkit/core';
2
+ import { LessonkitTelemetryEvent } from '@lxpack/tracking-schema';
3
+
4
+ /**
5
+ * Map a `@lessonkit/core` telemetry event to the LXPack LessonKit telemetry shape.
6
+ */
7
+ declare function telemetryEventToLessonkit(event: TelemetryEvent): LessonkitTelemetryEvent | null;
8
+
9
+ export { telemetryEventToLessonkit as t };