tessera-learn 0.2.2 → 0.3.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.
Files changed (48) hide show
  1. package/AGENTS.md +161 -535
  2. package/README.md +2 -2
  3. package/dist/{audit-B9VHgVjk.js → audit-DkXqQTqn.js} +92 -38
  4. package/dist/audit-DkXqQTqn.js.map +1 -0
  5. package/dist/{build-commands-D127jw0J.js → build-commands-CyzuCDXg.js} +2 -2
  6. package/dist/{build-commands-D127jw0J.js.map → build-commands-CyzuCDXg.js.map} +1 -1
  7. package/dist/{inline-config-eHjv9XuA.js → inline-config-BEXyRqsJ.js} +2 -2
  8. package/dist/{inline-config-eHjv9XuA.js.map → inline-config-BEXyRqsJ.js.map} +1 -1
  9. package/dist/plugin/cli.d.ts.map +1 -1
  10. package/dist/plugin/cli.js +62 -54
  11. package/dist/plugin/cli.js.map +1 -1
  12. package/dist/plugin/index.d.ts +280 -3
  13. package/dist/plugin/index.d.ts.map +1 -1
  14. package/dist/plugin/index.js +3 -3
  15. package/dist/{plugin--8H9xQIl.js → plugin-CFUFgwHB.js} +126 -83
  16. package/dist/plugin-CFUFgwHB.js.map +1 -0
  17. package/package.json +12 -9
  18. package/src/components/DefaultLayout.svelte +2 -5
  19. package/src/components/Quiz.svelte +18 -26
  20. package/src/plugin/a11y/audit.ts +8 -13
  21. package/src/plugin/a11y-cli.ts +1 -4
  22. package/src/plugin/ast.ts +9 -2
  23. package/src/plugin/cli.ts +46 -48
  24. package/src/plugin/csp.ts +59 -0
  25. package/src/plugin/duplicate-cli.ts +37 -1
  26. package/src/plugin/export.ts +56 -27
  27. package/src/plugin/index.ts +117 -61
  28. package/src/plugin/manifest.ts +3 -23
  29. package/src/plugin/new-cli.ts +2 -0
  30. package/src/plugin/validate-cli.ts +10 -4
  31. package/src/plugin/validation.ts +48 -12
  32. package/src/runtime/App.svelte +10 -8
  33. package/src/runtime/Sidebar.svelte +3 -1
  34. package/src/runtime/adapters/cmi5.ts +59 -402
  35. package/src/runtime/adapters/discovery.ts +11 -0
  36. package/src/runtime/adapters/index.ts +27 -60
  37. package/src/runtime/adapters/lms-error.ts +61 -0
  38. package/src/runtime/adapters/scorm2004.ts +2 -1
  39. package/src/runtime/adapters/web.ts +19 -4
  40. package/src/runtime/adapters/xapi-launch-base.ts +346 -0
  41. package/src/runtime/adapters/xapi.ts +26 -0
  42. package/src/runtime/types.ts +19 -1
  43. package/src/runtime/xapi/publisher.ts +5 -1
  44. package/src/runtime/xapi/setup.ts +24 -15
  45. package/src/virtual.d.ts +4 -1
  46. package/templates/course/course.config.js +1 -0
  47. package/dist/audit-B9VHgVjk.js.map +0 -1
  48. package/dist/plugin--8H9xQIl.js.map +0 -1
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { c as validateProject, n as runAudit, r as resolvePackageRoot, s as reportValidationIssues } from "../audit-B9VHgVjk.js";
2
+ import { c as validateProject, n as runAudit, r as resolvePackageRoot, s as reportValidationIssues } from "../audit-DkXqQTqn.js";
3
3
  import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
+ import { randomUUID } from "node:crypto";
5
6
  //#region src/plugin/validate-cli.ts
6
- function runValidate(projectRoot) {
7
+ function runValidate(projectRoot, { showA11yTip = true } = {}) {
7
8
  const { errors, warnings } = validateProject(projectRoot);
8
9
  reportValidationIssues({
9
10
  errors,
@@ -16,7 +17,7 @@ function runValidate(projectRoot) {
16
17
  }
17
18
  if (warnings.length > 0) console.log(`\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m`);
18
19
  else console.log("\x1B[32m[tessera]\x1B[0m Validation passed — no issues found.");
19
- console.log("\x1B[2m[tessera] Static checks only. For a full runtime accessibility audit, run: pnpm exec tessera a11y\x1B[0m");
20
+ if (showA11yTip) console.log(`\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: pnpm a11y ${basename(projectRoot)}\x1b[0m`);
20
21
  return 0;
21
22
  }
22
23
  //#endregion
@@ -29,7 +30,6 @@ const VALID_THRESHOLDS = [
29
30
  ];
30
31
  function parseA11yArgs(argv) {
31
32
  let threshold;
32
- let rebuild = false;
33
33
  for (let i = 0; i < argv.length; i++) {
34
34
  const arg = argv[i];
35
35
  if (arg === "--threshold") {
@@ -39,13 +39,12 @@ function parseA11yArgs(argv) {
39
39
  error: `--threshold must be one of: ${VALID_THRESHOLDS.join(", ")}`
40
40
  };
41
41
  threshold = value;
42
- } else if (arg === "--build") rebuild = true;
43
- else return {
42
+ } else return {
44
43
  ok: false,
45
44
  error: `Unknown argument: ${arg}`
46
45
  };
47
46
  }
48
- const args = { rebuild };
47
+ const args = {};
49
48
  if (threshold !== void 0) args.threshold = threshold;
50
49
  return {
51
50
  ok: true,
@@ -209,15 +208,37 @@ function runNew(name, cwd) {
209
208
  console.error(`[tessera new] Course "${name}" already exists.`);
210
209
  return 1;
211
210
  }
212
- copyTemplate(join(resolvePackageRoot(), "templates", "course"), courseDir, { PROJECT_TITLE: toTitleCase(name) });
211
+ copyTemplate(join(resolvePackageRoot(), "templates", "course"), courseDir, {
212
+ PROJECT_TITLE: toTitleCase(name),
213
+ COURSE_ID: `urn:uuid:${randomUUID()}`
214
+ });
213
215
  const rel = relative(workspaceRoot, courseDir);
214
216
  console.log(`\nCreated ${rel}.\n\nNext steps:\n pnpm tessera dev ${name}\n`);
215
217
  return 0;
216
218
  }
217
219
  //#endregion
218
220
  //#region src/plugin/duplicate-cli.ts
221
+ const ID_LINE = /^([ \t]*'?id'?[ \t]*:[ \t]*)('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`|[^\n,}]*)/m;
222
+ const DEFAULT_OBJECT = /export\s+default\s*\{/;
223
+ function reidentifyCourse(courseRoot) {
224
+ const configPath = join(courseRoot, "course.config.js");
225
+ if (!existsSync(configPath)) return;
226
+ const text = readFileSync(configPath, "utf-8");
227
+ const newId = `'urn:uuid:${randomUUID()}'`;
228
+ if (ID_LINE.test(text)) {
229
+ writeFileSync(configPath, text.replace(ID_LINE, `$1${newId}`));
230
+ return;
231
+ }
232
+ const obj = DEFAULT_OBJECT.exec(text);
233
+ if (obj) {
234
+ const at = obj.index + obj[0].length;
235
+ writeFileSync(configPath, `${text.slice(0, at)}\n id: ${newId},${text.slice(at)}`);
236
+ return;
237
+ }
238
+ console.warn(`[tessera duplicate] Could not set a unique id in ${configPath} — the copy shares the source's identity. Add a unique "id" manually.`);
239
+ }
219
240
  const HELP = "Usage: tessera duplicate <source> <new>\n\nCopy courses/<source>/ to courses/<new>/ within the current workspace.";
220
- const SKIP = new Set([
241
+ const SKIP = /* @__PURE__ */ new Set([
221
242
  "node_modules",
222
243
  "dist",
223
244
  "a11y-report.json",
@@ -256,6 +277,7 @@ function runDuplicate(source, target, cwd) {
256
277
  recursive: true,
257
278
  filter: (src) => src === srcDir || !skip(src)
258
279
  });
280
+ reidentifyCourse(destDir);
259
281
  const rel = relative(workspaceRoot, destDir);
260
282
  const srcRel = relative(workspaceRoot, srcDir);
261
283
  console.log(`\nCreated ${rel} (duplicated from ${srcRel}).\n\nRemember to update the title in ${rel}/course.config.js.\n\nNext steps:\n pnpm tessera dev ${target}\n`);
@@ -277,8 +299,7 @@ Commands:
277
299
  Run a command from inside a course folder, or name the course explicitly.
278
300
 
279
301
  a11y/check options:
280
- --threshold <minor|moderate|serious|critical> Failing impact (default: serious)
281
- --build Force a fresh build first`;
302
+ --threshold <minor|moderate|serious|critical> Failing impact (default: serious)`;
282
303
  function splitCourseArg(rest) {
283
304
  if (rest.length > 0 && !rest[0].startsWith("-")) return {
284
305
  course: rest[0],
@@ -289,57 +310,44 @@ function splitCourseArg(rest) {
289
310
  flags: rest
290
311
  };
291
312
  }
313
+ const COURSE_COMMANDS = {
314
+ dev: async (courseRoot, workspaceRoot) => (await import("../build-commands-CyzuCDXg.js")).runDev(courseRoot, workspaceRoot),
315
+ export: async (courseRoot, workspaceRoot) => (await import("../build-commands-CyzuCDXg.js")).runBuild(courseRoot, workspaceRoot),
316
+ validate: (courseRoot) => runValidate(courseRoot),
317
+ a11y: (courseRoot, workspaceRoot, flags) => runA11y(courseRoot, workspaceRoot, flags),
318
+ check: (courseRoot, workspaceRoot, flags) => {
319
+ const validateCode = runValidate(courseRoot, { showA11yTip: false });
320
+ if (validateCode !== 0) return validateCode;
321
+ return runA11y(courseRoot, workspaceRoot, flags);
322
+ }
323
+ };
292
324
  async function main(argv, cwd = process.cwd()) {
293
325
  const [sub, ...rest] = argv;
294
326
  if (sub === "new") return runNew(rest[0], cwd);
295
327
  if (sub === "duplicate") return runDuplicate(rest[0], rest[1], cwd);
296
- switch (sub) {
297
- case "dev":
298
- case "export":
299
- case "validate":
300
- case "a11y":
301
- case "check": {
302
- if (rest.includes("--help") || rest.includes("-h")) {
303
- console.log(USAGE);
304
- return 0;
305
- }
306
- const { course, flags } = splitCourseArg(rest);
307
- const resolved = resolveCourse(cwd, course);
308
- if (!resolved.ok) {
309
- console.error(`[tessera] ${resolved.error}`);
310
- return 1;
311
- }
312
- const { courseRoot, workspaceRoot } = resolved;
313
- switch (sub) {
314
- case "dev": {
315
- const { runDev } = await import("../build-commands-D127jw0J.js");
316
- return runDev(courseRoot, workspaceRoot);
317
- }
318
- case "export": {
319
- const { runBuild } = await import("../build-commands-D127jw0J.js");
320
- return runBuild(courseRoot, workspaceRoot);
321
- }
322
- case "validate": return runValidate(courseRoot);
323
- case "check": {
324
- const validateCode = runValidate(courseRoot);
325
- if (validateCode !== 0) return validateCode;
326
- return runA11y(courseRoot, workspaceRoot, flags);
327
- }
328
- case "a11y": return runA11y(courseRoot, workspaceRoot, flags);
329
- }
330
- return 0;
331
- }
332
- case "--help":
333
- case "-h":
328
+ if (sub !== void 0 && Object.hasOwn(COURSE_COMMANDS, sub)) {
329
+ if (rest.includes("--help") || rest.includes("-h")) {
334
330
  console.log(USAGE);
335
331
  return 0;
336
- case void 0:
337
- console.error(`No command given.\n\n${USAGE}`);
338
- return 1;
339
- default:
340
- console.error(`Unknown command: ${sub}\n\n${USAGE}`);
332
+ }
333
+ const { course, flags } = splitCourseArg(rest);
334
+ const resolved = resolveCourse(cwd, course);
335
+ if (!resolved.ok) {
336
+ console.error(`[tessera] ${resolved.error}`);
341
337
  return 1;
338
+ }
339
+ return COURSE_COMMANDS[sub](resolved.courseRoot, resolved.workspaceRoot, flags);
340
+ }
341
+ if (sub === "--help" || sub === "-h") {
342
+ console.log(USAGE);
343
+ return 0;
344
+ }
345
+ if (sub === void 0) {
346
+ console.error(`No command given.\n\n${USAGE}`);
347
+ return 1;
342
348
  }
349
+ console.error(`Unknown command: ${sub}\n\n${USAGE}`);
350
+ return 1;
343
351
  }
344
352
  if (import.meta.main) main(process.argv.slice(2)).then((code) => process.exit(code));
345
353
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/validate-cli.ts","../../src/plugin/a11y-cli.ts","../../src/plugin/project-name.ts","../../src/plugin/course-root.ts","../../src/plugin/template-copy.ts","../../src/plugin/new-cli.ts","../../src/plugin/duplicate-cli.ts","../../src/plugin/cli.ts"],"sourcesContent":["import { validateProject, reportValidationIssues } from './validation.js';\n\nexport function runValidate(projectRoot: string): number {\n const { errors, warnings } = validateProject(projectRoot);\n\n reportValidationIssues({ errors, warnings });\n\n if (errors.length > 0) {\n const summary =\n `Validation failed with ${errors.length} error(s)` +\n (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +\n '.';\n console.error(`\\n\\x1b[31m${summary}\\x1b[0m`);\n return 1;\n }\n\n if (warnings.length > 0) {\n console.log(\n `\\n\\x1b[33mValidation passed with ${warnings.length} warning(s).\\x1b[0m`,\n );\n } else {\n console.log(\n '\\x1b[32m[tessera]\\x1b[0m Validation passed — no issues found.',\n );\n }\n console.log(\n '\\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: pnpm exec tessera a11y\\x1b[0m',\n );\n return 0;\n}\n","import { runAudit, type AuditOptions, type ImpactLevel } from './a11y/audit.js';\n\nconst VALID_THRESHOLDS: ImpactLevel[] = [\n 'minor',\n 'moderate',\n 'serious',\n 'critical',\n];\n\nexport type ParsedA11yArgs =\n | { ok: true; args: AuditOptions }\n | { ok: false; error: string };\n\nexport function parseA11yArgs(argv: string[]): ParsedA11yArgs {\n let threshold: ImpactLevel | undefined;\n let rebuild = false;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === '--threshold') {\n const value = argv[++i] as ImpactLevel;\n if (!VALID_THRESHOLDS.includes(value)) {\n return {\n ok: false,\n error: `--threshold must be one of: ${VALID_THRESHOLDS.join(', ')}`,\n };\n }\n threshold = value;\n } else if (arg === '--build') {\n rebuild = true;\n } else {\n return { ok: false, error: `Unknown argument: ${arg}` };\n }\n }\n\n const args: AuditOptions = { rebuild };\n if (threshold !== undefined) args.threshold = threshold;\n return { ok: true, args };\n}\n\nexport async function runA11y(\n projectRoot: string,\n workspaceRoot: string,\n argv: string[],\n): Promise<number> {\n const parsed = parseA11yArgs(argv);\n if (!parsed.ok) {\n console.error(`[tessera a11y] ${parsed.error}`);\n return 1;\n }\n return runAudit(projectRoot, workspaceRoot, parsed.args);\n}\n","// Pure, dependency-free helpers shared by the `tessera new` subcommand and the\n// `create-tessera` scaffolder. Kept import-free so create-tessera can bundle it\n// at build time without dragging in the rest of the plugin (vite, svelte, …).\n\n// npm package name rules: 1-214 chars, lowercase, must start with [a-z0-9],\n// allowed chars [a-z0-9._-], no leading dot or underscore.\nexport function validateProjectName(\n name: string,\n label = 'Project name',\n): string | null {\n if (!name) return `${label} is required`;\n if (name.length > 214) return `${label} must be 214 characters or fewer`;\n if (name !== name.toLowerCase()) return `${label} must be lowercase`;\n if (!/^[a-z0-9]/.test(name)) {\n return `${label} must start with a letter or digit`;\n }\n if (!/^[a-z0-9._-]+$/.test(name)) {\n return `${label} may only contain lowercase letters, digits, \"-\", \"_\", and \".\"`;\n }\n return null;\n}\n\nexport function toTitleCase(slug: string): string {\n return slug\n .split(/[-_.\\s]+/)\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","import { existsSync, readdirSync, statSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { validateProjectName } from './project-name.js';\n\nexport interface ResolvedCourse {\n ok: true;\n courseRoot: string;\n workspaceRoot: string;\n}\n\nexport type ResolveResult = ResolvedCourse | { ok: false; error: string };\n\nfunction isDir(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\nfunction isCourse(dir: string): boolean {\n return existsSync(join(dir, 'course.config.js'));\n}\n\n// A workspace is the nearest ancestor holding a courses/ directory. The walk\n// includes the starting dir, so the workspace root resolves to itself.\nexport function findWorkspaceRoot(cwd: string): string | null {\n for (let dir = resolve(cwd); ; dir = dirname(dir)) {\n if (isDir(join(dir, 'courses'))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n }\n}\n\nfunction scanCourses(workspaceRoot: string): {\n courses: string[];\n malformed: string[];\n} {\n const coursesDir = join(workspaceRoot, 'courses');\n const courses: string[] = [];\n const malformed: string[] = [];\n try {\n for (const e of readdirSync(coursesDir, { withFileTypes: true })) {\n if (!e.isDirectory()) continue;\n const dir = join(coursesDir, e.name);\n if (isCourse(dir)) courses.push(e.name);\n else if (isDir(join(dir, 'pages'))) malformed.push(e.name);\n }\n } catch {\n return { courses, malformed };\n }\n return { courses: courses.sort(), malformed: malformed.sort() };\n}\n\nexport function listCourses(workspaceRoot: string): string[] {\n return scanCourses(workspaceRoot).courses;\n}\n\nexport function listMalformedCourses(workspaceRoot: string): string[] {\n return scanCourses(workspaceRoot).malformed;\n}\n\nconst NOT_A_WORKSPACE =\n 'Not inside a Tessera workspace — no `courses/` directory was found at or above the current directory.';\n\nfunction malformedHint(malformed: string[]): string {\n if (malformed.length === 0) return '';\n return (\n `\\nSkipped (missing course.config.js):\\n` +\n malformed.map((c) => ` courses/${c}`).join('\\n')\n );\n}\n\nfunction listHint(workspaceRoot: string): string {\n const { courses, malformed } = scanCourses(workspaceRoot);\n if (courses.length === 0) {\n return (\n '\\nNo courses found. Create one with `tessera new <name>`.' +\n malformedHint(malformed)\n );\n }\n return (\n `\\nAvailable courses:\\n${courses.map((c) => ` ${c}`).join('\\n')}` +\n '\\nName one (`tessera <command> <course>`) or cd into its folder.' +\n malformedHint(malformed)\n );\n}\n\n// A name argument always wins; otherwise the cwd must itself be a course. There\n// is deliberately no \"single course → use it implicitly\" rule, so a bare command\n// never changes meaning when a second course is added.\nexport function resolveCourse(cwd: string, name?: string): ResolveResult {\n const here = resolve(cwd);\n\n if (name) {\n const nameError = validateProjectName(name, 'the name');\n if (nameError) {\n return {\n ok: false,\n error: `Invalid course name \"${name}\" — ${nameError}.`,\n };\n }\n const workspaceRoot = findWorkspaceRoot(here);\n if (!workspaceRoot) return { ok: false, error: NOT_A_WORKSPACE };\n const courseRoot = join(workspaceRoot, 'courses', name);\n if (!isCourse(courseRoot)) {\n return {\n ok: false,\n error: `Course \"${name}\" not found in courses/.${listHint(workspaceRoot)}`,\n };\n }\n return { ok: true, courseRoot, workspaceRoot };\n }\n\n if (isCourse(here)) {\n const workspaceRoot = findWorkspaceRoot(here) ?? dirname(dirname(here));\n return { ok: true, courseRoot: here, workspaceRoot };\n }\n\n const workspaceRoot = findWorkspaceRoot(here);\n if (!workspaceRoot) return { ok: false, error: NOT_A_WORKSPACE };\n return {\n ok: false,\n error: `No course specified.${listHint(workspaceRoot)}`,\n };\n}\n","import {\n copyFileSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\n// npm's tarball packing strips/renames leading-dot files, so templates store\n// them prefixed and we restore the dot on copy. (create-vite convention.)\nconst RENAME: Record<string, string> = {\n _gitignore: '.gitignore',\n _gitkeep: '.gitkeep',\n};\n\n// Text files get token substitution; everything else is copied byte-for-byte.\nconst TEXT = /\\.(svelte|js|ts|json|css|md|html)$/;\n\nfunction applyTokens(s: string, tokens: Record<string, string>): string {\n return s.replace(/__([A-Z_]+)__/g, (m, key) =>\n key in tokens ? tokens[key] : m,\n );\n}\n\nexport function copyTemplate(\n srcDir: string,\n destDir: string,\n tokens: Record<string, string>,\n): void {\n mkdirSync(destDir, { recursive: true });\n for (const entry of readdirSync(srcDir, { withFileTypes: true })) {\n const src = join(srcDir, entry.name);\n const dest = join(destDir, RENAME[entry.name] ?? entry.name);\n if (entry.isDirectory()) {\n copyTemplate(src, dest, tokens);\n } else if (TEXT.test(entry.name)) {\n writeFileSync(dest, applyTokens(readFileSync(src, 'utf-8'), tokens));\n } else {\n copyFileSync(src, dest);\n }\n }\n}\n","import { existsSync } from 'node:fs';\nimport { join, relative, resolve } from 'node:path';\nimport { findWorkspaceRoot } from './course-root.js';\nimport { validateProjectName, toTitleCase } from './project-name.js';\nimport { copyTemplate } from './template-copy.js';\nimport { resolvePackageRoot } from './package-root.js';\n\n// `tessera new <name>` — stamp a new course into courses/<name> inside the\n// current workspace. No install step: the workspace already owns the deps.\nexport function runNew(name: string | undefined, cwd: string): number {\n if (name === '--help' || name === '-h') {\n console.log(\n 'Usage: tessera new <name>\\n\\n' +\n 'Scaffold a new course into courses/<name> inside the current workspace.',\n );\n return 0;\n }\n if (!name) {\n console.error('Usage: tessera new <name>');\n return 1;\n }\n\n const nameError = validateProjectName(name, 'Course name');\n if (nameError) {\n console.error(`[tessera new] ${nameError}`);\n return 1;\n }\n\n const workspaceRoot = findWorkspaceRoot(resolve(cwd));\n if (!workspaceRoot) {\n console.error(\n '[tessera new] Not inside a Tessera workspace — run this from a workspace (a directory with a `courses/` folder).',\n );\n return 1;\n }\n\n const courseDir = join(workspaceRoot, 'courses', name);\n if (existsSync(courseDir)) {\n console.error(`[tessera new] Course \"${name}\" already exists.`);\n return 1;\n }\n\n const templateDir = join(resolvePackageRoot(), 'templates', 'course');\n copyTemplate(templateDir, courseDir, {\n PROJECT_TITLE: toTitleCase(name),\n });\n\n const rel = relative(workspaceRoot, courseDir);\n console.log(`\\nCreated ${rel}.\\n\\nNext steps:\\n pnpm tessera dev ${name}\\n`);\n return 0;\n}\n","import { cpSync, existsSync } from 'node:fs';\nimport { basename, join, relative } from 'node:path';\nimport { resolveCourse } from './course-root.js';\nimport { validateProjectName } from './project-name.js';\n\nconst HELP =\n 'Usage: tessera duplicate <source> <new>\\n\\n' +\n 'Copy courses/<source>/ to courses/<new>/ within the current workspace.';\n\n// Generated/build artifacts that should never travel with a verbatim copy. The\n// a11y throwaway build and Vite's cache live under node_modules, so they're\n// already pruned by the node_modules skip; the rest are belt-and-suspenders.\nconst SKIP = new Set(['node_modules', 'dist', 'a11y-report.json', '.vite']);\n\nfunction skip(srcPath: string): boolean {\n const name = basename(srcPath);\n return SKIP.has(name) || name.startsWith('.tessera');\n}\n\n// `tessera duplicate <source> <new>` — copy an existing course verbatim within\n// the current workspace. Unlike `new`, there is no template stamping: the JS\n// config (including its title) is copied untouched.\nexport function runDuplicate(\n source: string | undefined,\n target: string | undefined,\n cwd: string,\n): number {\n if (\n source === '--help' ||\n source === '-h' ||\n target === '--help' ||\n target === '-h'\n ) {\n console.log(HELP);\n return 0;\n }\n if (!source || !target) {\n console.error('Usage: tessera duplicate <source> <new>');\n return 1;\n }\n\n const nameError = validateProjectName(target, 'Course name');\n if (nameError) {\n console.error(`[tessera duplicate] ${nameError}`);\n return 1;\n }\n\n const resolved = resolveCourse(cwd, source);\n if (!resolved.ok) {\n console.error(`[tessera duplicate] ${resolved.error}`);\n return 1;\n }\n const { courseRoot: srcDir, workspaceRoot } = resolved;\n\n const destDir = join(workspaceRoot, 'courses', target);\n if (existsSync(destDir)) {\n console.error(`[tessera duplicate] Course \"${target}\" already exists.`);\n return 1;\n }\n\n cpSync(srcDir, destDir, {\n recursive: true,\n filter: (src) => src === srcDir || !skip(src),\n });\n\n const rel = relative(workspaceRoot, destDir);\n const srcRel = relative(workspaceRoot, srcDir);\n console.log(\n `\\nCreated ${rel} (duplicated from ${srcRel}).\\n\\n` +\n `Remember to update the title in ${rel}/course.config.js.\\n\\n` +\n `Next steps:\\n pnpm tessera dev ${target}\\n`,\n );\n return 0;\n}\n","#!/usr/bin/env node\nimport { runValidate } from './validate-cli.js';\nimport { runA11y } from './a11y-cli.js';\nimport { runNew } from './new-cli.js';\nimport { runDuplicate } from './duplicate-cli.js';\nimport { resolveCourse } from './course-root.js';\n\nconst USAGE = `Usage: tessera <command> [course] [options]\n\nCommands:\n new <name> Scaffold a new course into courses/<name>\n duplicate <source> <new> Copy courses/<source> to courses/<new>\n dev [course] Start the Vite dev server\n export [course] Build and package the course for its LMS standard\n validate [course] Fast static structure checks\n a11y [course] Runtime accessibility audit (builds + drives Playwright)\n check [course] Run validate, then a11y\n\nRun a command from inside a course folder, or name the course explicitly.\n\na11y/check options:\n --threshold <minor|moderate|serious|critical> Failing impact (default: serious)\n --build Force a fresh build first`;\n\n// The course is a leading positional: `tessera <cmd> [course] [flags]`. Only the\n// first token can be the course, and only when it isn't a flag — otherwise a flag\n// value (e.g. the `serious` in `--threshold serious`) would be misread as a name.\nexport function splitCourseArg(rest: string[]): {\n course?: string;\n flags: string[];\n} {\n if (rest.length > 0 && !rest[0].startsWith('-')) {\n return { course: rest[0], flags: rest.slice(1) };\n }\n return { course: undefined, flags: rest };\n}\n\nexport async function main(\n argv: string[],\n cwd: string = process.cwd(),\n): Promise<number> {\n const [sub, ...rest] = argv;\n\n if (sub === 'new') return runNew(rest[0], cwd);\n if (sub === 'duplicate') return runDuplicate(rest[0], rest[1], cwd);\n\n switch (sub) {\n case 'dev':\n case 'export':\n case 'validate':\n case 'a11y':\n case 'check': {\n if (rest.includes('--help') || rest.includes('-h')) {\n console.log(USAGE);\n return 0;\n }\n const { course, flags } = splitCourseArg(rest);\n const resolved = resolveCourse(cwd, course);\n if (!resolved.ok) {\n console.error(`[tessera] ${resolved.error}`);\n return 1;\n }\n const { courseRoot, workspaceRoot } = resolved;\n\n switch (sub) {\n case 'dev': {\n const { runDev } = await import('./build-commands.js');\n return runDev(courseRoot, workspaceRoot);\n }\n case 'export': {\n const { runBuild } = await import('./build-commands.js');\n return runBuild(courseRoot, workspaceRoot);\n }\n case 'validate':\n return runValidate(courseRoot);\n case 'check': {\n const validateCode = runValidate(courseRoot);\n if (validateCode !== 0) return validateCode;\n return runA11y(courseRoot, workspaceRoot, flags);\n }\n case 'a11y':\n return runA11y(courseRoot, workspaceRoot, flags);\n }\n return 0;\n }\n case '--help':\n case '-h':\n console.log(USAGE);\n return 0;\n case undefined:\n console.error(`No command given.\\n\\n${USAGE}`);\n return 1;\n default:\n console.error(`Unknown command: ${sub}\\n\\n${USAGE}`);\n return 1;\n }\n}\n\n// import.meta.main is true only when this module is the program entry point,\n// and resolves symlinks itself (pnpm/npm bin shims) — Node >= 24.\nif (import.meta.main) {\n void main(process.argv.slice(2)).then((code) => process.exit(code));\n}\n"],"mappings":";;;;;AAEA,SAAgB,YAAY,aAA6B;CACvD,MAAM,EAAE,QAAQ,aAAa,gBAAgB,WAAW;CAExD,uBAAuB;EAAE;EAAQ;CAAS,CAAC;CAE3C,IAAI,OAAO,SAAS,GAAG;EACrB,MAAM,UACJ,0BAA0B,OAAO,OAAO,cACvC,SAAS,SAAS,IAAI,QAAQ,SAAS,OAAO,eAAe,MAC9D;EACF,QAAQ,MAAM,aAAa,QAAQ,QAAQ;EAC3C,OAAO;CACT;CAEA,IAAI,SAAS,SAAS,GACpB,QAAQ,IACN,oCAAoC,SAAS,OAAO,oBACtD;MAEA,QAAQ,IACN,+DACF;CAEF,QAAQ,IACN,iHACF;CACA,OAAO;AACT;;;AC3BA,MAAM,mBAAkC;CACtC;CACA;CACA;CACA;AACF;AAMA,SAAgB,cAAc,MAAgC;CAC5D,IAAI;CACJ,IAAI,UAAU;CAEd,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,IAAI,QAAQ,eAAe;GACzB,MAAM,QAAQ,KAAK,EAAE;GACrB,IAAI,CAAC,iBAAiB,SAAS,KAAK,GAClC,OAAO;IACL,IAAI;IACJ,OAAO,+BAA+B,iBAAiB,KAAK,IAAI;GAClE;GAEF,YAAY;EACd,OAAO,IAAI,QAAQ,WACjB,UAAU;OAEV,OAAO;GAAE,IAAI;GAAO,OAAO,qBAAqB;EAAM;CAE1D;CAEA,MAAM,OAAqB,EAAE,QAAQ;CACrC,IAAI,cAAc,KAAA,GAAW,KAAK,YAAY;CAC9C,OAAO;EAAE,IAAI;EAAM;CAAK;AAC1B;AAEA,eAAsB,QACpB,aACA,eACA,MACiB;CACjB,MAAM,SAAS,cAAc,IAAI;CACjC,IAAI,CAAC,OAAO,IAAI;EACd,QAAQ,MAAM,kBAAkB,OAAO,OAAO;EAC9C,OAAO;CACT;CACA,OAAO,SAAS,aAAa,eAAe,OAAO,IAAI;AACzD;;;AC7CA,SAAgB,oBACd,MACA,QAAQ,gBACO;CACf,IAAI,CAAC,MAAM,OAAO,GAAG,MAAM;CAC3B,IAAI,KAAK,SAAS,KAAK,OAAO,GAAG,MAAM;CACvC,IAAI,SAAS,KAAK,YAAY,GAAG,OAAO,GAAG,MAAM;CACjD,IAAI,CAAC,YAAY,KAAK,IAAI,GACxB,OAAO,GAAG,MAAM;CAElB,IAAI,CAAC,iBAAiB,KAAK,IAAI,GAC7B,OAAO,GAAG,MAAM;CAElB,OAAO;AACT;AAEA,SAAgB,YAAY,MAAsB;CAChD,OAAO,KACJ,MAAM,UAAU,EAChB,OAAO,OAAO,EACd,KAAK,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,CAAC,EACjD,KAAK,GAAG;AACb;;;AChBA,SAAS,MAAM,MAAuB;CACpC,IAAI;EACF,OAAO,SAAS,IAAI,EAAE,YAAY;CACpC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,SAAS,KAAsB;CACtC,OAAO,WAAW,KAAK,KAAK,kBAAkB,CAAC;AACjD;AAIA,SAAgB,kBAAkB,KAA4B;CAC5D,KAAK,IAAI,MAAM,QAAQ,GAAG,IAAK,MAAM,QAAQ,GAAG,GAAG;EACjD,IAAI,MAAM,KAAK,KAAK,SAAS,CAAC,GAAG,OAAO;EAExC,IADe,QAAQ,GACd,MAAM,KAAK,OAAO;CAC7B;AACF;AAEA,SAAS,YAAY,eAGnB;CACA,MAAM,aAAa,KAAK,eAAe,SAAS;CAChD,MAAM,UAAoB,CAAC;CAC3B,MAAM,YAAsB,CAAC;CAC7B,IAAI;EACF,KAAK,MAAM,KAAK,YAAY,YAAY,EAAE,eAAe,KAAK,CAAC,GAAG;GAChE,IAAI,CAAC,EAAE,YAAY,GAAG;GACtB,MAAM,MAAM,KAAK,YAAY,EAAE,IAAI;GACnC,IAAI,SAAS,GAAG,GAAG,QAAQ,KAAK,EAAE,IAAI;QACjC,IAAI,MAAM,KAAK,KAAK,OAAO,CAAC,GAAG,UAAU,KAAK,EAAE,IAAI;EAC3D;CACF,QAAQ;EACN,OAAO;GAAE;GAAS;EAAU;CAC9B;CACA,OAAO;EAAE,SAAS,QAAQ,KAAK;EAAG,WAAW,UAAU,KAAK;CAAE;AAChE;AAUA,MAAM,kBACJ;AAEF,SAAS,cAAc,WAA6B;CAClD,IAAI,UAAU,WAAW,GAAG,OAAO;CACnC,OACE,4CACA,UAAU,KAAK,MAAM,aAAa,GAAG,EAAE,KAAK,IAAI;AAEpD;AAEA,SAAS,SAAS,eAA+B;CAC/C,MAAM,EAAE,SAAS,cAAc,YAAY,aAAa;CACxD,IAAI,QAAQ,WAAW,GACrB,OACE,8DACA,cAAc,SAAS;CAG3B,OACE,yBAAyB,QAAQ,KAAK,MAAM,KAAK,GAAG,EAAE,KAAK,IAAI,EAAA;oEAE/D,cAAc,SAAS;AAE3B;AAKA,SAAgB,cAAc,KAAa,MAA8B;CACvE,MAAM,OAAO,QAAQ,GAAG;CAExB,IAAI,MAAM;EACR,MAAM,YAAY,oBAAoB,MAAM,UAAU;EACtD,IAAI,WACF,OAAO;GACL,IAAI;GACJ,OAAO,wBAAwB,KAAK,MAAM,UAAU;EACtD;EAEF,MAAM,gBAAgB,kBAAkB,IAAI;EAC5C,IAAI,CAAC,eAAe,OAAO;GAAE,IAAI;GAAO,OAAO;EAAgB;EAC/D,MAAM,aAAa,KAAK,eAAe,WAAW,IAAI;EACtD,IAAI,CAAC,SAAS,UAAU,GACtB,OAAO;GACL,IAAI;GACJ,OAAO,WAAW,KAAK,0BAA0B,SAAS,aAAa;EACzE;EAEF,OAAO;GAAE,IAAI;GAAM;GAAY;EAAc;CAC/C;CAEA,IAAI,SAAS,IAAI,GAEf,OAAO;EAAE,IAAI;EAAM,YAAY;EAAM,eADf,kBAAkB,IAAI,KAAK,QAAQ,QAAQ,IAAI,CAAC;CACnB;CAGrD,MAAM,gBAAgB,kBAAkB,IAAI;CAC5C,IAAI,CAAC,eAAe,OAAO;EAAE,IAAI;EAAO,OAAO;CAAgB;CAC/D,OAAO;EACL,IAAI;EACJ,OAAO,uBAAuB,SAAS,aAAa;CACtD;AACF;;;AClHA,MAAM,SAAiC;CACrC,YAAY;CACZ,UAAU;AACZ;AAGA,MAAM,OAAO;AAEb,SAAS,YAAY,GAAW,QAAwC;CACtE,OAAO,EAAE,QAAQ,mBAAmB,GAAG,QACrC,OAAO,SAAS,OAAO,OAAO,CAChC;AACF;AAEA,SAAgB,aACd,QACA,SACA,QACM;CACN,UAAU,SAAS,EAAE,WAAW,KAAK,CAAC;CACtC,KAAK,MAAM,SAAS,YAAY,QAAQ,EAAE,eAAe,KAAK,CAAC,GAAG;EAChE,MAAM,MAAM,KAAK,QAAQ,MAAM,IAAI;EACnC,MAAM,OAAO,KAAK,SAAS,OAAO,MAAM,SAAS,MAAM,IAAI;EAC3D,IAAI,MAAM,YAAY,GACpB,aAAa,KAAK,MAAM,MAAM;OACzB,IAAI,KAAK,KAAK,MAAM,IAAI,GAC7B,cAAc,MAAM,YAAY,aAAa,KAAK,OAAO,GAAG,MAAM,CAAC;OAEnE,aAAa,KAAK,IAAI;CAE1B;AACF;;;ACjCA,SAAgB,OAAO,MAA0B,KAAqB;CACpE,IAAI,SAAS,YAAY,SAAS,MAAM;EACtC,QAAQ,IACN,sGAEF;EACA,OAAO;CACT;CACA,IAAI,CAAC,MAAM;EACT,QAAQ,MAAM,2BAA2B;EACzC,OAAO;CACT;CAEA,MAAM,YAAY,oBAAoB,MAAM,aAAa;CACzD,IAAI,WAAW;EACb,QAAQ,MAAM,iBAAiB,WAAW;EAC1C,OAAO;CACT;CAEA,MAAM,gBAAgB,kBAAkB,QAAQ,GAAG,CAAC;CACpD,IAAI,CAAC,eAAe;EAClB,QAAQ,MACN,kHACF;EACA,OAAO;CACT;CAEA,MAAM,YAAY,KAAK,eAAe,WAAW,IAAI;CACrD,IAAI,WAAW,SAAS,GAAG;EACzB,QAAQ,MAAM,yBAAyB,KAAK,kBAAkB;EAC9D,OAAO;CACT;CAGA,aADoB,KAAK,mBAAmB,GAAG,aAAa,QACrC,GAAG,WAAW,EACnC,eAAe,YAAY,IAAI,EACjC,CAAC;CAED,MAAM,MAAM,SAAS,eAAe,SAAS;CAC7C,QAAQ,IAAI,aAAa,IAAI,uCAAuC,KAAK,GAAG;CAC5E,OAAO;AACT;;;AC7CA,MAAM,OACJ;AAMF,MAAM,OAAO,IAAI,IAAI;CAAC;CAAgB;CAAQ;CAAoB;AAAO,CAAC;AAE1E,SAAS,KAAK,SAA0B;CACtC,MAAM,OAAO,SAAS,OAAO;CAC7B,OAAO,KAAK,IAAI,IAAI,KAAK,KAAK,WAAW,UAAU;AACrD;AAKA,SAAgB,aACd,QACA,QACA,KACQ;CACR,IACE,WAAW,YACX,WAAW,QACX,WAAW,YACX,WAAW,MACX;EACA,QAAQ,IAAI,IAAI;EAChB,OAAO;CACT;CACA,IAAI,CAAC,UAAU,CAAC,QAAQ;EACtB,QAAQ,MAAM,yCAAyC;EACvD,OAAO;CACT;CAEA,MAAM,YAAY,oBAAoB,QAAQ,aAAa;CAC3D,IAAI,WAAW;EACb,QAAQ,MAAM,uBAAuB,WAAW;EAChD,OAAO;CACT;CAEA,MAAM,WAAW,cAAc,KAAK,MAAM;CAC1C,IAAI,CAAC,SAAS,IAAI;EAChB,QAAQ,MAAM,uBAAuB,SAAS,OAAO;EACrD,OAAO;CACT;CACA,MAAM,EAAE,YAAY,QAAQ,kBAAkB;CAE9C,MAAM,UAAU,KAAK,eAAe,WAAW,MAAM;CACrD,IAAI,WAAW,OAAO,GAAG;EACvB,QAAQ,MAAM,+BAA+B,OAAO,kBAAkB;EACtE,OAAO;CACT;CAEA,OAAO,QAAQ,SAAS;EACtB,WAAW;EACX,SAAS,QAAQ,QAAQ,UAAU,CAAC,KAAK,GAAG;CAC9C,CAAC;CAED,MAAM,MAAM,SAAS,eAAe,OAAO;CAC3C,MAAM,SAAS,SAAS,eAAe,MAAM;CAC7C,QAAQ,IACN,aAAa,IAAI,oBAAoB,OAAO,wCACP,IAAI,wDACJ,OAAO,GAC9C;CACA,OAAO;AACT;;;AClEA,MAAM,QAAQ;;;;;;;;;;;;;;;;AAoBd,SAAgB,eAAe,MAG7B;CACA,IAAI,KAAK,SAAS,KAAK,CAAC,KAAK,GAAG,WAAW,GAAG,GAC5C,OAAO;EAAE,QAAQ,KAAK;EAAI,OAAO,KAAK,MAAM,CAAC;CAAE;CAEjD,OAAO;EAAE,QAAQ,KAAA;EAAW,OAAO;CAAK;AAC1C;AAEA,eAAsB,KACpB,MACA,MAAc,QAAQ,IAAI,GACT;CACjB,MAAM,CAAC,KAAK,GAAG,QAAQ;CAEvB,IAAI,QAAQ,OAAO,OAAO,OAAO,KAAK,IAAI,GAAG;CAC7C,IAAI,QAAQ,aAAa,OAAO,aAAa,KAAK,IAAI,KAAK,IAAI,GAAG;CAElE,QAAQ,KAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,SAAS;GACZ,IAAI,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,IAAI,GAAG;IAClD,QAAQ,IAAI,KAAK;IACjB,OAAO;GACT;GACA,MAAM,EAAE,QAAQ,UAAU,eAAe,IAAI;GAC7C,MAAM,WAAW,cAAc,KAAK,MAAM;GAC1C,IAAI,CAAC,SAAS,IAAI;IAChB,QAAQ,MAAM,aAAa,SAAS,OAAO;IAC3C,OAAO;GACT;GACA,MAAM,EAAE,YAAY,kBAAkB;GAEtC,QAAQ,KAAR;IACE,KAAK,OAAO;KACV,MAAM,EAAE,WAAW,MAAM,OAAO;KAChC,OAAO,OAAO,YAAY,aAAa;IACzC;IACA,KAAK,UAAU;KACb,MAAM,EAAE,aAAa,MAAM,OAAO;KAClC,OAAO,SAAS,YAAY,aAAa;IAC3C;IACA,KAAK,YACH,OAAO,YAAY,UAAU;IAC/B,KAAK,SAAS;KACZ,MAAM,eAAe,YAAY,UAAU;KAC3C,IAAI,iBAAiB,GAAG,OAAO;KAC/B,OAAO,QAAQ,YAAY,eAAe,KAAK;IACjD;IACA,KAAK,QACH,OAAO,QAAQ,YAAY,eAAe,KAAK;GACnD;GACA,OAAO;EACT;EACA,KAAK;EACL,KAAK;GACH,QAAQ,IAAI,KAAK;GACjB,OAAO;EACT,KAAK,KAAA;GACH,QAAQ,MAAM,wBAAwB,OAAO;GAC7C,OAAO;EACT;GACE,QAAQ,MAAM,oBAAoB,IAAI,MAAM,OAAO;GACnD,OAAO;CACX;AACF;AAIA,IAAI,OAAO,KAAK,MACd,KAAU,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAE,MAAM,SAAS,QAAQ,KAAK,IAAI,CAAC"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/validate-cli.ts","../../src/plugin/a11y-cli.ts","../../src/plugin/project-name.ts","../../src/plugin/course-root.ts","../../src/plugin/template-copy.ts","../../src/plugin/new-cli.ts","../../src/plugin/duplicate-cli.ts","../../src/plugin/cli.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport { validateProject, reportValidationIssues } from './validation.js';\n\nexport function runValidate(\n projectRoot: string,\n { showA11yTip = true }: { showA11yTip?: boolean } = {},\n): number {\n const { errors, warnings } = validateProject(projectRoot);\n\n reportValidationIssues({ errors, warnings });\n\n if (errors.length > 0) {\n const summary =\n `Validation failed with ${errors.length} error(s)` +\n (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +\n '.';\n console.error(`\\n\\x1b[31m${summary}\\x1b[0m`);\n return 1;\n }\n\n if (warnings.length > 0) {\n console.log(\n `\\n\\x1b[33mValidation passed with ${warnings.length} warning(s).\\x1b[0m`,\n );\n } else {\n console.log(\n '\\x1b[32m[tessera]\\x1b[0m Validation passed — no issues found.',\n );\n }\n if (showA11yTip) {\n console.log(\n `\\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: pnpm a11y ${basename(projectRoot)}\\x1b[0m`,\n );\n }\n return 0;\n}\n","import { runAudit, type AuditOptions, type ImpactLevel } from './a11y/audit.js';\n\nconst VALID_THRESHOLDS: ImpactLevel[] = [\n 'minor',\n 'moderate',\n 'serious',\n 'critical',\n];\n\nexport type ParsedA11yArgs =\n | { ok: true; args: AuditOptions }\n | { ok: false; error: string };\n\nexport function parseA11yArgs(argv: string[]): ParsedA11yArgs {\n let threshold: ImpactLevel | undefined;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === '--threshold') {\n const value = argv[++i] as ImpactLevel;\n if (!VALID_THRESHOLDS.includes(value)) {\n return {\n ok: false,\n error: `--threshold must be one of: ${VALID_THRESHOLDS.join(', ')}`,\n };\n }\n threshold = value;\n } else {\n return { ok: false, error: `Unknown argument: ${arg}` };\n }\n }\n\n const args: AuditOptions = {};\n if (threshold !== undefined) args.threshold = threshold;\n return { ok: true, args };\n}\n\nexport async function runA11y(\n projectRoot: string,\n workspaceRoot: string,\n argv: string[],\n): Promise<number> {\n const parsed = parseA11yArgs(argv);\n if (!parsed.ok) {\n console.error(`[tessera a11y] ${parsed.error}`);\n return 1;\n }\n return runAudit(projectRoot, workspaceRoot, parsed.args);\n}\n","// Pure, dependency-free helpers shared by the `tessera new` subcommand and the\n// `create-tessera` scaffolder. Kept import-free so create-tessera can bundle it\n// at build time without dragging in the rest of the plugin (vite, svelte, …).\n\n// npm package name rules: 1-214 chars, lowercase, must start with [a-z0-9],\n// allowed chars [a-z0-9._-], no leading dot or underscore.\nexport function validateProjectName(\n name: string,\n label = 'Project name',\n): string | null {\n if (!name) return `${label} is required`;\n if (name.length > 214) return `${label} must be 214 characters or fewer`;\n if (name !== name.toLowerCase()) return `${label} must be lowercase`;\n if (!/^[a-z0-9]/.test(name)) {\n return `${label} must start with a letter or digit`;\n }\n if (!/^[a-z0-9._-]+$/.test(name)) {\n return `${label} may only contain lowercase letters, digits, \"-\", \"_\", and \".\"`;\n }\n return null;\n}\n\nexport function toTitleCase(slug: string): string {\n return slug\n .split(/[-_.\\s]+/)\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","import { existsSync, readdirSync, statSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { validateProjectName } from './project-name.js';\n\nexport interface ResolvedCourse {\n ok: true;\n courseRoot: string;\n workspaceRoot: string;\n}\n\nexport type ResolveResult = ResolvedCourse | { ok: false; error: string };\n\nfunction isDir(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\nfunction isCourse(dir: string): boolean {\n return existsSync(join(dir, 'course.config.js'));\n}\n\n// A workspace is the nearest ancestor holding a courses/ directory. The walk\n// includes the starting dir, so the workspace root resolves to itself.\nexport function findWorkspaceRoot(cwd: string): string | null {\n for (let dir = resolve(cwd); ; dir = dirname(dir)) {\n if (isDir(join(dir, 'courses'))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n }\n}\n\nfunction scanCourses(workspaceRoot: string): {\n courses: string[];\n malformed: string[];\n} {\n const coursesDir = join(workspaceRoot, 'courses');\n const courses: string[] = [];\n const malformed: string[] = [];\n try {\n for (const e of readdirSync(coursesDir, { withFileTypes: true })) {\n if (!e.isDirectory()) continue;\n const dir = join(coursesDir, e.name);\n if (isCourse(dir)) courses.push(e.name);\n else if (isDir(join(dir, 'pages'))) malformed.push(e.name);\n }\n } catch {\n return { courses, malformed };\n }\n return { courses: courses.sort(), malformed: malformed.sort() };\n}\n\nexport function listCourses(workspaceRoot: string): string[] {\n return scanCourses(workspaceRoot).courses;\n}\n\nexport function listMalformedCourses(workspaceRoot: string): string[] {\n return scanCourses(workspaceRoot).malformed;\n}\n\nconst NOT_A_WORKSPACE =\n 'Not inside a Tessera workspace — no `courses/` directory was found at or above the current directory.';\n\nfunction malformedHint(malformed: string[]): string {\n if (malformed.length === 0) return '';\n return (\n `\\nSkipped (missing course.config.js):\\n` +\n malformed.map((c) => ` courses/${c}`).join('\\n')\n );\n}\n\nfunction listHint(workspaceRoot: string): string {\n const { courses, malformed } = scanCourses(workspaceRoot);\n if (courses.length === 0) {\n return (\n '\\nNo courses found. Create one with `tessera new <name>`.' +\n malformedHint(malformed)\n );\n }\n return (\n `\\nAvailable courses:\\n${courses.map((c) => ` ${c}`).join('\\n')}` +\n '\\nName one (`tessera <command> <course>`) or cd into its folder.' +\n malformedHint(malformed)\n );\n}\n\n// A name argument always wins; otherwise the cwd must itself be a course. There\n// is deliberately no \"single course → use it implicitly\" rule, so a bare command\n// never changes meaning when a second course is added.\nexport function resolveCourse(cwd: string, name?: string): ResolveResult {\n const here = resolve(cwd);\n\n if (name) {\n const nameError = validateProjectName(name, 'the name');\n if (nameError) {\n return {\n ok: false,\n error: `Invalid course name \"${name}\" — ${nameError}.`,\n };\n }\n const workspaceRoot = findWorkspaceRoot(here);\n if (!workspaceRoot) return { ok: false, error: NOT_A_WORKSPACE };\n const courseRoot = join(workspaceRoot, 'courses', name);\n if (!isCourse(courseRoot)) {\n return {\n ok: false,\n error: `Course \"${name}\" not found in courses/.${listHint(workspaceRoot)}`,\n };\n }\n return { ok: true, courseRoot, workspaceRoot };\n }\n\n if (isCourse(here)) {\n const workspaceRoot = findWorkspaceRoot(here) ?? dirname(dirname(here));\n return { ok: true, courseRoot: here, workspaceRoot };\n }\n\n const workspaceRoot = findWorkspaceRoot(here);\n if (!workspaceRoot) return { ok: false, error: NOT_A_WORKSPACE };\n return {\n ok: false,\n error: `No course specified.${listHint(workspaceRoot)}`,\n };\n}\n","import {\n copyFileSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n writeFileSync,\n} from 'node:fs';\nimport { join } from 'node:path';\n\n// npm's tarball packing strips/renames leading-dot files, so templates store\n// them prefixed and we restore the dot on copy. (create-vite convention.)\nconst RENAME: Record<string, string> = {\n _gitignore: '.gitignore',\n _gitkeep: '.gitkeep',\n};\n\n// Text files get token substitution; everything else is copied byte-for-byte.\nconst TEXT = /\\.(svelte|js|ts|json|css|md|html)$/;\n\nfunction applyTokens(s: string, tokens: Record<string, string>): string {\n return s.replace(/__([A-Z_]+)__/g, (m, key) =>\n key in tokens ? tokens[key] : m,\n );\n}\n\nexport function copyTemplate(\n srcDir: string,\n destDir: string,\n tokens: Record<string, string>,\n): void {\n mkdirSync(destDir, { recursive: true });\n for (const entry of readdirSync(srcDir, { withFileTypes: true })) {\n const src = join(srcDir, entry.name);\n const dest = join(destDir, RENAME[entry.name] ?? entry.name);\n if (entry.isDirectory()) {\n copyTemplate(src, dest, tokens);\n } else if (TEXT.test(entry.name)) {\n writeFileSync(dest, applyTokens(readFileSync(src, 'utf-8'), tokens));\n } else {\n copyFileSync(src, dest);\n }\n }\n}\n","import { existsSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport { join, relative, resolve } from 'node:path';\nimport { findWorkspaceRoot } from './course-root.js';\nimport { validateProjectName, toTitleCase } from './project-name.js';\nimport { copyTemplate } from './template-copy.js';\nimport { resolvePackageRoot } from './package-root.js';\n\n// `tessera new <name>` — stamp a new course into courses/<name> inside the\n// current workspace. No install step: the workspace already owns the deps.\nexport function runNew(name: string | undefined, cwd: string): number {\n if (name === '--help' || name === '-h') {\n console.log(\n 'Usage: tessera new <name>\\n\\n' +\n 'Scaffold a new course into courses/<name> inside the current workspace.',\n );\n return 0;\n }\n if (!name) {\n console.error('Usage: tessera new <name>');\n return 1;\n }\n\n const nameError = validateProjectName(name, 'Course name');\n if (nameError) {\n console.error(`[tessera new] ${nameError}`);\n return 1;\n }\n\n const workspaceRoot = findWorkspaceRoot(resolve(cwd));\n if (!workspaceRoot) {\n console.error(\n '[tessera new] Not inside a Tessera workspace — run this from a workspace (a directory with a `courses/` folder).',\n );\n return 1;\n }\n\n const courseDir = join(workspaceRoot, 'courses', name);\n if (existsSync(courseDir)) {\n console.error(`[tessera new] Course \"${name}\" already exists.`);\n return 1;\n }\n\n const templateDir = join(resolvePackageRoot(), 'templates', 'course');\n copyTemplate(templateDir, courseDir, {\n PROJECT_TITLE: toTitleCase(name),\n COURSE_ID: `urn:uuid:${randomUUID()}`,\n });\n\n const rel = relative(workspaceRoot, courseDir);\n console.log(`\\nCreated ${rel}.\\n\\nNext steps:\\n pnpm tessera dev ${name}\\n`);\n return 0;\n}\n","import { cpSync, existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport { basename, join, relative } from 'node:path';\nimport { resolveCourse } from './course-root.js';\nimport { validateProjectName } from './project-name.js';\n\n// The top-level `id` property as scaffolders write it: the key (bare or quoted)\n// first on its own line, then its value. Anchoring to line start keeps `id:`\n// inside a comment or a string value from matching, and CourseConfig has no\n// nested `id`, so the first match is always the course identity.\nconst ID_LINE =\n /^([ \\t]*'?id'?[ \\t]*:[ \\t]*)('(?:[^'\\\\]|\\\\.)*'|\"(?:[^\"\\\\]|\\\\.)*\"|`(?:[^`\\\\]|\\\\.)*`|[^\\n,}]*)/m;\nconst DEFAULT_OBJECT = /export\\s+default\\s*\\{/;\n\n// A verbatim copy inherits the source's `id`; mint a fresh one so the duplicate\n// is a distinct course (own storage key + LRS activity id).\nfunction reidentifyCourse(courseRoot: string): void {\n const configPath = join(courseRoot, 'course.config.js');\n if (!existsSync(configPath)) return;\n const text = readFileSync(configPath, 'utf-8');\n const newId = `'urn:uuid:${randomUUID()}'`;\n\n if (ID_LINE.test(text)) {\n writeFileSync(configPath, text.replace(ID_LINE, `$1${newId}`));\n return;\n }\n const obj = DEFAULT_OBJECT.exec(text);\n if (obj) {\n const at = obj.index + obj[0].length;\n writeFileSync(\n configPath,\n `${text.slice(0, at)}\\n id: ${newId},${text.slice(at)}`,\n );\n return;\n }\n console.warn(\n `[tessera duplicate] Could not set a unique id in ${configPath} — the copy shares the source's identity. Add a unique \"id\" manually.`,\n );\n}\n\nconst HELP =\n 'Usage: tessera duplicate <source> <new>\\n\\n' +\n 'Copy courses/<source>/ to courses/<new>/ within the current workspace.';\n\n// Generated/build artifacts that should never travel with a verbatim copy. The\n// a11y throwaway build and Vite's cache live under node_modules, so they're\n// already pruned by the node_modules skip; the rest are belt-and-suspenders.\nconst SKIP = new Set(['node_modules', 'dist', 'a11y-report.json', '.vite']);\n\nfunction skip(srcPath: string): boolean {\n const name = basename(srcPath);\n return SKIP.has(name) || name.startsWith('.tessera');\n}\n\n// `tessera duplicate <source> <new>` — copy an existing course verbatim within\n// the current workspace. Unlike `new`, there is no template stamping: the JS\n// config (including its title) is copied untouched.\nexport function runDuplicate(\n source: string | undefined,\n target: string | undefined,\n cwd: string,\n): number {\n if (\n source === '--help' ||\n source === '-h' ||\n target === '--help' ||\n target === '-h'\n ) {\n console.log(HELP);\n return 0;\n }\n if (!source || !target) {\n console.error('Usage: tessera duplicate <source> <new>');\n return 1;\n }\n\n const nameError = validateProjectName(target, 'Course name');\n if (nameError) {\n console.error(`[tessera duplicate] ${nameError}`);\n return 1;\n }\n\n const resolved = resolveCourse(cwd, source);\n if (!resolved.ok) {\n console.error(`[tessera duplicate] ${resolved.error}`);\n return 1;\n }\n const { courseRoot: srcDir, workspaceRoot } = resolved;\n\n const destDir = join(workspaceRoot, 'courses', target);\n if (existsSync(destDir)) {\n console.error(`[tessera duplicate] Course \"${target}\" already exists.`);\n return 1;\n }\n\n cpSync(srcDir, destDir, {\n recursive: true,\n filter: (src) => src === srcDir || !skip(src),\n });\n reidentifyCourse(destDir);\n\n const rel = relative(workspaceRoot, destDir);\n const srcRel = relative(workspaceRoot, srcDir);\n console.log(\n `\\nCreated ${rel} (duplicated from ${srcRel}).\\n\\n` +\n `Remember to update the title in ${rel}/course.config.js.\\n\\n` +\n `Next steps:\\n pnpm tessera dev ${target}\\n`,\n );\n return 0;\n}\n","#!/usr/bin/env node\nimport { runValidate } from './validate-cli.js';\nimport { runA11y } from './a11y-cli.js';\nimport { runNew } from './new-cli.js';\nimport { runDuplicate } from './duplicate-cli.js';\nimport { resolveCourse } from './course-root.js';\n\nconst USAGE = `Usage: tessera <command> [course] [options]\n\nCommands:\n new <name> Scaffold a new course into courses/<name>\n duplicate <source> <new> Copy courses/<source> to courses/<new>\n dev [course] Start the Vite dev server\n export [course] Build and package the course for its LMS standard\n validate [course] Fast static structure checks\n a11y [course] Runtime accessibility audit (builds + drives Playwright)\n check [course] Run validate, then a11y\n\nRun a command from inside a course folder, or name the course explicitly.\n\na11y/check options:\n --threshold <minor|moderate|serious|critical> Failing impact (default: serious)`;\n\n// The course is a leading positional: `tessera <cmd> [course] [flags]`. Only the\n// first token can be the course, and only when it isn't a flag — otherwise a flag\n// value (e.g. the `serious` in `--threshold serious`) would be misread as a name.\nexport function splitCourseArg(rest: string[]): {\n course?: string;\n flags: string[];\n} {\n if (rest.length > 0 && !rest[0].startsWith('-')) {\n return { course: rest[0], flags: rest.slice(1) };\n }\n return { course: undefined, flags: rest };\n}\n\ntype CourseCommand = (\n courseRoot: string,\n workspaceRoot: string,\n flags: string[],\n) => number | Promise<number>;\n\nconst COURSE_COMMANDS: Record<string, CourseCommand> = {\n dev: async (courseRoot, workspaceRoot) =>\n (await import('./build-commands.js')).runDev(courseRoot, workspaceRoot),\n export: async (courseRoot, workspaceRoot) =>\n (await import('./build-commands.js')).runBuild(courseRoot, workspaceRoot),\n validate: (courseRoot) => runValidate(courseRoot),\n a11y: (courseRoot, workspaceRoot, flags) =>\n runA11y(courseRoot, workspaceRoot, flags),\n check: (courseRoot, workspaceRoot, flags) => {\n const validateCode = runValidate(courseRoot, { showA11yTip: false });\n if (validateCode !== 0) return validateCode;\n return runA11y(courseRoot, workspaceRoot, flags);\n },\n};\n\nexport async function main(\n argv: string[],\n cwd: string = process.cwd(),\n): Promise<number> {\n const [sub, ...rest] = argv;\n\n if (sub === 'new') return runNew(rest[0], cwd);\n if (sub === 'duplicate') return runDuplicate(rest[0], rest[1], cwd);\n\n if (sub !== undefined && Object.hasOwn(COURSE_COMMANDS, sub)) {\n if (rest.includes('--help') || rest.includes('-h')) {\n console.log(USAGE);\n return 0;\n }\n const { course, flags } = splitCourseArg(rest);\n const resolved = resolveCourse(cwd, course);\n if (!resolved.ok) {\n console.error(`[tessera] ${resolved.error}`);\n return 1;\n }\n return COURSE_COMMANDS[sub](\n resolved.courseRoot,\n resolved.workspaceRoot,\n flags,\n );\n }\n\n if (sub === '--help' || sub === '-h') {\n console.log(USAGE);\n return 0;\n }\n if (sub === undefined) {\n console.error(`No command given.\\n\\n${USAGE}`);\n return 1;\n }\n console.error(`Unknown command: ${sub}\\n\\n${USAGE}`);\n return 1;\n}\n\n// import.meta.main is true only when this module is the program entry point,\n// and resolves symlinks itself (pnpm/npm bin shims) — Node >= 24.\nif (import.meta.main) {\n void main(process.argv.slice(2)).then((code) => process.exit(code));\n}\n"],"mappings":"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"}
@@ -1,11 +1,260 @@
1
1
  import { Plugin } from "vite";
2
+ import { SvelteMap, SvelteSet } from "svelte/reactivity";
2
3
 
4
+ //#region src/runtime/xapi/types.d.ts
5
+ /**
6
+ * xAPI types used by the publisher and registry. These mirror the relevant
7
+ * subset of the xAPI 1.0.3 spec — the publisher only models Agents (not
8
+ * Groups) for v1, and only the statement fields actually exercised by
9
+ * Tessera or surfaced to authors.
10
+ */
11
+ /**
12
+ * Identified xAPI Agent. Exactly one of `mbox` / `mbox_sha1sum` / `openid` /
13
+ * `account` must be present (the IFI rule). The publisher validates this on
14
+ * any actor it resolves; values that fail produce a runtime error rather
15
+ * than a silent LRS 400.
16
+ */
17
+ interface XAPIAgent {
18
+ name?: string;
19
+ mbox?: string;
20
+ mbox_sha1sum?: string;
21
+ openid?: string;
22
+ account?: {
23
+ homePage: string;
24
+ name: string;
25
+ };
26
+ objectType?: 'Agent';
27
+ member?: unknown;
28
+ }
29
+ //#endregion
30
+ //#region src/runtime/types.d.ts
31
+ /**
32
+ * Quiz enum domains as runtime tuples. The unions below derive from these, and
33
+ * the build-time validator imports them too — so the accepted value set has a
34
+ * single source and can't drift between the types and the validator.
35
+ */
36
+ declare const FEEDBACK_MODES: readonly ["review", "immediate", "never"];
37
+ declare const RETRY_MODES: readonly ["full", "incorrect-only"];
38
+ /**
39
+ * Per-page quiz configuration. Single source of truth — the build plugin
40
+ * extracts this from `pageConfig.quiz` and embeds it in the manifest;
41
+ * the runtime reads it from there. Keep field shapes in sync.
42
+ */
43
+ interface QuizConfig {
44
+ graded?: boolean;
45
+ gatesProgress?: boolean;
46
+ maxAttempts?: number;
47
+ feedbackMode?: (typeof FEEDBACK_MODES)[number];
48
+ retryMode?: (typeof RETRY_MODES)[number];
49
+ }
50
+ interface CourseConfig {
51
+ title: string;
52
+ /** Stable, unique course identity (e.g. 'urn:uuid:…'). Seeds the web
53
+ * localStorage key and the cmi5/xAPI LRS activity id; scaffolders generate one.
54
+ * Absent → both fall back to a fixed value, colliding across courses. */
55
+ id?: string;
56
+ description?: string;
57
+ author?: string;
58
+ version?: string;
59
+ /** BCP-47 language tag for <html lang>. Defaults to 'en'. WCAG 3.1.1. */
60
+ language?: string;
61
+ /** Accessibility checker configuration. */
62
+ a11y?: A11yConfig;
63
+ branding?: {
64
+ logo?: string;
65
+ primaryColor?: string;
66
+ fontFamily?: string;
67
+ };
68
+ navigation: {
69
+ mode: 'free' | 'sequential';
70
+ canAccess?: AccessFn;
71
+ };
72
+ completion: ManualCompletion | QuizCompletion | PercentageCompletion;
73
+ /** Optional under "manual"; required under "quiz". */
74
+ scoring: {
75
+ passingScore: number;
76
+ };
77
+ export: {
78
+ standard: 'web' | 'scorm12' | 'scorm2004' | 'cmi5' | 'xapi';
79
+ /** Web export only: extend the baseline Content-Security-Policy. Each key is
80
+ * a directive; its sources are appended (unioned) onto the baseline. `false`
81
+ * drops the CSP meta entirely (for deployments that set a CSP header).
82
+ * Ignored unless `standard` is 'web'. */
83
+ csp?: false | Record<string, string[]>;
84
+ };
85
+ /**
86
+ * Optional xAPI destination(s) for custom statement publishing via
87
+ * `useXAPI()`. A single object or an array of destinations. Under cmi5
88
+ * export, the sentinel `endpoint: 'lms'` re-uses the LMS launch's
89
+ * credentials and shares the cmi5 adapter's queue.
90
+ */
91
+ xapi?: XAPIConfig | XAPIConfig[];
92
+ }
93
+ /** Accessibility checker configuration. */
94
+ interface A11yConfig {
95
+ /** Build-gate severity for promotable Tier-1 rules + Tier-1a warnings. */
96
+ level?: 'warn' | 'error';
97
+ /** axe ruleset tags for the Tier-2 runtime auditor. */
98
+ standard?: 'wcag2a' | 'wcag2aa' | 'wcag21aa';
99
+ /** Per-rule escape hatch matched literally against each diagnostic's ID. */
100
+ ignore?: string[];
101
+ }
102
+ interface ManualCompletion {
103
+ mode: 'manual';
104
+ /**
105
+ * Set to "page" to opt into a build-time check that at least one page
106
+ * declares `completesOn: "view"`. Omit to skip the check; both completion
107
+ * paths still work at runtime.
108
+ */
109
+ trigger?: 'page';
110
+ /** When set, markComplete() also flips successStatus. Omit for unknown. */
111
+ requireSuccessStatus?: 'passed' | 'failed';
112
+ }
113
+ interface QuizCompletion {
114
+ mode: 'quiz';
115
+ }
116
+ interface PercentageCompletion {
117
+ mode: 'percentage';
118
+ percentageThreshold?: number;
119
+ }
120
+ /**
121
+ * cmi5 launch-inherited destination. Only valid under `export.standard:
122
+ * 'cmi5'`. Auth, actor, activityId, and registration are taken from the
123
+ * launch URL, so no other fields are accepted.
124
+ */
125
+ interface XAPILMSConfig {
126
+ endpoint: 'lms';
127
+ }
128
+ /**
129
+ * Explicit LRS destination. The author provides every field. `actor` is
130
+ * optional under SCORM (synthesized from `cmi.core.student_id` /
131
+ * `cmi.learner_id`) and required under web.
132
+ */
133
+ interface XAPIExplicitConfig {
134
+ /** Absolute http(s) URL of the LRS Statements endpoint base. */
135
+ endpoint: string;
136
+ /**
137
+ * Basic-auth credential value (the part after "Basic "), or a function
138
+ * that resolves one. Function form is re-invoked once on 401 to cover
139
+ * short-lived tokens.
140
+ */
141
+ auth: string | (() => string | Promise<string>);
142
+ /**
143
+ * Identified Agent or a resolver function. Required for web export;
144
+ * optional under SCORM where it can be synthesized from the LMS data
145
+ * model. Optional under cmi5 where it can be inherited from the launch.
146
+ */
147
+ actor?: XAPIAgent | (() => XAPIAgent | Promise<XAPIAgent>);
148
+ /** xAPI activity IRI scoped to this destination. */
149
+ activityId: string;
150
+ /** Optional UUID v4 — primarily a cmi5 launch concept. */
151
+ registration?: string;
152
+ /**
153
+ * Override for the SCORM-derived actor's `account.homePage`. Defaults
154
+ * to the activityId origin when activityId is http(s); required when
155
+ * activityId uses a non-http(s) scheme.
156
+ */
157
+ actorAccountHomePage?: string;
158
+ }
159
+ type XAPIConfig = XAPILMSConfig | XAPIExplicitConfig;
160
+ //#endregion
161
+ //#region src/plugin/manifest.d.ts
162
+ interface ManifestPage {
163
+ index: number;
164
+ title: string;
165
+ slug: string;
166
+ importPath: string;
167
+ quiz: QuizConfig | null;
168
+ completesOn?: 'view';
169
+ }
170
+ interface ManifestLesson {
171
+ title: string;
172
+ slug: string;
173
+ pages: ManifestPage[];
174
+ }
175
+ interface ManifestSection {
176
+ title: string;
177
+ slug: string;
178
+ lessons: ManifestLesson[];
179
+ }
180
+ interface Manifest {
181
+ sections: ManifestSection[];
182
+ pages: ManifestPage[];
183
+ totalPages: number;
184
+ }
185
+ //#endregion
186
+ //#region src/runtime/progress.svelte.d.ts
187
+ declare class ProgressState {
188
+ #private;
189
+ constructor(quizGradedIndices: ReadonlySet<number>, config: CourseConfig, totalPages: number);
190
+ visitedPages: SvelteSet<number>;
191
+ quizScores: SvelteMap<number, number>;
192
+ /**
193
+ * Chunk progress — for pages that reveal content in stages (Continue buttons).
194
+ * Maps pageIndex → highest revealed chunk index (0-based).
195
+ */
196
+ chunkProgress: SvelteMap<number, number>;
197
+ /**
198
+ * Per-page standalone question scores from `useQuestion`. pageIndex → (questionId → score 0-100).
199
+ * Tracked separately from `quizScores` because <Quiz> blocks score as a unit
200
+ * while standalone questions score individually and average per page.
201
+ */
202
+ standaloneQuestionScores: SvelteMap<number, Map<string, number>>;
203
+ /**
204
+ * Set of page indices that have at least one graded standalone question.
205
+ * Pages in this set contribute to course success status via their standalone average.
206
+ */
207
+ gradedStandalonePages: SvelteSet<number>;
208
+ /**
209
+ * Monotonic counter incremented on every persistable state mutation. App.svelte
210
+ * subscribes to this single signal to schedule a coalesced save.
211
+ */
212
+ version: number;
213
+ get manuallyCompleted(): boolean;
214
+ /** Idempotent — only the first call per session has an effect. */
215
+ markCompleteManually(): void;
216
+ markVisited(pageIndex: number): void;
217
+ quizCompleted(pageIndex: number, score: number): void;
218
+ /** Record the highest chunk index revealed on a page. Only advances forward. */
219
+ markChunk(pageIndex: number, chunkIndex: number): void;
220
+ /** Highest chunk revealed on a page, or -1 if none. */
221
+ getChunk(pageIndex: number): number;
222
+ markStandaloneQuestion(pageIndex: number, questionId: string, score: number, graded: boolean): void;
223
+ /** Average of standalone question scores on a page, or 0 if none. */
224
+ getPageStandaloneAverage(pageIndex: number): number;
225
+ completionStatus: "incomplete" | "complete";
226
+ successStatus: "passed" | "failed" | "unknown";
227
+ /**
228
+ * Effective graded score for LMS reporting — same union and averaging as
229
+ * successStatus, so score and success status can't disagree.
230
+ */
231
+ gradedScore(): {
232
+ average: number;
233
+ attempted: boolean;
234
+ };
235
+ }
236
+ //#endregion
237
+ //#region src/runtime/access.d.ts
238
+ interface AccessContext {
239
+ pageIndex: number;
240
+ page: ManifestPage;
241
+ manifest: Manifest;
242
+ progress: ProgressState;
243
+ config: CourseConfig;
244
+ }
245
+ /**
246
+ * Predicate deciding whether a page is accessible to the learner.
247
+ *
248
+ * Runs synchronously on every derived re-evaluation — keep it cheap. It is a
249
+ * runtime-side check only: the LMS does not enforce these rules. Authors who
250
+ * need true sequencing must rely on the LMS standard's own activity rules.
251
+ */
252
+ type AccessFn = (ctx: AccessContext) => boolean;
253
+ //#endregion
3
254
  //#region src/plugin/a11y/audit.d.ts
4
255
  interface AuditOptions {
5
256
  /** Minimum violation impact that fails the run (CI gate). Default 'serious'. */
6
257
  threshold?: ImpactLevel;
7
- /** Force a fresh `vite build` even if dist/ exists. */
8
- rebuild?: boolean;
9
258
  }
10
259
  type ImpactLevel = 'minor' | 'moderate' | 'serious' | 'critical';
11
260
  /**
@@ -17,6 +266,34 @@ declare function runAudit(projectRoot: string, workspaceRoot: string, options?:
17
266
  //#endregion
18
267
  //#region src/plugin/index.d.ts
19
268
  declare function tesseraPlugin(): (Plugin<any> | Plugin<any>[])[];
269
+ /** Fill runtime defaults into a parsed course.config.js. Exported for tests. */
270
+ declare function mergeCourseConfig(userConfig: Partial<CourseConfig>): {
271
+ title: string;
272
+ navigation: {
273
+ mode: string;
274
+ canAccess?: AccessFn;
275
+ };
276
+ completion: {};
277
+ scoring: {
278
+ passingScore: number;
279
+ };
280
+ export: {
281
+ standard: string;
282
+ csp?: false | Record<string, string[]>;
283
+ };
284
+ id?: string | undefined;
285
+ description?: string | undefined;
286
+ author?: string | undefined;
287
+ version?: string | undefined;
288
+ language?: string | undefined;
289
+ a11y?: A11yConfig | undefined;
290
+ branding?: {
291
+ logo?: string;
292
+ primaryColor?: string;
293
+ fontFamily?: string;
294
+ } | undefined;
295
+ xapi?: (XAPIConfig | XAPIConfig[]) | undefined;
296
+ };
20
297
  //#endregion
21
- export { type AuditOptions, type ImpactLevel, runAudit, tesseraPlugin };
298
+ export { type AuditOptions, type ImpactLevel, mergeCourseConfig, runAudit, tesseraPlugin };
22
299
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/plugin/a11y/audit.ts","../../src/plugin/index.ts"],"mappings":";;;UAOiB,YAAA;EAAA;EAEf,SAAA,GAAY,WAAW;;EAEvB,OAAA;AAAA;AAAA,KAGU,WAAA;;;;;;iBAoUU,QAAA,CACpB,WAAA,UACA,aAAA,UACA,OAAA,GAAS,YAAA,GACR,OAAO;;;iBClOM,aAAA,KAAa,MAAA,QAAA,MAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/runtime/xapi/types.ts","../../src/runtime/types.ts","../../src/plugin/manifest.ts","../../src/runtime/progress.svelte.ts","../../src/runtime/access.ts","../../src/plugin/a11y/audit.ts","../../src/plugin/index.ts"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { n as runAudit } from "../audit-B9VHgVjk.js";
2
- import { t as tesseraPlugin } from "../plugin--8H9xQIl.js";
3
- export { runAudit, tesseraPlugin };
1
+ import { n as runAudit } from "../audit-DkXqQTqn.js";
2
+ import { n as tesseraPlugin, t as mergeCourseConfig } from "../plugin-CFUFgwHB.js";
3
+ export { mergeCourseConfig, runAudit, tesseraPlugin };